""" Unit Tests for Dice System Tests cryptographic dice rolling system with all roll types. """ import pytest from uuid import uuid4 from app.core.dice import DiceSystem from app.core.roll_types import RollType, AbRoll, JumpRoll, FieldingRoll, D20Roll class TestDiceSystemBasic: """Test basic DiceSystem functionality""" def test_dice_system_creation(self): """Test creating a DiceSystem""" dice = DiceSystem() assert dice is not None assert len(dice._roll_history) == 0 def test_singleton_access(self): """Test singleton dice_system access""" from app.core.dice import dice_system assert dice_system is not None class TestAbRolls: """Test at-bat rolls""" def test_roll_ab_basic(self): """Test basic at-bat roll""" dice = DiceSystem() roll = dice.roll_ab(league_id="sba") assert isinstance(roll, AbRoll) assert roll.roll_type == RollType.AB assert roll.league_id == "sba" assert 1 <= roll.d6_one <= 6 assert 1 <= roll.d6_two_a <= 6 assert 1 <= roll.d6_two_b <= 6 assert 1 <= roll.chaos_d20 <= 20 assert 1 <= roll.resolution_d20 <= 20 assert roll.d6_two_total == roll.d6_two_a + roll.d6_two_b def test_roll_ab_with_game_id(self): """Test at-bat roll with game_id""" dice = DiceSystem() game_id = uuid4() roll = dice.roll_ab(league_id="pd", game_id=game_id) assert roll.game_id == game_id assert roll.league_id == "pd" def test_roll_ab_adds_to_history(self): """Test that AB rolls are added to history""" dice = DiceSystem() initial_count = len(dice._roll_history) dice.roll_ab(league_id="sba") assert len(dice._roll_history) == initial_count + 1 assert isinstance(dice._roll_history[-1], AbRoll) def test_roll_ab_unique_roll_ids(self): """Test that each roll gets unique ID""" dice = DiceSystem() roll1 = dice.roll_ab(league_id="sba") roll2 = dice.roll_ab(league_id="sba") assert roll1.roll_id != roll2.roll_id def test_roll_ab_wild_pitch_check_distribution(self): """Test that wild pitch checks occur (roll 1 on chaos_d20)""" dice = DiceSystem() found_wp_check = False for _ in range(100): roll = dice.roll_ab(league_id="sba") if roll.check_wild_pitch: found_wp_check = True assert roll.chaos_d20 == 1 assert 1 <= roll.resolution_d20 <= 20 break # Should find at least one in 100 rolls (probability ~99.4%) assert found_wp_check, "No wild pitch check found in 100 rolls" def test_roll_ab_passed_ball_check_distribution(self): """Test that passed ball checks occur (roll 2 on chaos_d20)""" dice = DiceSystem() found_pb_check = False for _ in range(100): roll = dice.roll_ab(league_id="sba") if roll.check_passed_ball: found_pb_check = True assert roll.chaos_d20 == 2 assert 1 <= roll.resolution_d20 <= 20 break assert found_pb_check, "No passed ball check found in 100 rolls" class TestJumpRolls: """Test jump rolls""" def test_roll_jump_basic(self): """Test basic jump roll""" dice = DiceSystem() roll = dice.roll_jump(league_id="sba") assert isinstance(roll, JumpRoll) assert roll.roll_type == RollType.JUMP assert roll.league_id == "sba" assert 1 <= roll.check_roll <= 20 def test_roll_jump_normal(self): """Test normal jump (check_roll >= 3)""" dice = DiceSystem() found_normal = False for _ in range(50): roll = dice.roll_jump(league_id="sba") if roll.check_roll >= 3: found_normal = True assert roll.jump_dice_a is not None assert roll.jump_dice_b is not None assert 1 <= roll.jump_dice_a <= 6 assert 1 <= roll.jump_dice_b <= 6 assert roll.jump_total == roll.jump_dice_a + roll.jump_dice_b assert roll.resolution_roll is None break assert found_normal def test_roll_jump_pickoff(self): """Test pickoff check (check_roll == 1)""" dice = DiceSystem() found_pickoff = False for _ in range(100): roll = dice.roll_jump(league_id="sba") if roll.check_roll == 1: found_pickoff = True assert roll.is_pickoff_check assert not roll.is_balk_check assert roll.resolution_roll is not None assert 1 <= roll.resolution_roll <= 20 assert roll.jump_dice_a is None assert roll.jump_dice_b is None break assert found_pickoff def test_roll_jump_balk(self): """Test balk check (check_roll == 2)""" dice = DiceSystem() found_balk = False for _ in range(100): roll = dice.roll_jump(league_id="sba") if roll.check_roll == 2: found_balk = True assert roll.is_balk_check assert not roll.is_pickoff_check assert roll.resolution_roll is not None assert roll.jump_dice_a is None break assert found_balk def test_roll_jump_adds_to_history(self): """Test that jump rolls are added to history""" dice = DiceSystem() initial_count = len(dice._roll_history) dice.roll_jump(league_id="pd") assert len(dice._roll_history) == initial_count + 1 class TestFieldingRolls: """Test fielding rolls""" def test_roll_fielding_basic(self): """Test basic fielding roll""" dice = DiceSystem() roll = dice.roll_fielding(position="SS", league_id="sba") assert isinstance(roll, FieldingRoll) assert roll.roll_type == RollType.FIELDING assert roll.position == "SS" assert roll.league_id == "sba" assert 1 <= roll.d20 <= 20 assert 1 <= roll.d6_one <= 6 assert 1 <= roll.d6_two <= 6 assert 1 <= roll.d6_three <= 6 assert 1 <= roll.d100 <= 100 assert 3 <= roll.error_total <= 18 def test_roll_fielding_all_positions(self): """Test fielding roll for all valid positions""" dice = DiceSystem() positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] for pos in positions: roll = dice.roll_fielding(position=pos, league_id="sba") assert roll.position == pos def test_roll_fielding_invalid_position(self): """Test that invalid position raises error""" dice = DiceSystem() with pytest.raises(ValueError, match="Invalid position"): dice.roll_fielding(position="DH", league_id="sba") def test_roll_fielding_sba_rare_play(self): """Test SBA rare play detection (d100 == 1)""" dice = DiceSystem() found_rare = False # Rare play is 1%, so test many times for _ in range(500): roll = dice.roll_fielding(position="CF", league_id="sba") if roll.d100 == 1: found_rare = True assert roll.is_rare_play break # Note: Might occasionally fail due to randomness (0.6% chance) # In production, we'd mock the dice for deterministic testing def test_roll_fielding_pd_rare_play(self): """Test PD rare play detection (error_total == 5)""" dice = DiceSystem() found_rare = False # error_total of 5 is about 2.7% chance (3d6: 1+1+3, 1+2+2) for _ in range(500): roll = dice.roll_fielding(position="1B", league_id="pd") if roll.error_total == 5: found_rare = True assert roll.is_rare_play break def test_roll_fielding_adds_to_history(self): """Test that fielding rolls are added to history""" dice = DiceSystem() initial_count = len(dice._roll_history) dice.roll_fielding(position="3B", league_id="pd") assert len(dice._roll_history) == initial_count + 1 class TestD20Rolls: """Test generic d20 rolls""" def test_roll_d20_basic(self): """Test basic d20 roll""" dice = DiceSystem() roll = dice.roll_d20(league_id="sba") assert isinstance(roll, D20Roll) assert roll.roll_type == RollType.D20 assert roll.league_id == "sba" assert 1 <= roll.roll <= 20 def test_roll_d20_with_game_id(self): """Test d20 roll with game_id""" dice = DiceSystem() game_id = uuid4() roll = dice.roll_d20(league_id="pd", game_id=game_id) assert roll.game_id == game_id def test_roll_d20_adds_to_history(self): """Test that d20 rolls are added to history""" dice = DiceSystem() initial_count = len(dice._roll_history) dice.roll_d20(league_id="sba") assert len(dice._roll_history) == initial_count + 1 def test_roll_d20_distribution(self): """Test d20 distribution is roughly uniform""" dice = DiceSystem() rolls = [dice.roll_d20(league_id="sba").roll for _ in range(1000)] # Count occurrences of each value counts = {i: rolls.count(i) for i in range(1, 21)} # Each value should appear roughly 50 times (1000/20) # Allow for variance - check all values appear at least 20 times for value, count in counts.items(): assert count >= 20, f"Value {value} appeared only {count} times" class TestRollHistory: """Test roll history management""" def test_get_roll_history_all(self): """Test getting all roll history""" dice = DiceSystem() dice.clear_history() dice.roll_ab(league_id="sba") dice.roll_jump(league_id="sba") dice.roll_fielding(position="SS", league_id="sba") history = dice.get_roll_history() assert len(history) == 3 def test_get_roll_history_by_type(self): """Test filtering history by roll type""" dice = DiceSystem() dice.clear_history() dice.roll_ab(league_id="sba") dice.roll_ab(league_id="sba") dice.roll_jump(league_id="sba") dice.roll_fielding(position="SS", league_id="sba") ab_rolls = dice.get_roll_history(roll_type=RollType.AB) assert len(ab_rolls) == 2 assert all(r.roll_type == RollType.AB for r in ab_rolls) def test_get_roll_history_by_game(self): """Test filtering history by game_id""" dice = DiceSystem() dice.clear_history() game1 = uuid4() game2 = uuid4() dice.roll_ab(league_id="sba", game_id=game1) dice.roll_ab(league_id="sba", game_id=game1) dice.roll_ab(league_id="sba", game_id=game2) game1_rolls = dice.get_roll_history(game_id=game1) assert len(game1_rolls) == 2 assert all(r.game_id == game1 for r in game1_rolls) def test_get_roll_history_limit(self): """Test limit parameter""" dice = DiceSystem() dice.clear_history() for _ in range(10): dice.roll_d20(league_id="sba") limited = dice.get_roll_history(limit=5) assert len(limited) == 5 def test_get_rolls_since(self): """Test getting rolls since timestamp""" dice = DiceSystem() dice.clear_history() game_id = uuid4() import pendulum import time # Roll some dice roll1 = dice.roll_ab(league_id="sba", game_id=game_id) # Use roll1's timestamp as the cutoff point timestamp = roll1.timestamp # Sleep briefly to ensure roll2 has a later timestamp time.sleep(0.01) roll2 = dice.roll_jump(league_id="sba", game_id=game_id) # Get rolls since timestamp (should only get roll2 because timestamp >= is used) # We want rolls AFTER roll1, so add a tiny offset recent = dice.get_rolls_since(game_id, timestamp.add(microseconds=1)) assert len(recent) == 1 assert recent[0].roll_type == RollType.JUMP def test_verify_roll(self): """Test roll verification""" dice = DiceSystem() roll = dice.roll_d20(league_id="sba") assert dice.verify_roll(roll.roll_id) assert not dice.verify_roll("nonexistent_id") def test_clear_history(self): """Test clearing roll history""" dice = DiceSystem() dice.roll_ab(league_id="sba") dice.roll_jump(league_id="sba") assert len(dice._roll_history) > 0 dice.clear_history() assert len(dice._roll_history) == 0 class TestDistributionStats: """Test distribution statistics""" def test_get_distribution_stats(self): """Test getting distribution statistics""" dice = DiceSystem() dice.clear_history() dice.roll_ab(league_id="sba") dice.roll_ab(league_id="sba") dice.roll_jump(league_id="sba") stats = dice.get_distribution_stats() assert stats["total_rolls"] == 3 assert stats["by_type"]["ab"] == 2 assert stats["by_type"]["jump"] == 1 def test_get_distribution_stats_by_type(self): """Test getting stats for specific roll type""" dice = DiceSystem() dice.clear_history() dice.roll_ab(league_id="sba") dice.roll_ab(league_id="sba") dice.roll_jump(league_id="sba") ab_stats = dice.get_distribution_stats(roll_type=RollType.AB) assert ab_stats["total_rolls"] == 2 def test_get_stats(self): """Test get_stats helper method""" dice = DiceSystem() dice.clear_history() dice.roll_ab(league_id="sba") dice.roll_fielding(position="SS", league_id="sba") stats = dice.get_stats() assert stats["total_rolls"] == 2 assert "by_type" in stats class TestCryptographicRandomness: """Test that dice use cryptographic randomness""" def test_unique_roll_ids(self): """Test that roll IDs are unique""" dice = DiceSystem() roll_ids = set() for _ in range(100): roll = dice.roll_d20(league_id="sba") roll_ids.add(roll.roll_id) # All 100 should be unique assert len(roll_ids) == 100 def test_roll_id_format(self): """Test roll ID format (hex string)""" dice = DiceSystem() roll = dice.roll_d20(league_id="sba") # Should be hex string (16 chars for 8 bytes) assert len(roll.roll_id) == 16 assert all(c in '0123456789abcdef' for c in roll.roll_id)