Spaces:
Running
Running
| """ | |
| Devil's Dozen - Base Classes Tests | |
| Tests for dataclasses, enums, and validation utilities. | |
| """ | |
| import pytest | |
| from src.engine.base import ( | |
| DiceRoll, | |
| DiceType, | |
| GameConfig, | |
| GameMode, | |
| ScoringBreakdown, | |
| ScoringCategory, | |
| ScoringResult, | |
| Tier, | |
| TurnState, | |
| ) | |
| from src.engine.validators import ( | |
| validate_dice_values, | |
| validate_held_indices, | |
| validate_player_count, | |
| validate_score, | |
| validate_target_score, | |
| ) | |
| class TestDiceType: | |
| """Tests for DiceType enum.""" | |
| def test_d6_value(self): | |
| assert DiceType.D6.value == 6 | |
| def test_d20_value(self): | |
| assert DiceType.D20.value == 20 | |
| class TestGameMode: | |
| """Tests for GameMode enum.""" | |
| def test_peasants_gamble_value(self): | |
| assert GameMode.PEASANTS_GAMBLE.value == "peasants_gamble" | |
| def test_alchemists_ascent_value(self): | |
| assert GameMode.ALCHEMISTS_ASCENT.value == "alchemists_ascent" | |
| class TestTier: | |
| """Tests for Tier enum.""" | |
| def test_tier_values(self): | |
| assert Tier.RED.value == 1 | |
| assert Tier.GREEN.value == 2 | |
| assert Tier.BLUE.value == 3 | |
| class TestDiceRoll: | |
| """Tests for DiceRoll dataclass.""" | |
| def test_create_valid_d6_roll(self): | |
| roll = DiceRoll(values=(1, 2, 3, 4, 5, 6), dice_type=DiceType.D6) | |
| assert len(roll) == 6 | |
| assert roll.values == (1, 2, 3, 4, 5, 6) | |
| def test_create_valid_d20_roll(self): | |
| roll = DiceRoll(values=(1, 10, 20), dice_type=DiceType.D20) | |
| assert len(roll) == 3 | |
| def test_invalid_d6_value_raises(self): | |
| with pytest.raises(ValueError, match="Invalid die value 7"): | |
| DiceRoll(values=(1, 2, 7), dice_type=DiceType.D6) | |
| def test_invalid_d20_value_raises(self): | |
| with pytest.raises(ValueError, match="Invalid die value 21"): | |
| DiceRoll(values=(1, 21), dice_type=DiceType.D20) | |
| def test_zero_value_raises(self): | |
| with pytest.raises(ValueError, match="Invalid die value 0"): | |
| DiceRoll(values=(0, 1, 2), dice_type=DiceType.D6) | |
| def test_negative_value_raises(self): | |
| with pytest.raises(ValueError, match="Invalid die value -1"): | |
| DiceRoll(values=(-1, 1, 2), dice_type=DiceType.D6) | |
| def test_from_sequence_list(self): | |
| roll = DiceRoll.from_sequence([1, 2, 3], DiceType.D6) | |
| assert roll.values == (1, 2, 3) | |
| def test_indexing(self): | |
| roll = DiceRoll(values=(1, 2, 3), dice_type=DiceType.D6) | |
| assert roll[0] == 1 | |
| assert roll[2] == 3 | |
| class TestScoringBreakdown: | |
| """Tests for ScoringBreakdown dataclass.""" | |
| def test_create_breakdown(self): | |
| breakdown = ScoringBreakdown( | |
| category=ScoringCategory.THREE_OF_A_KIND, | |
| dice_values=(4, 4, 4), | |
| points=400, | |
| description="Three 4s" | |
| ) | |
| assert breakdown.points == 400 | |
| assert breakdown.category == ScoringCategory.THREE_OF_A_KIND | |
| class TestScoringResult: | |
| """Tests for ScoringResult dataclass.""" | |
| def test_has_scoring_dice(self): | |
| result = ScoringResult( | |
| points=100, | |
| breakdown=tuple(), | |
| scoring_dice_indices=frozenset({0}), | |
| is_bust=False | |
| ) | |
| assert result.has_scoring_dice is True | |
| def test_no_scoring_dice(self): | |
| result = ScoringResult( | |
| points=0, | |
| breakdown=tuple(), | |
| scoring_dice_indices=frozenset(), | |
| is_bust=True | |
| ) | |
| assert result.has_scoring_dice is False | |
| def test_str_bust(self): | |
| result = ScoringResult( | |
| points=0, | |
| breakdown=tuple(), | |
| scoring_dice_indices=frozenset(), | |
| is_bust=True | |
| ) | |
| assert "BUST" in str(result) | |
| def test_str_with_breakdown(self): | |
| breakdown = ScoringBreakdown( | |
| category=ScoringCategory.SINGLE_ONE, | |
| dice_values=(1,), | |
| points=100, | |
| description="Single 1" | |
| ) | |
| result = ScoringResult( | |
| points=100, | |
| breakdown=(breakdown,), | |
| scoring_dice_indices=frozenset({0}), | |
| is_bust=False | |
| ) | |
| output = str(result) | |
| assert "100" in output | |
| assert "Single 1" in output | |
| class TestTurnState: | |
| """Tests for TurnState dataclass.""" | |
| def test_available_dice_count(self): | |
| state = TurnState( | |
| active_dice=(1, 2, 3, 4, 5, 6), | |
| held_indices=frozenset({0, 1}), | |
| turn_score=0, | |
| roll_count=1 | |
| ) | |
| assert state.available_dice_count == 4 | |
| def test_held_dice_values(self): | |
| state = TurnState( | |
| active_dice=(1, 2, 3, 4, 5, 6), | |
| held_indices=frozenset({0, 2}), | |
| turn_score=0, | |
| roll_count=1 | |
| ) | |
| assert state.held_dice_values == (1, 3) | |
| def test_unheld_dice_values(self): | |
| state = TurnState( | |
| active_dice=(1, 2, 3, 4, 5, 6), | |
| held_indices=frozenset({0, 2}), | |
| turn_score=0, | |
| roll_count=1 | |
| ) | |
| assert state.unheld_dice_values == (2, 4, 5, 6) | |
| class TestGameConfig: | |
| """Tests for GameConfig dataclass.""" | |
| def test_valid_peasants_gamble_config(self): | |
| config = GameConfig( | |
| mode=GameMode.PEASANTS_GAMBLE, | |
| target_score=5000, | |
| num_players=4 | |
| ) | |
| assert config.target_score == 5000 | |
| def test_invalid_peasants_gamble_target(self): | |
| with pytest.raises(ValueError, match="must be one of"): | |
| GameConfig( | |
| mode=GameMode.PEASANTS_GAMBLE, | |
| target_score=4000, | |
| num_players=2 | |
| ) | |
| def test_valid_alchemists_ascent_config(self): | |
| config = GameConfig( | |
| mode=GameMode.ALCHEMISTS_ASCENT, | |
| target_score=250, | |
| num_players=3 | |
| ) | |
| assert config.target_score == 250 | |
| def test_invalid_alchemists_ascent_target(self): | |
| with pytest.raises(ValueError, match="must be 250"): | |
| GameConfig( | |
| mode=GameMode.ALCHEMISTS_ASCENT, | |
| target_score=300, | |
| num_players=2 | |
| ) | |
| def test_invalid_player_count_low(self): | |
| with pytest.raises(ValueError, match="between 2 and 4"): | |
| GameConfig( | |
| mode=GameMode.PEASANTS_GAMBLE, | |
| target_score=5000, | |
| num_players=1 | |
| ) | |
| def test_invalid_player_count_high(self): | |
| with pytest.raises(ValueError, match="between 2 and 4"): | |
| GameConfig( | |
| mode=GameMode.PEASANTS_GAMBLE, | |
| target_score=5000, | |
| num_players=5 | |
| ) | |
| class TestValidators: | |
| """Tests for validation utilities.""" | |
| class TestValidateDiceValues: | |
| def test_valid_d6_values(self): | |
| result = validate_dice_values([1, 2, 3, 4, 5, 6], DiceType.D6) | |
| assert result == (1, 2, 3, 4, 5, 6) | |
| def test_empty_with_min_zero(self): | |
| result = validate_dice_values([], DiceType.D6, min_count=0) | |
| assert result == tuple() | |
| def test_empty_with_min_one_raises(self): | |
| with pytest.raises(ValueError, match="At least 1"): | |
| validate_dice_values([], DiceType.D6, min_count=1) | |
| def test_too_many_dice(self): | |
| with pytest.raises(ValueError, match="At most 6"): | |
| validate_dice_values([1, 2, 3, 4, 5, 6, 7], DiceType.D6, max_count=6) | |
| def test_invalid_value(self): | |
| with pytest.raises(ValueError, match="must be between 1 and 6"): | |
| validate_dice_values([1, 2, 7], DiceType.D6) | |
| def test_non_integer_value(self): | |
| with pytest.raises(ValueError, match="must be an integer"): | |
| validate_dice_values([1, 2, "three"], DiceType.D6) | |
| class TestValidateHeldIndices: | |
| def test_valid_indices(self): | |
| result = validate_held_indices({0, 2, 4}, dice_count=6) | |
| assert result == frozenset({0, 2, 4}) | |
| def test_empty_indices(self): | |
| result = validate_held_indices(set(), dice_count=6) | |
| assert result == frozenset() | |
| def test_out_of_range_index(self): | |
| with pytest.raises(ValueError, match="out of range"): | |
| validate_held_indices({0, 6}, dice_count=6) | |
| def test_negative_index(self): | |
| with pytest.raises(ValueError, match="out of range"): | |
| validate_held_indices({-1, 0}, dice_count=6) | |
| class TestValidateScore: | |
| def test_valid_positive_score(self): | |
| assert validate_score(100) == 100 | |
| def test_zero_score(self): | |
| assert validate_score(0) == 0 | |
| def test_negative_score_raises(self): | |
| with pytest.raises(ValueError, match="cannot be negative"): | |
| validate_score(-100) | |
| def test_negative_score_allowed(self): | |
| assert validate_score(-100, allow_negative=True) == -100 | |
| class TestValidatePlayerCount: | |
| def test_valid_counts(self): | |
| for count in [2, 3, 4]: | |
| assert validate_player_count(count) == count | |
| def test_invalid_count(self): | |
| with pytest.raises(ValueError, match="must be 2-4"): | |
| validate_player_count(5) | |
| class TestValidateTargetScore: | |
| def test_valid_target(self): | |
| assert validate_target_score(5000) == 5000 | |
| def test_invalid_target_zero(self): | |
| with pytest.raises(ValueError, match="must be positive"): | |
| validate_target_score(0) | |
| def test_invalid_target_option(self): | |
| with pytest.raises(ValueError, match="must be one of"): | |
| validate_target_score(4000, valid_options={3000, 5000, 10000}) | |