PyCatan-AI / tests /unit /test_actions.py
shon
1
ad0932c
"""
Unit tests for pycatan.actions module.
Tests all action types, data structures, validation, and utility functions.
"""
import pytest
from datetime import datetime
from unittest.mock import patch
from pycatan.management.actions import (
Action, ActionType, ActionResult, GameState, PlayerState, BoardState,
GamePhase, TurnPhase, create_build_settlement_action, create_build_road_action,
create_trade_action
)
class TestActionType:
"""Test ActionType enum."""
def test_all_action_types_exist(self):
"""Verify all expected action types are defined."""
expected_actions = [
'BUILD_SETTLEMENT', 'BUILD_CITY', 'BUILD_ROAD',
'TRADE_PROPOSE', 'TRADE_ACCEPT', 'TRADE_REJECT', 'TRADE_COUNTER', 'TRADE_BANK',
'USE_DEV_CARD', 'BUY_DEV_CARD',
'ROLL_DICE', 'END_TURN', 'END_GAME',
'ROBBER_MOVE', 'DISCARD_CARDS', 'STEAL_CARD',
'PLACE_STARTING_SETTLEMENT', 'PLACE_STARTING_ROAD'
]
for action_name in expected_actions:
assert hasattr(ActionType, action_name), f"ActionType.{action_name} not found"
def test_action_type_values_are_unique(self):
"""Ensure all action type values are unique."""
values = [action.value for action in ActionType]
assert len(values) == len(set(values)), "ActionType values are not unique"
class TestAction:
"""Test Action dataclass."""
def test_basic_action_creation(self):
"""Test creating a basic action."""
action = Action(
action_type=ActionType.END_TURN,
player_id=1
)
assert action.action_type == ActionType.END_TURN
assert action.player_id == 1
assert action.parameters == {}
assert isinstance(action.timestamp, datetime)
def test_action_with_parameters(self):
"""Test creating an action with parameters."""
params = {'point_coords': (1, 2)}
action = Action(
action_type=ActionType.BUILD_SETTLEMENT,
player_id=0,
parameters=params
)
assert action.parameters == params
def test_action_validation_success(self):
"""Test successful action validation."""
# Action with required parameters should validate successfully
action = Action(
action_type=ActionType.BUILD_SETTLEMENT,
player_id=0,
parameters={'point_coords': (1, 2)}
)
# Should not raise exception
def test_action_validation_missing_params(self):
"""Test action validation with missing required parameters."""
with pytest.raises(ValueError, match="missing required parameters"):
Action(
action_type=ActionType.BUILD_SETTLEMENT,
player_id=0,
parameters={} # Missing point_coords
)
def test_action_validation_trade_propose(self):
"""Test validation for trade propose action."""
# Valid trade propose
action = Action(
action_type=ActionType.TRADE_PROPOSE,
player_id=0,
parameters={
'offer': {'wood': 1},
'request': {'brick': 1},
'target_player': 1
}
)
# Should not raise exception
# Missing target_player
with pytest.raises(ValueError, match="missing required parameters"):
Action(
action_type=ActionType.TRADE_PROPOSE,
player_id=0,
parameters={
'offer': {'wood': 1},
'request': {'brick': 1}
# Missing target_player
}
)
def test_action_validation_no_requirements(self):
"""Test actions that don't require parameters."""
actions_without_requirements = [
ActionType.ROLL_DICE,
ActionType.END_TURN,
ActionType.BUY_DEV_CARD
]
for action_type in actions_without_requirements:
action = Action(
action_type=action_type,
player_id=0
)
# Should not raise exception
class TestGameState:
"""Test GameState dataclass."""
def test_default_game_state(self):
"""Test creating a default game state."""
state = GameState()
assert state.game_id == ""
assert state.turn_number == 0
assert state.current_player == 0
assert state.game_phase == GamePhase.SETUP_FIRST_ROUND
assert state.turn_phase == TurnPhase.ROLL_DICE
assert state.dev_cards_available == 25
assert len(state.players_state) == 0
assert state.dice_rolled is None
assert len(state.pending_trades) == 0
assert len(state.action_history) == 0
def test_game_state_with_players(self):
"""Test game state with players."""
players = [
PlayerState(player_id=0, name="Alice", cards=[], dev_cards=[],
settlements=[], cities=[], roads=[], victory_points=0,
longest_road_length=0, has_longest_road=False,
has_largest_army=False, knights_played=0),
PlayerState(player_id=1, name="Bob", cards=[], dev_cards=[],
settlements=[], cities=[], roads=[], victory_points=0,
longest_road_length=0, has_longest_road=False,
has_largest_army=False, knights_played=0)
]
state = GameState(players_state=players)
assert len(state.players_state) == 2
assert state.players_state[0].name == "Alice"
assert state.players_state[1].name == "Bob"
def test_get_player_state(self):
"""Test getting player state by ID."""
players = [
PlayerState(player_id=0, name="Alice", cards=[], dev_cards=[],
settlements=[], cities=[], roads=[], victory_points=0,
longest_road_length=0, has_longest_road=False,
has_largest_army=False, knights_played=0),
PlayerState(player_id=2, name="Charlie", cards=[], dev_cards=[],
settlements=[], cities=[], roads=[], victory_points=0,
longest_road_length=0, has_longest_road=False,
has_largest_army=False, knights_played=0)
]
state = GameState(players_state=players)
# Existing player
alice = state.get_player_state(0)
assert alice is not None
assert alice.name == "Alice"
# Non-existing player
missing = state.get_player_state(1)
assert missing is None
def test_get_current_player_state(self):
"""Test getting current player state."""
players = [
PlayerState(player_id=0, name="Alice", cards=[], dev_cards=[],
settlements=[], cities=[], roads=[], victory_points=0,
longest_road_length=0, has_longest_road=False,
has_largest_army=False, knights_played=0),
PlayerState(player_id=1, name="Bob", cards=[], dev_cards=[],
settlements=[], cities=[], roads=[], victory_points=0,
longest_road_length=0, has_longest_road=False,
has_largest_army=False, knights_played=0)
]
state = GameState(players_state=players, current_player=1)
current = state.get_current_player_state()
assert current is not None
assert current.name == "Bob"
class TestActionResult:
"""Test ActionResult dataclass."""
def test_success_result_creation(self):
"""Test creating a success result."""
state = GameState()
result = ActionResult.success_result(state, [0, 1])
assert result.success is True
assert result.error_message is None
assert result.updated_state == state
assert result.affected_players == [0, 1]
assert result.status_code == "ALL_GOOD"
def test_success_result_default_players(self):
"""Test success result with default affected players."""
state = GameState()
result = ActionResult.success_result(state)
assert result.affected_players == []
def test_failure_result_creation(self):
"""Test creating a failure result."""
error_msg = "Invalid move"
status_code = "ERR_BLOCKED"
result = ActionResult.failure_result(error_msg, status_code)
assert result.success is False
assert result.error_message == error_msg
assert result.updated_state is None
assert result.affected_players == []
assert result.status_code == status_code
def test_failure_result_default_status(self):
"""Test failure result with default status code."""
result = ActionResult.failure_result("Error occurred")
assert result.status_code == ""
class TestUtilityFunctions:
"""Test utility functions for action creation."""
def test_create_build_settlement_action(self):
"""Test building settlement action creation."""
# Normal settlement
action = create_build_settlement_action(player_id=1, point_coords=(2, 3))
assert action.action_type == ActionType.BUILD_SETTLEMENT
assert action.player_id == 1
assert action.parameters['point_coords'] == (2, 3)
def test_create_build_settlement_action_starting(self):
"""Test building starting settlement action creation."""
action = create_build_settlement_action(player_id=0, point_coords=(1, 1), is_starting=True)
assert action.action_type == ActionType.PLACE_STARTING_SETTLEMENT
assert action.player_id == 0
assert action.parameters['point_coords'] == (1, 1)
def test_create_build_road_action(self):
"""Test building road action creation."""
# Normal road
action = create_build_road_action(player_id=2, start_coords=(1, 1), end_coords=(1, 2))
assert action.action_type == ActionType.BUILD_ROAD
assert action.player_id == 2
assert action.parameters['start_coords'] == (1, 1)
assert action.parameters['end_coords'] == (1, 2)
def test_create_build_road_action_starting(self):
"""Test building starting road action creation."""
action = create_build_road_action(player_id=1, start_coords=(2, 2), end_coords=(2, 3), is_starting=True)
assert action.action_type == ActionType.PLACE_STARTING_ROAD
assert action.player_id == 1
assert action.parameters['start_coords'] == (2, 2)
assert action.parameters['end_coords'] == (2, 3)
def test_create_trade_action_player(self):
"""Test player-to-player trade action creation."""
offer = {'wood': 2}
request = {'brick': 1}
action = create_trade_action(player_id=0, offer=offer, request=request, target_player=2)
assert action.action_type == ActionType.TRADE_PROPOSE
assert action.player_id == 0
assert action.parameters['offer'] == offer
assert action.parameters['request'] == request
assert action.parameters['target_player'] == 2
def test_create_trade_action_bank(self):
"""Test bank trade action creation."""
offer = {'wood': 4}
request = {'brick': 1}
action = create_trade_action(player_id=1, offer=offer, request=request)
assert action.action_type == ActionType.TRADE_BANK
assert action.player_id == 1
assert action.parameters['offer'] == offer
assert action.parameters['request'] == request
assert 'target_player' not in action.parameters
class TestDataStructureIntegrity:
"""Test data structure relationships and integrity."""
def test_game_state_action_history_integration(self):
"""Test that actions can be properly stored in game state history."""
action = Action(ActionType.BUILD_SETTLEMENT, player_id=0, parameters={'point_coords': (1, 1)})
state = GameState(action_history=[action])
assert len(state.action_history) == 1
assert state.action_history[0].action_type == ActionType.BUILD_SETTLEMENT
def test_player_state_data_consistency(self):
"""Test player state data consistency."""
player = PlayerState(
player_id=0,
name="Test Player",
cards=['wood', 'brick'],
dev_cards=['knight'],
settlements=[(1, 1)],
cities=[(2, 2)],
roads=[((1, 1), (1, 2))],
victory_points=3,
longest_road_length=5,
has_longest_road=True,
has_largest_army=False,
knights_played=2
)
assert player.player_id == 0
assert len(player.cards) == 2
assert len(player.dev_cards) == 1
assert len(player.settlements) == 1
assert len(player.cities) == 1
assert len(player.roads) == 1
def test_action_timestamp_creation(self):
"""Test that action timestamp is properly set."""
import time
# Record time before and after action creation
time_before = datetime.now()
time.sleep(0.001) # Small delay to ensure timestamp difference
action = Action(ActionType.END_TURN, player_id=0)
time.sleep(0.001)
time_after = datetime.now()
# Verify timestamp is between before and after
assert time_before < action.timestamp < time_after
if __name__ == '__main__':
pytest.main([__file__, '-v'])