diff --git a/src/scripts/cos_level.py b/src/scripts/cos_level.py index f448c37..f2aa7bd 100644 --- a/src/scripts/cos_level.py +++ b/src/scripts/cos_level.py @@ -280,9 +280,7 @@ class Level: break - # 8. Tile painting (WFC) - possibilities = None - while possibilities or possibilities is None: - possibilities = ct.wfc_pass(self.grid, possibilities) + # 8. Tile painting (Wang autotiling) + ct.paint_tiles(self.grid) return feature_coords diff --git a/src/scripts/cos_tiles.py b/src/scripts/cos_tiles.py index 0054b54..f6123e2 100644 --- a/src/scripts/cos_tiles.py +++ b/src/scripts/cos_tiles.py @@ -1,223 +1,38 @@ -tiles = {} -deltas = [ - (-1, -1), ( 0, -1), (+1, -1), - (-1, 0), ( 0, 0), (+1, 0), - (-1, +1), ( 0, +1), (+1, +1) - ] +import mcrfpy -class TileInfo: - GROUND, WALL, DONTCARE = True, False, None - chars = { - "X": WALL, - "_": GROUND, - "?": DONTCARE - } - symbols = {v: k for k, v in chars.items()} +# Load tileset and Wang set once at module level +_tileset = mcrfpy.TileSetFile("assets/kenney_TD_MR_IP.tsx") +_wang_set = _tileset.wang_set("dungeon") +_Terrain = _wang_set.terrain_enum() +# _Terrain.WALL = 1, _Terrain.GROUND = 2 - def __init__(self, values:dict): - self._values = values - self.rules = [] - self.chance = 1.0 - @staticmethod - def from_grid(grid, xy:tuple): - values = {} - x_max, y_max = int(grid.grid_size.x), int(grid.grid_size.y) - for d in deltas: - tx, ty = d[0] + xy[0], d[1] + xy[1] - if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: - values[d] = True +def paint_tiles(grid): + """Apply Wang tile autotiling based on grid walkability.""" + w = int(grid.grid_size.x) + h = int(grid.grid_size.y) + + # Build terrain map from walkability + dm = mcrfpy.DiscreteMap((w, h)) + for y in range(h): + for x in range(w): + if grid.at((x, y)).walkable: + dm.set(x, y, int(_Terrain.GROUND)) else: - values[d] = grid.at((tx, ty)).walkable - return TileInfo(values) + dm.set(x, y, int(_Terrain.WALL)) - @staticmethod - def from_string(s): - values = {} - for d, c in zip(deltas, s): - values[d] = TileInfo.chars[c] - return TileInfo(values) + # Resolve Wang tiles + tiles = _wang_set.resolve(dm) - def __hash__(self): - """for use as a dictionary key""" - return hash(tuple(self._values.items())) - - def match(self, other:"TileInfo"): - for d, rule in self._values.items(): - if rule is TileInfo.DONTCARE: continue - if other._values[d] is TileInfo.DONTCARE: continue - if rule != other._values[d]: return False - return True - - def show(self): - nine = ['', '', '\n'] * 3 - for k, end in zip(deltas, nine): - c = TileInfo.symbols[self._values[k]] - print(c, end=end) - - def __repr__(self): - return f"" - -cardinal_directions = { - "N": ( 0, -1), - "S": ( 0, +1), - "E": (-1, 0), - "W": (+1, 0) -} - -def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False): - cardinal, allowed_tile = rule - dxy = cardinal_directions[cardinal.upper()] - tx, ty = xy[0] + dxy[0], xy[1] + dxy[1] - #print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}") - if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified - x_max, y_max = int(grid.grid_size.x), int(grid.grid_size.y) - if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: - return False - return grid.at((tx, ty)).tilesprite == allowed_tile - -import random -tile_of_last_resort = 431 - -def find_possible_tiles(grid, x, y, unverified_tiles=None, pass_unverified=False): - ti = TileInfo.from_grid(grid, (x, y)) - if unverified_tiles is None: unverified_tiles = [] - matches = [(k, v) for k, v in tiles.items() if k.match(ti)] - if not matches: - return [] - possible = [] - if not any([tileinfo.rules for tileinfo, _ in matches]): - # make weighted choice, as the tile does not depend on verification - wts = [k.chance for k, v in matches] - tileinfo, tile = random.choices(matches, weights=wts)[0] - return [tile] - - for tileinfo, tile in matches: - if not tileinfo.rules: - possible.append(tile) - continue - for r in tileinfo.rules: #for r in ...: if ... continue == more readable than an "any" 1-liner - p = special_rule_verify(r, grid, (x,y), - unverified_tiles=unverified_tiles, - pass_unverified = pass_unverified - ) - if p: - possible.append(tile) - continue - return list(set(list(possible))) - -def wfc_first_pass(grid): - w, h = int(grid.grid_size.x), int(grid.grid_size.y) - possibilities = {} - for x in range(0, w): - for y in range(0, h): - matches = find_possible_tiles(grid, x, y, pass_unverified=True) - if len(matches) == 0: - grid.at((x, y)).tilesprite = tile_of_last_resort - possibilities[(x,y)] = matches - elif len(matches) == 1: - grid.at((x, y)).tilesprite = matches[0] + # Apply to grid, with fallback for unmatched patterns + for y in range(h): + for x in range(w): + tile_id = tiles[y * w + x] + if tile_id >= 0: + grid.at((x, y)).tilesprite = tile_id else: - possibilities[(x,y)] = matches - return possibilities - -def wfc_pass(grid, possibilities=None): - w, h = int(grid.grid_size.x), int(grid.grid_size.y) - if possibilities is None: - #print("first pass results:") - possibilities = wfc_first_pass(grid) - counts = {} - for v in possibilities.values(): - if len(v) in counts: counts[len(v)] += 1 - else: counts[len(v)] = 1 - #print(counts) - return possibilities - elif len(possibilities) == 0: - print("We're done!") - return - old_possibilities = possibilities - possibilities = {} - for (x, y) in old_possibilities.keys(): - matches = find_possible_tiles(grid, x, y, unverified_tiles=old_possibilities.keys(), pass_unverified = True) - if len(matches) == 0: - print((x,y), matches) - grid.at((x, y)).tilesprite = tile_of_last_resort - possibilities[(x,y)] = matches - elif len(matches) == 1: - grid.at((x, y)).tilesprite = matches[0] - else: - grid.at((x, y)).tilesprite = -1 - possibilities[(x,y)] = matches - - if len(possibilities) == len(old_possibilities): - #print("No more tiles could be solved without collapse") - counts = {} - for v in possibilities.values(): - if len(v) in counts: counts[len(v)] += 1 - else: counts[len(v)] = 1 - #print(counts) - if 0 in counts: del counts[0] - if len(counts) == 0: - print("Contrats! You broke it! (insufficient tile defs to solve remaining tiles)") - return [] - target = min(list(counts.keys())) - while possibilities: - for (x, y) in possibilities.keys(): - if len(possibilities[(x, y)]) != target: - continue - ti = TileInfo.from_grid(grid, (x, y)) - matches = [(k, v) for k, v in tiles.items() if k.match(ti)] - verifiable_matches = find_possible_tiles(grid, x, y, unverified_tiles=possibilities.keys()) - if not verifiable_matches: continue - #print(f"collapsing {(x, y)} ({target} choices)") - matches = [(k, v) for k, v in matches if v in verifiable_matches] - wts = [k.chance for k, v in matches] - tileinfo, tile = random.choices(matches, weights=wts)[0] - grid.at((x, y)).tilesprite = tile - del possibilities[(x, y)] - break - else: - selected = random.choice(list(possibilities.keys())) - #print(f"No tiles have verifable solutions: QUANTUM -> {selected}") - # sprinkle some quantumness on it - ti = TileInfo.from_grid(grid, (x, y)) - matches = [(k, v) for k, v in tiles.items() if k.match(ti)] - wts = [k.chance for k, v in matches] - if not wts: - print(f"This one: {(x,y)} {matches}\n{wts}") - del possibilities[(x, y)] - return possibilities - tileinfo, tile = random.choices(matches, weights=wts)[0] - grid.at((x, y)).tilesprite = tile - del possibilities[(x, y)] - - return possibilities - -#with open("scripts/tile_def.txt", "r") as f: -with open("scripts/simple_tiles.txt", "r") as f: - for block in f.read().split('\n\n'): - info, constraints = block.split('\n', 1) - if '#' in info: - info, comment = info.split('#', 1) - rules = [] - if '@' in info: - info, *block_rules = info.split('@') - #print(block_rules) - for r in block_rules: - rules.append((r[0], int(r[1:]))) - #cardinal_dir = block_rules[0] - #partner - if ':' not in info: - tile_id = int(info) - chance = 1.0 - else: - tile_id, chance = info.split(':') - tile_id = int(tile_id) - chance = float(chance.strip()) - constraints = constraints.replace('\n', '') - k = TileInfo.from_string(constraints) - k.rules = rules - k.chance = chance - tiles[k] = tile_id - - + # Fallback: open floor (145) for ground, solid wall (251) for walls + if grid.at((x, y)).walkable: + grid.at((x, y)).tilesprite = 145 + else: + grid.at((x, y)).tilesprite = 251 diff --git a/src/scripts/simple_tiles.txt b/src/scripts/simple_tiles.txt deleted file mode 100644 index 12b666d..0000000 --- a/src/scripts/simple_tiles.txt +++ /dev/null @@ -1,144 +0,0 @@ -145# open space -??? -?_? -??? - -184:0.03# open space variant -??? -?_? -??? - -146# lone wall / pillar -___ -_X_ -___ - -132# top left corner -?_? -_XX -?X? - -133# plain horizontal wall -??? -XXX -?_? - -182:0.04# plain horizontal wall variant -??? -XXX -?_? - -183:0.04# plain horizontal wall variant -??? -XXX -?_? - -157:0.01# plain horizontal wall variant -??? -XXX -?_? - -135# top right corner -?_? -XX_ -?X? - -144@N132@s144@n144@n192@s192@S156@n171@s169@n180# Left side wall. Space on both sides rule may make the dungeon less robust (no double-walls allowed) -?X? -?X_ -?X? - -147@N135@s147@n147@n193@s193@S159@n170@s168@n181# Right side wall -?X? -_X? -?X? - -156# bottom left corner -?X? -_XX -?_? - -159# bottom right corner -?X? -XX_ -?_? - -192@n144@s144@s169# vertical T, left wall -?X? -?XX -?X? - -193@n147@s147@s168# vertical T, right wall -?X? -XX? -?X? - -180@s144@s144@s169# horizontal T, left wall -??? -XXX -?X? - -181@s147@s147@s168# horizontal T, right wall -??? -XXX -?X? - -195@W133@W182@W183@W157# wall for edge of a gap -??_ -XX_ -?__ - -195 -?__ -XX_ -?__ - -194@E133@E182@E183@E157# wall for edge of a gap (R) -_?? -_XX -__? - -194 -__? -_XX -__? - -195@W133@W182@W183@W157# wall for edge of a gap -?__ -XX_ -??_ - -194@E133@E182@E183@E157# wall for edge of a gap (R) -__? -_XX -_?? - -195@W133@W182@W183@W157# wall for edge of a gap -??_ -XX_ -?__ - -194@E133@E182@E183@E157# wall for edge of a gap (R) -_?? -_XX -__? - -168@n147@n170@n135@n181@n193# right vertical wall, gap below -?X? -_X_ -?_? - -169@n144@n171@n132@n192@n180# left vertical wall, gap below -?X? -_X_ -?_? - -170@s147@s168@s133@s182@s183@s157@s193@s181# right vertical wall, gap above -?_? -_X_ -?X? - -171@s144@s169@s133@s182@s183@s157@s171@s180# left vertical wall, gap above -?_? -_X_ -?X?