Source code for pykrieg.pieces

"""Unit type system for Pykrieg.

This module implements complete unit type system with 6 unit types
(Infantry, Cavalry, Cannon, Relay, Swift Cannon, Swift Relay),
each with their specific combat statistics from official rules.

Note: Arsenals are terrain structures (not units) as of version 0.2.1.
See Board.set_arsenal() for arsenal placement.

Unit Statistics:
- Infantry: Atk 4 / Def 6 / Move 1 / Range 2
- Cavalry: Atk 4 / Def 5 / Move 2 / Range 2
- Cannon: Atk 5 / Def 8 / Move 1 / Range 3
- Relay: Atk 0 / Def 1 / Move 1 / Range 0
- Swift Cannon: Atk 5 / Def 8 / Move 2 / Range 3
- Swift Relay: Atk 0 / Def 1 / Move 2 / Range 0
"""

from typing import TYPE_CHECKING, Optional

from . import constants

if TYPE_CHECKING:
    from .board import Board


[docs] class Unit: """Base class for all unit types. This class provides the foundation for all unit types in the game. Each unit type has specific combat statistics (attack, defense, movement, range) and belongs to a player (NORTH or SOUTH). """
[docs] def __init__(self, owner: str): """Initialize a unit with an owner. Args: owner: The player who owns this unit ('NORTH' or 'SOUTH') """ self._owner = owner
@property def owner(self) -> str: """Return unit owner (NORTH or SOUTH).""" return self._owner @property def unit_type(self) -> str: """Return unit type string.""" raise NotImplementedError @property def attack(self) -> int: """Return attack value.""" raise NotImplementedError @property def defense(self) -> int: """Return defense value.""" raise NotImplementedError @property def movement(self) -> int: """Return movement range in squares.""" raise NotImplementedError @property def range(self) -> Optional[int]: """Return attack range in squares, or None for structures.""" raise NotImplementedError
[docs] def is_combat_unit(self) -> bool: """Check if this is a combat unit (can attack). Returns: True if the unit has an attack value > 0 """ return self.attack > 0
[docs] def is_structure(self) -> bool: """Check if this is a structure (cannot move). Returns: True if the unit has movement = 0 """ return self.movement == 0
def __repr__(self) -> str: """Return string representation of the unit.""" return f"{self.unit_type}({self.owner})" def __eq__(self, other: object) -> bool: """Check equality based on unit type and owner.""" if not isinstance(other, Unit): return False return (self.unit_type == other.unit_type and self.owner == other.owner) def __hash__(self) -> int: """Return hash based on unit type and owner.""" return hash((self.unit_type, self.owner)) # ===================================================================== # 0.2.0: Effective Stats Methods (Lines of Communication) # =====================================================================
[docs] def get_effective_attack(self, board: "Board") -> int: """Get effective attack value, considering online/offline status. Args: board: The Board object to check online status Returns: Effective attack value (0 if offline and not a relay) """ # Import here to avoid circular import from .constants import UNIT_RELAY, UNIT_SWIFT_RELAY # Relays/swift relays can have non-zero attack even when offline # (but actually they have 0 attack always) if self.unit_type in (UNIT_RELAY, UNIT_SWIFT_RELAY): return self.attack # Check if network has been calculated # If network was never calculated, assume all units are online (use base stats) if hasattr(board, '_network_calculated') and board._network_calculated: # Other units need to be online to have attack # Check if unit is active in the network if hasattr(board, '_is_unit_active'): # Find unit's position on board for row in range(board.rows): for col in range(board.cols): unit = board.get_unit(row, col) if unit is self: if board._is_unit_active(row, col, self.owner): return self.attack return 0 # Fallback: return base attack if network not calculated return self.attack
[docs] def get_effective_defense(self, board: "Board") -> int: """Get effective defense value, considering online/offline status. Args: board: The Board object to check online status Returns: Effective defense value (0 if offline and not a relay) """ # Import here to avoid circular import from .constants import UNIT_RELAY, UNIT_SWIFT_RELAY # Relays/swift relays always have their defense value if self.unit_type in (UNIT_RELAY, UNIT_SWIFT_RELAY): return self.defense # Check if network has been calculated # If network was never calculated, assume all units are online (use base stats) if hasattr(board, '_network_calculated') and board._network_calculated: # Other units need to be online to have defense # Check if unit is active in the network if hasattr(board, '_is_unit_active'): # Find unit's position on board for row in range(board.rows): for col in range(board.cols): unit = board.get_unit(row, col) if unit is self: if board._is_unit_active(row, col, self.owner): return self.defense return 0 # Fallback: return base defense if network not calculated return self.defense
[docs] def get_effective_range(self, board: "Board") -> Optional[int]: """Get effective range value, considering online/offline status. Args: board: The Board object to check online status Returns: Effective range value (0 if offline and not a relay, None for structures) """ # Import here to avoid circular import from .constants import UNIT_RELAY, UNIT_SWIFT_RELAY # Relays/swift relays have 0 range always if self.unit_type in (UNIT_RELAY, UNIT_SWIFT_RELAY): return self.range # Check if network has been calculated # If network was never calculated, assume all units are online (use base stats) if hasattr(board, '_network_calculated') and board._network_calculated: # Other units need to be online to have range # Check if unit is active in the network if hasattr(board, '_is_unit_active'): # Find unit's position on board for row in range(board.rows): for col in range(board.cols): unit = board.get_unit(row, col) if unit is self: if board._is_unit_active(row, col, self.owner): return self.range return 0 # Fallback: return base range if network not calculated return self.range
[docs] def get_effective_movement(self, board: "Board") -> int: """Get effective movement value, considering online/offline status. Args: board: The Board object to check online status Returns: Effective movement value (0 if offline and not a relay) """ # Import here to avoid circular import from .constants import UNIT_RELAY, UNIT_SWIFT_RELAY # Relays/swift relays can move even when offline if self.unit_type in (UNIT_RELAY, UNIT_SWIFT_RELAY): return self.movement # Check if network has been calculated # If network was never calculated, assume all units are online (use base stats) if hasattr(board, '_network_calculated') and board._network_calculated: # Other units need to be online to move # Check if unit is active in the network if hasattr(board, '_is_unit_active'): # Find unit's position on board for row in range(board.rows): for col in range(board.cols): unit = board.get_unit(row, col) if unit is self: if board._is_unit_active(row, col, self.owner): return self.movement return 0 # Fallback: return base movement if network not calculated return self.movement
[docs] class Infantry(Unit): """Infantry unit: attack=4, defense=6, movement=1, range=2""" unit_type = constants.UNIT_INFANTRY attack = 4 defense = 6 movement = 1 range = 2
[docs] class Cavalry(Unit): """Cavalry unit: attack=4, defense=5, movement=2, range=2""" unit_type = constants.UNIT_CAVALRY attack = 4 defense = 5 movement = 2 range = 2
[docs] class Cannon(Unit): """Cannon unit: attack=5, defense=8, movement=1, range=3""" unit_type = constants.UNIT_CANNON attack = 5 defense = 8 movement = 1 range = 3
[docs] class Relay(Unit): """Relay unit: attack=0, defense=1, movement=1, range=0""" unit_type = constants.UNIT_RELAY attack = 0 defense = 1 movement = 1 range = 0
[docs] class SwiftCannon(Unit): """Swift Cannon unit: attack=5, defense=8, movement=2, range=3""" unit_type = constants.UNIT_SWIFT_CANNON attack = 5 defense = 8 movement = 2 range = 3
[docs] class SwiftRelay(Unit): """Swift Relay unit: attack=0, defense=1, movement=2, range=0""" unit_type = constants.UNIT_SWIFT_RELAY attack = 0 defense = 1 movement = 2 range = 0
[docs] def create_piece(unit_type: str, owner: str) -> Unit: """Factory function to create unit instances from type strings. This is the recommended way to create units, as it validates unit_type and owner parameters. Args: unit_type: String representing unit type owner: 'NORTH' or 'SOUTH' Returns: Unit subclass instance Raises: ValueError: If unit_type or owner is invalid Note: Arsenal is no longer a unit type. Arsenals are now terrain structures. Use Board.set_arsenal() to place an arsenal. Examples: >>> unit = create_piece("INFANTRY", "NORTH") >>> unit.unit_type 'INFANTRY' >>> unit.owner 'NORTH' """ unit_classes = { constants.UNIT_INFANTRY: Infantry, constants.UNIT_CAVALRY: Cavalry, constants.UNIT_CANNON: Cannon, constants.UNIT_RELAY: Relay, constants.UNIT_SWIFT_CANNON: SwiftCannon, constants.UNIT_SWIFT_RELAY: SwiftRelay, } if owner not in (constants.PLAYER_NORTH, constants.PLAYER_SOUTH): raise ValueError(f"Invalid owner: {owner}") if unit_type not in unit_classes: raise ValueError(f"Invalid unit type: {unit_type}") return unit_classes[unit_type](owner)