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 ccbb60f..758dfed 100644 --- a/WFC/wavefunctioncollapse.py +++ b/WFC/wavefunctioncollapse.py @@ -1,27 +1,58 @@ 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 _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') + if not os.path.isdir(img_dir): + 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_from_filename(fn), os.path.join(img_dir, fn)) for fn in image_files] + for tid, fpath in image_catalogue: + 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): + # 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) class WaveFunctionCollapse: @@ -46,9 +77,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 +101,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 +115,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..cbb089e 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): 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)))