Crypt of Sokoban remaster continued
This commit is contained in:
parent
686e4fc1b2
commit
2681cbd957
3 changed files with 33 additions and 364 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"<TileInfo {self._values}>"
|
||||
|
||||
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]
|
||||
# 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 = -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
|
||||
|
||||
|
||||
grid.at((x, y)).tilesprite = 251
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
Loading…
Add table
Add a link
Reference in a new issue