Source code for haive.games.dominoes.state

"""Comprehensive state management system for Dominoes gameplay and strategic analysis.

This module provides sophisticated state models for Dominoes games with complete
support for tile tracking, board management, strategic analysis, and game flow
control. The state system maintains both traditional dominoes mechanics and
advanced strategic context for AI decision-making.

The state system supports:
- Complete tile tracking with hand and boneyard management
- Strategic analysis history for multiplayer gameplay
- Board state validation and move legality checking
- Game progression tracking with pass and block detection
- Performance metrics and statistical analysis
- Multiple game variants and scoring systems

Examples:
    Creating a new game state::

        state = DominoesState.initialize(
            player_names=["player1", "player2"],
            tiles_per_hand=7
        )
        assert state.turn in ["player1", "player2"]
        assert state.game_status == "ongoing"

    Accessing game information::

        # Check board state
        left_open = state.left_value
        right_open = state.right_value
        board_display = state.board_string

        # Check hand sizes
        hand_sizes = state.hand_sizes
        tiles_remaining = state.boneyard_size

    Tracking strategic analysis::

        analysis = DominoesAnalysis(
            hand_strength="Strong high-value tiles",
            blocking_opportunities=["Block 5-5 connection"],
            optimal_plays=["Play 6-4 on right end"],
            endgame_strategy="Hold doubles for scoring"
        )
        state.add_analysis(analysis, "player1")

    Game state queries::

        # Check game completion
        if state.is_game_over():
            winner = state.winner
            final_scores = state.scores

        # Strategic position analysis
        playable_tiles = state.get_playable_tiles("player1")
        board_control = state.board_control_analysis

Note:
    All state models use Pydantic for validation and support both JSON
    serialization and integration with LangGraph for distributed gameplay.

"""

import random
from typing import Literal

from pydantic import Field, computed_field

from haive.games.dominoes.models import DominoesAnalysis, DominoMove, DominoTile
from haive.games.framework.base.state import GameState


[docs] class DominoesState(GameState): """Comprehensive state management for Dominoes gameplay with strategic analysis. support. This class provides complete state management for Dominoes games, supporting both traditional dominoes mechanics and strategic analysis. The state system maintains tile tracking, board management, strategic context, and performance metrics for advanced AI decision-making and game analysis. The state system supports: - Complete tile tracking with hand and boneyard management - Strategic analysis history for multiplayer gameplay with learning capability - Board state validation and move legality checking - Game progression tracking with pass and block detection - Performance metrics and statistical analysis for gameplay optimization - Multiple game variants and scoring systems The game follows traditional dominoes rules: - Each player starts with 7 tiles (configurable) - Players take turns placing tiles that match board ends - Game ends when a player plays all tiles or board is blocked - Scoring typically based on remaining tiles in opponents' hands Attributes: players (List[str]): List of player names in turn order. Maintains consistent ordering for gameplay flow. hands (Dict[str, List[DominoTile]]): Current tiles in each player's hand. Private information tracked for game management. board (List[DominoTile]): Tiles currently placed on the board. Represents the train/line of connected dominoes. boneyard (List[DominoTile]): Undealt tiles available for drawing. Used when players cannot play and must draw. turn (str): Current player's turn identifier. Cycles through players list for turn management. game_status (Literal): Current game state with completion detection. Tracks ongoing play, wins, and draw conditions. move_history (List[Union[DominoMove, Literal["pass"]]]): Complete move history. Includes both tile placements and pass actions. last_passes (int): Count of consecutive passes for block detection. Used to determine when board is blocked. scores (Dict[str, int]): Current scores for each player. Updated based on game variant scoring rules. winner (Optional[str]): Winner identifier if game completed. Set when victory conditions are met. player1_analysis (List[DominoesAnalysis]): Strategic analysis history for player1. Tracks reasoning and decision-making patterns. player2_analysis (List[DominoesAnalysis]): Strategic analysis history for player2. Tracks reasoning and decision-making patterns. Examples: Creating a new game state:: state = DominoesState.initialize( player_names=["Alice", "Bob"], tiles_per_hand=7 ) assert state.turn in ["Alice", "Bob"] assert len(state.players) == 2 assert all(len(hand) == 7 for hand in state.hands.values()) Accessing game information:: # Check board state left_open = state.left_value # Value that can be matched on left right_open = state.right_value # Value that can be matched on right board_display = state.board_string # Human-readable board # Check hand and boneyard sizes hand_sizes = state.hand_sizes tiles_remaining = state.boneyard_size total_tiles = state.total_tiles_in_play Managing strategic analysis:: analysis = DominoesAnalysis( hand_strength="Strong concentration of 5s and 6s", blocking_opportunities=["Block opponent's 3-3 double"], optimal_plays=["Play 5-2 on left end for control"], endgame_strategy="Hold 6-6 double for final scoring" ) state.add_analysis(analysis, "Alice") # Access latest strategic insights latest_analysis = state.get_latest_analysis("Alice") Game state queries:: # Check game completion if state.is_game_over(): winner = state.winner final_scores = state.scores game_summary = state.game_summary # Strategic position analysis playable_tiles = state.get_playable_tiles("Alice") board_control = state.board_control_analysis tile_distribution = state.tile_distribution_analysis Advanced game analysis:: # Performance metrics stats = state.game_statistics print(f"Moves played: {stats['total_moves']}") print(f"Pass rate: {stats['pass_percentage']:.1f}%") # Strategic evaluation position_eval = state.position_evaluation print(f"Board control: {position_eval['board_control']}") print(f"Hand strength: {position_eval['hand_strength_analysis']}") Note: The state uses Pydantic for validation and supports both JSON serialization and integration with LangGraph for distributed game systems. All tile operations maintain game rule consistency and strategic context. """ players: list[str] = Field( ..., min_items=2, max_items=4, description="List of player names in turn order (2-4 players supported)", ) hands: dict[str, list[DominoTile]] = Field( ..., description="Current tiles in each player's hand (private information)" ) board: list[DominoTile] = Field( default_factory=list, description="Tiles currently placed on the board in connection order", ) boneyard: list[DominoTile] = Field( default_factory=list, description="Undealt tiles available for drawing when players cannot play", ) turn: str = Field( ..., description="Current player's turn identifier (must be in players list)" ) game_status: Literal["ongoing", "player1_win", "player2_win", "draw"] = Field( default="ongoing", description="Current game state: ongoing play, player victory, or draw", ) move_history: list[DominoMove | Literal["pass"]] = Field( default_factory=list, description="Complete chronological history of moves and passes", ) last_passes: int = Field( default=0, ge=0, description="Number of consecutive passes (for block detection)", ) scores: dict[str, int] = Field( default_factory=dict, description="Current scores for each player (updated by game variant rules)", ) winner: str | None = Field( default=None, description="Winner identifier if game completed, None if ongoing" ) player1_analysis: list[DominoesAnalysis] = Field( default_factory=list, description="Strategic analysis history for player1 with reasoning patterns", ) player2_analysis: list[DominoesAnalysis] = Field( default_factory=list, description="Strategic analysis history for player2 with reasoning patterns", ) @computed_field @property def left_value(self) -> int | None: """Get the value on the left end of the board that can be matched. Returns: Optional[int]: Value that can be matched on left end, None if board empty. """ if not self.board: return None return self.board[0].left @computed_field @property def right_value(self) -> int | None: """Get the value on the right end of the board that can be matched. Returns: Optional[int]: Value that can be matched on right end, None if board empty. """ if not self.board: return None return self.board[-1].right @computed_field @property def board_string(self) -> str: """Get a human-readable string representation of the board. Returns: str: Visual representation of the domino train with connecting lines. """ if not self.board: return "Empty board" board_str = "" for i, tile in enumerate(self.board): if i > 0: # Add a connecting character between tiles board_str += "-" board_str += str(tile) return board_str @computed_field @property def hand_sizes(self) -> dict[str, int]: """Get the current hand sizes for all players.""" return {player: len(self.hands[player]) for player in self.players} @computed_field @property def boneyard_size(self) -> int: """Get the number of tiles remaining in the boneyard.""" return len(self.boneyard) @computed_field @property def total_tiles_in_play(self) -> int: """Get the total number of tiles currently in players' hands and on board.""" hand_tiles = sum(len(hand) for hand in self.hands.values()) board_tiles = len(self.board) return hand_tiles + board_tiles @computed_field @property def is_blocked(self) -> bool: """Check if the board is blocked (all players have passed).""" return self.last_passes >= len(self.players) @computed_field @property def game_statistics(self) -> dict[str, int | float | str]: """Generate comprehensive game statistics.""" total_moves = len(self.move_history) pass_count = sum(1 for move in self.move_history if move == "pass") return { "total_moves": total_moves, "pass_count": pass_count, "pass_percentage": ( (pass_count / total_moves * 100) if total_moves > 0 else 0 ), "tiles_on_board": len(self.board), "tiles_in_boneyard": self.boneyard_size, "consecutive_passes": self.last_passes, "game_phase": ( "endgame" if self.total_tiles_in_play < 10 else "midgame" if self.total_tiles_in_play < 20 else "opening" ), "board_blocked": self.is_blocked, } @computed_field @property def position_evaluation(self) -> dict[str, str | int | float]: """Generate strategic position evaluation.""" hand_sizes = self.hand_sizes min_hand_size = min(hand_sizes.values()) leader = [ player for player, size in hand_sizes.items() if size == min_hand_size ][0] return { "hand_strength_leader": leader, "minimum_hand_size": min_hand_size, "hand_size_spread": max(hand_sizes.values()) - min_hand_size, "board_control": "dynamic" if self.board else "balanced", "tiles_remaining_total": sum(hand_sizes.values()), "endgame_proximity": min_hand_size <= 3, }
[docs] def add_analysis(self, analysis: DominoesAnalysis, player: str) -> None: """Add strategic analysis for a player.""" if player not in self.players: raise ValueError(f"Player '{player}' not in game") if player == "player1": self.player1_analysis = list(self.player1_analysis) + [analysis] elif player == "player2": self.player2_analysis = list(self.player2_analysis) + [analysis]
[docs] def get_latest_analysis(self, player: str) -> DominoesAnalysis | None: """Get the latest analysis for a player.""" if player == "player1": return self.player1_analysis[-1] if self.player1_analysis else None elif player == "player2": return self.player2_analysis[-1] if self.player2_analysis else None return None
[docs] def get_playable_tiles(self, player: str) -> list[DominoTile]: """Get tiles that a player can currently play.""" if player not in self.players: return [] player_hand = self.hands[player] # If board is empty, any tile can be played if not self.board: return player_hand[:] # Check which tiles can match board ends playable = [] left_val = self.left_value right_val = self.right_value for tile in player_hand: if ( tile.left == left_val or tile.right == left_val or tile.left == right_val or tile.right == right_val ): playable.append(tile) return playable
[docs] def is_game_over(self) -> bool: """Check if the game is over.""" return self.game_status != "ongoing"
[docs] @classmethod def initialize( cls, player_names: list[str] | None = None, tiles_per_hand: int = 7 ) -> "DominoesState": """Initialize a new dominoes game with proper tile distribution. Args: player_names: List of player names. Defaults to ["player1", "player2"]. tiles_per_hand: Number of tiles to deal to each player. Default is 7. Returns: DominoesState: A new game state ready to play. """ if player_names is None: player_names = ["player1", "player2"] # Create all tiles from 0-0 to 6-6 (standard double-six set) all_tiles = [] for i in range(7): for j in range(i, 7): # Create tiles from 0-0 to 6-6 all_tiles.append(DominoTile(left=i, right=j)) # Shuffle the tiles for random distribution random.shuffle(all_tiles) # Deal tiles to players hands = {} for player in player_names: hands[player] = all_tiles[:tiles_per_hand] all_tiles = all_tiles[tiles_per_hand:] # Remaining tiles go to the boneyard boneyard = all_tiles # Determine who goes first using traditional dominoes rules # Rule 1: Player with highest double goes first first_player = player_names[0] # Default fallback highest_double = -1 for player in player_names: for tile in hands[player]: if tile.is_double() and tile.left > highest_double: highest_double = tile.left first_player = player # Rule 2: If no doubles, player with highest-value tile goes first if highest_double == -1: highest_sum = -1 for player in player_names: for tile in hands[player]: tile_sum = tile.sum() if tile_sum > highest_sum: highest_sum = tile_sum first_player = player # Create the initial state with proper initialization return cls( players=player_names, hands=hands, board=[], boneyard=boneyard, turn=first_player, game_status="ongoing", move_history=[], last_passes=0, scores=dict.fromkeys(player_names, 0), )
model_config = {"arbitrary_types_allowed": True}