ramanaprabhusana commited on
Commit
16a19cb
·
verified ·
1 Parent(s): afcf918

Deploy RunRate Lab cricket analytics coach

Browse files
Files changed (9) hide show
  1. .dockerignore +7 -0
  2. .gitignore +14 -0
  3. .streamlit/config.toml +12 -0
  4. Dockerfile +19 -0
  5. Makefile +16 -0
  6. README.md +158 -5
  7. app.py +0 -0
  8. requirements.txt +4 -0
  9. tests/test_runrate_lab.py +126 -0
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
5
+ .playwright-cli/
6
+ .git/
7
+ tests/__pycache__/
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
5
+ .playwright-cli/
6
+ .pytest_cache/
7
+ .streamlit/secrets.toml
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ *.log
12
+ output/
13
+ dist/
14
+ build/
.streamlit/config.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#22C55E"
3
+ backgroundColor = "#F7FAF5"
4
+ secondaryBackgroundColor = "#EAF5E5"
5
+ textColor = "#0F2418"
6
+ font = "sans serif"
7
+
8
+ [server]
9
+ maxUploadSize = 200
10
+
11
+ [browser]
12
+ gatherUsageStats = false
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1
6
+
7
+ WORKDIR /app
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --upgrade pip \
11
+ && pip install -r requirements.txt
12
+
13
+ COPY . .
14
+
15
+ EXPOSE 8501
16
+
17
+ HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8501/_stcore/health')"
18
+
19
+ CMD ["streamlit", "run", "app.py", "--server.address=0.0.0.0", "--server.port=8501"]
Makefile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: install run test check
2
+
3
+ install:
4
+ python3 -m venv .venv
5
+ .venv/bin/python -m pip install --upgrade pip
6
+ .venv/bin/python -m pip install -r requirements.txt
7
+
8
+ run:
9
+ .venv/bin/streamlit run app.py
10
+
11
+ test:
12
+ .venv/bin/python -m unittest discover -s tests
13
+
14
+ check:
15
+ .venv/bin/python -m compileall app.py tests
16
+ .venv/bin/python -m unittest discover -s tests
README.md CHANGED
@@ -1,10 +1,163 @@
1
  ---
2
- title: Runrate Lab
3
- emoji: 🏆
4
- colorFrom: red
5
- colorTo: red
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: RunRate Lab
3
+ emoji: 🏏
4
+ colorFrom: green
5
+ colorTo: yellow
6
  sdk: docker
7
+ app_port: 8501
8
  pinned: false
9
+ short_description: Learn cricket analytics through a T20 chase.
10
  ---
11
 
12
+ # 🏏 RunRate Lab
13
+
14
+ **Play. Analyze. Improve.**
15
+
16
+ RunRate Lab is an interactive cricket analytics learning game. You play a T20 chase in a 2.5D cricket arcade, choose shot intent and aim lane, swing at live deliveries, and learn how analytics explains every outcome through expected runs, wicket risk, variance, Monte Carlo simulation, and decision quality.
17
+
18
+ ## What You Learn
19
+
20
+ - **Expected runs:** the long-run value of a shot before the random result appears.
21
+ - **Wicket risk:** the chance that a shot leads to dismissal.
22
+ - **Variance:** why a good decision can fail and a bad decision can succeed.
23
+ - **Decision quality:** whether the shot created enough value for the risk accepted.
24
+ - **Monte Carlo simulation:** repeated trials that estimate the probability of `0`, `1`, `2`, `3`, `4`, `6`, or `W`.
25
+ - **Match pressure:** how required run rate changes which risks are rational.
26
+
27
+ ## How To Play
28
+
29
+ 1. Click **Start T20 Chase**.
30
+ 2. Choose **Defend**, **Drive**, **Cut / Pull**, or **Loft**.
31
+ 3. Choose **Leg Side**, **Straight**, or **Off Side**.
32
+ 4. Hold mouse, touch, or Space to start the delivery and charge your shot.
33
+ 5. Release near the green impact band.
34
+ 6. Read the **Analytics Coach** panel after every ball, then press **Next Ball**.
35
+ 7. Use the **Interactive Analytics Lab** to recreate a shot and change one input at a time.
36
+ 8. Complete the **Learning Path** missions to build analytics intuition.
37
+
38
+ Keyboard controls:
39
+
40
+ - `1`: Defend
41
+ - `2`: Drive
42
+ - `3`: Cut / Pull
43
+ - `4`: Loft
44
+ - `A` and `D`: change aim lane
45
+ - `Space`: hold and release swing
46
+ - `Enter`: next ball during review
47
+
48
+ ## Game Features
49
+
50
+ - 2.5D batting-end view with pitch depth, field ring, crease, stumps, and ball scaling.
51
+ - Keyframed batter rig with helmet, torso, pads, arms, gloves, and an attached bat.
52
+ - Shot intent and aim lane choices before every delivery.
53
+ - Required review pause after each ball so the coach panel remains readable.
54
+ - Result toast, boundary flash, wicket shake, shot trails, and replay-style commentary.
55
+ - Process streak and Process Score after every over showing average xRuns, actual runs, average wicket risk, best decision, and variance flag.
56
+ - In-game settings for reduced flashing, color-safe feedback, and the first-ball hold-release prompt.
57
+
58
+ ## Learning Path
59
+
60
+ RunRate Lab includes five guided modules:
61
+
62
+ 1. **Expected Runs:** compare two shot intents against the same delivery.
63
+ 2. **Wicket Risk:** complete an over using only shots under a target risk budget.
64
+ 3. **Variance And Luck:** identify lucky and unlucky outcomes.
65
+ 4. **Timing Sensitivity:** compare early, perfect, and late releases for the same setup.
66
+ 5. **Match Pressure:** compare Drive and Loft at different required run rates.
67
+
68
+ ## Project Structure
69
+
70
+ - `app.py`: Streamlit app, browser game, analytics lab, learning path, and charts.
71
+ - `requirements.txt`: minimal runtime dependencies.
72
+ - `Dockerfile`: Hugging Face Docker Space deployment.
73
+ - `.streamlit/config.toml`: app theme and Streamlit settings.
74
+ - `tests/test_runrate_lab.py`: focused analytics tests.
75
+
76
+ ## Local Setup
77
+
78
+ ```bash
79
+ python3 -m venv .venv
80
+ .venv/bin/python -m pip install --upgrade pip
81
+ .venv/bin/python -m pip install -r requirements.txt
82
+ ```
83
+
84
+ ## Run Locally
85
+
86
+ ```bash
87
+ .venv/bin/streamlit run app.py
88
+ ```
89
+
90
+ Open:
91
+
92
+ ```text
93
+ http://localhost:8501
94
+ ```
95
+
96
+ ## Test
97
+
98
+ ```bash
99
+ make check
100
+ ```
101
+
102
+ Or run only unit tests:
103
+
104
+ ```bash
105
+ .venv/bin/python -m unittest discover -s tests
106
+ ```
107
+
108
+ ## Docker
109
+
110
+ Build and run locally:
111
+
112
+ ```bash
113
+ docker build -t runrate-lab .
114
+ docker run --rm -p 8501:8501 runrate-lab
115
+ ```
116
+
117
+ Open:
118
+
119
+ ```text
120
+ http://localhost:8501
121
+ ```
122
+
123
+ ## Deploy To Hugging Face Spaces
124
+
125
+ 1. Create a new Hugging Face Space.
126
+ 2. Choose **Docker** as the SDK.
127
+ 3. Clone the Space repository.
128
+ 4. Copy these files into the Space repository:
129
+ - `app.py`
130
+ - `requirements.txt`
131
+ - `Dockerfile`
132
+ - `.streamlit/config.toml`
133
+ - `README.md`
134
+ 5. Commit:
135
+
136
+ ```bash
137
+ git add .
138
+ git commit -m "Build RunRate Lab cricket analytics coach"
139
+ ```
140
+
141
+ 6. Push:
142
+
143
+ ```bash
144
+ git push
145
+ ```
146
+
147
+ 7. Wait for the Space build logs to finish.
148
+ 8. Open the Space URL and verify:
149
+ - the app loads
150
+ - the game starts
151
+ - shot intent and aim lane controls work
152
+ - hold-and-release swing works
153
+ - the bat remains attached to both glove anchors during the swing
154
+ - a delivery pauses on the review state until **Next Ball** is pressed
155
+ - the Analytics Coach updates after each ball
156
+ - Analytics Lab charts render
157
+ - no app console errors appear
158
+
159
+ ## Notes
160
+
161
+ - No external APIs, model downloads, database, login, or webcam access are required.
162
+ - The canvas game runs client-side for smooth animation.
163
+ - The analytics lab uses deterministic simulation seeds so repeated slider setups are stable for learning.
app.py ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit
2
+ pandas
3
+ numpy
4
+ plotly
tests/test_runrate_lab.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+
3
+ from streamlit.testing.v1 import AppTest
4
+
5
+ import app
6
+
7
+
8
+ class RunRateLabAnalyticsTests(unittest.TestCase):
9
+ def test_probabilities_sum_to_approximately_100(self):
10
+ probabilities, metrics = app.simulate_outcome_distribution(
11
+ "Length Ball",
12
+ timing_error_ms=0,
13
+ power=70,
14
+ required_rate=8.5,
15
+ samples=5000,
16
+ )
17
+ self.assertAlmostEqual(probabilities["Probability"].sum(), 100.0, places=6)
18
+ self.assertGreater(metrics["expected_runs"], 0)
19
+ self.assertEqual(metrics["shot_intent"], "Drive")
20
+ self.assertEqual(metrics["aim_lane"], "Straight")
21
+
22
+ def test_perfect_timing_beats_large_timing_error(self):
23
+ pitch = app.get_pitch("Length Ball")
24
+ perfect_contact = app.calculate_contact(0, 70, pitch)
25
+ late_contact = app.calculate_contact(180, 70, pitch)
26
+ self.assertGreater(perfect_contact, late_contact)
27
+
28
+ def test_required_rate_increases_wicket_risk(self):
29
+ pitch = app.get_pitch("Cutter")
30
+ contact = app.calculate_contact(20, 85, pitch)
31
+ normal_risk = app.calculate_wicket_risk(contact, 85, 20, 8.0, pitch)
32
+ pressure_risk = app.calculate_wicket_risk(contact, 85, 20, 12.0, pitch)
33
+ self.assertGreater(pressure_risk, normal_risk)
34
+
35
+ def test_overcharged_power_increases_wicket_risk(self):
36
+ pitch = app.get_pitch("Bouncer")
37
+ contact = app.calculate_contact(0, 95, pitch)
38
+ normal_risk = app.calculate_wicket_risk(contact, 80, 0, 8.0, pitch)
39
+ overhit_risk = app.calculate_wicket_risk(contact, 95, 0, 8.0, pitch)
40
+ self.assertGreater(overhit_risk, normal_risk)
41
+
42
+ def test_loft_has_higher_upside_and_risk_than_defend(self):
43
+ pitch = app.get_pitch("Half Volley")
44
+ defend_contact = app.calculate_contact(0, 88, pitch, "Defend", "Straight")
45
+ defend_risk = app.calculate_wicket_risk(defend_contact, 88, 0, 8.0, pitch, "Defend", "Straight")
46
+ defend_xruns = app.calculate_expected_runs(defend_contact, 88, defend_risk, "Defend", "Straight")
47
+
48
+ loft_contact = app.calculate_contact(0, 88, pitch, "Loft", "Straight")
49
+ loft_risk = app.calculate_wicket_risk(loft_contact, 88, 0, 8.0, pitch, "Loft", "Straight")
50
+ loft_xruns = app.calculate_expected_runs(loft_contact, 88, loft_risk, "Loft", "Straight")
51
+
52
+ self.assertGreater(loft_risk, defend_risk)
53
+ self.assertGreater(loft_xruns, defend_xruns)
54
+
55
+ def test_correct_shot_delivery_matchup_improves_contact(self):
56
+ pitch = app.get_pitch("Bouncer")
57
+ drive_contact = app.calculate_contact(0, 76, pitch, "Drive", "Straight")
58
+ pull_contact = app.calculate_contact(0, 76, pitch, "Cut / Pull", "Straight")
59
+ self.assertGreater(pull_contact, drive_contact)
60
+
61
+ def test_decision_quality_thresholds(self):
62
+ self.assertEqual(app.classify_decision_quality(4.5, 0.12), "Excellent Process")
63
+ self.assertEqual(app.classify_decision_quality(3.0, 0.20), "Good Process")
64
+ self.assertEqual(app.classify_decision_quality(4.0, 0.35), "Risky But Rational")
65
+ self.assertEqual(app.classify_decision_quality(1.2, 0.18), "Low-Value Shot")
66
+ self.assertEqual(app.classify_decision_quality(1.9, 0.35), "Poor Risk Tradeoff")
67
+
68
+ def test_luck_classification(self):
69
+ self.assertEqual(app.classify_luck(6, 2.0, False, 0.15), "Lucky Result")
70
+ self.assertEqual(app.classify_luck(0, 4.0, False, 0.12), "Unlucky Result")
71
+ self.assertEqual(app.classify_luck(0, 3.0, True, 0.10), "Unlucky Result")
72
+ self.assertEqual(app.classify_luck(2, 2.2, False, 0.15), "Expected Result")
73
+
74
+ def test_generated_game_model_uses_python_constants(self):
75
+ rendered = app.build_game_html(180)
76
+ self.assertNotIn("__DELIVERIES_JSON__", rendered)
77
+ self.assertNotIn("__SHOTS_JSON__", rendered)
78
+ self.assertNotIn("__AIMS_JSON__", rendered)
79
+ for delivery in app.PITCH_MODEL["Delivery"]:
80
+ self.assertIn(delivery, rendered)
81
+ for shot in app.SHOT_MODEL["Shot"]:
82
+ self.assertIn(shot, rendered)
83
+ for aim in app.AIM_MODEL["Aim"]:
84
+ self.assertIn(aim, rendered)
85
+
86
+ def test_sample_table_has_ten_trials(self):
87
+ table = app.simulate_sample_table("Yorker", 0, 70, 9.0, shot_intent="Defend", aim_lane="Straight")
88
+ self.assertEqual(len(table), 10)
89
+ self.assertIn("Shot", table.columns)
90
+ self.assertIn("Aim", table.columns)
91
+ self.assertIn("Decision", table.columns)
92
+
93
+ def test_learning_summary_handles_partial_logs(self):
94
+ summary = app.build_learning_summary([{"actual_runs": 4}, {"unexpected": 99}])
95
+ self.assertEqual(summary["balls"], 2.0)
96
+ self.assertEqual(summary["avg_xruns"], 0.0)
97
+ self.assertEqual(summary["avg_wicket_risk"], 0.0)
98
+ self.assertEqual(summary["avg_actual_runs"], 4.0)
99
+
100
+ def test_game_pauses_for_review_between_balls(self):
101
+ self.assertIn('id="nextBallBtn"', app.GAME_HTML)
102
+ self.assertIn('state.mode = "between"', app.GAME_HTML)
103
+ self.assertIn('state.mode === "between" ? "Review"', app.GAME_HTML)
104
+ self.assertIn("Shot Intent", app.GAME_HTML)
105
+ self.assertIn("Aim Lane", app.GAME_HTML)
106
+ self.assertIn("Process Score", app.GAME_HTML)
107
+ self.assertIn('id="processPill"', app.GAME_HTML)
108
+ self.assertIn('id="settingsModal"', app.GAME_HTML)
109
+ self.assertIn("Reduce Flashing", app.GAME_HTML)
110
+ self.assertIn("hidden-during-play", app.GAME_HTML)
111
+ self.assertIn("hidden-for-review", app.GAME_HTML)
112
+ self.assertIn("Coach ready. Press Next Ball when done.", app.GAME_HTML)
113
+ self.assertIn("Restart Match", app.GAME_HTML)
114
+
115
+
116
+ class RunRateLabStreamlitTests(unittest.TestCase):
117
+ def test_streamlit_app_renders(self):
118
+ test_app = AppTest.from_file("app.py")
119
+ test_app.run(timeout=15)
120
+ self.assertFalse(test_app.exception)
121
+ self.assertTrue(any("RunRate Lab" in title.value for title in test_app.title))
122
+ self.assertGreaterEqual(len(test_app.metric), 4)
123
+
124
+
125
+ if __name__ == "__main__":
126
+ unittest.main()