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

182 lines
No EOL
5.6 KiB
Python
Executable file

from collections import deque
def solve_nurikabe(grid, verbose=True):
"""
Nurikabe puzzle solver.
grid: 2D list, each element in {'A', 'X', '0'..'9'}
- digit: island with size hint
- 'X': island cell (without specific numeric hint)
- 'A': sea cell
verbose: bool, whether to print intermediate solving process.
Returns: a 2D list (same format) representing the solved puzzle.
Requirements:
1) Exactly one digit (size hint) per island, and the island size == that digit.
2) All sea cells are connected (single sea), and no 2x2 sea block.
3) No island lacking a digit hint.
"""
if verbose:
print("=== Nurikabe Solver: Start ===")
print("Puzzle Input:")
for row in grid:
print(" " + " ".join(row))
print()
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
# Make a modifiable copy of the grid
solution = [row[:] for row in grid]
# Identify cells that can be changed (not digit)
fill_cells = []
for r in range(rows):
for c in range(cols):
if not solution[r][c].isdigit():
fill_cells.append((r, c))
if verbose:
print(f"[Info] There are {len(fill_cells)} fillable cells.\n")
# Helper functions
def in_bounds(r, c):
return 0 <= r < rows and 0 <= c < cols
def is_island(val):
return val == 'X' or val.isdigit()
def get_island_region(r, c):
""" BFS to get connected island region and its digit set """
queue = deque([(r, c)])
region = {(r, c)}
digits = set()
while queue:
rr, cc = queue.popleft()
val = solution[rr][cc]
if val.isdigit():
digits.add(val)
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = rr + dr, cc + dc
if in_bounds(nr, nc) and is_island(solution[nr][nc]) and (nr, nc) not in region:
region.add((nr, nc))
queue.append((nr, nc))
return region, digits
def check_islands():
visited = set()
for r in range(rows):
for c in range(cols):
if is_island(solution[r][c]) and (r, c) not in visited:
region, digits = get_island_region(r, c)
visited |= region
if len(digits) != 1:
return False
digit_val = next(iter(digits))
if len(region) != int(digit_val):
return False
return True
def check_sea():
sea_cells = [(r, c) for r in range(rows) for c in range(cols) if solution[r][c] == 'A']
if not sea_cells: # if puzzle requires at least some sea
return False
visited = set([sea_cells[0]])
queue = deque([sea_cells[0]])
while queue:
rr, cc = queue.popleft()
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = rr + dr, cc + dc
if in_bounds(nr, nc) and solution[nr][nc] == 'A' and (nr, nc) not in visited:
visited.add((nr, nc))
queue.append((nr, nc))
if len(visited) != len(sea_cells): # sea not connected
return False
# no 2x2 all sea
for rr in range(rows - 1):
for cc in range(cols - 1):
block = [solution[rr][cc],
solution[rr][cc+1],
solution[rr+1][cc],
solution[rr+1][cc+1]]
if all(x == 'A' for x in block):
return False
return True
def is_valid():
return check_islands() and check_sea()
def backtrack(idx=0):
if idx == len(fill_cells):
# All fillable cells assigned, do final check
if verbose:
print("[*] All cells filled, validating solution...")
if is_valid():
if verbose:
print("[√] Valid solution found!\n")
return True
else:
if verbose:
print("[X] Validation failed, backtrack.\n")
return False
r, c = fill_cells[idx]
original_val = solution[r][c]
# Try 'A' or 'X'
for candidate in ['A', 'X']:
solution[r][c] = candidate
if verbose:
print(f"[Try] idx={idx}, cell=({r},{c}), set='{candidate}'")
if backtrack(idx + 1):
return True
# Undo assignment if not successful
solution[r][c] = original_val
if verbose:
print(f" -> Backtrack: reset cell=({r},{c}) to '{original_val}'")
return False
if verbose:
print("=== Start Backtracking ===\n")
success = backtrack(0)
if not success:
raise ValueError("\n[Conclusion] Puzzle has no valid solution under given constraints.")
if verbose:
print("=== Final Solved Puzzle ===")
for row in solution:
print(" ".join(row))
return solution
# ============ Example test ============ #
if __name__ == "__main__":
# puzzle = [
# ['X','X','X','X','5'],
# ['X','X','X','X','X'],
# ['1','X','X','4','X'],
# ['X','X','X','X','X'],
# ['X','X','X','X','1']
# ]
puzzle = [
['4','X','1','X','X'],
['X','X','X','X','3'],
['X','X','2','X','X'],
['X','X','X','X','X'],
['X','X','X','X','X']
]
result = solve_nurikabe(puzzle, verbose=False)
print("\n[Solved Output]:")
for row in result:
print(" ".join(row))