feat: penalty box elapsed timer — expose box_elapsed_jam_ms (Option A) - #4
feat: penalty box elapsed timer — expose box_elapsed_jam_ms (Option A)#4a1ly404 wants to merge 2 commits into
Conversation
…cumulator Track jam-time elapsed per in-box skater using Option C (jam-clock delta accumulator). Only accumulates while jam_running was True at the previous tick, which naturally pauses the timer during lineup, timeouts, and jam resets without any extra logic. - SkaterPosition.box_elapsed_jam_ms: Optional[int] — None when not in box, >= 0 ms while in box. Consumer computes remaining = max(0, 30_000 - elapsed). - ScoreboardClient._tick_box_timers() called after each state update. - _prev_jam_running guard prevents spurious large delta on jam-start transition. - 6 new tests covering: init None, entry=0, accumulation, pause between jams, box exit reset, and single-skater isolation.
There was a problem hiding this comment.
Pull request overview
Adds a per-skater “jam-time elapsed while in the penalty box” counter to the live state so overlays can compute remaining penalty time client-side (Option A).
Changes:
- Add
SkaterPosition.box_elapsed_jam_ms: Optional[int]to expose raw elapsed jam-time while a skater is in the box. - Implement
_tick_box_timers()inScoreboardClientto accumulate elapsed time using jam clock deltas gated byjam_running. - Add async client tests covering initialization, accumulation, pause behavior, and reset on box exit.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
models.py |
Extends SkaterPosition with box_elapsed_jam_ms and documents consumer-side remaining-time computation. |
client.py |
Tracks per-position elapsed timers and wires ticking into the WS receive loop and TeamState mapping. |
tests/test_client.py |
Adds new tests validating box_elapsed_jam_ms behavior across key state transitions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Maintain per-position elapsed counters — reset on box exit. | ||
| for team_n in (1, 2): | ||
| for crg_pos in POSITION_MAP.values(): | ||
| key = f"{team_n}.{crg_pos}" | ||
| in_box = ( | ||
| self._get(f"Team({team_n}).Position({crg_pos}).PenaltyBox", bool) | ||
| or False | ||
| ) | ||
| if not in_box: | ||
| self._box_elapsed.pop(key, None) | ||
| elif key not in self._box_elapsed: | ||
| # Skater just entered — initialise at zero | ||
| self._box_elapsed[key] = 0 |
There was a problem hiding this comment.
_tick_box_timers() updates the in-box membership (popping/initializing _box_elapsed) before applying the jam-clock delta. If a scoreboard patch includes both a PenaltyBox transition and a jam clock tick in the same message, this ordering can undercount on box exit (removed before last delta) and overcount on box entry (newly-added key receives delta for time before they entered). Consider a two-phase approach: first add delta to the keys that were already present in _box_elapsed (representing prior in-box state), then apply membership changes from the current PenaltyBox values (pop/initialize).
… tests Address Copilot review on PR #4. Bug: membership updates (pop/init _box_elapsed) ran before the jam-clock delta was applied. A skater entering the box in the same message as a clock tick would incorrectly receive elapsed credit for the interval before they entered; a skater exiting would miss that final delta. Fix: apply delta to the previous in-box set (Phase 1) before updating membership from the new PenaltyBox values (Phase 2). Tests added: - test_box_elapsed_entry_simultaneous_with_clock_tick: entry + clock tick in same message leaves elapsed at 0, not 2000. - test_box_elapsed_exit_simultaneous_with_clock_tick: exit + clock tick in same message; other skaters unaffected, exiting skater ends up None.
|
Closing — client has decided to handle penalty timing in their own overlay workflow rather than derive it server-side. The |
Overview
Implements Option A for the PP Timer discussion: expose raw jam-time elapsed per in-box skater as
box_elapsed_jam_ms: Optional[int]onSkaterPosition.The consumer (overlay) computes remaining time:
Stacking is also consumer-side:
Math.max(0, penalty_count * 30_000 - box_elapsed_jam_ms).Compare with: feat/pp-timer-countdown which bakes
30_000 - elapsedinto the server.How it works (Option C — jam-clock accumulator)
CRG does not broadcast a penalty countdown clock. It sends
PenaltyBox: true/falseper skater.box_elapsed_jam_msis derived entirely from data already being received:_tick_box_timers()computesdelta = prev_jam_clock_ms - jam_clock_msjam_running = Truejam_runningflips True at jam start (clock jumps from 0 → 120 s)in_box → False, the counter is removed (Nonereturned)in_box → True, counter initialises at0Changes
models.pySkaterPosition.box_elapsed_jam_ms: Optional[int] = Noneclient.py_tick_box_timers(),_prev_jam_clock_ms,_prev_jam_running, wired into receive loop and_team()tests/test_client.pyDecision pending
Waiting for customer preference between this PR and feat/pp-timer-countdown. Do not merge until decided.