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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,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.

> **`game_state`:** Usually mirrors CRG's raw `State` field, but may be normalized for display use.
> For example, if `Clock(Intermission).Running` is `true`, `/live` returns `"Intermission"`
> even if the raw scoreboard `State` still says `"Running"`.
Comment on lines +244 to +246

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

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

The inline-code examples include escaped quotes (e.g. \"Intermission\", \"Running\"), which will render the backslashes in Markdown. Since these are code spans, the quotes don’t need escaping—use "Intermission"/"Running" without the backslashes (or just "Intermission" as plain text) so the README renders cleanly.

Copilot uses AI. Check for mistakes.

> **`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).
Expand Down
4 changes: 4 additions & 0 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ def get_live_state(self) -> LiveState:
for field, (suffix, typ) in GAME_FIELD_MAP.items()
}
clock_timeout_running = self._get("Clock(Timeout).Running", bool)
# Check if intermission clock is running — if so, override game_state to show intermission
intermission_clock_running = self._get("Clock(Intermission).Running", bool)
if intermission_clock_running:
game_fields["game_state"] = "Intermission"
Comment on lines +263 to +266

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

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

This introduces new behavior (overriding game_state to "Intermission" when Clock(Intermission).Running is true) but there’s no regression test covering it. Please add a test in tests/test_client.py that pushes ScoreBoard.CurrentGame.Clock(Intermission).Running: True (with a non-intermission State) and asserts client.get_live_state().game_state == "Intermission" (and ideally that it reverts when the flag goes false).

Copilot uses AI. Check for mistakes.
Comment on lines +263 to +266

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

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

game_state is mapped from the scoreboard’s State field via GAME_FIELD_MAP, but this change can synthesize a new value ("Intermission") based on the intermission clock. To avoid surprising API consumers, please document this behavior (e.g., in the LiveState.game_state field description / README) so it’s clear game_state may not always reflect the raw scoreboard State.

Copilot uses AI. Check for mistakes.
# Pop timeout_clock_ms so we can conditionally suppress it below
# without it conflicting with the **game_fields unpack.
raw_timeout_clock_ms = game_fields.pop("timeout_clock_ms", None)
Expand Down
10 changes: 9 additions & 1 deletion models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@ class LiveState(BaseModel):
period_clock: Optional[str] = None
jam_running: Optional[bool] = None
in_jam: Optional[bool] = None
game_state: Optional[str] = None
game_state: Optional[str] = Field(
default=None,
description=(
"Display-oriented game state. Usually mirrors CRG's State field, "
"but may be synthesized from clock activity for clearer downstream use. "
'For example, when Clock(Intermission).Running is true this is returned as "Intermission" '
'even if the raw CRG State still reads "Running".'
),
)
timeout_type: Optional[str] = Field(
default=None,
description=(
Expand Down
25 changes: 25 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,31 @@ async def test_get_live_state_maps_clocks(mock_server):
await asyncio.sleep(0.05)


@pytest.mark.asyncio
async def test_get_live_state_intermission_clock_overrides_game_state(mock_server):
client, task = await _connected_client(mock_server)
try:
await mock_server.push_update({
"ScoreBoard.CurrentGame.State": "Running",
"ScoreBoard.CurrentGame.Clock(Intermission).Running": True,
})
await asyncio.sleep(0.1)

state = client.get_live_state()
assert state.game_state == "Intermission"

await mock_server.push_update({
"ScoreBoard.CurrentGame.Clock(Intermission).Running": False,
})
await asyncio.sleep(0.1)

state = client.get_live_state()
assert state.game_state == "Running"
finally:
client.stop()
await asyncio.sleep(0.05)


@pytest.mark.asyncio
async def test_get_live_state_maps_jammer_info(mock_server):
client, task = await _connected_client(mock_server)
Expand Down
Loading