Spaces:
Sleeping
Sleeping
Deploy RunRate Lab cricket analytics coach
Browse files- .dockerignore +7 -0
- .gitignore +14 -0
- .streamlit/config.toml +12 -0
- Dockerfile +19 -0
- Makefile +16 -0
- README.md +158 -5
- app.py +0 -0
- requirements.txt +4 -0
- 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|