Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions CLIENT_FIELD_CROSSWALK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Client Field Crosswalk (Basic)

This document maps the client’s requested scoreboard display values against the current `GET /live` response.

Scope is intentionally basic and focused on what is needed for a first pass display.

## 1) Requested fields vs `GET /live`

| Client requested value | Available in `GET /live` | Field(s) |
|---|---|---|
| Period | Yes | `period` |
| Jam | Yes | `jam` |
| Period Clock | Yes | `period_clock_ms` |
| Jam Clock | Yes | `jam_clock_ms` |
| Home Score | Yes | `team1.score` |
| Away Score | Yes | `team2.score` |
| Home Jam Score | Yes | `team1.jam_score` |
| Away Jam Score | Yes | `team2.jam_score` |
| Home PP Timer | Yes (via skater box timer) | `team1.jammer.in_box`, `team1.jammer.box_time_remaining_s` |
| Away PP Timer | Yes (via skater box timer) | `team2.jammer.in_box`, `team2.jammer.box_time_remaining_s` |
| Home Player Tracker (1-5) | Yes | `team1.jammer`, `team1.pivot`, `team1.blocker1`, `team1.blocker2`, `team1.blocker3` |
| Away Player Tracker (1-5) | Yes | `team2.jammer`, `team2.pivot`, `team2.blocker1`, `team2.blocker2`, `team2.blocker3` |

Notes:
- Player tracker rows include skater `name`, `number`, and penalty-box state (`in_box`, `box_time_remaining_s`).
- PP timer can be rendered from jammer box state/timer for each team.

## 2) Key values currently missing from `GET /live`

The following are not currently mapped into `GET /live` and should be added for the production broadcast display:

1. Lead jammer skater (explicit display field)
- Today, lead can be inferred via `team1.lead` / `team2.lead` plus jammer identity, but there is no explicit top-level “lead jammer skater” value.

2. Timeout state and type
- Team timeout
- Official timeout
- Official review

3. Timeout ownership and counters
- Which team called timeout/review
- Team timeouts remaining per team
- Official review availability/status per team
Comment on lines +35 to +43

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This crosswalk claims “Timeout state and type” are currently missing from GET /live, but this PR adds timeout_type. Please update this section (and the later “Timeout package (to add)” bullet) to reflect that timeout_type is now available, while ownership/counters/timers may still be missing.

Copilot uses AI. Check for mistakes.

4. Timeout/review timer behavior
- Team timeout clock (fixed 60s)
- Official timeout timing (variable duration)
- Official review timing (variable duration)

5. Post-timeout to jam-start phase
- A display flag/timer for the window between timeout end and next jam start.

## 3) Rules/operations notes to confirm with league

- Team timeouts: each team has three total, but event policy should confirm whether tracked per game or displayed per half in this production.
- Official review: one per team per half, and may be retained if successful (once).
- Official timeout and official review durations are not fixed.

These rules are officiating policy context. The API should still expose neutral fields (type, owner, running, remaining, total/used/remaining) so the display can apply event-specific presentation.

## 4) Minimal overview fields for immediate implementation

For the fastest basic display, include this overview set:

- State: `period`, `jam`, `game_state`, `in_jam`, `jam_running`
- Clocks: `period_clock_ms`, `jam_clock_ms`
- Scores: `team1.score`, `team2.score`, `team1.jam_score`, `team2.jam_score`
- Lead and star pass indicators: `team1.lead`, `team2.lead`, `team1.star_pass`, `team2.star_pass`
- Player tracker: all five positions for both teams (name, number, in-box, box timer)
- Timeout package (to add): timeout type, owner, running flag, remaining ms (when fixed), team timeout counts, official review status, post-timeout phase indicator

## 5) Follow-up implementation plan

1. Use `GET /raw` during a live timeout/review sequence to capture exact CRG key names.
2. Add timeout/review fields to `models.py` (`LiveState` and/or `TeamState`).
3. Map those keys in `client.py` field maps.
4. Add tests for team timeout, official timeout, official review, and post-timeout phase.
5. Update display UI to render timeout/review banner and counters.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Clean, mapped live game state. Poll this at whatever rate suits your overlay (20
"jam_running": true,
"in_jam": true,
"game_state": "Running",
"timeout_type": null,
"state_age_seconds": 0.1,
"team1": {
"name": "Home Team",
Expand Down Expand Up @@ -109,6 +110,10 @@ Clean, mapped live game state. Poll this at whatever rate suits your overlay (20
> `null` means no update has been received yet (proxy just connected). If this grows above a few
> seconds while `connected` is `true`, the scoreboard may be frozen.

> **`timeout_type`:** Normalized timeout/review state derived from `game_state`.
> Values: `team_timeout`, `official_timeout`, `official_review`, `timeout`, or `null`.
> It is forced to `null` when `jam_running` is `true` (play resumed).

### `GET /raw`
Full flat state dict as received from the scoreboard WebSocket. Useful for discovering all available fields or debugging.

Expand Down Expand Up @@ -136,6 +141,10 @@ The proxy is designed to **stay running no matter what**:

See [EXTENDING.md](EXTENDING.md) for a guide on adding new fields, endpoints, and more.

## Client display field crosswalk

For a client-facing summary of requested scoreboard fields, current `/live` coverage, and missing timeout/review data needed for broadcast UI, see [CLIENT_FIELD_CROSSWALK.md](CLIENT_FIELD_CROSSWALK.md).

## Running tests

```powershell
Expand Down
26 changes: 26 additions & 0 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,40 @@ def _team(self, n: int) -> TeamState:
)
return TeamState(**flat_fields, **positions)

@staticmethod
def _normalize_timeout_type(game_state: Optional[str], jam_running: Optional[bool]) -> Optional[str]:
"""Normalize timeout/review from game state; clear once jam is running again."""
if jam_running:
return None
if not game_state:
return None

state = game_state.strip().lower()
if "official review" in state:
return "official_review"
if "official timeout" in state:
return "official_timeout"
if "team timeout" in state:
return "team_timeout"
if "timeout" in state:
return "timeout"
if "review" in state:
return "official_review"
return None
Comment on lines +218 to +229

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New branches in _normalize_timeout_type (generic "timeout" and generic "review" matches) are currently untested. Add client tests that cover these fallbacks (e.g., a game_state containing only "Timeout" and one containing only "Review") to prevent regressions in the normalization logic.

Copilot uses AI. Check for mistakes.

def get_live_state(self) -> LiveState:
"""Build and return a LiveState model from current raw state."""
game_fields = {
field: self._get(suffix, typ)
for field, (suffix, typ) in GAME_FIELD_MAP.items()
}
timeout_type = self._normalize_timeout_type(
game_state=game_fields.get("game_state"),
jam_running=game_fields.get("jam_running"),
)
return LiveState(
**game_fields,
timeout_type=timeout_type,
team1=self._team(1),
team2=self._team(2),
)
Expand Down
8 changes: 8 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ class LiveState(BaseModel):
jam_running: Optional[bool] = None
in_jam: Optional[bool] = None
game_state: Optional[str] = None
timeout_type: Optional[str] = Field(
default=None,
description=(
"Normalized timeout/review state. "
"One of: team_timeout, official_timeout, official_review, timeout, or null. "
"Resets to null when jam_running is true."
),
)
state_age_seconds: Optional[float] = None
team1: TeamState = Field(default_factory=TeamState)
team2: TeamState = Field(default_factory=TeamState)
Expand Down
6 changes: 6 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ async def test_live_jammer_names(app_client):
assert data["team2"]["jammer"]["name"] == "Lightning Bolt"


async def test_live_includes_timeout_type_field(app_client):
resp = await app_client.get("/live")
data = resp.json()
assert "timeout_type" in data

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new /live test only checks that the timeout_type key exists. Since the docs/example show it as null when not in a timeout, consider asserting the default value is None for the initial "Running" state, and/or add an integration test that pushes a timeout update and verifies /live returns the normalized value.

Suggested change
assert "timeout_type" in data
assert "timeout_type" in data
assert data["timeout_type"] is None

Copilot uses AI. Check for mistakes.


async def test_live_reflects_score_update(app_client, mock_server):
await mock_server.push_update({
"ScoreBoard.CurrentGame.Team(2).Score": 50,
Expand Down
Loading
Loading