Skip to content
Draft
Show file tree
Hide file tree
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
127 changes: 127 additions & 0 deletions Domain/Puzzles/ChessRanger/ChessRangerSolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from copy import deepcopy

from Domain.Board.Grid import Grid
from Domain.Board.Position import Position
from Domain.Puzzles.GameSolver import GameSolver


class ChessRangerSolver(GameSolver):
def __init__(self, grid: Grid):
self.grid = grid
self.initial_grid = grid

def get_solution(self) -> list[tuple[Position, Position]]:
pieces = self._get_pieces(self.grid)
# We need N-1 moves to reach 1 piece
target_moves = len(pieces) - 1
if target_moves < 0:
return []

solution = self._solve(self.grid, [], target_moves)
return solution if solution else []

def get_other_solution(self) -> Grid:
# Not applicable usually, as we want *a* solution
return None

def _get_pieces(self, grid: Grid) -> dict[Position, str]:
pieces = {}
for r in range(grid.rows_number):
for c in range(grid.columns_number):
val = grid[r][c]
if val is not None:
pieces[Position(r, c)] = val
return pieces

def _solve(self, grid: Grid, moves: list[tuple[Position, Position]], target_moves: int):
if len(moves) == target_moves:
return moves

pieces = self._get_pieces(grid)

# Optimization: Sort pieces? No, just iterate.
for pos, piece_type in pieces.items():
valid_moves = self._get_valid_moves(grid, pos, piece_type)
for target_pos in valid_moves:
# Execute move
captured_piece = grid[target_pos]
new_grid_matrix = [row[:] for row in grid.matrix]
new_grid_matrix[target_pos.r][target_pos.c] = piece_type
new_grid_matrix[pos.r][pos.c] = None
new_grid = Grid(new_grid_matrix)

new_moves = moves + [(pos, target_pos)]

result = self._solve(new_grid, new_moves, target_moves)
if result:
return result

return None

def _get_valid_moves(self, grid: Grid, start: Position, piece_type: str) -> list[Position]:
moves = []

# Piece types: K, Q, R, B, N, P
# Only capture moves allowed. Target must be occupied.

directions = []
is_single_step = False
is_knight = False
is_pawn = False

pt = piece_type.upper()

if pt == 'K': # King
directions = [(dr, dc) for dr in [-1, 0, 1] for dc in [-1, 0, 1] if not (dr == 0 and dc == 0)]
is_single_step = True
elif pt == 'Q': # Queen
directions = [(dr, dc) for dr in [-1, 0, 1] for dc in [-1, 0, 1] if not (dr == 0 and dc == 0)]
elif pt == 'R': # Rook
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
elif pt == 'B': # Bishop
directions = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
elif pt == 'N': # Knight
is_knight = True
jumps = [(-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1)]
elif pt == 'P': # Pawn
is_pawn = True
# Assuming white pawns moving UP (-1 row) for diagonal capture
# BUT Solitaire Chess usually has no orientation.
# Usually pawns capture diagonally forward (up-left, up-right).
# If the board has "black" pawns they might move down.
# Let's assume standard "White" pawns moving UP unless specified.
# Capture: (-1, -1), (-1, 1)
captures = [(-1, -1), (-1, 1)]


if is_knight:
for dr, dc in jumps:
r, c = start.r + dr, start.c + dc
if 0 <= r < grid.rows_number and 0 <= c < grid.columns_number:
if grid[r][c] is not None: # Must be a capture
moves.append(Position(r, c))

elif is_pawn:
# TODO: verify pawn direction. For now assume Up.
for dr, dc in captures:
r, c = start.r + dr, start.c + dc
if 0 <= r < grid.rows_number and 0 <= c < grid.columns_number:
if grid[r][c] is not None:
moves.append(Position(r, c))

else: # Sliding pieces (and King)
for dr, dc in directions:
r, c = start.r + dr, start.c + dc
while 0 <= r < grid.rows_number and 0 <= c < grid.columns_number:
if grid[r][c] is not None:
# Found a piece to capture
moves.append(Position(r, c))
break # Cannot jump over

if is_single_step:
break # King stops after 1 step

r += dr
c += dc

return moves
Empty file.
125 changes: 125 additions & 0 deletions Domain/Puzzles/ChessRanger/tests/ChessRangerSolver_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import unittest

from Domain.Board.Grid import Grid
from Domain.Board.Position import Position
from Domain.Puzzles.ChessRanger.ChessRangerSolver import ChessRangerSolver


class ChessRangerSolverTests(unittest.TestCase):
def test_solve_simple_rook_capture(self):
# R . P
# . . .
# . . .
# Rook at (0,0) captures Pawn at (0,2)
grid_data = [
['R', None, 'P'],
[None, None, None],
[None, None, None]
]
grid = Grid(grid_data)
solver = ChessRangerSolver(grid)
solution = solver.get_solution()

self.assertIsNotNone(solution)
self.assertEqual(len(solution), 1)
self.assertEqual(solution[0], (Position(0, 0), Position(0, 2)))

def test_solve_two_step_capture(self):
# R . P . P
# R captures P1, then P2? No, R moves to P1's spot.
# R at (0,0). P1 at (0,2). P2 at (0,4).
# Move 1: R(0,0) -> P1(0,2). Board: . . R . P
# Move 2: R(0,2) -> P2(0,4). Board: . . . . R
grid_data = [
['R', None, 'P', None, 'P']
]
grid = Grid(grid_data)
solver = ChessRangerSolver(grid)
solution = solver.get_solution()

self.assertIsNotNone(solution)
self.assertEqual(len(solution), 2)
self.assertEqual(solution[0], (Position(0, 0), Position(0, 2)))
self.assertEqual(solution[1], (Position(0, 2), Position(0, 4)))

def test_knight_move(self):
# N . .
# . . P
grid_data = [
['N', None, None],
[None, None, 'P']
]
grid = Grid(grid_data)
solver = ChessRangerSolver(grid)
solution = solver.get_solution()

self.assertEqual(len(solution), 1)
self.assertEqual(solution[0], (Position(0, 0), Position(1, 2)))

def test_bishop_move(self):
# B . .
# . P .
# . . P
# B(0,0) -> P(1,1) -> P(2,2)
grid_data = [
['B', None, None],
[None, 'P', None],
[None, None, 'P']
]
grid = Grid(grid_data)
solver = ChessRangerSolver(grid)
solution = solver.get_solution()

self.assertEqual(len(solution), 2)
self.assertEqual(solution[0], (Position(0, 0), Position(1, 1)))
self.assertEqual(solution[1], (Position(1, 1), Position(2, 2)))

def test_cannot_jump(self):
# R P P
# Rook cannot capture the second pawn directly
grid_data = [
['R', 'P', 'P']
]
grid = Grid(grid_data)
solver = ChessRangerSolver(grid)
# R->P1, then R->P2
solution = solver.get_solution()
self.assertEqual(len(solution), 2)
self.assertEqual(solution[0], (Position(0, 0), Position(0, 1)))
self.assertEqual(solution[1], (Position(0, 1), Position(0, 2)))

def test_no_solution(self):
# R . .
# . . .
# . . P
# Rook cannot reach P (diagonal)
grid_data = [
['R', None, None],
[None, None, None],
[None, None, 'P']
]
grid = Grid(grid_data)
solver = ChessRangerSolver(grid)
solution = solver.get_solution()
self.assertEqual(solution, [])

def test_king_single_step(self):
# K . P
# King cannot reach P in one step
grid_data = [
['K', None, 'P']
]
grid = Grid(grid_data)
solver = ChessRangerSolver(grid)
solution = solver.get_solution()
self.assertEqual(solution, [])

# K P
grid_data_2 = [['K', 'P']]
grid2 = Grid(grid_data_2)
solver2 = ChessRangerSolver(grid2)
sol2 = solver2.get_solution()
self.assertEqual(len(sol2), 1)

if __name__ == '__main__':
unittest.main()
88 changes: 88 additions & 0 deletions GridPlayers/PuzzleMobiles/PuzzleChessRangerPlayer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from time import sleep

from Domain.Board.Position import Position
from GridPlayers.PuzzleMobiles.PuzzlesMobilePlayer import PuzzlesMobilePlayer


class PuzzleChessRangerPlayer(PuzzlesMobilePlayer):
def play(self, solution: list[tuple[Position, Position]]):
for move in solution:
start_pos, end_pos = move
self.click_cell(start_pos)
self.click_cell(end_pos)
sleep(0.2) # Small delay for animation

def click_cell(self, position: Position):
# Calculate index
# Usually cells are indexed 0 to N-1
# Need to know columns number.
# But we don't have grid dimensions here easily unless we store it or re-scrape.
# However, Playwright locator can use nth(index).

# We need the columns number.
# PuzzlesMobilePlayer doesn't seem to store grid info.
# But looking at other players, they usually assume standard indexing.
# Let's check if we can click by coordinates or if we need to get cells first.

# Re-fetching cells is safe.
cells = self.page.locator('.cell')

# We need to know columns count to calculate index.
# Can we infer it?
# Or we can click by pixel position if we knew it.

# Better: get all cells and calculate row/col from styles or count.
# But that's slow.

# Assumption: The grid doesn't change dimensions.
# We can count columns once.

# Wait, `solution` contains positions.
# If we just get all `.cell` elements, we can index them if we know `columns_number`.

# Let's get columns number from the first row logic

# Note: If this is called per move, it might be slow.
# But for Playwright it's okay.

# Get all cells
all_cells = cells.all()
if not all_cells:
return

# Determine columns number
# We can assume the grid is rectangular and check `top` style of the first few cells.
# Or count how many have same `top` as the first one.

first_cell_top = all_cells[0].get_attribute('style')
# Parse top value... simple heuristic: count until top changes?
# But cells might be ordered by DOM, which usually follows row-major order.

# Let's count how many cells have the same top offset as the first one.
count = 0
import re
top_regex = re.compile(r'top:\s*(\d+)px')

first_match = top_regex.search(first_cell_top)
first_top = first_match.group(1) if first_match else None

columns = 0
if first_top:
for cell in all_cells:
style = cell.get_attribute('style')
match = top_regex.search(style)
if match and match.group(1) == first_top:
columns += 1
else:
break
else:
# Fallback if style parsing fails
columns = 1 # Should not happen

index = position.r * columns + position.c
if 0 <= index < len(all_cells):
all_cells[index].click()

def __init__(self, page, *args, **kwargs):
super().__init__(page, *args, **kwargs)
self.page = page
Loading