From ee4dbee89c6b575a1bba83509bd00a646bf2d04b Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Thu, 6 Mar 2025 21:37:50 +0100 Subject: [PATCH 1/4] Add first draft of database creation script --- pelita/create_db.py | 203 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 pelita/create_db.py diff --git a/pelita/create_db.py b/pelita/create_db.py new file mode 100644 index 000000000..a677ab790 --- /dev/null +++ b/pelita/create_db.py @@ -0,0 +1,203 @@ +import json + +import jsbeautifier +import networkx as nx +from rich.pretty import pprint +from rich.progress import track + +from pelita.layout import get_available_layouts, get_layout_by_name, parse_layout +from pelita.maze_generator import find_chamber + + +def position_in_maze(pos, shape): + x, y = pos + w, h = shape + + return 0 <= x < w and 0 <= y < h + + +def walls_to_graph(walls, shape): + w, h = shape + directions = [(1, 0), (0, 1), (-1, 0), (0, -1)] + + graph = nx.Graph() + # define nodes for maze + for x in range(w): + for y in range(h): + if (x, y) not in walls: + # this is a free position, get its neighbors + for delta_x, delta_y in directions: + neighbor = (x + delta_x, y + delta_y) + # we don't need to check for getting neighbors out of the maze + # because our mazes are all surrounded by walls, i.e. our + # deltas will not put us out of the maze + if neighbor not in walls and position_in_maze(neighbor, shape): + # this is a genuine neighbor, add an edge in the graph + graph.add_edge((x, y), neighbor) + return graph + + +def find_cuts(graph): + G = graph.copy() + cuts = [] + while True: + cut, chamber = find_chamber(G) + if cut: + G.remove_nodes_from(chamber) + cuts.append(cut) + else: + break + return cuts + + +def find_cuts_faster(graph): + return list(nx.articulation_points(graph)) + + +def find_chambers(graph, cuts, deadends, shape): + w, h = shape + G = graph.copy() + + nodes = cuts + deadends + subgraph = graph.subgraph(nodes) + tunnels = [sorted(t) for t in nx.connected_components(subgraph)] + + G.remove_nodes_from(nodes) + + # remove main chamber + chambers = [] + for chamber in nx.connected_components(G): + skip = False + for node in chamber: + if w // 2 - 1 <= node[0] <= w // 2: + skip = True + break + + if skip: + continue + + chambers.extend(chamber) + + nodes = set(cuts + deadends + chambers) + + subgraph = graph.subgraph(nodes) + chambers = [sorted(c) for c in nx.connected_components(subgraph)] + + # remove "fake" chambers only connected to articulation points + # for c, chamber in enumerate(chambers): + # G = graph.copy() + # connections = [] + # for edge in graph.edges: + # a, b = edge + # if (a in G.nodes and b in chamber) or (b in G.nodes and a in chamber): + # connections.append(edge) + # if connections: + # print("BICONNECTED") + # print(connections) + # print(chamber) + + return chambers + + +def chambers_to_food( + layout: str, + chambers: list[tuple[int, int]], + temp_layout_path="/tmp/pelita_marked.layout", +): + with open(temp_layout_path, mode="w") as temp_layout_file: + line_number = 0 + for line in layout.splitlines(): + line = line.replace(".", " ") + for x, y in chambers: + if y == line_number and line[x] not in ["a", "b", "x", "y"]: + line = line[:x] + "." + line[x + 1 :] + temp_layout_file.write(line + "\n") + line_number += 1 + return temp_layout_path + + +def find_dead_ends(graph): + """Find dead ends in a graph.""" + + dead_ends = [node for node in graph.nodes() if graph.degree(node) == 1] + return dead_ends + + +names = [] + +for size, deadend in [ + ("small", False), + ("small", True), + ("normal", False), + ("normal", True), +]: + names.extend(get_available_layouts(size=size, dead_ends=deadend)) + +layouts = [get_layout_by_name(name) for name in names] +layouts = [parse_layout(layout) for layout in layouts] + +objs = [] + +custom_layout = """################################ +# ### . #. .. y# +# #### # ##### . #.### #x# +# # . . . # # # +# ## . . ##. ###.#### # +# # #### # . ..# . # # +##### # # . ## . #.#####.#.# +# .# ####. . # # . # # +# # . # # . .#### #. # +#.#.#####.# . ## . # # . # # +# # . #.. . # #### # # +# # ###.### .## . . . # +# # # . . . # . # # +#a# ###.# . ##### # ### # # # +#b .. .# . . # +################################ +""" + +# names = ["custom_layout"] +# layouts = [parse_layout(custom_layout)] + +names = ["dead_ends_normal_723"] +layouts = [parse_layout(get_layout_by_name("dead_ends_normal_723"))] + +for name, layout in track(zip(names, layouts), description="Processing..."): + obj = dict() + print() + + obj["name"] = name + obj["shape"] = (width, height) = shape = layout["shape"] + obj["food"] = len(layout["food"]) // 2 + + walls = layout["walls"] + + graph = walls_to_graph(walls, shape) + + deadends = find_dead_ends(graph) + obj["deadends"] = n_deadends = len(deadends) // 2 + + cuts = find_cuts_faster(graph) + chambers = find_chambers(graph, cuts, deadends, shape) + obj["chambers"] = n_chambers = len(chambers) // 2 + pprint(chambers) + + chamber_tiles = [] + for chamber in chambers: + chamber_tiles.extend(chamber) + + obj["chamber_size"] = len(chamber_tiles) // 2 + chambers_to_food(get_layout_by_name(name), chamber_tiles, f"/tmp/{name}.layout") + # chambers_to_food(custom_layout, chamber_tiles, f"/tmp/{name}.layout") + + pprint(obj) + + if n_chambers == 0: + assert n_deadends == 0 + + objs.append(obj) + +options = jsbeautifier.default_options() +options.indent_size = 2 +with open("db.json", "wt") as db: + db.write(jsbeautifier.beautify(json.dumps(objs), options)) From 020544e6877596c07c5c5ec2438c8821cb31c1b0 Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Thu, 6 Mar 2025 22:56:52 +0100 Subject: [PATCH 2/4] Finish database creation with chamber painting --- pelita/create_db.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/pelita/create_db.py b/pelita/create_db.py index a677ab790..c6ff94376 100644 --- a/pelita/create_db.py +++ b/pelita/create_db.py @@ -99,6 +99,26 @@ def find_chambers(graph, cuts, deadends, shape): return chambers +def paint_chambers(graph, cuts, shape): + w, h = shape + chamber_tiles = set() + for cut in cuts: + G = graph.copy() + G.remove_node(cut) + + # remove main chamber + for chamber in nx.connected_components(G): + max_x = max(chamber, key=lambda n: n[0])[0] + min_x = min(chamber, key=lambda n: n[0])[0] + if not (min_x < w // 2 and max_x >= w // 2): + chamber_tiles.update(set(chamber)) + + subgraph = graph.subgraph(chamber_tiles) + chambers = list(nx.connected_components(subgraph)) + + return chambers, chamber_tiles + + def chambers_to_food( layout: str, chambers: list[tuple[int, int]], @@ -159,8 +179,8 @@ def find_dead_ends(graph): # names = ["custom_layout"] # layouts = [parse_layout(custom_layout)] -names = ["dead_ends_normal_723"] -layouts = [parse_layout(get_layout_by_name("dead_ends_normal_723"))] +# names = ["normal_079"] +# layouts = [parse_layout(get_layout_by_name("normal_079"))] for name, layout in track(zip(names, layouts), description="Processing..."): obj = dict() @@ -178,13 +198,10 @@ def find_dead_ends(graph): obj["deadends"] = n_deadends = len(deadends) // 2 cuts = find_cuts_faster(graph) - chambers = find_chambers(graph, cuts, deadends, shape) - obj["chambers"] = n_chambers = len(chambers) // 2 + pprint(cuts) + chambers, chamber_tiles = paint_chambers(graph, cuts, shape) pprint(chambers) - - chamber_tiles = [] - for chamber in chambers: - chamber_tiles.extend(chamber) + obj["chambers"] = n_chambers = len(chambers) // 2 obj["chamber_size"] = len(chamber_tiles) // 2 chambers_to_food(get_layout_by_name(name), chamber_tiles, f"/tmp/{name}.layout") From fd2b676d1250845da1d921278f56e68ecc439150 Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Fri, 7 Mar 2025 00:08:54 +0100 Subject: [PATCH 3/4] Rework functions after profiling --- pelita/create_db.py | 56 ++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/pelita/create_db.py b/pelita/create_db.py index c6ff94376..53a57ef22 100644 --- a/pelita/create_db.py +++ b/pelita/create_db.py @@ -1,9 +1,8 @@ import json +from itertools import product import jsbeautifier import networkx as nx -from rich.pretty import pprint -from rich.progress import track from pelita.layout import get_available_layouts, get_layout_by_name, parse_layout from pelita.maze_generator import find_chamber @@ -18,22 +17,27 @@ def position_in_maze(pos, shape): def walls_to_graph(walls, shape): w, h = shape - directions = [(1, 0), (0, 1), (-1, 0), (0, -1)] + directions = [(1, 0), (0, 1), (0, -1)] graph = nx.Graph() # define nodes for maze - for x in range(w): - for y in range(h): - if (x, y) not in walls: - # this is a free position, get its neighbors - for delta_x, delta_y in directions: - neighbor = (x + delta_x, y + delta_y) - # we don't need to check for getting neighbors out of the maze - # because our mazes are all surrounded by walls, i.e. our - # deltas will not put us out of the maze - if neighbor not in walls and position_in_maze(neighbor, shape): - # this is a genuine neighbor, add an edge in the graph - graph.add_edge((x, y), neighbor) + + coords = product(range(w), range(h)) + not_walls = set(coords) - set(walls) + + edges = [] + for x, y in not_walls: + # this is a free position, get its neighbors + for delta_x, delta_y in directions: + neighbor = (x + delta_x, y + delta_y) + # we don't need to check for getting neighbors out of the maze + # because our mazes are all surrounded by walls, i.e. our + # deltas will not put us out of the maze + if neighbor not in walls and position_in_maze(neighbor, shape): + # this is a genuine neighbor, add an edge in the graph + edges.append(((x, y), neighbor)) + + graph.add_edges_from(edges) return graph @@ -102,8 +106,10 @@ def find_chambers(graph, cuts, deadends, shape): def paint_chambers(graph, cuts, shape): w, h = shape chamber_tiles = set() + G = graph + for cut in cuts: - G = graph.copy() + edges = list(G.edges(cut)) G.remove_node(cut) # remove main chamber @@ -112,6 +118,8 @@ def paint_chambers(graph, cuts, shape): min_x = min(chamber, key=lambda n: n[0])[0] if not (min_x < w // 2 and max_x >= w // 2): chamber_tiles.update(set(chamber)) + G.add_node(cut) + G.add_edges_from(edges) subgraph = graph.subgraph(chamber_tiles) chambers = list(nx.connected_components(subgraph)) @@ -146,9 +154,9 @@ def find_dead_ends(graph): names = [] for size, deadend in [ - ("small", False), - ("small", True), - ("normal", False), + # ("small", False), + # ("small", True), + # ("normal", False), ("normal", True), ]: names.extend(get_available_layouts(size=size, dead_ends=deadend)) @@ -182,9 +190,9 @@ def find_dead_ends(graph): # names = ["normal_079"] # layouts = [parse_layout(get_layout_by_name("normal_079"))] -for name, layout in track(zip(names, layouts), description="Processing..."): +for s, (name, layout) in enumerate(zip(names, layouts)): + print(s) obj = dict() - print() obj["name"] = name obj["shape"] = (width, height) = shape = layout["shape"] @@ -198,17 +206,13 @@ def find_dead_ends(graph): obj["deadends"] = n_deadends = len(deadends) // 2 cuts = find_cuts_faster(graph) - pprint(cuts) chambers, chamber_tiles = paint_chambers(graph, cuts, shape) - pprint(chambers) obj["chambers"] = n_chambers = len(chambers) // 2 obj["chamber_size"] = len(chamber_tiles) // 2 - chambers_to_food(get_layout_by_name(name), chamber_tiles, f"/tmp/{name}.layout") + # chambers_to_food(get_layout_by_name(name), chamber_tiles, f"/tmp/{name}.layout") # chambers_to_food(custom_layout, chamber_tiles, f"/tmp/{name}.layout") - pprint(obj) - if n_chambers == 0: assert n_deadends == 0 From db3c4ea335680036d128a47ba1c9e19e3938ce10 Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Fri, 7 Mar 2025 13:13:29 +0100 Subject: [PATCH 4/4] Slim down walls_to_graph --- pelita/create_db.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/pelita/create_db.py b/pelita/create_db.py index 53a57ef22..c885d132a 100644 --- a/pelita/create_db.py +++ b/pelita/create_db.py @@ -16,28 +16,8 @@ def position_in_maze(pos, shape): def walls_to_graph(walls, shape): - w, h = shape - directions = [(1, 0), (0, 1), (0, -1)] - - graph = nx.Graph() - # define nodes for maze - - coords = product(range(w), range(h)) - not_walls = set(coords) - set(walls) - - edges = [] - for x, y in not_walls: - # this is a free position, get its neighbors - for delta_x, delta_y in directions: - neighbor = (x + delta_x, y + delta_y) - # we don't need to check for getting neighbors out of the maze - # because our mazes are all surrounded by walls, i.e. our - # deltas will not put us out of the maze - if neighbor not in walls and position_in_maze(neighbor, shape): - # this is a genuine neighbor, add an edge in the graph - edges.append(((x, y), neighbor)) - - graph.add_edges_from(edges) + graph = nx.grid_2d_graph(*shape) + graph.remove_nodes_from(walls) return graph