Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Optimize Maze Generation #856

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 43 additions & 46 deletions pelita/maze_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def walls_to_graph(maze):
"""

h, w = maze.shape
directions = [west, east, north, south]
directions = [east, south]

graph = nx.Graph()
# define nodes for maze
Expand Down Expand Up @@ -260,36 +260,34 @@ def remove_all_dead_ends(maze):
dead_ends = find_dead_ends(maze_graph, width)
if len(dead_ends) == 0:
break

remove_dead_end(dead_ends[0], maze)

def find_chamber(graph):
"""Detect chambers (rooms with a single square entrance).

Return (entrance, chamber), where `entrance` is the node representing the
entrance to the chamber (None if no chamber is found), and `chamber` is the
list of nodes within the chamber (empty list if no nodes are in the chamber).

The entrance to a chamber is a node that when removed from the graph
will result in the graph to be split into two disconnected graphs."""
# minimum_node_cut returns a set of nodes of minimum cardinality that
# disconnects the graph. This means that we have a chamber if the length
# of this set is one, i.e. there is one node that when removed disconnects
# the graph
cuts = nx.minimum_node_cut(graph)
if len(cuts) > 1:
# no chambers, yeah!
return None, []
entrance = cuts.pop()
# remove the cut, i.e. put a wall on the entrance
lgraph = nx.restricted_view(graph, [entrance],[])
# now get the resulting subgraphs
subgraphs = sorted(nx.connected_components(lgraph), key=len)
# let's get the smallest subgraph: this is going to be a chamber
# (other subgraphs are other chambers (if any) and the 'rest' of the graph
# return a list of nodes, instead of a set
chamber = list(subgraphs[0])
return entrance, chamber

def find_chambers(graph, shape):
cuts = set(nx.articulation_points(graph))

h, w = shape
chamber_tiles = set()
G = graph

for cut in cuts:
edges = list(G.edges(cut))
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))
G.add_node(cut)
G.add_edges_from(edges)

subgraph = graph.subgraph(chamber_tiles)
chambers = list(nx.connected_components(subgraph))

return chambers, chamber_tiles


def get_neighboring_walls(maze, locs):
"""Given a list of coordinates in the maze, return all neighboring walls.
Expand Down Expand Up @@ -323,24 +321,23 @@ def get_neighboring_walls(maze, locs):
def remove_all_chambers(maze, rng=None):
rng = default_rng(rng)

maze_graph = walls_to_graph(maze)
# this will find one of the chambers, if there is any
entrance, chamber = find_chamber(maze_graph)
while entrance is not None:
# get all the walls around the chamber
walls = get_neighboring_walls(maze, chamber)
# choose a wall at random among the neighboring one and get rid of it
bad_wall = rng.choice(walls)
maze[bad_wall[1], bad_wall[0]] = E
# we may have opened a door into this chamber, but there may be more
# chambers to get rid of. Or, the wall we picked wasn't good enough and
# didn't really open a new door to the chamber. I have no idea how to
# distinguish this two cases. If we knew how to, we would spare quite
# a few iterations here?
# Well, as long as we keep on doing this we will eventually get rid
# of all the chambers
while True:
maze_graph = walls_to_graph(maze)
entrance, chamber = find_chamber(maze_graph)
# this will find one of the chambers, if there is any
# entrance, chamber = find_chamber(maze_graph)
chambers, chamber_tiles = find_chambers(maze_graph, maze.shape)

if not chambers:
break

for chamber in chambers:
# get all the walls around the chamber
walls = get_neighboring_walls(maze, chamber)

# choose a wall at random among the neighboring one and get rid of it
if walls:
bad_wall = rng.choice(walls)
maze[bad_wall[1], bad_wall[0]] = E


def add_food(maze, max_food, rng=None):
Expand Down
29 changes: 12 additions & 17 deletions test/test_maze_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,13 @@ def test_remove_multiple_dead_ends():
expected_maze = mg.str_to_maze(expected_maze)
assert np.all(maze == expected_maze)

def test_find_chamber():
graph = mg.walls_to_graph(maze)
width = maze.shape[1]
dead_ends = mg.find_dead_ends(graph, width)
assert len(dead_ends) == 0


def test_find_chambers():
# This maze has one single chamber, whose entrance is one of the
# nodes (1,2), (1,3) or (1,4)
maze_chamber = """############
Expand All @@ -204,28 +210,17 @@ def test_find_chamber():
# now check that we detect it
graph = mg.walls_to_graph(maze)
# there are actually two nodes that can be considered entrances
entrance, chamber = mg.find_chamber(graph)
assert entrance in ((1,2), (1,3), (1,4))
# check that the chamber contains the right nodes. Convert to set, because
# the order is irrelevant
if entrance == (1,4):
expected_chamber = {(1,1), (1,2), (1,3), (2,1), (2,2), (3,1), (3,2)}
elif entrance == (1,3):
expected_chamber = {(1,1), (1,2), (2,1), (2,2), (3,1), (3,2)}
else:
expected_chamber = {(1,1), (2,1), (2,2), (3,1), (3,2)}
assert set(chamber) == expected_chamber
chambers, chamber_tiles = mg.find_chambers(graph, maze_orig.shape)
assert len(chambers) == 1
assert chambers[0] == {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (3, 1), (3, 2)}

# now remove the chamber and verify that we don't detect anything
# we just remove wall (4,1) manually
maze = mg.str_to_maze(maze_chamber)
maze[1,4] = mg.E # REMEMBER! Indexing is maze[y,x]!!!
graph = mg.walls_to_graph(maze)
entrance, chamber = mg.find_chamber(graph)
assert entrance is None
assert chamber == []


chambers, chamber_tiles = mg.find_chambers(graph, maze_orig.shape)
assert chambers == []


maze_one_chamber = """############
Expand Down