init-commit

This commit is contained in:
lilinyang 2025-05-23 15:27:15 +08:00
commit 18a552597a
3461 changed files with 1150579 additions and 0 deletions

View file

@ -0,0 +1,198 @@
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))

View file

@ -0,0 +1,251 @@
from collections import deque
def solve_arrow_maze(grid, start=(0, 0)):
"""
Solve the arrow maze puzzle where each cell has an arrow (, , , , , , , ),
or the '' end symbol. From each cell, you can move 1 or more steps in that cell's
arrow direction (if it's not the '' cell).
Parameters
----------
grid : list[list[str]]
2D list of strings. Each element is one of:
'', '', '', '', '', '', '', '', or '' (end).
start : (int, int)
(row, column) index of the starting cell.
Returns
-------
str
A string with the answer in the format:
"[[r1, r2, r3, ...]]"
Where each row is space-separated and rows are separated by commas.
- The position of each cell on the path that is an inflection point
(including start, any direction change, and the end) is labeled
in ascending order of encountering the direction changes.
- Cells not on the path are labeled '0'.
"""
# Map from arrow symbol to row, col deltas
DIRECTIONS = {
'': (-1, 0),
'': ( 1, 0),
'': ( 0, -1),
'': ( 0, 1),
'': (-1, -1),
'': (-1, 1),
'': ( 1, -1),
'': ( 1, 1),
}
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
if rows == 0 or cols == 0:
raise ValueError("Grid must not be empty.")
# Locate the end cell (just for validation); not strictly needed,
# but good to confirm the puzzle has a valid end.
end_cell = None
for r in range(rows):
for c in range(cols):
if grid[r][c] == '':
end_cell = (r, c)
break
if end_cell is not None:
break
if not end_cell:
raise ValueError("No '' (end) cell found in the grid.")
# Check start in bounds
if not (0 <= start[0] < rows and 0 <= start[1] < cols):
raise ValueError("Start position is out of grid bounds.")
# BFS to find any path from start to end
queue = deque()
queue.append(start)
visited = set()
visited.add(start)
history = []
# Parent dict for reconstructing path: parent[(r, c)] = (pr, pc)
parent = {}
# Helper to check in-bounds
def in_bounds(r, c):
return 0 <= r < rows and 0 <= c < cols
found_end = False
while queue and not found_end:
if found_end == False:
candidates_needed2explore = ""
for item in queue:
r,c = item
candidates_needed2explore = candidates_needed2explore + f"({r},{c}),"
history.append("The candidate positions need to explore are: "+candidates_needed2explore +"\n")
r, c = queue.popleft()
if found_end == False:
history.append(f"select position ({r},{c}) '{grid[r][c]}' to explore.\n")
if grid[r][c] == '':
# Already at end
found_end = True
break
# Current cell arrow
if grid[r][c] not in DIRECTIONS:
# If it's an invalid symbol (not arrow, not '○'), skip
raise ValueError
dr, dc = DIRECTIONS[grid[r][c]]
# Try moving k steps in that direction
step = 1
while True:
nr = r + step * dr
nc = c + step * dc
if not in_bounds(nr, nc):
history.append(f"Chose step={step}. Position ({nr},{nc}) is out of the bounds. So let's explore next node.\n")
# Out of bounds or invalid => stop exploring further steps
break
if (nr, nc) not in visited:
history.append(f"Chose step={step}. Add position ({nr},{nc}) to candidates.")
visited.add((nr, nc))
parent[(nr, nc)] = (r, c)
queue.append((nr, nc))
if grid[nr][nc] == '':
#history.append((nr, nc, f"checking ({nr},{nc}) and ({nr},{nc}) is the end point"))
history[-1] = history[-1]+ f"Check position ({nr},{nc}). Position ({nr},{nc}) is the end point!\n"
found_end = True
break
else:
#history.append((nr, nc, f"check ({nr},{nc}) and ({nr},{nc}) not the end point"))
history[-1] = history[-1]+ f"Check position ({nr},{nc}). Position ({nr},{nc}) is not the end point.\n"
else:
history.append(f"Chose step={step}. Position ({nr},{nc}) has been explored. So skip the position ({nr},{nc}).\n")
step += 1
if found_end:
break
# If we never found the end, puzzle is unsolvable from this start
if not found_end:
history.append(f"Fail! Candidates are empty! We explore all posibility but can't find the solution. So No path can be found to the end cell ''.")
#raise ValueError("No path found to the end cell '○'.")
return False, history
else:
history.append(f"Find the solution! Now backtrack the full path.\n")
# Reconstruct path from the end to the start
path = []
cur = end_cell
while cur in parent or cur == start:
path.append(cur)
if cur == start:
history.append("Back to the start node. Here is the final answer:\n")
break
cur_for_history = cur
cur = parent[cur]
history.append(f"Track ({cur_for_history[0]},{cur_for_history[1]}) and find the parent node is ({cur[0]},{cur[1]}).\n")
path.reverse() # Now it goes from start -> end
# Prepare the result grid (same dimensions), fill with 0
result = [[0]*cols for _ in range(rows)]
# Label inflection points:
# - Start cell gets the first label (1).
# - Every time the direction changes from one cell to the next, we increment the label.
# - End cell gets labeled last.
inflection_label = 0
prev_dir = None
for i, (r, c) in enumerate(path):
symbol = grid[r][c]
# For the end cell '○', we treat it as a different "direction" to ensure it is labeled
current_dir = symbol if symbol in DIRECTIONS else ''
if current_dir != prev_dir:
inflection_label += 1
result[r][c] = inflection_label
prev_dir = current_dir
else:
# It's on the path, but not a new inflection => could set to something else if needed
# The puzzle examples do NOT label those. We leave them as 0 in the final output
pass
# If you do *not* want the end cell labeled, remove the above logic that sets it.
# Convert result 2D array into the puzzle's required string format
# "[[r1, r2, ...], [r1, r2, ...], ...]"
# However, from the examples, the format seems to be:
# "[[1 0 2,0 0 0,0 0 3]]"
# i.e. each row is "space-separated" and the rows are separated by commas.
rows_strings = []
for rr in range(rows):
row_str = " ".join(str(result[rr][cc]) for cc in range(cols))
rows_strings.append(row_str)
final_answer = "[[" + ",".join(rows_strings) + "]]"
history.append(final_answer)
history = ''.join(history)
return result, history
# ------------------------------------------------------------------------------
# Example usage:
if __name__ == "__main__":
# Example 1:
grid1 = [
['', '', ''],
['', '', ''],
['', '', ''],
]
ans1, history = solve_arrow_maze(grid1, start=(0, 0))
print("Example 1 Answer:", ans1)
print(history)
# Expect something like "[[1 0 2,0 0 0,0 0 3]]"
# depending on how you label inflections
# Example 2:
grid2 = [
['', '', ''],
['', '', ''],
['', '', ''],
]
ans2, history = solve_arrow_maze(grid2, start=(0, 0))
print("Example 2 Answer:", ans2)
print(history)
# Expect something like "[[1 0 0,0 0 0,0 0 2]]"
# Example 2:
grid3 = [
['', '', ''],
['', '', ''],
['', '', ''],
]
ans3, history = solve_arrow_maze(grid3, start=(0, 0))
print("Example 3 Answer:", ans3)
print(history)
# Expect something like "[[1 0 0,0 0 0,0 0 2]]"
grid3 = [
['', '', '',""],
['', '', '',""],
['', '', '',""],
]
ans3, history = solve_arrow_maze(grid3, start=(0, 0))
print("Example 3 Answer:", ans3)
print(history)
# Expect something like "[[1 0 0,0 0 0,0 0 2]]"

View file

@ -0,0 +1,218 @@
import re
def parse_candidate_path(answer_str):
"""
Parse a candidate_path string in the format:
"[[1 0 0,0 0 0,0 0 2]]"
into a 2D list of integers.
"""
# 1) Remove the outer brackets "[[" and "]]"
# 2) Split into rows by comma
# 3) Each row is space-separated integers
# Example input: "[[1 0 0,0 0 0,0 0 2]]"
# Trim leading/trailing brackets
trimmed = answer_str.strip()
if trimmed.startswith("[["):
trimmed = trimmed[2:]
if trimmed.endswith("]]"):
trimmed = trimmed[:-2]
# Now we have something like: "1 0 0,0 0 0,0 0 2"
# Split by commas to get rows
row_strs = trimmed.split(",")
grid_of_ints = []
for row_str in row_strs:
# row_str might look like "1 0 0" or "0 0 2"
row_str = row_str.strip()
if not row_str:
continue
# split by spaces
vals = row_str.split()
row_ints = [int(v) for v in vals]
grid_of_ints.append(row_ints)
return grid_of_ints
def arrow_maze_validator(
grid, start_position, answer
):
"""
Validate whether a candidate_path in puzzle's format (e.g. "[[1 0 0,0 0 0,0 0 2]]")
is a correct solution to the arrow maze.
Parameters
----------
grid : list[list[str]]
A 2D grid of arrow symbols or ''.
Example:
[
['', '', ''],
['', '', ''],
['', '', ''],
]
start_position : (int, int)
(row, col) of the starting cell.
answer : list
The proposed solution in the format "[[...]]"
0 => not on path
1 => first visited cell
2 => second visited cell
etc.
Returns
-------
bool
True if the path is valid, False otherwise.
"""
# Directions dictionary: maps arrow symbol -> (dr, dc)
DIRECTIONS = {
'': (-1, 0),
'': ( 1, 0),
'': ( 0, -1),
'': ( 0, 1),
'': (-1, -1),
'': (-1, 1),
'': ( 1, -1),
'': ( 1, 1),
}
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
def in_bounds(r, c):
return 0 <= r < rows and 0 <= c < cols
#$candidate_grid = parse_candidate_path(answer_str)
candidate_grid = answer
# Sanity check: the candidate_grid should match the same dimensions as 'grid'
if len(candidate_grid) != rows:
return False
for row_vals in candidate_grid:
if len(row_vals) != cols:
return False
# 2. Extract the labeled cells: (label, (row, col))
# We only care about label > 0
labeled_cells = []
for r in range(rows):
for c in range(cols):
label = candidate_grid[r][c]
if label > 0:
labeled_cells.append((label, (r, c)))
# If no labeled cells, invalid
if not labeled_cells:
return False
# 3. Sort by label ascending
labeled_cells.sort(key=lambda x: x[0]) # sort by label number
# This gives us an ordered path: [ (1, (r1,c1)), (2, (r2,c2)), ... ]
# 4. The path in terms of coordinates:
path = [cell_coord for _, cell_coord in labeled_cells]
# 5. Check that label "1" is at start_position
if path[0] != start_position:
return False
# 6. Validate each consecutive step in path
for i in range(len(path) - 1):
(r1, c1) = path[i]
(r2, c2) = path[i + 1]
if not in_bounds(r1, c1) or not in_bounds(r2, c2):
return False
# If the current cell is the end symbol '○' but we still have more steps, invalid
if grid[r1][c1] == '':
return False
# Arrow in the current cell:
arrow_symbol = grid[r1][c1]
if arrow_symbol not in DIRECTIONS:
return False # not an arrow and not the end symbol
(dr, dc) = DIRECTIONS[arrow_symbol]
delta_r = r2 - r1
delta_c = c2 - c1
# Must move in a positive integer multiple of (dr, dc).
if dr == 0 and dc == 0:
return False # shouldn't happen with valid arrows
# Horizontal or vertical
if dr == 0:
# vertical movement is zero => must move horizontally
# check we didn't move in row, must move in col
if delta_r != 0:
return False
# direction must match sign of dc
if dc > 0 and delta_c <= 0:
return False
if dc < 0 and delta_c >= 0:
return False
elif dc == 0:
# horizontal movement is zero => must move in row
if delta_c != 0:
return False
if dr > 0 and delta_r <= 0:
return False
if dr < 0 and delta_r >= 0:
return False
else:
# diagonal
if delta_r == 0 or delta_c == 0:
return False # can't be diagonal if one is zero
if (dr > 0 and delta_r <= 0) or (dr < 0 and delta_r >= 0):
return False
if (dc > 0 and delta_c <= 0) or (dc < 0 and delta_c >= 0):
return False
# check integer multiples
if (delta_r % dr) != 0 or (delta_c % dc) != 0:
return False
factor_r = delta_r // dr
factor_c = delta_c // dc
if factor_r != factor_c or factor_r <= 0:
return False
# 7. Check last labeled cell is the '○' cell
last_r, last_c = path[-1]
if not in_bounds(last_r, last_c):
return False
if grid[last_r][last_c] != '':
return False
# If all checks pass, it's a valid solution
return True
# ------------------------------
# Example usage:
if __name__ == "__main__":
# A small 3×3 arrow maze with start=(0, 0):
# Grid (3x3):
# → ↙ ↓
# ↖ ↓ ↙
# ↑ ← ○
grid = [
['', '', ''],
['', '', ''],
['', '', ''],
]
start_position = (0, 0)
# Example candidate_path string:
# "[[1 0 0,0 0 0,0 0 2]]"
# This claims:
# row=0 col=0 => 1 (start)
# row=2 col=2 => 2 (end)
candidate_path_str = "[[1 0 2,0 0 0,0 0 3]]"
is_valid = arrow_maze_validator(
grid, start_position, candidate_path_str
)
print("Is the candidate path valid?", is_valid)