Source code for haive.games.fox_and_geese.models

"""Comprehensive data models for the Fox and Geese asymmetric strategy game.

This module defines the complete set of data structures for the classic Fox and Geese
game, providing models for position tracking, move validation, strategic analysis,
and game state management. The implementation supports the traditional asymmetric
gameplay where one fox attempts to capture geese while geese try to trap the fox.

Fox and Geese is a classic asymmetric strategy game involving:
- One fox piece that can move and capture in any direction
- Multiple geese pieces that can only move forward and sideways
- 7x7 board with specific starting positions
- Victory conditions: fox captures enough geese OR geese trap the fox
- Strategic depth through positioning and tactical maneuvering

Key Models:
    FoxAndGeesePosition: Board coordinate representation with validation
    FoxAndGeeseMove: Complete move description with capture mechanics
    FoxAndGeeseAnalysis: Strategic evaluation for AI decision-making

Examples:
    Working with positions::

        from haive.games.fox_and_geese.models import FoxAndGeesePosition

        # Create board positions
        fox_start = FoxAndGeesePosition(row=0, col=3)  # Fox starting position
        geese_line = FoxAndGeesePosition(row=6, col=2)  # Geese back line

        # Positions are hashable for set operations
        positions = {fox_start, geese_line}
        print(fox_start)  # "(0, 3)"

    Making moves::

        from haive.games.fox_and_geese.models import FoxAndGeeseMove

        # Fox move with capture
        fox_capture = FoxAndGeeseMove(
            from_pos=FoxAndGeesePosition(row=2, col=2),
            to_pos=FoxAndGeesePosition(row=4, col=4),
            piece_type="fox",
            capture=FoxAndGeesePosition(row=3, col=3)
        )

        # Goose defensive move
        goose_move = FoxAndGeeseMove(
            from_pos=FoxAndGeesePosition(row=5, col=1),
            to_pos=FoxAndGeesePosition(row=4, col=1),
            piece_type="goose"
        )

    Strategic analysis::

        from haive.games.fox_and_geese.models import FoxAndGeeseAnalysis

        analysis = FoxAndGeeseAnalysis(
            advantage="fox",
            advantage_level=7,
            key_features=["fox has breakthrough", "geese scattered"],
            fox_strategy="Push through center, target isolated geese",
            geese_strategy="Regroup and form defensive line",
            critical_squares=["(3,3)", "(4,4)", "(5,5)"],
            explanation="Fox has tactical advantage with open center control"
        )

The models provide comprehensive support for asymmetric game analysis and
strategic AI development with proper validation and immutable data structures.

"""

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field


[docs] class FoxAndGeesePosition(BaseModel): """Immutable position coordinate on the Fox and Geese board. Represents a specific square on the 7x7 Fox and Geese board using zero-indexed row and column coordinates. The position is immutable and hashable, making it suitable for use in sets and as dictionary keys. The board layout follows traditional Fox and Geese conventions: - Row 0: Top of the board (fox starting area) - Row 6: Bottom of the board (geese starting area) - Column 0-6: Left to right across the board Attributes: row: Row coordinate (0-6) from top to bottom of the board. col: Column coordinate (0-6) from left to right of the board. Examples: Creating positions:: # Fox starting position (center top) fox_start = FoxAndGeesePosition(row=0, col=3) # Geese starting positions (bottom row) geese_positions = [ FoxAndGeesePosition(row=6, col=i) for i in range(7) ] # Center board position center = FoxAndGeesePosition(row=3, col=3) Position validation:: # Valid positions valid_pos = FoxAndGeesePosition(row=5, col=2) assert 0 <= valid_pos.row < 7 assert 0 <= valid_pos.col < 7 # Invalid positions raise validation errors try: invalid = FoxAndGeesePosition(row=7, col=3) # Row too high except ValueError as e: print(f"Invalid position: {e}") Working with position sets:: # Positions are hashable occupied_squares = { FoxAndGeesePosition(row=0, col=3), # Fox FoxAndGeesePosition(row=6, col=0), # Goose FoxAndGeesePosition(row=6, col=1), # Goose } # Check if position is occupied test_pos = FoxAndGeesePosition(row=3, col=3) is_occupied = test_pos in occupied_squares Strategic context:: # Corner positions (strategic for fox) corners = [ FoxAndGeesePosition(row=0, col=0), FoxAndGeesePosition(row=0, col=6), FoxAndGeesePosition(row=6, col=0), FoxAndGeesePosition(row=6, col=6) ] # Center control positions center_squares = [ FoxAndGeesePosition(row=3, col=3), FoxAndGeesePosition(row=3, col=4), FoxAndGeesePosition(row=4, col=3), FoxAndGeesePosition(row=4, col=4) ] Note: The position is frozen (immutable) to ensure data integrity and enable use as dictionary keys and in sets. String representation uses mathematical coordinate notation: "(row, col)". """ # Enable frozen for proper hashing in sets model_config = ConfigDict(frozen=True) row: int = Field(ge=0, lt=7, description="Row (0-6)") col: int = Field(ge=0, lt=7, description="Column (0-6)") def __str__(self) -> str: """String representation of the position. Returns: str: Position in format "(row, col)". Examples: Position display:: pos = FoxAndGeesePosition(row=3, col=4) print(pos) # "(3, 4)" """ return f"({self.row}, {self.col})" def __eq__(self, other) -> bool: """Check equality with another position. Args: other: Object to compare with. Returns: bool: True if positions have same row and column. """ if not isinstance(other, FoxAndGeesePosition): return False return self.row == other.row and self.col == other.col def __hash__(self) -> int: """Generate hash for use in sets and dictionaries. Returns: int: Hash value based on row and column coordinates. """ return hash((self.row, self.col))
[docs] class FoxAndGeeseMove(BaseModel): """Represents a move in Fox and Geese. This class defines the structure of a move in Fox and Geese, which includes a starting position, an ending position, a piece type, and an optional captured position. """ # Enable frozen for consistency model_config = ConfigDict(frozen=True) from_pos: FoxAndGeesePosition = Field(description="Starting position") to_pos: FoxAndGeesePosition = Field(description="Ending position") piece_type: Literal["fox", "goose"] = Field(description="Type of piece moved") capture: FoxAndGeesePosition | None = Field( default=None, description="Position of captured goose, if any" ) def __str__(self) -> str: if self.capture: return f"{self.piece_type.capitalize()} moves from {self.from_pos} to { self.to_pos }, capturing at {self.capture}" return f"{self.piece_type.capitalize()} moves from {self.from_pos} to { self.to_pos }" def __eq__(self, other) -> bool: """ Eq . Args: other: [TODO: Add description] Returns: [TODO: Add return description] """ if not isinstance(other, FoxAndGeeseMove): return False return ( self.from_pos == other.from_pos and self.to_pos == other.to_pos and self.piece_type == other.piece_type and self.capture == other.capture ) def __hash__(self) -> int: """ Hash . Returns: [TODO: Add return description] """ return hash((self.from_pos, self.to_pos, self.piece_type, self.capture))
[docs] class FoxAndGeeseAnalysis(BaseModel): """Analysis of a Fox and Geese position. This class defines the structure of an analysis of a Fox and Geese position, which includes an advantage, an advantage level, key features, fox strategy, geese strategy, and critical squares. """ model_config = ConfigDict(frozen=True) advantage: Literal["fox", "geese", "equal"] = Field( description="Which side has the advantage" ) advantage_level: int = Field(ge=0, le=10, description="Level of advantage (0-10)") key_features: list[str] = Field( default_factory=list, description="Key strategic features of the position" ) fox_strategy: str = Field(description="Recommended strategy for the fox") geese_strategy: str = Field(description="Recommended strategy for the geese") critical_squares: list[str] = Field( default_factory=list, description="Critical squares or formations in the position", ) explanation: str = Field(description="Detailed explanation of the analysis") def __eq__(self, other) -> bool: """ Eq . Args: other: [TODO: Add description] Returns: [TODO: Add return description] """ if not isinstance(other, FoxAndGeeseAnalysis): return False return ( self.advantage == other.advantage and self.advantage_level == other.advantage_level and self.key_features == other.key_features and self.fox_strategy == other.fox_strategy and self.geese_strategy == other.geese_strategy and self.critical_squares == other.critical_squares and self.explanation == other.explanation ) def __hash__(self) -> int: """ Hash . Returns: [TODO: Add return description] """ return hash( ( self.advantage, self.advantage_level, tuple(self.key_features), self.fox_strategy, self.geese_strategy, tuple(self.critical_squares), self.explanation, ) )