Skip to content
Merged
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
202 changes: 120 additions & 82 deletions pelita/maze_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,91 +121,129 @@ def distribute_food(all_tiles, chamber_tiles, trapped_food, total_food, rng=None
def add_wall_and_split(partition, walls, ngaps, vertical, rng=None):
rng = default_rng(rng)

(xmin, ymin), (xmax, ymax) = partition

# the size of the maze partition we work on
width = xmax - xmin + 1
height = ymax - ymin + 1

# if the partition is too small, stop
if height < 3 and width < 3:
return walls

# insert a wall only if there is some space in the around it in the
# orthogonal direction, i.e.:
# if the wall is vertical, then the relevant length is the width
# if the wall is horizontal, then the relevant length is the height
partition_length = width if vertical else height
if partition_length < rng.randint(3, 5):
return walls

# the raw/column to put the horizontal/vertical wall on
# the position is calculated starting from the left/top of the maze partition
# and then a random offset is added -> the resulting raw/column must not
# exceed the available length
pos = xmin if vertical else ymin
pos += rng.randint(1, partition_length - 2)

# the maximum length of the wall is the space we have in the same direction
# of the wall in the partition, i.e.
# if the wall is vertical, the maximum length is the height
# if the wall is horizontal, the maximum length is the width
max_length = height if vertical else width

# We can start with a full wall, but we want to make sure that we do not
# block the entrances to this partition. The entrances are
# - the tile before the beginning of this wall [entrance] and
# - the tile after the end of this wall [exit]
# if entrance or exit are _not_ walls, then the wall must leave the neighboring
# tiles also empty, i.e. the wall must be shortened accordingly
if vertical:
entrance_before = (pos, ymin - 1)
entrance_after = (pos, ymin + max_length)
begin = 0 if entrance_before in walls else 1
end = max_length if entrance_after in walls else max_length-1
wall = {(pos, ymin+y) for y in range(begin, end)}
else:
entrance_before = (xmin - 1, pos)
entrance_after = (xmin + max_length, pos)
begin = 0 if entrance_before in walls else 1
end = max_length if entrance_after in walls else max_length-1
wall = {(xmin+x, pos) for x in range(begin, end)}

# place the requested number of gaps in the otherwise full wall
# these gaps are indices in the direction of the wall, i.e.
# x if horizontal and y if vertical
# TODO: when we drop compatibility with numpy, this can be more easily done
# by just sampling ngaps out of the full wall set, i.e.
# gaps = rng.sample(wall, k=ngaps)
# for gap in gaps:
# wall.remove(gap)
ngaps = max(1, ngaps)
wall_pos = list(range(max_length))
rng.shuffle(wall_pos)

for gap in wall_pos[:ngaps]:
if vertical:
wall.discard((pos, ymin+gap))
else:
wall.discard((xmin+gap, pos))
# store partitions in an expanding list
# alongside the number of gaps in wall and its orientation
partitions = [partition + (ngaps, vertical)]

# partition index
p = 0

# The infinite loop is always exiting, since the position of the walls
# `pos` is in `[xmin + 1, xmax - (xmin + 1)]` or
# in `[ymin + 1, ymax - (ymin + 1)]`, respectively, and thus always
# yielding partitions smaller than the current partition.
# The checks for `height < 3`, `width < 3` and
# `partition_length < rng.randint(3, 5)` ensure no further addition of
# partitions, and `p += 1` in those checks and after partitioning ensure
# that we always advance in the list of partitions.
#
# So, partitions always shrink, no new partitions are added once they
# shrank below a threshold, and the loop increases the list index in
# every case.
while True:
# get the next partition of any is available
try:
partition = partitions[p]
except IndexError:
break

(xmin, ymin), (xmax, ymax), ngaps, vertical = partition

# the size of the maze partition we work on
width = xmax - xmin + 1
height = ymax - ymin + 1

# if the partition is too small, move on with the next one
if height < 3 and width < 3:
p += 1
continue

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

# define the two new partitions of the maze generated by this wall
# these are the parts of the maze to the left/right of a vertical wall
# or the top/bottom of a horizontal wall
if vertical:
partitions = [((xmin, ymin), (pos-1, ymax)),
((pos+1, ymin), (xmax, ymax))]
else:
partitions = [((xmin, ymin), (xmax, pos-1)),
((xmin, pos+1), (xmax, ymax))]
# the row/column to put the horizontal/vertical wall on
# the position is calculated starting from the left/top of the maze partition
# and then a random offset is added -> the resulting raw/column must not
# exceed the available length
pos = xmin if vertical else ymin
pos += rng.randint(1, partition_length - 2)

# the maximum length of the wall is the space we have in the same direction
# of the wall in the partition, i.e.
# if the wall is vertical, the maximum length is the height
# if the wall is horizontal, the maximum length is the width
max_length = height if vertical else width

# We can start with a full wall, but we want to make sure that we do not
# block the entrances to this partition. The entrances are
# - the tile before the beginning of this wall [entrance] and
# - the tile after the end of this wall [exit]
# if entrance or exit are _not_ walls, then the wall must leave the neighboring
# tiles also empty, i.e. the wall must be shortened accordingly
if vertical:
entrance_before = (pos, ymin - 1)
entrance_after = (pos, ymin + max_length)
begin = 0 if entrance_before in walls else 1
end = max_length if entrance_after in walls else max_length - 1
wall = {(pos, ymin + y) for y in range(begin, end)}
else:
entrance_before = (xmin - 1, pos)
entrance_after = (xmin + max_length, pos)
begin = 0 if entrance_before in walls else 1
end = max_length if entrance_after in walls else max_length - 1
wall = {(xmin + x, pos) for x in range(begin, end)}

# place the requested number of gaps in the otherwise full wall
# these gaps are indices in the direction of the wall, i.e.
# x if horizontal and y if vertical
# TODO: when we drop compatibility with numpy, this can be more easily done
# by just sampling ngaps out of the full wall set, i.e.
# gaps = rng.sample(wall, k=ngaps)
# for gap in gaps:
# wall.remove(gap)
ngaps = max(1, ngaps)
wall_pos = list(range(max_length))
rng.shuffle(wall_pos)

for gap in wall_pos[:ngaps]:
if vertical:
wall.discard((pos, ymin + gap))
else:
wall.discard((xmin + gap, pos))

# collect this wall into the global wall set
walls |= wall

# define the two new partitions of the maze generated by this wall
# these are the parts of the maze to the left/right of a vertical wall
# or the top/bottom of a horizontal wall
ngaps = max(1, ngaps // 2)

for partition in partitions:
walls |= add_wall_and_split(
partition, walls, max(1, ngaps // 2), not vertical, rng=rng
)
if vertical:
new = [
((xmin, ymin), (pos - 1, ymax), ngaps, not vertical),
((pos + 1, ymin), (xmax, ymax), ngaps, not vertical),
]
else:
new = [
((xmin, ymin), (xmax, pos - 1), ngaps, not vertical),
((xmin, pos + 1), (xmax, ymax), ngaps, not vertical),
]

# queue the new partitions next;
# ensures maze stability
partitions.insert(p + 1, new[1])
partitions.insert(p + 1, new[0])

# increase the partition index
p += 1

return walls

Expand Down
Loading