Skip to content

Commit d2c6f14

Browse files
authored
Merge pull request #944 from jbdyn/make-maze-generation-iterative
Make Maze Generation Iterative
2 parents 13646e4 + 10fca05 commit d2c6f14

1 file changed

Lines changed: 120 additions & 82 deletions

File tree

pelita/maze_generator.py

Lines changed: 120 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -121,91 +121,129 @@ def distribute_food(all_tiles, chamber_tiles, trapped_food, total_food, rng=None
121121
def add_wall_and_split(partition, walls, ngaps, vertical, rng=None):
122122
rng = default_rng(rng)
123123

124-
(xmin, ymin), (xmax, ymax) = partition
125-
126-
# the size of the maze partition we work on
127-
width = xmax - xmin + 1
128-
height = ymax - ymin + 1
129-
130-
# if the partition is too small, stop
131-
if height < 3 and width < 3:
132-
return walls
133-
134-
# insert a wall only if there is some space in the around it in the
135-
# orthogonal direction, i.e.:
136-
# if the wall is vertical, then the relevant length is the width
137-
# if the wall is horizontal, then the relevant length is the height
138-
partition_length = width if vertical else height
139-
if partition_length < rng.randint(3, 5):
140-
return walls
141-
142-
# the raw/column to put the horizontal/vertical wall on
143-
# the position is calculated starting from the left/top of the maze partition
144-
# and then a random offset is added -> the resulting raw/column must not
145-
# exceed the available length
146-
pos = xmin if vertical else ymin
147-
pos += rng.randint(1, partition_length - 2)
148-
149-
# the maximum length of the wall is the space we have in the same direction
150-
# of the wall in the partition, i.e.
151-
# if the wall is vertical, the maximum length is the height
152-
# if the wall is horizontal, the maximum length is the width
153-
max_length = height if vertical else width
154-
155-
# We can start with a full wall, but we want to make sure that we do not
156-
# block the entrances to this partition. The entrances are
157-
# - the tile before the beginning of this wall [entrance] and
158-
# - the tile after the end of this wall [exit]
159-
# if entrance or exit are _not_ walls, then the wall must leave the neighboring
160-
# tiles also empty, i.e. the wall must be shortened accordingly
161-
if vertical:
162-
entrance_before = (pos, ymin - 1)
163-
entrance_after = (pos, ymin + max_length)
164-
begin = 0 if entrance_before in walls else 1
165-
end = max_length if entrance_after in walls else max_length-1
166-
wall = {(pos, ymin+y) for y in range(begin, end)}
167-
else:
168-
entrance_before = (xmin - 1, pos)
169-
entrance_after = (xmin + max_length, pos)
170-
begin = 0 if entrance_before in walls else 1
171-
end = max_length if entrance_after in walls else max_length-1
172-
wall = {(xmin+x, pos) for x in range(begin, end)}
173-
174-
# place the requested number of gaps in the otherwise full wall
175-
# these gaps are indices in the direction of the wall, i.e.
176-
# x if horizontal and y if vertical
177-
# TODO: when we drop compatibility with numpy, this can be more easily done
178-
# by just sampling ngaps out of the full wall set, i.e.
179-
# gaps = rng.sample(wall, k=ngaps)
180-
# for gap in gaps:
181-
# wall.remove(gap)
182-
ngaps = max(1, ngaps)
183-
wall_pos = list(range(max_length))
184-
rng.shuffle(wall_pos)
185-
186-
for gap in wall_pos[:ngaps]:
187-
if vertical:
188-
wall.discard((pos, ymin+gap))
189-
else:
190-
wall.discard((xmin+gap, pos))
124+
# store partitions in an expanding list
125+
# alongside the number of gaps in wall and its orientation
126+
partitions = [partition + (ngaps, vertical)]
127+
128+
# partition index
129+
p = 0
130+
131+
# The infinite loop is always exiting, since the position of the walls
132+
# `pos` is in `[xmin + 1, xmax - (xmin + 1)]` or
133+
# in `[ymin + 1, ymax - (ymin + 1)]`, respectively, and thus always
134+
# yielding partitions smaller than the current partition.
135+
# The checks for `height < 3`, `width < 3` and
136+
# `partition_length < rng.randint(3, 5)` ensure no further addition of
137+
# partitions, and `p += 1` in those checks and after partitioning ensure
138+
# that we always advance in the list of partitions.
139+
#
140+
# So, partitions always shrink, no new partitions are added once they
141+
# shrank below a threshold, and the loop increases the list index in
142+
# every case.
143+
while True:
144+
# get the next partition of any is available
145+
try:
146+
partition = partitions[p]
147+
except IndexError:
148+
break
149+
150+
(xmin, ymin), (xmax, ymax), ngaps, vertical = partition
151+
152+
# the size of the maze partition we work on
153+
width = xmax - xmin + 1
154+
height = ymax - ymin + 1
155+
156+
# if the partition is too small, move on with the next one
157+
if height < 3 and width < 3:
158+
p += 1
159+
continue
191160

192-
# collect this wall into the global wall set
193-
walls |= wall
161+
# insert a wall only if there is some space in the around it in the
162+
# orthogonal direction, i.e.:
163+
# if the wall is vertical, then the relevant length is the width
164+
# if the wall is horizontal, then the relevant length is the height,
165+
# otherwise move on with the next one
166+
partition_length = width if vertical else height
167+
if partition_length < rng.randint(3, 5):
168+
p += 1
169+
continue
194170

195-
# define the two new partitions of the maze generated by this wall
196-
# these are the parts of the maze to the left/right of a vertical wall
197-
# or the top/bottom of a horizontal wall
198-
if vertical:
199-
partitions = [((xmin, ymin), (pos-1, ymax)),
200-
((pos+1, ymin), (xmax, ymax))]
201-
else:
202-
partitions = [((xmin, ymin), (xmax, pos-1)),
203-
((xmin, pos+1), (xmax, ymax))]
171+
# the row/column to put the horizontal/vertical wall on
172+
# the position is calculated starting from the left/top of the maze partition
173+
# and then a random offset is added -> the resulting raw/column must not
174+
# exceed the available length
175+
pos = xmin if vertical else ymin
176+
pos += rng.randint(1, partition_length - 2)
177+
178+
# the maximum length of the wall is the space we have in the same direction
179+
# of the wall in the partition, i.e.
180+
# if the wall is vertical, the maximum length is the height
181+
# if the wall is horizontal, the maximum length is the width
182+
max_length = height if vertical else width
183+
184+
# We can start with a full wall, but we want to make sure that we do not
185+
# block the entrances to this partition. The entrances are
186+
# - the tile before the beginning of this wall [entrance] and
187+
# - the tile after the end of this wall [exit]
188+
# if entrance or exit are _not_ walls, then the wall must leave the neighboring
189+
# tiles also empty, i.e. the wall must be shortened accordingly
190+
if vertical:
191+
entrance_before = (pos, ymin - 1)
192+
entrance_after = (pos, ymin + max_length)
193+
begin = 0 if entrance_before in walls else 1
194+
end = max_length if entrance_after in walls else max_length - 1
195+
wall = {(pos, ymin + y) for y in range(begin, end)}
196+
else:
197+
entrance_before = (xmin - 1, pos)
198+
entrance_after = (xmin + max_length, pos)
199+
begin = 0 if entrance_before in walls else 1
200+
end = max_length if entrance_after in walls else max_length - 1
201+
wall = {(xmin + x, pos) for x in range(begin, end)}
202+
203+
# place the requested number of gaps in the otherwise full wall
204+
# these gaps are indices in the direction of the wall, i.e.
205+
# x if horizontal and y if vertical
206+
# TODO: when we drop compatibility with numpy, this can be more easily done
207+
# by just sampling ngaps out of the full wall set, i.e.
208+
# gaps = rng.sample(wall, k=ngaps)
209+
# for gap in gaps:
210+
# wall.remove(gap)
211+
ngaps = max(1, ngaps)
212+
wall_pos = list(range(max_length))
213+
rng.shuffle(wall_pos)
214+
215+
for gap in wall_pos[:ngaps]:
216+
if vertical:
217+
wall.discard((pos, ymin + gap))
218+
else:
219+
wall.discard((xmin + gap, pos))
220+
221+
# collect this wall into the global wall set
222+
walls |= wall
223+
224+
# define the two new partitions of the maze generated by this wall
225+
# these are the parts of the maze to the left/right of a vertical wall
226+
# or the top/bottom of a horizontal wall
227+
ngaps = max(1, ngaps // 2)
204228

205-
for partition in partitions:
206-
walls |= add_wall_and_split(
207-
partition, walls, max(1, ngaps // 2), not vertical, rng=rng
208-
)
229+
if vertical:
230+
new = [
231+
((xmin, ymin), (pos - 1, ymax), ngaps, not vertical),
232+
((pos + 1, ymin), (xmax, ymax), ngaps, not vertical),
233+
]
234+
else:
235+
new = [
236+
((xmin, ymin), (xmax, pos - 1), ngaps, not vertical),
237+
((xmin, pos + 1), (xmax, ymax), ngaps, not vertical),
238+
]
239+
240+
# queue the new partitions next;
241+
# ensures maze stability
242+
partitions.insert(p + 1, new[1])
243+
partitions.insert(p + 1, new[0])
244+
245+
# increase the partition index
246+
p += 1
209247

210248
return walls
211249

0 commit comments

Comments
 (0)