Skip to content

CTF scoreboard

The scoreboard streams over WebSockets: solves, score adjustments, and awards move the board the moment they land, for everyone watching.

The board opens when the competition starts and stays readable after it ends as a final, read-only result — competition status is a separate axis from both pause and freeze. While a competition is still Not started, the scoreboard is closed to competitors (staff can see it).

  • Standings per team (team mode) or competitor (individual mode) — total points are the sum of solve awards, judge/automation score adjustments, hint costs, and award points. Ties follow the standard CTF convention: equal points rank by the earliest time the subject reached its current score.
  • First blood — the first solve of each challenge is marked (a lightning-bolt icon next to the solver).
  • Dynamic values — decay-scored challenges re-value all their solvers on each new solve, so the board always reflects current worth.
  • Divisions — when the competition defines brackets, a division filter ranks within the selected division; staff assign divisions inline from the board.
  • The top-10 chart — a bar chart of the leaders above the table, with per-bar labels and a detail tooltip on hover or keyboard focus.
  • Score over time (v1.6.0) — beside the standings, a line chart plots the top ten entrants’ cumulative score as it climbed over the event, so you can watch overtakes and stalls instead of just the final ordering. Each series ends exactly on that subject’s table total — the chart and the table are the same numbers, drawn two ways. It respects the freeze and the division filter like the rest of the board: competitors see the frozen picture, and a scoreboard_freeze holder viewing the live board sees the live lines; a bracket filter scopes the series to that division. It rides the same activity connection as the board — no manual refresh — needs challenge_view, and only appears once the competition has started.

A freeze stops the public board from moving — competitors keep solving and their points still count; the standings just stop updating publicly until the unfreeze. This is the classic end-game tension mechanic.

  • Freezing requires the scoreboard_freeze permission and shows a confirmation explaining exactly these semantics; the frozen board carries a persistent note.
  • The board everyone sees is computed as of the freeze instant — dynamic values by solve count at that moment; later solves, adjustments, awards, and hint costs excluded.
  • The freeze covers every competitor-visible solve surface, not just the board: per-challenge solver lists and solve counts, dashboards, the participants roster, and the spectator insights all apply the same cutoff (tightened in v1.2.0 — GHSA-q69m-7mgh-r2gh — and v1.4.0 closed the last gap: the per-challenge detail view’s solve count and dynamic value now clamp to the cutoff like every other read), so a frozen board can’t be reconstructed from side channels.
  • Staff can still read the live board (?live=true — the UI does this for staff automatically).
  • Freeze and unfreeze emit scoreboard.frozen / scoreboard.unfrozen, and both exist as automation actions — a common rule is on competition.ended → freeze the board.

Freezing is different from pausing: a pause stops competitors submitting; a freeze only stops the public board moving.

An explicit per-competition opt-in (Settings → General) exposes a read-only, unauthenticated scoreboard for projectors and spectators:

  • /public lists every competition that opted in; /public/<competition-id> is the standalone board (no login, branded with your site settings).
  • Only public, non-archived, opted-in competitions are served — anything else 404s, so private events are never disclosed.
  • Beyond the table, the page shows competition insight cards (most solved, most attempted, first-blood leader, fastest solve) and a live cumulative points timeline of the top ten, so spectators can watch overtakes as they happen.
  • The spectator board respects the freeze like any competitor view — and so does every score-derived insight and the timeline, so the page never leaks what the frozen board hides.

Since v1.3.0, the spectator page carries a big-screen mode for live, in-person events. A Venue mode button on /public/<competition-id> (or ?venue=1 directly — bookmarkable and refresh-safe) switches to a full-screen display that auto-rotates through the scoreboard (top twelve), the insight stats and highlights, and the points timeline every 15 seconds (?interval=<seconds> overrides, clamped 5–120).

  • First-blood splashes — a new first blood interrupts the rotation with a five-second full-screen takeover, driven by a recent-solves feed the page polls every few seconds.
  • Controls — arrow keys change slide, Space pauses, F toggles fullscreen, +/− nudge the display zoom (0.6×–2×, v1.4.0), Esc exits, with on-screen buttons and slide dots for the mouse.
  • Scales to the display (v1.4.0) — type and layout grow with the screen, from a laptop up to a 4K projector wall, with the +/− keys as an operator fine-tune.
  • Same opt-in, same freeze rules — venue mode rides the spectator board’s per-competition opt-in (no separate setting), and a frozen competition emits no post-cutoff solves, so no splash can leak what the board hides.

A second per-competition opt-in exposes GET /api/public/competitions/<id>/ctftime in the CTFtime scoreboard-feed format ({"standings":[{"pos","team","score"}]}), so rated events can plug straight into CTFtime. The feed URL is shown on Settings → General once enabled.

Judges with score_override can adjust scores (bonus or penalty, with a reason — emits score.adjusted) and grant point-carrying awards. Both fold into the board live and appear in the audit log.