← NewTon DC Tournament Manager

QR Code Communication Protocol

NewTon DC Tournament Manager ↔ Chalker

Context

The Tournament Manager (TM) and Chalker communicate via QR codes — no network infrastructure required. Two message types cover the full match lifecycle:

The payload schemas and integrity checking are designed to be transport-agnostic — the same format lends itself to network communication as well.

The QR workflow was added without breaking changes. The underlying tournament data structure is compatible with the one used since the earliest beta — no migration has ever been required in the lifetime of the software, and the addition of QR communication is no exception.


Protocol Design

Design Principles

  1. Transport-agnostic schema: Payload format is designed to work beyond QR if needed.
  2. Compact field names: Short keys minimize QR code size, keeping the QR version low and easy to scan.
  3. CRC-32 integrity: Detects corruption during scanning, alongside QR’s built-in error correction.
  4. Replay protection: TM checks match state before applying a result — rescanning a completed match is rejected.
  5. Schema versioning: v field enables forward-compatible changes.
  6. Offline-first: No network, no shared secrets required.

Identification Fields (Present in All Messages)

FieldDescriptionExample
vSchema version1
tMessage type"a" (assign) or "r" (result)
midMatch ID (from TM)"FS-1-3"
tidTournament ID"1708123456789"
sidServer/TM instance ID (12-char hex)"f47ac10b58cc"
tsUnix timestamp (seconds)1708123456
crcCRC-32 of all other fields"a1b2c3d4"

The sid (server ID) is a 12-character hex string generated once per TM installation and persisted in global config. It identifies which TM instance created the match. 12 hex chars = 48 bits of entropy (281 trillion possible values) — collision risk is negligible for local tournament use.


Message Schemas

Match Assignment (TM → Chalker QR)

{
  "v": 1,
  "t": "a",
  "mid": "FS-1-3",
  "tid": "1708123456789",
  "sid": "f47ac10b58cc",
  "p1": "John Smith",
  "p2": "Jane Doe",
  "sc": 501,
  "bo": 3,
  "mr": 13,
  "ln": 3,
  "ref": "Frank",
  "ts": 1708123456,
  "crc": "a1b2c3d4"
}

Estimated size: ~180–220 bytes → QR version ~7–9, very easy to scan.

FieldTypeDescription
p1stringPlayer 1 name
p2stringPlayer 2 name
scnumberStarting score — from Global Config (101 / 201 / 301 / 501, default 501)
bonumberBest-of legs — from Global Config match configuration
mrnumberMax rounds before tiebreak — from Global Config (default 13, range 7–20)
lnnumberLane number (1–20). Optional. Omitted if unassigned.
refstringReferee name. Optional. Omitted if unassigned.

Match Result (Chalker → TM QR)

The result payload carries raw visit scores only — no computed stats. The TM derives everything (averages, score ranges, high finishes, best/worst leg, etc.) from the raw scores. This keeps the payload minimal and makes the TM the single source of truth for statistics.

What the TM derives from raw scores:

What the TM cannot derive (must be in payload):

{
  "v": 1,
  "t": "r",
  "mid": "FS-1-3",
  "tid": "1708123456789",
  "sid": "f47ac10b58cc",
  "p1": "Klas Krantz",
  "p2": "Hans Blomqvist",
  "t1": "Nya Pikado C",
  "t2": "Newton A",
  "sc": 501,
  "bo": 5,
  "mr": 13,
  "ln": 3,
  "w": 2,
  "fls": 2,
  "legs": [
    { "w": 2, "s": "SSkoFlEcNxULExU7AA==|GVFCXzwKKTkhAQgMAA==", "cd": 0 },
    { "w": 2, "s": "LS0DLV8NHTYpPw==|KBpRPFUcNChJEA==", "cd": 3 },
    { "w": 1, "s": "GgtfHTkZDR48NwkGVQ==|ZBoWCxwaGlMsZRIA", "cd": 3 }
  ],
  "ts": 1708123789,
  "crc": "d4c3b2a1"
}

Estimated size: ~380–500 bytes (typical Bo3–Bo5) → QR version ~12–16, easy to scan.

FieldTypeDescription
p1stringPlayer 1 name
p2stringPlayer 2 name
t1stringTeam 1 name. Omitted if not set.
t2stringTeam 2 name. Omitted if not set.
scnumberStarting score (501, 301, etc.)
bonumberBest-of legs
mrnumberMax rounds before tiebreak
lnnumberLane number. Omitted if unassigned.
w1 or 2Match winner (player number)
fls1 or 2First leg starter (alternates each leg)
legsarrayPer-leg data
legs[].w1 or 2Leg winner (essential for tiebreaks)
legs[].sstringBase64-encoded visit scores: P1_SCORES|P2_SCORES
legs[].cd0–3Checkout darts used (0 = tiebreak, 1–3 = darts on final visit)

Dropped fields: wl (winner’s legs) and ll (loser’s legs) are not in the payload — the TM derives them by counting legs[].w values.

Null field omission: Fields with no value (t1, t2, ln) are omitted entirely rather than sent as null. The TM treats missing fields as unset.

Score Encoding

Each visit score (0–180) fits in a single unsigned byte. Per leg, both players’ visit scores are packed into byte arrays, base64-encoded, and joined with | (pipe is not in the base64 alphabet, so it’s a safe separator).

Encode: [73, 45, 40, 22, 81, ...] → Uint8Array → btoa() → "SSkoFlE..."
Decode: atob("SSkoFlE...") → Uint8Array → [73, 45, 40, 22, 81, ...]
Combined: "SSkoFlEcNxULExU7AA==|GVFCXzwKKTkhAQgMAA=="
           ← Player 1 scores →  ← Player 2 scores →

This saves ~45% compared to JSON number arrays. A 13-round leg goes from ~90 chars ("v1":[73,45,...],"v2":[25,81,...]) to ~50 chars ("s":"...base64...|...base64...").

Tiebreak Handling

When max rounds (mr) is reached without a checkout, a separate tiebreak round is played: 3 darts each, highest score wins the leg. This deciding round is not recorded on the scoresheet. The legs[].w field is the only record of who won, and legs[].cd = 0 signals the leg was a tiebreak.

Size Analysis

FormatLegs × RoundsPayloadQR Version
Bo3, typical2–3 × 8~380 bytes~12
Bo5, all 5 legs5 × 10~520 bytes~16
Bo7, all max rounds7 × 13~680 bytes~20
Bo13, all max rounds13 × 13~1100 bytes~30
QR v40 limit (EC-M)2331 bytes40

Even the absolute worst case (Bo13, all legs going to max rounds with long player/team names) stays well within QR capacity with ~50% headroom.


CRC-32 Integrity Module

Purpose

Detects data corruption from scanning errors, alongside QR’s built-in error correction.

Implementation

A standalone utility module (js/newton-integrity.js / chalker/js/newton-integrity.js) — same file, shared between TM and Chalker:

const NewtonIntegrity = {
  // CRC-32 lookup table (generated once)
  _table: null,

  // Compute CRC-32 of a string → 8-char hex
  crc32(str) { ... },

  // Sign a payload object: compute CRC of JSON-sorted fields, append as 'crc'
  sign(payload) { ... },

  // Verify a payload: extract 'crc', recompute, compare
  verify(payload) { ... }
};

Key design decisions:

Why CRC-32 (not SHA-256)


TM Result Application

Applying the Match Result

completeMatch(matchId, winnerPlayerNumber, winnerLegs, loserLegs) handles everything structural — winner declaration, bracket advancement, and the history transaction. Called directly with data from the QR payload. The TM derives winnerLegs and loserLegs by counting legs[].w values.

Player Achievements

Achievements are stored on player.stats, not on the match:

FieldTypeDescription
oneEightiescounterVisit scores of exactly 180
tonscounterVisit scores 100–179
highOutsarrayCheckout scores ≥ 101 (stored as individual values)
shortLegsarrayDart counts for short legs (stored as individual dart counts)
lollipopscounterVisit scores of exactly 3 (cosmetic)

All are fully derivable from the result QR payload:

AchievementDerivation
180sAll visits where score === 180
TonsAll visits where 100 ≤ score ≤ 179
High outsLast visit in the winner’s score array, if score ≥ 101
Short legs(winner_visits − 1) × 3 + cd darts
LollipopsAll visits where score === 3

Stats Extraction Module

The Chalker delivers raw facts — visit scores, leg winners, checkout dart counts. It does not compute, interpret, or summarise. The TM is solely responsible for deriving meaning from the raw data.

A dedicated module (js/newton-stats.js) handles all extraction. It is a pure function module — takes raw leg data in, returns structured stats out. No side effects. No coupling to QR, bracket, or player management.

const NewtonStats = {
  extractMatchStats(legs, startingScore, firstLegStarter) { ... },
  extractPlayerStats(legs, playerIndex, startingScore) { ... },
  extractAchievements(legs, playerIndex, startingScore) { ... },
  count180s(visits) { ... },
  countTons(visits) { ... },
  highOuts(legs, playerIndex, startingScore) { ... },
  shortLegs(legs, playerIndex) { ... },
  legAverages(legs, playerIndex) { ... },
  matchAverage(legs, playerIndex) { ... },
};

The individual extractors are callable independently — the QR result flow calls what it needs without pulling in unrelated machinery. No extractor depends on another module. New stat types are new named functions; existing callers are unaffected.

Match Completion Flow

When the result preview modal is open after a successful scan, the operator sees the winner, score, and extracted achievements. They choose how to record the result:

Option A — Score only

Option B — Score + achievements

The operator decides. The completionType: 'QR' and achievements field together give the undo dialog full knowledge of what was recorded and how.

Replay Protection Covers Achievements

completeMatch() is blocked if the match is already completed. Achievements are additive on player.stats — applying them a second time corrupts the totals. The achievement application is gated by the same state check: if the match is already completed, neither completeMatch() nor the achievement application is called.


Replay Protection

When the TM scans a result QR for match FS-1-3:

  1. Look up match FS-1-3 in the tournament
  2. Match must be live (started, not yet completed) — any other state is rejected with a specific reason
  3. Verify tid matches current tournament — reject if from a different tournament
  4. Verify sid matches this TM instance — reject if from a different server

Simple and robust — no nonces or timestamps required.


TM-Side Score Validation

After CRC verification passes, the TM performs arithmetic validation on the decoded scores. This catches encoding/decoding bugs and Chalker scoring bugs that CRC cannot detect — CRC only proves the data wasn’t corrupted in transit, not that it was correct in the first place.

CheckRuleCatches
Score rangeEvery visit score is 0–180Encoding errors, byte overflow
Visit count parityBoth players have equal visits, or first-thrower has exactly +1Data corruption, missing visits
Checkout sumNormal leg (cd > 0): winner’s scores sum to scScoring bugs, wrong winner
Tiebreak sumTiebreak leg (cd == 0): neither player’s scores sum to scMisclassified tiebreak
Winner consistencylegs[].w matches the player whose scores sum to scWrong leg winner assignment
Leg countWinner has exactly ceil(bo/2) legs wonImpossible match outcome

Soft warning, not hard rejection: If validation fails, the TM accepts the data but flags it visually. Rejecting the QR would force manual entry — worse UX for what might be a harmless edge case. The derived stats will also look visibly wrong, which is self-correcting.


Server ID

A 12-character hex string generated once when the TM is first used, stored in global config (localStorage):

if (!config.serverId) {
  config.serverId = crypto.randomUUID().replace(/-/g, '').substring(0, 12);
  saveGlobalConfig();
}

12 hex chars = 48 bits of entropy (281 trillion possible values) — collision risk is negligible for local tournament use.

Purpose:


Flows

Match Assignment Flow (TM → Chalker)

  1. Operator clicks QR on a live match card in Match Controls
  2. TM generates assignment payload with match details + CRC
  3. TM displays the assignment QR code in a modal
  4. Chalker operator taps QR in the New Match dialog — camera opens immediately
  5. Chalker scans the QR, parses the payload, verifies CRC
  6. Chalker auto-populates: player names, starting score, best-of, match ID, tournament ID
  7. Chalker stores mid, tid, sid in match state for inclusion in the result QR
  8. Operator confirms the assignment and selects the starting player — match begins

Match Result Flow (Chalker → TM)

  1. Match completes on Chalker
  2. Chalker builds result payload with raw visit scores, leg winners, and checkout dart counts + CRC
  3. Operator taps Stats, then Result QR — Chalker displays the result QR code
  4. TM operator clicks Scan QR Results in the Match Controls footer
  5. TM parses payload, verifies CRC
  6. TM checks mid, tid, sid against current tournament
  7. TM verifies match is live (not already completed)
  8. TM applies result: calls completeMatch() with winner + leg scores
  9. TM stores stats from QR payload in the match record

Manual Fallback (No Camera/HTTPS)

If QR scanning isn’t available, both sides fall back gracefully:


QR Library Choices

SideNeedLibraryNotes
TMGenerate assignment QRqrcode-generator (~8KB)Generates QR as canvas/SVG
TMScan result QRBarcodeDetector API (native) + jsQR fallback (~250KB)jsQR used on Windows Enterprise where BarcodeDetector is unavailable
ChalkerScan assignment QRBarcodeDetector API (native)Mobile/tablet Chrome always has it; no fallback needed
ChalkerGenerate result QRqrcode-generator (~8KB)Displayed via Stats → Result QR

All libraries are standalone JS — no npm or build process required. Loaded via <script> tag.