strat-gameplay-webapp/backend/tests/unit/config/test_play_outcome.py
Cal Corum 6880b6d5ad CLAUDE: Complete Week 6 - granular PlayOutcome integration and metadata support
- Renamed check_d20 → chaos_d20 throughout dice system
- Expanded PlayOutcome enum with granular variants (SINGLE_1/2, DOUBLE_2/3, GROUNDBALL_A/B/C, etc.)
- Integrated PlayOutcome from app.config into PlayResolver
- Added play_metadata support for uncapped hit tracking
- Updated all tests (139/140 passing)

Week 6: 100% Complete - Ready for Phase 3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 20:29:06 -05:00

226 lines
9.4 KiB
Python

"""
Unit tests for PlayOutcome enum.
Tests helper methods and outcome categorization.
"""
import pytest
from app.config import PlayOutcome
class TestPlayOutcomeHelpers:
"""Tests for PlayOutcome helper methods."""
def test_is_hit_for_singles(self):
"""Single outcomes are hits."""
assert PlayOutcome.SINGLE_1.is_hit() is True
assert PlayOutcome.SINGLE_2.is_hit() is True
assert PlayOutcome.SINGLE_UNCAPPED.is_hit() is True
assert PlayOutcome.BP_SINGLE.is_hit() is True
def test_is_hit_for_extra_bases(self):
"""Extra base hits are hits."""
assert PlayOutcome.DOUBLE_2.is_hit() is True
assert PlayOutcome.DOUBLE_3.is_hit() is True
assert PlayOutcome.DOUBLE_UNCAPPED.is_hit() is True
assert PlayOutcome.TRIPLE.is_hit() is True
assert PlayOutcome.HOMERUN.is_hit() is True
assert PlayOutcome.BP_HOMERUN.is_hit() is True
def test_is_hit_false_for_outs(self):
"""Outs are not hits."""
assert PlayOutcome.STRIKEOUT.is_hit() is False
assert PlayOutcome.GROUNDBALL_A.is_hit() is False
assert PlayOutcome.FLYOUT_A.is_hit() is False
assert PlayOutcome.BP_FLYOUT.is_hit() is False
def test_is_hit_false_for_walks(self):
"""Walks are not hits."""
assert PlayOutcome.WALK.is_hit() is False
assert PlayOutcome.INTENTIONAL_WALK.is_hit() is False
def test_is_out_for_standard_outs(self):
"""Standard outs are outs."""
assert PlayOutcome.STRIKEOUT.is_out() is True
assert PlayOutcome.GROUNDBALL_A.is_out() is True
assert PlayOutcome.GROUNDBALL_B.is_out() is True
assert PlayOutcome.GROUNDBALL_C.is_out() is True
assert PlayOutcome.FLYOUT_A.is_out() is True
assert PlayOutcome.FLYOUT_B.is_out() is True
assert PlayOutcome.FLYOUT_C.is_out() is True
assert PlayOutcome.LINEOUT.is_out() is True
assert PlayOutcome.POPOUT.is_out() is True
def test_is_out_for_baserunning_outs(self):
"""Baserunning outs are outs."""
assert PlayOutcome.CAUGHT_STEALING.is_out() is True
assert PlayOutcome.PICK_OFF.is_out() is True
def test_is_out_for_ballpark_outs(self):
"""Ballpark outs are outs."""
assert PlayOutcome.BP_FLYOUT.is_out() is True
assert PlayOutcome.BP_LINEOUT.is_out() is True
def test_is_out_false_for_hits(self):
"""Hits are not outs."""
assert PlayOutcome.SINGLE_1.is_out() is False
assert PlayOutcome.HOMERUN.is_out() is False
def test_is_walk(self):
"""Walk outcomes are walks."""
assert PlayOutcome.WALK.is_walk() is True
assert PlayOutcome.INTENTIONAL_WALK.is_walk() is True
def test_is_walk_false_for_hits(self):
"""Hits are not walks."""
assert PlayOutcome.SINGLE_1.is_walk() is False
assert PlayOutcome.HOMERUN.is_walk() is False
def test_is_walk_false_for_hbp(self):
"""HBP is not a walk (different stat)."""
assert PlayOutcome.HIT_BY_PITCH.is_walk() is False
def test_is_uncapped(self):
"""Uncapped outcomes are uncapped."""
assert PlayOutcome.SINGLE_UNCAPPED.is_uncapped() is True
assert PlayOutcome.DOUBLE_UNCAPPED.is_uncapped() is True
def test_is_uncapped_false_for_normal_hits(self):
"""Normal hits are not uncapped."""
assert PlayOutcome.SINGLE_1.is_uncapped() is False
assert PlayOutcome.DOUBLE_2.is_uncapped() is False
assert PlayOutcome.TRIPLE.is_uncapped() is False
def test_is_interrupt(self):
"""Interrupt plays are interrupts."""
assert PlayOutcome.WILD_PITCH.is_interrupt() is True
assert PlayOutcome.PASSED_BALL.is_interrupt() is True
assert PlayOutcome.STOLEN_BASE.is_interrupt() is True
assert PlayOutcome.CAUGHT_STEALING.is_interrupt() is True
assert PlayOutcome.BALK.is_interrupt() is True
assert PlayOutcome.PICK_OFF.is_interrupt() is True
def test_is_interrupt_false_for_normal_plays(self):
"""Normal plays are not interrupts."""
assert PlayOutcome.SINGLE_1.is_interrupt() is False
assert PlayOutcome.STRIKEOUT.is_interrupt() is False
assert PlayOutcome.WALK.is_interrupt() is False
def test_is_extra_base_hit(self):
"""Extra base hits are identified correctly."""
assert PlayOutcome.DOUBLE_2.is_extra_base_hit() is True
assert PlayOutcome.DOUBLE_3.is_extra_base_hit() is True
assert PlayOutcome.DOUBLE_UNCAPPED.is_extra_base_hit() is True
assert PlayOutcome.TRIPLE.is_extra_base_hit() is True
assert PlayOutcome.HOMERUN.is_extra_base_hit() is True
assert PlayOutcome.BP_HOMERUN.is_extra_base_hit() is True
def test_is_extra_base_hit_false_for_singles(self):
"""Singles are not extra base hits."""
assert PlayOutcome.SINGLE_1.is_extra_base_hit() is False
assert PlayOutcome.SINGLE_UNCAPPED.is_extra_base_hit() is False
assert PlayOutcome.BP_SINGLE.is_extra_base_hit() is False
class TestGetBasesAdvanced:
"""Tests for get_bases_advanced() method."""
def test_singles_advance_one_base(self):
"""Singles advance one base."""
assert PlayOutcome.SINGLE_1.get_bases_advanced() == 1
assert PlayOutcome.SINGLE_2.get_bases_advanced() == 1
assert PlayOutcome.SINGLE_UNCAPPED.get_bases_advanced() == 1
assert PlayOutcome.BP_SINGLE.get_bases_advanced() == 1
def test_doubles_advance_two_bases(self):
"""Doubles advance two bases."""
assert PlayOutcome.DOUBLE_2.get_bases_advanced() == 2
assert PlayOutcome.DOUBLE_3.get_bases_advanced() == 2
assert PlayOutcome.DOUBLE_UNCAPPED.get_bases_advanced() == 2
def test_triples_advance_three_bases(self):
"""Triples advance three bases."""
assert PlayOutcome.TRIPLE.get_bases_advanced() == 3
def test_homeruns_advance_four_bases(self):
"""Home runs advance four bases."""
assert PlayOutcome.HOMERUN.get_bases_advanced() == 4
assert PlayOutcome.BP_HOMERUN.get_bases_advanced() == 4
def test_outs_advance_zero_bases(self):
"""Outs advance zero bases."""
assert PlayOutcome.STRIKEOUT.get_bases_advanced() == 0
assert PlayOutcome.GROUNDBALL_A.get_bases_advanced() == 0
assert PlayOutcome.FLYOUT_A.get_bases_advanced() == 0
def test_walks_advance_zero_bases(self):
"""Walks advance zero bases (forced advancement handled separately)."""
assert PlayOutcome.WALK.get_bases_advanced() == 0
assert PlayOutcome.INTENTIONAL_WALK.get_bases_advanced() == 0
def test_interrupts_advance_zero_bases(self):
"""Interrupts advance zero bases (advancement handled by interrupt logic)."""
assert PlayOutcome.WILD_PITCH.get_bases_advanced() == 0
assert PlayOutcome.STOLEN_BASE.get_bases_advanced() == 0
class TestPlayOutcomeValues:
"""Tests for PlayOutcome string values."""
def test_outcome_string_values(self):
"""Outcome values match expected strings."""
assert PlayOutcome.STRIKEOUT.value == "strikeout"
assert PlayOutcome.SINGLE_1.value == "single_1"
assert PlayOutcome.SINGLE_UNCAPPED.value == "single_uncapped"
assert PlayOutcome.WILD_PITCH.value == "wild_pitch"
assert PlayOutcome.BP_HOMERUN.value == "bp_homerun"
def test_can_create_from_string(self):
"""Can create PlayOutcome from string value."""
outcome = PlayOutcome("strikeout")
assert outcome == PlayOutcome.STRIKEOUT
def test_uncapped_uses_code_friendly_names(self):
"""Uncapped outcomes use underscores not parentheses."""
assert "(" not in PlayOutcome.SINGLE_UNCAPPED.value
assert "(" not in PlayOutcome.DOUBLE_UNCAPPED.value
assert "_" in PlayOutcome.SINGLE_UNCAPPED.value
class TestPlayOutcomeCompleteness:
"""Tests to ensure all outcome categories are covered."""
def test_all_hits_categorized(self):
"""All hit outcomes are properly categorized."""
hit_outcomes = {
PlayOutcome.SINGLE_1, PlayOutcome.SINGLE_2, PlayOutcome.DOUBLE_2,
PlayOutcome.DOUBLE_3, PlayOutcome.TRIPLE,
PlayOutcome.HOMERUN, PlayOutcome.SINGLE_UNCAPPED,
PlayOutcome.DOUBLE_UNCAPPED, PlayOutcome.BP_HOMERUN,
PlayOutcome.BP_SINGLE
}
for outcome in hit_outcomes:
assert outcome.is_hit() is True, f"{outcome} should be a hit"
def test_all_outs_categorized(self):
"""All out outcomes are properly categorized."""
out_outcomes = {
PlayOutcome.STRIKEOUT,
PlayOutcome.GROUNDBALL_A, PlayOutcome.GROUNDBALL_B, PlayOutcome.GROUNDBALL_C,
PlayOutcome.FLYOUT_A, PlayOutcome.FLYOUT_B, PlayOutcome.FLYOUT_C,
PlayOutcome.LINEOUT, PlayOutcome.POPOUT,
PlayOutcome.CAUGHT_STEALING, PlayOutcome.PICK_OFF,
PlayOutcome.BP_FLYOUT, PlayOutcome.BP_LINEOUT
}
for outcome in out_outcomes:
assert outcome.is_out() is True, f"{outcome} should be an out"
def test_all_interrupts_categorized(self):
"""All interrupt outcomes are properly categorized."""
interrupt_outcomes = {
PlayOutcome.WILD_PITCH, PlayOutcome.PASSED_BALL,
PlayOutcome.STOLEN_BASE, PlayOutcome.CAUGHT_STEALING,
PlayOutcome.BALK, PlayOutcome.PICK_OFF
}
for outcome in interrupt_outcomes:
assert outcome.is_interrupt() is True, f"{outcome} should be an interrupt"