InternBootcamp/internbootcamp/libs/arrowmaze/maze_generator.py
2025-05-23 15:27:15 +08:00

198 lines
No EOL
6.7 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import random
ARROW_SYMBOLS = {
(-1, 0): '',
( 1, 0): '',
( 0, -1): '',
( 0, 1): '',
(-1, -1): '',
(-1, 1): '',
( 1, -1): '',
( 1, 1): '',
}
# Inverse mapping from symbol back to (dr, dc) if needed:
# REVERSE_ARROW = {v: k for k, v in ARROW_SYMBOLS.items()}
def generate_arrow_maze(n, m, start, end, max_attempts=10000, max_solution_step=20, seed=None):
"""
Generate an n×m arrow maze with a guaranteed path from `start` to `end`.
Parameters
----------
n : int
Number of rows.
m : int
Number of columns.
start : (int, int)
(row, col) of the start cell (0-based).
end : (int, int)
(row, col) of the end cell (0-based).
max_attempts : int
Maximum attempts to try building a path in case of random backtracking failures.
Returns
-------
list[list[str]]
A 2D grid of strings. Each cell is one of the 8 arrow symbols or '' for the end cell.
Guaranteed at least one path under the "move multiple steps" rule.
Raises
------
ValueError
If a path cannot be generated within `max_attempts`.
"""
if seed != None:
random.seed(seed)
# Basic checks
if not (0 <= start[0] < n and 0 <= start[1] < m):
raise ValueError("Start position out of bounds.")
if not (0 <= end[0] < n and 0 <= end[1] < m):
raise ValueError("End position out of bounds.")
if start == end:
raise ValueError("Start and end cannot be the same cell.")
# We will first build a path (a list of cells) from start to end using random DFS-like backtracking.
# Then we will fill the "arrows" along that path to ensure a valid path.
# Finally, fill in random arrows for the other cells.
# For convenience, store the path of cells as a list of (row, col).
path = []
global now_step_number
# We will do a backtracking function. The function attempts to build a path from "current" cell to "end".
# If it succeeds, it returns True and has the path in the `path`.
# If it fails, it returns False.
def in_bounds(r, c):
return 0 <= r < n and 0 <= c < m
def backtrack(current):
"""Attempt to build a path from `current` to `end` via random expansions."""
# Add the current cell to path
global now_step_number
now_step_number += 1
path.append(current)
# If current == end, we've made the path successfully
if current == end:
return True
if now_step_number > max_solution_step:
path.pop()
now_step_number -= 1
return False
# Try random directions in a shuffled order
directions = list(ARROW_SYMBOLS.keys())
random.shuffle(directions)
# For each direction, try steps of size 1.. up to max possible in that direction
for (dr, dc) in directions:
# The maximum step we can take in this direction so we don't go out of bounds
max_step = 1
while True:
nr = current[0] + (max_step * dr)
nc = current[1] + (max_step * dc)
if not in_bounds(nr, nc):
break
max_step += 1
# Now max_step - 1 is the largest valid step
if max_step <= 1:
# We can't move in this direction at all
continue
# We can choose any step in range [1, max_step-1]
step_sizes = list(range(1, max_step))
random.shuffle(step_sizes)
for step in step_sizes:
nr = current[0] + step * dr
nc = current[1] + step * dc
# Check if the next cell is not yet in path (avoid immediate loops)
if (nr, nc) not in path:
# Recurse
if backtrack((nr, nc)):
return True
# else skip because it's already in path (avoid cycles)
# If no direction/step led to a solution, backtrack
path.pop()
now_step_number -= 1
return False
# Try multiple times to build a path (sometimes random choices fail to find a path)
attempts = 0
success = False
while attempts < max_attempts:
path.clear()
now_step_number = 0
if backtrack(start):
success = True
break
attempts += 1
if not success:
raise ValueError("Could not generate a path with random backtracking after many attempts.")
# Now `path` is our sequence of cells from start to end.
# Next we build the grid of arrows. We'll mark all cells with random arrows first, then override the path.
grid = [[None for _ in range(m)] for _ in range(n)]
# Assign random arrows to every cell initially
directions_list = list(ARROW_SYMBOLS.values())
for r in range(n):
for c in range(m):
grid[r][c] = random.choice(directions_list)
# Mark the end cell with '○'
er, ec = end
grid[er][ec] = ''
# Now override the path cells (except the end).
# If path[i] = (r1, c1) leads to path[i+1] = (r2, c2),
# we find direction = (r2-r1, c2-c1) -> arrow symbol.
# We put that symbol in grid[r1][c1]. The last cell in the path is the end cell → '○'.
for i in range(len(path) - 1):
r1, c1 = path[i]
r2, c2 = path[i+1]
dr = r2 - r1
dc = c2 - c1
symbol = ARROW_SYMBOLS.get((dr, dc), None)
if symbol is None:
# This should never happen if (dr, dc) is one of the 8 directions.
# But we might have multi-steps combined. We only store the "macro step" as if it were a single arrow.
# Because in the puzzle, moving multiple steps in the same direction is allowed in one arrow cell.
# So the direction is the *normalized* version, i.e., sign of dr/dc if non-zero.
# For example, if dr=2, dc=-2 => direction is (1, -1) or (1, -1) repeated.
# Let's define a quick normalization.
ndr = 0 if dr == 0 else (dr // abs(dr))
ndc = 0 if dc == 0 else (dc // abs(dc))
symbol = ARROW_SYMBOLS.get((ndr, ndc))
grid[r1][c1] = symbol
# print("standard path we select:",path)
return grid
# ---------------------------
# Example usage / testing code
if __name__ == "__main__":
# Example: generate a 6x8 maze, start at (0,0), end at (5,7).
# You can change these freely.
n, m = 10, 8
start = (0, 0)
end = (5, 7)
# For reproducibility, remove or change for more randomness
maze = generate_arrow_maze(n, m, start, end, max_solution_step=15, seed=0)
# Print the generated maze
for row in maze:
print(" ".join(row))