Spaces:
Configuration error
Configuration error
| """ | |
| 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']) | |