Skip to content

feat: penalty box elapsed timer — expose box_elapsed_jam_ms (Option A) - #4

Closed
a1ly404 wants to merge 2 commits into
feat/player-trackerfrom
feat/pp-timer-elapsed
Closed

feat: penalty box elapsed timer — expose box_elapsed_jam_ms (Option A)#4
a1ly404 wants to merge 2 commits into
feat/player-trackerfrom
feat/pp-timer-elapsed

Conversation

@a1ly404

@a1ly404 a1ly404 commented Apr 1, 2026

Copy link
Copy Markdown
Owner

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] on SkaterPosition.

The consumer (overlay) computes remaining time:

const remaining = Math.max(0, 30_000 - box_elapsed_jam_ms)

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 - elapsed into the server.


How it works (Option C — jam-clock accumulator)

CRG does not broadcast a penalty countdown clock. It sends PenaltyBox: true/false per skater. box_elapsed_jam_ms is derived entirely from data already being received:

  • On each state update, _tick_box_timers() computes delta = prev_jam_clock_ms - jam_clock_ms
  • Delta only accumulates when both the current and previous tick had jam_running = True
    • This prevents a large spurious delta when jam_running flips True at jam start (clock jumps from 0 → 120 s)
    • Timer naturally pauses during lineup and timeouts without extra logic
  • When in_box → False, the counter is removed (None returned)
  • When in_box → True, counter initialises at 0

Changes

File Change
models.py SkaterPosition.box_elapsed_jam_ms: Optional[int] = None
client.py _tick_box_timers(), _prev_jam_clock_ms, _prev_jam_running, wired into receive loop and _team()
tests/test_client.py 6 new tests

Decision pending

Waiting for customer preference between this PR and feat/pp-timer-countdown. Do not merge until decided.

…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.
Copilot AI review requested due to automatic review settings April 1, 2026 17:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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() in ScoreboardClient to accumulate elapsed time using jam clock deltas gated by jam_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.

Comment thread client.py Outdated
Comment on lines +168 to +180
# 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

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

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

_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).

Copilot uses AI. Check for mistakes.
… 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.
@a1ly404

a1ly404 commented Apr 1, 2026

Copy link
Copy Markdown
Owner Author

Closing — client has decided to handle penalty timing in their own overlay workflow rather than derive it server-side. The in_box: bool field on feat/player-tracker gives their overlay the entry/exit signal it needs. A box_entered_at_ms wall-clock timestamp is being added to feat/player-tracker instead, which lets their overlay compute elapsed with a single arithmetic expression and no state tracking.

@a1ly404 a1ly404 closed this Apr 1, 2026
@a1ly404
a1ly404 deleted the feat/pp-timer-elapsed branch April 1, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants