From 54b097b7a8258ba344700f983b80053826c62cd6 Mon Sep 17 00:00:00 2001 From: Steve Mayne Date: Mon, 20 Oct 2025 07:52:31 +0100 Subject: [PATCH 1/5] Fix typo --- WFC/wavefunctioncollapse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WFC/wavefunctioncollapse.py b/WFC/wavefunctioncollapse.py index ccbb60f..78f58e7 100644 --- a/WFC/wavefunctioncollapse.py +++ b/WFC/wavefunctioncollapse.py @@ -6,7 +6,7 @@ ELEMENTS = [ WS(0,'img/tile_0000.png', [[5,13,14,15],[1,2,11],[7,11,13],[2,5,9,15]]), - WS(1,'img/tile_0001.png', [[5,13.14,15],[1,2,11],[8,12,14],[0,1,10]]), + WS(1,'img/tile_0001.png', [[5,13,14,15],[1,2,11],[8,12,14],[0,1,10]]), WS(2,'img/tile_0002.png', [[5,14,15],[0,5,7,13],[9,10,15],[0,1,10]]), WS(3,'img/tile_0003.png', [[6,8,12],[4,14,15],[9,10,15],[6,7,12,8]]), WS(4,'img/tile_0004.png', [[6,8,12],[6,8,9,12],[7,11,13],[3,13,14]]), From 508b3d4a2457ed69be10d56aa078d562541ba955 Mon Sep 17 00:00:00 2001 From: Steve Mayne Date: Mon, 20 Oct 2025 10:31:51 +0100 Subject: [PATCH 2/5] Use tile pixels rather than compatability matrix --- WFC/.python-version | 1 + WFC/requirements.txt | 1 + WFC/wavefunctioncollapse.py | 78 ++++++++++++++++++++++++++----------- WFC/worldelement.py | 19 +++++---- WFC/worldsprite.py | 22 +++++++++-- 5 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 WFC/.python-version create mode 100644 WFC/requirements.txt diff --git a/WFC/.python-version b/WFC/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/WFC/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/WFC/requirements.txt b/WFC/requirements.txt new file mode 100644 index 0000000..5873083 --- /dev/null +++ b/WFC/requirements.txt @@ -0,0 +1 @@ +pygame==2.6.1 diff --git a/WFC/wavefunctioncollapse.py b/WFC/wavefunctioncollapse.py index 78f58e7..0c2762f 100644 --- a/WFC/wavefunctioncollapse.py +++ b/WFC/wavefunctioncollapse.py @@ -1,27 +1,60 @@ import pygame +import os +import re from operator import attrgetter from worldelement import WorldElement as WE from worldsprite import WorldSprite as WS -ELEMENTS = [ - WS(0,'img/tile_0000.png', [[5,13,14,15],[1,2,11],[7,11,13],[2,5,9,15]]), - WS(1,'img/tile_0001.png', [[5,13,14,15],[1,2,11],[8,12,14],[0,1,10]]), - WS(2,'img/tile_0002.png', [[5,14,15],[0,5,7,13],[9,10,15],[0,1,10]]), - WS(3,'img/tile_0003.png', [[6,8,12],[4,14,15],[9,10,15],[6,7,12,8]]), - WS(4,'img/tile_0004.png', [[6,8,12],[6,8,9,12],[7,11,13],[3,13,14]]), - WS(5,'img/tile_0005.png', [[5,13,14,15],[0,5,7,13],[0,1,2,5],[2,5,9,15]]), - WS(6,'img/tile_0006.png', [[1,6,8,10,11,12],[3,6,8,10,12],[3,4,6,8,12,14],[6,8,12]]), - WS(7,'img/tile_0007.png', [[0,4,7],[3,6,8,9,10,12,13],[7,11,13],[2,5,9,15]]), - WS(8,'img/tile_0008.png', [[1,6,8,10,11,12],[6,8,9,10,12],[4,6,8,12,14],[4,6,8,7,11,12]]), - WS(9,'img/tile_0009.png', [[2,3,9],[0,5,7,13],[9,10,15],[4,6,7,8,11]]), - WS(10,'img/tile_0010.png', [[2,3,9],[1,2,11],[6,8,12,14],[6,8,9,11,12]]), - WS(11,'img/tile_0011.png', [[0,4,7],[3,6,8,10,12],[3,4,6,8,12,14],[1,10]]), - WS(12,'img/tile_0012.png', [[6,8,10,11,12],[6,8,9,10,12],[3,4,6,8,12,14],[6,7,8,12]]), - WS(13,'img/tile_0013.png', [[0,4,7],[4,14,15],[0,1,2,4,5],[2,5,7,9,15]]), - WS(14,'img/tile_0014.png', [[1,6,8,10,11,12],[4,14,15],[0,1,2,3,5],[3,13,14]]), - WS(15,'img/tile_0015.png', [[2,3,9],[0,5,7,13],[0,1,2,5],[3,13,14]]) - ] +def _load_tiles_from_images(base_dir): + img_dir = os.path.join(base_dir, 'img') + tiles = [] + if not os.path.isdir(img_dir): + return tiles + image_files = [f for f in os.listdir(img_dir) if f.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif"))] + def extract_id(name): + m = re.search(r'(\d+)', name) + return int(m.group(1)) if m else None + # Prepare list of (id_or_None, fullpath) + image_catalogue = [(extract_id(fn), os.path.join(img_dir, fn)) for fn in image_files] + next_auto_id = 0 + for tid, fpath in image_catalogue: + if tid is None: + tid = next_auto_id + next_auto_id += 1 + tiles.append(WS(tid, fpath, edges=None)) + _build_compatibility(tiles) + return tiles + +def _build_compatibility(tiles): + # Precompute, for each tile, the list of compatible neighbor ids by direction (0:N,1:E,2:S,3:W) + def corners_match(a, b, dir_index): + # Corner indices: 0=NW, 1=NE, 2=SE, 3=SW + pairs_by_dir = { + 0: [(0, 3), (1, 2)], # North neighbor + 1: [(1, 0), (2, 3)], # East neighbor + 2: [(3, 0), (2, 1)], # South neighbor + 3: [(0, 1), (3, 2)], # West neighbor + } + for sc, nc in pairs_by_dir[dir_index]: + sc_col = a.corners.get(sc) + nc_col = b.corners.get(nc) + if sc_col is None or nc_col is None: + # Permissive if missing data + continue + if tuple(sc_col[:3]) != tuple(nc_col[:3]): + return False + return True + + for t in tiles: + t.compat = {0: [], 1: [], 2: [], 3: []} + for a in tiles: + for b in tiles: + for d in (0, 1, 2, 3): + if corners_match(a, b, d): + a.compat[d].append(b.id) + # For backward compatibility with older filtering code + a.neighbours = [a.compat[0], a.compat[1], a.compat[2], a.compat[3]] class WaveFunctionCollapse: @@ -46,9 +79,10 @@ def set_reset(self): def setup_world_elements(self): self.world_elements = [] + elements = _load_tiles_from_images(os.path.dirname(__file__)) for y in range(self.world_size): for x in range(self.world_size): - we = WE(self.screen, ELEMENTS, x, y) + we = WE(self.screen, elements, x, y) self.world_elements.append(we) def set_world_element_neighbours(self): @@ -69,11 +103,11 @@ def set_world_element_neighbours(self): we.set_neighbours([north, east, south, west]) def auto_collapse(self): - if self.auto_collapse_active != True: + if not self.auto_collapse_active: return current_time = pygame.time.get_ticks() if current_time - self.last_time >= self.auto_collapse_wait: - last_time = current_time + self.last_time = current_time next = self.world_elements.index(min(self.world_elements, key=attrgetter('entropy'))) self.world_elements[next].collapse() @@ -83,4 +117,4 @@ def collapse(self, collapse_index): def draw(self): for we in self.world_elements: - we.draw() \ No newline at end of file + we.draw() diff --git a/WFC/worldelement.py b/WFC/worldelement.py index 7527a33..4c8888e 100644 --- a/WFC/worldelement.py +++ b/WFC/worldelement.py @@ -18,35 +18,38 @@ def set_neighbours(self, neighbours): def draw(self): i = 0 for element in self.elements: - if element == None: + if not element: return element.draw(self.screen, len(self.elements), i, self.collapsed, self.pos) i+=1 def collapse(self): - if self.collapsed == True: + if self.collapsed: return self.collapsed = True self.elements = [self.elements[random.randrange(0, len(self.elements))]] self.entropy = 9999 for i in range(4): temp = [] - if self.neighbours[i] != None: + if self.neighbours[i]: for element in self.neighbours[i].elements: - if element == None or self.elements[0] == None: + if not (element and self.elements[0]): return - if element.id in self.elements[0].neighbours[i]: + if self._is_compatible(self.elements[0], element, i): temp.append(element) - if self.neighbours[i].collapsed == False: + if not self.neighbours[i].collapsed: if len(temp) == 0: temp.append(None) self.neighbours[i].elements = temp self.neighbours[i].update() def update(self): - if self.collapsed == False: + if not self.collapsed: self.entropy = len(self.elements) if self.entropy == 0: print(f"Sprite {self.pos} has zero entropy, consider checking neighbour assignments!") - + def _is_compatible(self, src_tile, neighbor_tile, dir_index): + allowed = src_tile.compat.get(dir_index) + if isinstance(allowed, (list, set)): + return neighbor_tile.id in allowed diff --git a/WFC/worldsprite.py b/WFC/worldsprite.py index 4c52cff..fc041e4 100644 --- a/WFC/worldsprite.py +++ b/WFC/worldsprite.py @@ -6,17 +6,31 @@ class WorldSprite: - def __init__(self,id, image, neighbours): + def __init__(self, id, image, edges=None): self.id = id self.image = pygame.image.load(image) - self.neighbours = neighbours - + # Store corner colors (NW, NE, SE, SW) as raw RGB tuples + self.corners = self._corners_from_image(self.image) + + def _corners_from_image(self, surface): + # Sample the color at each corner pixel and store as (R,G,B) + w, h = surface.get_width(), surface.get_height() + coords = { + 0: (0, 0), # NW + 1: (w - 1, 0), # NE + 2: (w - 1, h - 1), # SE + 3: (0, h - 1), # SW + } + def to_rgb(color): + return (color[0], color[1], color[2]) + return {k: to_rgb(surface.get_at(pos)) for k, pos in coords.items()} + def draw(self, screen, count, i, collapsed, pos): w = math.ceil(math.sqrt(count)) x = math.floor(i % w) y = math.floor(i / w) offset = (0,0) - if count == 1 and collapsed == True: + if count == 1 and collapsed: out_img = pygame.transform.scale(self.image, (SIZE, SIZE)) else: out_img = pygame.transform.scale(self.image, (SIZE / w * (1-BORDER), SIZE / w * (1 - BORDER))) From d19eb83ed459ea0f65497732cb014d7e4cd66c64 Mon Sep 17 00:00:00 2001 From: Steve Mayne Date: Mon, 20 Oct 2025 10:34:45 +0100 Subject: [PATCH 3/5] Remove deprecated edges kwarg --- WFC/wavefunctioncollapse.py | 2 +- WFC/worldsprite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WFC/wavefunctioncollapse.py b/WFC/wavefunctioncollapse.py index 0c2762f..00f9fdb 100644 --- a/WFC/wavefunctioncollapse.py +++ b/WFC/wavefunctioncollapse.py @@ -22,7 +22,7 @@ def extract_id(name): if tid is None: tid = next_auto_id next_auto_id += 1 - tiles.append(WS(tid, fpath, edges=None)) + tiles.append(WS(tid, fpath)) _build_compatibility(tiles) return tiles diff --git a/WFC/worldsprite.py b/WFC/worldsprite.py index fc041e4..cbb089e 100644 --- a/WFC/worldsprite.py +++ b/WFC/worldsprite.py @@ -6,7 +6,7 @@ class WorldSprite: - def __init__(self, id, image, edges=None): + def __init__(self, id, image): self.id = id self.image = pygame.image.load(image) # Store corner colors (NW, NE, SE, SW) as raw RGB tuples From ffc9e6b78b7270b779ca8ffc38fb01994228e17a Mon Sep 17 00:00:00 2001 From: Steve Mayne Date: Mon, 20 Oct 2025 10:39:05 +0100 Subject: [PATCH 4/5] Simplify tile loader --- WFC/wavefunctioncollapse.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/WFC/wavefunctioncollapse.py b/WFC/wavefunctioncollapse.py index 00f9fdb..4f6f719 100644 --- a/WFC/wavefunctioncollapse.py +++ b/WFC/wavefunctioncollapse.py @@ -6,26 +6,26 @@ from worldsprite import WorldSprite as WS +def _extract_id_from_filename(name): + m = re.search(r'(\d+)', name) + return int(m.group(1)) if m else None + + def _load_tiles_from_images(base_dir): img_dir = os.path.join(base_dir, 'img') - tiles = [] if not os.path.isdir(img_dir): - return tiles - image_files = [f for f in os.listdir(img_dir) if f.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif"))] - def extract_id(name): - m = re.search(r'(\d+)', name) - return int(m.group(1)) if m else None + raise Exception(f"Image directory not found: {img_dir}") + tiles = [] + image_files = [f for f in os.listdir(img_dir) if f.lower().endswith(".png")] # Prepare list of (id_or_None, fullpath) - image_catalogue = [(extract_id(fn), os.path.join(img_dir, fn)) for fn in image_files] - next_auto_id = 0 + image_catalogue = [(_extract_id_from_filename(fn), os.path.join(img_dir, fn)) for fn in image_files] for tid, fpath in image_catalogue: - if tid is None: - tid = next_auto_id - next_auto_id += 1 - tiles.append(WS(tid, fpath)) + if tid: + tiles.append(WS(tid, fpath)) _build_compatibility(tiles) return tiles + def _build_compatibility(tiles): # Precompute, for each tile, the list of compatible neighbor ids by direction (0:N,1:E,2:S,3:W) def corners_match(a, b, dir_index): From 452e75bc36e2e446c2356d0af4f103ebdd0cfdbf Mon Sep 17 00:00:00 2001 From: Steve Mayne Date: Mon, 20 Oct 2025 10:42:35 +0100 Subject: [PATCH 5/5] Remove legacy code --- WFC/wavefunctioncollapse.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/WFC/wavefunctioncollapse.py b/WFC/wavefunctioncollapse.py index 4f6f719..758dfed 100644 --- a/WFC/wavefunctioncollapse.py +++ b/WFC/wavefunctioncollapse.py @@ -53,8 +53,6 @@ def corners_match(a, b, dir_index): for d in (0, 1, 2, 3): if corners_match(a, b, d): a.compat[d].append(b.id) - # For backward compatibility with older filtering code - a.neighbours = [a.compat[0], a.compat[1], a.compat[2], a.compat[3]] class WaveFunctionCollapse: