"""State manager for the Fox and Geese game.
This module defines the state manager for the Fox and Geese game, which manages the
state of the game and provides methods for initializing, applying moves, checking game
status, and getting legal moves for the Fox and Geese game.
"""
# Standard library imports
import copy
import logging
from haive.games.fox_and_geese.models import FoxAndGeeseMove, FoxAndGeesePosition
from haive.games.fox_and_geese.state import FoxAndGeeseState
from haive.games.framework.base.state_manager import GameStateManager
# Local imports
logger = logging.getLogger(__name__)
[docs]
class FoxAndGeeseStateManager(GameStateManager[FoxAndGeeseState]):
"""Manager for Fox and Geese game state.
This class provides methods for initializing, applying moves, checking game status,
and getting legal moves for the Fox and Geese game.
"""
[docs]
@classmethod
def initialize(cls) -> FoxAndGeeseState:
"""Initialize a new Fox and Geese game.
Creates initial game state with fox in center position and geese
arranged in checkerboard pattern on the first two rows.
Returns:
FoxAndGeeseState: Initial game state with positions set up.
"""
logger.info("Initializing Fox and Geese game")
# Fox starts at the center
fox_position = FoxAndGeesePosition(row=3, col=3)
# Fox position created
# Geese start at the top - create as list first, then convert to set
geese_list = []
# Place geese on the first two rows in a checkerboard pattern
for row in range(2): # First two rows (0 and 1)
for col in range(7):
# Only place on "valid" squares (checkerboard pattern)
# For a 7x7 board, we typically use alternating squares
if (row + col) % 2 == 0: # Checkerboard pattern
geese_list.append(FoxAndGeesePosition(row=row, col=col))
# Convert to set after creating all positions
geese_positions = set(geese_list)
# Ensure FoxAndGeesePosition is hashable by adding __hash__ method if needed
# Or use frozenset if that's causing issues
return FoxAndGeeseState(
fox_position=fox_position,
geese_positions=geese_positions,
turn="fox", # Fox goes first
game_status="ongoing",
move_history=[],
winner=None, # Explicitly set winner to None
num_geese=len(geese_positions),
fox_analysis=[], # Initialize empty analysis lists
geese_analysis=[],
)
[docs]
@classmethod
def apply_move(
cls, state: FoxAndGeeseState, move: FoxAndGeeseMove
) -> FoxAndGeeseState:
"""Apply a move to the Fox and Geese state.
This method updates the state of the game based on the move made by the current
player. It handles both regular moves and capture moves, updating the position
of the fox and geese accordingly. It also updates the move history and switches
turns between fox and geese.
"""
# Create a deep copy of the state
new_state = copy.deepcopy(state)
# Update the position based on piece type
if move.piece_type == "fox":
new_state.fox_position = move.to_pos
# Handle capture if present
if move.capture:
new_state.geese_positions.remove(move.capture)
new_state.num_geese -= 1
else: # Goose
new_state.geese_positions.remove(move.from_pos)
new_state.geese_positions.add(move.to_pos)
# Add to move history
new_state.move_history.append(move)
# Switch turns
new_state.turn = "geese" if state.turn == "fox" else "fox"
# Check for game over
new_state = cls.check_game_status(new_state)
return new_state
[docs]
@classmethod
def get_legal_moves(cls, state: FoxAndGeeseState) -> list[FoxAndGeeseMove]:
"""Get all legal moves for the current state.
This method returns a list of all legal moves for the current player in the
given game state. It checks the current player's turn and calls the appropriate
method to get the legal moves for the fox or geese.
"""
moves = []
if state.turn == "fox":
# Fox can move diagonally in any direction
moves = cls._get_fox_moves(state)
else:
# Geese can only move diagonally forward
moves = cls._get_geese_moves(state)
return moves
@classmethod
def _get_fox_moves(cls, state: FoxAndGeeseState) -> list[FoxAndGeeseMove]:
"""Get all legal moves for the fox.
This method returns a list of all legal moves for the fox in the given game
state. It checks all possible diagonal directions from the fox's current
position and creates moves for each valid direction.
"""
moves = []
row, col = state.fox_position.row, state.fox_position.col
# Diagonal directions
directions = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
for dr, dc in directions:
# Regular move
new_row, new_col = row + dr, col + dc
if 0 <= new_row < 7 and 0 <= new_col < 7:
# Check if position is empty
new_pos = FoxAndGeesePosition(row=new_row, col=new_col)
if new_pos not in state.geese_positions:
moves.append(
FoxAndGeeseMove(
from_pos=state.fox_position,
to_pos=new_pos,
piece_type="fox",
)
)
# Capture move - jump over a goose
capture_row, capture_col = row + dr, col + dc
land_row, land_col = row + 2 * dr, col + 2 * dc
if (
0 <= capture_row < 7
and 0 <= capture_col < 7
and 0 <= land_row < 7
and 0 <= land_col < 7
):
capture_pos = FoxAndGeesePosition(row=capture_row, col=capture_col)
land_pos = FoxAndGeesePosition(row=land_row, col=land_col)
# Check if there's a goose to capture and landing spot is empty
if (
capture_pos in state.geese_positions
and land_pos not in state.geese_positions
):
moves.append(
FoxAndGeeseMove(
from_pos=state.fox_position,
to_pos=land_pos,
piece_type="fox",
capture=capture_pos,
)
)
return moves
@classmethod
def _get_geese_moves(cls, state: FoxAndGeeseState) -> list[FoxAndGeeseMove]:
"""Get all legal moves for the geese.
This method returns a list of all legal moves for the geese in the given game
state. It checks all possible diagonal directions from the geese's current
position and creates moves for each valid direction.
"""
moves = []
# Geese can only move diagonally forward (downward)
directions = [(1, -1), (1, 1)]
for goose in state.geese_positions:
row, col = goose.row, goose.col
for dr, dc in directions:
new_row, new_col = row + dr, col + dc
if 0 <= new_row < 7 and 0 <= new_col < 7:
# Check if position is empty
new_pos = FoxAndGeesePosition(row=new_row, col=new_col)
if (
new_pos not in state.geese_positions
and new_pos != state.fox_position
):
moves.append(
FoxAndGeeseMove(
from_pos=goose, to_pos=new_pos, piece_type="goose"
)
)
return moves
[docs]
@classmethod
def check_game_status(cls, state: FoxAndGeeseState) -> FoxAndGeeseState:
"""Check and update game status.
This method checks the current game status and updates the state accordingly. It
determines if the fox has won by capturing too many geese or if the geese have
won by trapping the fox. It also updates the game status and winner.
"""
# Fox wins if it captures too many geese
if state.num_geese < 4: # Assuming fox wins if fewer than 4 geese remain
state.game_status = "fox_win"
state.winner = "fox"
# Geese win if they trap the fox (fox has no legal moves)
elif state.turn == "fox" and not cls._get_fox_moves(state):
state.game_status = "geese_win"
state.winner = "geese"
return state