Source code for pykrieg.turn

"""Turn management system for Pykrieg.

This module implements turn structure, phase management, and turn
validation rules for the game.

Turn Structure:
- Turn Start: Resolve pending retreats
- Movement Phase: Up to 5 units can move
- Battle Phase: 1 attack or pass
- End Turn: Switch player, increment turn number

Scope for 0.1.4:
- Turn validation (movement limit, attack limit)
- Phase management
- Retreat resolution
- Turn state management
- Online/offline status NOT enforced (added in 0.2.0)
"""

from typing import Any, Dict, List, Set, Tuple

from .board import Board


[docs] class TurnValidationError(Exception): """Raised when a turn action violates turn rules.""" pass
[docs] class TurnState: """Represents the state of a turn. Attributes: moved_units: Set of original (row, col) for units moved this turn attacks_this_turn: Number of attacks made (0 or 1) current_phase: 'M' (Movement) or 'B' (Battle) pending_retreats: List of (row, col) for units that must retreat """
[docs] def __init__(self, moved_units: Set[Tuple[int, int]], attacks_this_turn: int, current_phase: str, pending_retreats: List[Tuple[int, int]]): self.moved_units = moved_units self.attacks_this_turn = attacks_this_turn self.current_phase = current_phase self.pending_retreats = pending_retreats
[docs] def to_dict(self) -> Dict[str, object]: """Convert turn state to dictionary for serialization.""" return { 'moved_units': list(self.moved_units), 'attacks_this_turn': self.attacks_this_turn, 'current_phase': self.current_phase, 'pending_retreats': self.pending_retreats, }
[docs] @classmethod def from_dict(cls, data: Dict[str, object]) -> 'TurnState': """Create TurnState from dictionary.""" moved_units_data = data.get('moved_units', []) attacks_data = data.get('attacks_this_turn', 0) phase_data = data.get('current_phase', 'M') retreats_data = data.get('pending_retreats', []) # Type cast for moved_units units_set: Set[Tuple[int, int]] = set() if moved_units_data and isinstance(moved_units_data, list): for item in moved_units_data: if isinstance(item, (list, tuple)) and len(item) == 2: units_set.add((int(item[0]), int(item[1]))) # Type cast for retreats retreats_list: List[Tuple[int, int]] = [] if retreats_data and isinstance(retreats_data, list): for item in retreats_data: if isinstance(item, (list, tuple)) and len(item) == 2: retreats_list.append((int(item[0]), int(item[1]))) return cls( moved_units=units_set, attacks_this_turn=int(attacks_data) if isinstance(attacks_data, (int, str)) else 0, current_phase=str(phase_data) if phase_data else 'M', pending_retreats=retreats_list, )
[docs] def get_turn_state(board: Board) -> TurnState: """Get the current turn state from board. Args: board: The game board Returns: TurnState object with current turn information """ return TurnState( moved_units=set(board._moved_units), attacks_this_turn=board._attacks_this_turn, current_phase=board._current_phase, pending_retreats=board.get_pending_retreats(), )
[docs] def validate_turn_action(board: Board, action_type: str, **kwargs: Any) -> bool: """Validate a turn action. Args: board: The game board action_type: Type of action ('move', 'attack', 'pass') **kwargs: Action-specific parameters Returns: True if action is valid, False otherwise """ if action_type == 'move': from_row = kwargs.get('from_row') from_col = kwargs.get('from_col') to_row = kwargs.get('to_row') to_col = kwargs.get('to_col') # Validate all parameters are present and are integers if not all(isinstance(x, int) for x in [from_row, from_col, to_row, to_col]): return False # Cast to int (mypy knows they're int after the check above) return board.validate_move(int(from_row), int(from_col), int(to_row), int(to_col)) # type: ignore[arg-type] elif action_type == 'attack': target_row = kwargs.get('target_row') target_col = kwargs.get('target_col') # Validate all parameters are present and are integers if not all(isinstance(x, int) for x in [target_row, target_col]): return False # Cast to int (mypy knows they're int after the check above) return board.validate_attack(int(target_row), int(target_col)) # type: ignore[arg-type] elif action_type == 'pass': return (board._current_phase == 'B' and board._attacks_this_turn == 0) else: return False
[docs] def can_end_turn(board: Board) -> bool: """Check if turn can be ended. Args: board: The game board Returns: True if turn can be ended, False otherwise Note: - In movement phase, can end turn at any time (0-5 moves) - In battle phase, must attack or pass before ending turn """ if board._current_phase == 'B': # Must have attacked or passed return board._attacks_this_turn == 1 else: # Movement phase: can end at any time return True
[docs] def get_turn_summary(board: Board) -> Dict: """Get a summary of the current turn state. Args: board: The game board Returns: Dictionary with turn summary information """ return { 'turn_number': board.turn_number, 'current_player': board.turn, 'current_phase': board.current_phase, 'moves_made': len(board._moved_unit_ids), 'moves_remaining': 5 - len(board._moved_unit_ids), 'attacks_made': board._attacks_this_turn, 'attacks_remaining': 1 - board._attacks_this_turn, 'pending_retreats': len(board.get_pending_retreats()), }