File size: 6,118 Bytes
16a19cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import unittest

from streamlit.testing.v1 import AppTest

import app


class RunRateLabAnalyticsTests(unittest.TestCase):
    def test_probabilities_sum_to_approximately_100(self):
        probabilities, metrics = app.simulate_outcome_distribution(
            "Length Ball",
            timing_error_ms=0,
            power=70,
            required_rate=8.5,
            samples=5000,
        )
        self.assertAlmostEqual(probabilities["Probability"].sum(), 100.0, places=6)
        self.assertGreater(metrics["expected_runs"], 0)
        self.assertEqual(metrics["shot_intent"], "Drive")
        self.assertEqual(metrics["aim_lane"], "Straight")

    def test_perfect_timing_beats_large_timing_error(self):
        pitch = app.get_pitch("Length Ball")
        perfect_contact = app.calculate_contact(0, 70, pitch)
        late_contact = app.calculate_contact(180, 70, pitch)
        self.assertGreater(perfect_contact, late_contact)

    def test_required_rate_increases_wicket_risk(self):
        pitch = app.get_pitch("Cutter")
        contact = app.calculate_contact(20, 85, pitch)
        normal_risk = app.calculate_wicket_risk(contact, 85, 20, 8.0, pitch)
        pressure_risk = app.calculate_wicket_risk(contact, 85, 20, 12.0, pitch)
        self.assertGreater(pressure_risk, normal_risk)

    def test_overcharged_power_increases_wicket_risk(self):
        pitch = app.get_pitch("Bouncer")
        contact = app.calculate_contact(0, 95, pitch)
        normal_risk = app.calculate_wicket_risk(contact, 80, 0, 8.0, pitch)
        overhit_risk = app.calculate_wicket_risk(contact, 95, 0, 8.0, pitch)
        self.assertGreater(overhit_risk, normal_risk)

    def test_loft_has_higher_upside_and_risk_than_defend(self):
        pitch = app.get_pitch("Half Volley")
        defend_contact = app.calculate_contact(0, 88, pitch, "Defend", "Straight")
        defend_risk = app.calculate_wicket_risk(defend_contact, 88, 0, 8.0, pitch, "Defend", "Straight")
        defend_xruns = app.calculate_expected_runs(defend_contact, 88, defend_risk, "Defend", "Straight")

        loft_contact = app.calculate_contact(0, 88, pitch, "Loft", "Straight")
        loft_risk = app.calculate_wicket_risk(loft_contact, 88, 0, 8.0, pitch, "Loft", "Straight")
        loft_xruns = app.calculate_expected_runs(loft_contact, 88, loft_risk, "Loft", "Straight")

        self.assertGreater(loft_risk, defend_risk)
        self.assertGreater(loft_xruns, defend_xruns)

    def test_correct_shot_delivery_matchup_improves_contact(self):
        pitch = app.get_pitch("Bouncer")
        drive_contact = app.calculate_contact(0, 76, pitch, "Drive", "Straight")
        pull_contact = app.calculate_contact(0, 76, pitch, "Cut / Pull", "Straight")
        self.assertGreater(pull_contact, drive_contact)

    def test_decision_quality_thresholds(self):
        self.assertEqual(app.classify_decision_quality(4.5, 0.12), "Excellent Process")
        self.assertEqual(app.classify_decision_quality(3.0, 0.20), "Good Process")
        self.assertEqual(app.classify_decision_quality(4.0, 0.35), "Risky But Rational")
        self.assertEqual(app.classify_decision_quality(1.2, 0.18), "Low-Value Shot")
        self.assertEqual(app.classify_decision_quality(1.9, 0.35), "Poor Risk Tradeoff")

    def test_luck_classification(self):
        self.assertEqual(app.classify_luck(6, 2.0, False, 0.15), "Lucky Result")
        self.assertEqual(app.classify_luck(0, 4.0, False, 0.12), "Unlucky Result")
        self.assertEqual(app.classify_luck(0, 3.0, True, 0.10), "Unlucky Result")
        self.assertEqual(app.classify_luck(2, 2.2, False, 0.15), "Expected Result")

    def test_generated_game_model_uses_python_constants(self):
        rendered = app.build_game_html(180)
        self.assertNotIn("__DELIVERIES_JSON__", rendered)
        self.assertNotIn("__SHOTS_JSON__", rendered)
        self.assertNotIn("__AIMS_JSON__", rendered)
        for delivery in app.PITCH_MODEL["Delivery"]:
            self.assertIn(delivery, rendered)
        for shot in app.SHOT_MODEL["Shot"]:
            self.assertIn(shot, rendered)
        for aim in app.AIM_MODEL["Aim"]:
            self.assertIn(aim, rendered)

    def test_sample_table_has_ten_trials(self):
        table = app.simulate_sample_table("Yorker", 0, 70, 9.0, shot_intent="Defend", aim_lane="Straight")
        self.assertEqual(len(table), 10)
        self.assertIn("Shot", table.columns)
        self.assertIn("Aim", table.columns)
        self.assertIn("Decision", table.columns)

    def test_learning_summary_handles_partial_logs(self):
        summary = app.build_learning_summary([{"actual_runs": 4}, {"unexpected": 99}])
        self.assertEqual(summary["balls"], 2.0)
        self.assertEqual(summary["avg_xruns"], 0.0)
        self.assertEqual(summary["avg_wicket_risk"], 0.0)
        self.assertEqual(summary["avg_actual_runs"], 4.0)

    def test_game_pauses_for_review_between_balls(self):
        self.assertIn('id="nextBallBtn"', app.GAME_HTML)
        self.assertIn('state.mode = "between"', app.GAME_HTML)
        self.assertIn('state.mode === "between" ? "Review"', app.GAME_HTML)
        self.assertIn("Shot Intent", app.GAME_HTML)
        self.assertIn("Aim Lane", app.GAME_HTML)
        self.assertIn("Process Score", app.GAME_HTML)
        self.assertIn('id="processPill"', app.GAME_HTML)
        self.assertIn('id="settingsModal"', app.GAME_HTML)
        self.assertIn("Reduce Flashing", app.GAME_HTML)
        self.assertIn("hidden-during-play", app.GAME_HTML)
        self.assertIn("hidden-for-review", app.GAME_HTML)
        self.assertIn("Coach ready. Press Next Ball when done.", app.GAME_HTML)
        self.assertIn("Restart Match", app.GAME_HTML)


class RunRateLabStreamlitTests(unittest.TestCase):
    def test_streamlit_app_renders(self):
        test_app = AppTest.from_file("app.py")
        test_app.run(timeout=15)
        self.assertFalse(test_app.exception)
        self.assertTrue(any("RunRate Lab" in title.value for title in test_app.title))
        self.assertGreaterEqual(len(test_app.metric), 4)


if __name__ == "__main__":
    unittest.main()