The Match Control page (URL: /event/$eventcode/control/) serves as the central command hub for tournament operations. It provides Head Referees, Scorekeepers, and Event Managers with real-time match scheduling, scoring, and administrative controls.
LOADrequires bothactiveState === "IDLE"andloadedState === "IDLE". A staged match must be unloaded first; an active match must be aborted or committed first.UNLOADclears the staged match (loadedMatch = null,loadedState = "IDLE").UNLOADis rejected when no staged match exists (loadedState === "IDLE").- The frontend auto-chains
UNLOAD → LOADwhen the operator clicks "Load Next Match" while a match is already staged.
The server transition layer asserts these invariants on read and write:
loadedState === "IDLE"impliesloadedMatch === null.loadedState !== "IDLE"impliesloadedMatch !== null.activeState === "IDLE"impliesactiveMatch === nullandactiveStartedAtMs === null.activeState !== "IDLE"impliesactiveMatch !== nullandactiveStartedAtMs !== null.
- Match-control state and sync versions are managed in-memory per event code.
- Current implementation assumes a single Bun process/event loop.
- Multi-process deployments need shared coordination (e.g. Redis/event-log backing) to preserve ordering and optimistic concurrency semantics.
The game uses a Flag Defense scoring system with four scoring sections:
| Section | Category | Scoring Mechanics |
|---|---|---|
| A | Flags Defended | Own alliance flags protected: L2 (25pts), L1 (20pts), Center (10pts) |
| B | Flag Offense | Enemy flags destroyed: Center (30pts), Other (10pts) |
| C | Bullet Penalty | Bullets in enemy backfield: Deduct 10pts per bullet |
| D | Endgame | Robot parking (0/10/15pts) + Golden flag bonus (10pts) |
Goal: Queue and display next match.
Steps:
-
Load Next Match
- Fetches next match from schedule
- Updates "Loaded Match" display (name, duration, team numbers)
- Shows Red and Blue alliance team assignments (e.g., "Red: 5, 2 Blue: 8, 3")
- Activates Show Preview button
-
Show Preview
- Displays match info and teams to audience
- Allows teams to take field positions
- Proceeds when ready
-
Show Match
- Displays match view to audience (ready state)
- Activates Start Match button
Goal: Run the match and record scores.
Steps:
-
Start Match
- Begins official 8:00 countdown timer
- Activates scoring input panels (red/blue alliances side-by-side)
- Shows real-time score calculation as data entered
-
Live Scoring Entry
Section A — Cờ được bảo vệ (Flags Defended):
- L2 Flags: 25 pts each
- L1 Flags: 20 pts each
- Lúa trung tâm: 10 pts each
Section B — Bắn phá (Flag Offense):
- Lúa rơi trung tâm: 30 pts each
- Other Flags Shot: 10 pts each
Section C — Đạn trên sân (Bullet Penalty):
- Deducts 10 pts per bullet in enemy backfield
Section D — Giai đoạn kết thúc (Endgame):
- Robot Parking: Không (0) / Một phần (10) / Toàn bộ (15)
- Golden Flags: 10 pts each
-
Score Calculation
- Total = Section A + Section B − Section C + Section D
- Auto-updates as operators enter data
-
Match Completion
- Timer reaches 0:00
- Final scores locked and displayed
- Ready for submission
Goal: Finalize and commit match results to tournament record.
Steps:
-
Review Scores
- Verify final scoring for both alliances
- Check for fouls/penalties applied correctly
- Confirm robot parking/positioning states
-
Commit & Post Last Match (Available after match completion)
- Locks scores from further modification
- Updates match state to COMMITTED
- Publishes results to public displays and records
- Enables scheduling of next match
-
Score Edit (Available for adjustment)
- If errors detected, scores can be reopened for correction
- Allows edit via dedicated "Score Edit" tab
- Re-submits corrected scores
Goal: Replay or reload a match.
Steps:
- In Schedule table, click Replay (for COMMITTED matches)
- Reloads match to "Loaded Match" position
- Proceed through Load → Preview → Show → Start sequence again
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ [Exit] 🌐 English ▼ 1234 - Tournament System running at 10.0.0.1 │
│ │
│ Match Control │
│ │
│ Loaded Match: Playoff Match 3 8:00 (Not Started) Red: 5, 2 Blue: 8, 3 │
│ Active Match: │
│ │
│ [Load Next Match] [Show Preview] [Randomize Field] [Show Random] [Show Match] │
│ [Start Match] [Commit & Post Last Match] │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ [Schedule] [Incomplete Matches] [Score Edit] [Active Match] [Settings] [Alliance Selection] [Video Switch]...│
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Match Round Field State Red Score Red R1 R2 B1 B2 Blue Blue Score Actions │
│ Playoff Match 1 1 1 COMMITTED 147 A1 5 2 1 14 A4 207 [Replay] [Post] [Edit] │
│ Playoff Match 2 1 2 COMMITTED 143 A2 8 3 4 12 A3 151 [Replay] [Post] [Edit] │
│ Playoff Match 3 2 1 UNPLAYED 0 A1 5 2 8 3 A2 0 [Play] [Enter Scores] │
│ Playoff Match 4 2 2 UNPLAYED 0 A4 1 14 4 12 A3 0 [Play] [Enter Scores] │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Status Indicators:
-
Loaded Match: Current match queued for execution
- Match name (e.g., "Playoff Match 3")
- Remaining time (e.g., "8:00")
- Status badge (Not Started, In Progress, Completed)
- Team numbers per alliance (e.g., "Red: 5, 2")
-
Active Match: Currently executing match (shown when timer running)
Primary control buttons for match flow, matching the image reference:
| Button | State | Usage | Notes |
|---|---|---|---|
| Load Next Match | Active initially | Queue next match from schedule | Primary action to begin |
| Show Preview | Active after load | Display team/match info to audience | Pre-match setup (green highlight when active) |
| Randomize Field | Active after load | Trigger randomization if applicable | Field preparation (green highlight when active) |
| Show Random | Active after setup | Display randomization to audience | Field preparation |
| Show Match | Active when ready | Display match view to audience | Ready for scoring |
| Start Match | Active after setup | Begin official 8:00 timer & scoring | Initiates live match |
| Commit & Post... | Active on complete | Finalize scores | Right-aligned, final step |
Workflow Sequence:
- Load Next Match → Load match from schedule
- Show Preview → Display match info to audience
- Randomize Field / Show Random → Set up field
- Show Match → Prepare audience display
- Start Match → Begin official timer, activate scoring panels
Button States:
- Active buttons: Regular contrast, with "Show Preview" and "Randomize Field" using standout colors (e.g., green
bg-green-600) when they are the next expected step. - Disabled buttons: Grayed out, not clickable
- Each enables the next logical step in sequence
Horizontal tabs for different control views:
| Tab | Purpose | Content |
|---|---|---|
| Schedule | View all matches | Full tournament schedule table (active state) |
| Incomplete Matches | Monitor unfinished | Filtered list of in-progress matches |
| Score Edit | Correct scores | Score adjustment interface |
| Active Match | Detailed scoring | Real-time scoring panels and timer |
| Settings | Event configuration | Test mode, timing, live scoring options |
| Alliance Selection | Team assignments | Alliance picker (disabled when active) |
| Video Switch | Broadcast control | Camera/stream selection |
| Present Awards | Results display | Award ceremony visuals |
| Help | Documentation | Context-sensitive help |
| Column | Type | Styling Notes |
|---|---|---|
| Match | Text | Match ID (Playoff Match 1, etc.) |
| Round | Number | Tournament round using tabular-nums |
| Field | Number | Field assignment, tabular alignment |
| State | Badge | COMMITTED, UNPLAYED, IN_PROGRESS |
| Red Score | Number | Total red alliance points, tabular font |
| Red | Text | Red Alliance ID (e.g., A1), shown during playoffs |
| R1 / R2 | Number | Team numbers, red alliance tint |
| B1 / B2 | Number | Team numbers, blue alliance tint |
| Blue | Text | Blue Alliance ID (e.g., A2), shown during playoffs |
| Blue Score | Number | Total blue alliance points, tabular font |
| Actions | Buttons | Context-dependent button set (e.g., Replay, Post, Edit, Play, Enter Scores) |
Played Matches (COMMITTED):
[Replay]→ Restart match[Post]→ Post to displays (if not posted)[Edit]→ Modify score details
Unplayed Matches (UNPLAYED):
[Play]→ Start match sequence[Enter Scores]→ Manual score input
In-Progress Matches (IN_PROGRESS):
- Timer shows active countdown
- Scoring panels live update
- Actions locked during play
The ScoringEntryPage component handles live scoring entry for each alliance during a match.
interface ScoringEntryPageProps {
alliance: "blue" | "red"; // Alliance being scored
eventCode: string; // Event identifier
fieldNumber: string; // Field number
matchNumber: number; // Match number
onNavigate: (path: string) => void;
}interface ScoringState {
flagsL2Defended: number; // Section A - L2 flags (25 pts each)
flagsL1Defended: number; // Section A - L1 flags (20 pts each)
flagsCenterDefended: number; // Section A - Center flags (10 pts each)
flagsCenterShot: number; // Section B - Center flags shot (30 pts each)
flagsOtherShot: number; // Section B - Other flags shot (10 pts each)
bulletsInEnemyZone: number; // Section C - Bullets (−10 pts each)
robotParking: 0 | 1 | 2; // Section D - Parking (0=None, 1=Partial 10pts, 2=Full 15pts)
goldenFlagsBonus: number; // Section D - Golden flags (10 pts each)
}┌─────────────────────────────────────────┐
│ [Red Alliance Scoring] Match M3 F1 │ (Color-coded header)
├─────────────────────────────────────────┤
│ A — Số cờ được bảo vệ │
│ Lúa tầng 2 [−] 1 [+] 25 điểm / 1 │
│ Lúa tầng 1 [−] 2 [+] 20 điểm / 1 │
│ Lúa trung tâm[−] 1 [+] 10 điểm / 1 │
│ │
│ B — Bắn phá trên sân đối phương │
│ Lúa rơi trung tâm [−] 2 [+] 30 pts │
│ Lúa rơi khác [−] 1 [+] 10 pts │
│ │
│ C — Số đạn trên sân đối phương │
│ Số đạn [−] 0 [+] Loại bỏ cờ │
│ │
│ D — Giai đoạn kết thúc trận đấu │
│ Vị trí đỗ: [Không] [Một phần] [Toàn] │
│ Selected: Toàn bộ (15pts) │
│ Cờ vàng [−] 0 [+] 10 điểm / 1 │
│ │
│ ╔════════════════════════════════╗ │
│ ║ Tổng điểm: 147 ║ │
│ ╚════════════════════════════════╝ │
│ │
│ [Submit Score] (Full width button) │
└─────────────────────────────────────────┘
const calcTotal = (s: ScoringState): number => {
const scoreA =
s.flagsL2Defended * 25 +
s.flagsL1Defended * 20 +
s.flagsCenterDefended * 10;
const scoreB =
s.flagsCenterShot * 30 +
s.flagsOtherShot * 10;
const scoreC = -(s.bulletsInEnemyZone * 10); // Penalty
const scoreD =
(s.robotParking === 2 ? 15 : s.robotParking === 1 ? 10 : 0) +
s.goldenFlagsBonus * 10;
return Math.max(0, scoreA + scoreB + scoreC + scoreD);
};CounterRow:
- Label + point hint text
- Decrement button (−)
- Display value (bold, right-aligned)
- Increment button (+)
- Prevents negative values
SectionHeader:
- Colored accent stripe (alliance color)
- Uppercase section label
Parking State Toggle:
- Three exclusive buttons: Không / Một phần / Toàn bộ
- Active state highlighted with alliance color
- Only one state selectable at a time
The MatchScoresheetPage component displays match scores in read-only format for review and analysis.
interface MatchScoresheetPageProps {
eventCode: string;
matchName: string; // Format: Q1, P5, E2 (Q=quals, P=practice, E=elims)
allianceFilter?: "red" | "blue"; // Optional: show only one alliance
onNavigate: (path: string) => void;
token: string | null;
}- Match Name Parsing: Extracts match type and number from name string (regex:
^([QEP])(\d+)$) - Alliance Toggle: Mobile-friendly selector (Blue / Red / All)
- Read-only Display: Shows all four scoring sections per alliance
- Flexible Layout: Single column (mobile) or two-column (desktop)
- Back Navigation: Returns to match results page
interface MatchHistoryItem {
ts: number;
alliance: "red" | "blue";
aSecondTierFlags: number; // Section A - L2 flags
aFirstTierFlags: number; // Section A - L1 flags
aCenterFlags: number; // Section A - Center flags
bCenterFlagDown: number; // Section B - Center shot
bBaseFlagsDown: number; // Section B - Other shot
cOpponentBackfieldBullets: number; // Section C - Bullets
dRobotParkState: number; // Section D - Parking state
dGoldFlagsDefended: number; // Section D - Golden flags
scoreA: number;
scoreB: number;
scoreC: number;
scoreD: number;
scoreTotal: number; // Total calculated score
}┌─────────────────────────────────────┐
│ ← Back to Match Results │
│ Scoresheet for Q1 │
├─────────────────────────────────────┤
│ [Blue] [Red] [All] (Mobile toggle) │
├─────────────────────────────────────┤
│ RED ALLIANCE BLUE ALLIANCE │
├─────────────────────────────────────┤
│ A — Số cờ được bảo vệ │
│ Lúa tầng 2 25 pts/1 2 │
│ Lúa tầng 1 20 pts/1 1 │
│ Lúa trung tâm 10 pts/1 1 │
│ │
│ B — Bắn phá │
│ Bắn hạ cờ TT 30 pts/1 2 │
│ Lúa rơi khác 10 pts/1 1 │
│ │
│ C — Đạn trên sân │
│ Số đạn loại bỏ cờ 0 │
│ │
│ D — Giai đoạn kết thúc │
│ Vị trí đỗ [Toàn bộ] 15 │
│ Cờ vàng 10 pts/1 1 │
│ │
│ ╔══════════════════════════════╗ │
│ ║ Tổng điểm: 147 ║ │
│ ╚══════════════════════════════╝ │
└─────────────────────────────────────┘
// Display scoresheet for Red alliance only
<MatchScoresheetPage
eventCode="event123"
matchName="Q1"
allianceFilter="red"
token={authToken}
onNavigate={navigate}
/>
// Display both alliances with toggle
<MatchScoresheetPage
eventCode="event123"
matchName="P5"
token={authToken}
onNavigate={navigate}
/>- Enter Test Mode → Load dummy match for testing
- Advance to Quals → Move to qualification matches
- Return to Practice → Return to practice matches
| Setting | Default | Effect |
|---|---|---|
| Use Live Scoring | ON | Enable real-time score input |
| Require Referee Init Submit Before Start | OFF | Referee must confirm readiness |
| Enable Penalty Referee Tablets | OFF | Remote penalty entry devices |
| Enable HR Match Control (Beta) | OFF | Head referee tablet integration |
| Allow External Randomization | OFF | External field randomizer device |
| Setting | Options | Effect |
|---|---|---|
| Flip Alliances | OFF | Red on right; ON = Red on left |
| Score Tab Contrast | Standard / High | Display contrast adjustment |
| Setting | Options | Effect |
|---|---|---|
| Sync Delay | 5s, 10s, 15s, 30s | Network sync polling interval |
UNPLAYED
├─ Load Next Match
└─ ▼ PENDING
PENDING
├─ Show Preview
├─ Randomize Field
└─ ▼ READY
READY
├─ Show Match
└─ Start Match
└─ ▼ IN_PROGRESS
IN_PROGRESS
├─ Live Scoring Input
├─ Timer Running
└─ [Timer Expires]
└─ ▼ COMPLETED
COMPLETED
├─ Review Scores
├─ Optional: Edit
└─ Commit & Post
└─ ▼ COMMITTED
COMMITTED
├─ Published to public displays
├─ Locked from modification
├─ [Replay] available
└─ Schedule continues
| State | Available Buttons |
|---|---|
| UNPLAYED | Load Next Match |
| PENDING | Show Preview, Randomize Field |
| READY | Show Match, Start Match |
| IN_PROGRESS | (Timer active, scoring input only) |
| COMPLETED | Commit & Post Last Match, Edit |
| COMMITTED | Load Next Match, Replay, Post, Edit |
- Sync Delay: Configurable polling interval (5-30s)
- Network State: Displayed in footer ("6m behind" = scheduling lag)
- Field Status: Real-time updates from field tablets
- Score State: Atomic commits with validation
- Connection Loss: UI shows "network error" with retry
- Score Conflict: Shows mismatch alert; operator chooses version
- Timer Desync: Resynchronizes with server on next interval
- Incomplete Submit: Prevents progression until scores complete
- Use
--alliance-redand--alliance-blueCSS variables for alliance indication - Ensure hover states (e.g.,
hover:bg-muted/50) on interactive rows - High contrast button labels for visibility
- Semantic status badges (✓, ●, ○) + color + text
- Tabular Numbers: All scores, team numbers, round/field use
font-variant-numeric: tabular-nums - Page Title:
h1or.page-titleclass, centered - Labels: Muted foreground color for descriptive text
- Values: Bold/bright foreground for data
- Schedule table: Horizontal scroll on small screens
- Action buttons: Stack vertically if needed
- Tabs: Horizontal scroll with show/hide for less critical tabs
- All buttons and links keyboard-accessible
- Tab order follows visual flow
- ARIA labels on status indicators
- Form inputs properly labeled
- ScoringEntryPage → Operator enters alliance scores
- MatchScoresheetPage → Review scores before submission
- Schedule Table → Final scores appear in tournament listing
ScoringEntryPage requires:
alliance: "red" | "blue" (determined by which operator/tablet)eventCode,fieldNumber,matchNumber: Match contextonNavigate: Back button handler
MatchScoresheetPage requires:
matchName: Parsed to determine match type/numbereventCode: Event contexttoken: Authentication for API calls- Optional
allianceFilter: Restrict to one alliance
Local State (within components):
// ScoringEntryPage
const [score, setScore] = useState<ScoringState>(INITIAL_STATE);
const total = calcTotal(score);
// MatchScoresheetPage
const { scoresheet, isLoading, error } = useMatchScoresheet(
eventCode,
matchType,
matchNumber,
token,
enabled
);State Updates:
- Counters increment/decrement:
setScore(s => ({ ...s, field: value })) - Parking toggle:
setScore(s => ({ ...s, robotParking: newValue })) - Total recalculates automatically via
calcTotal(score)
CounterRow:
- Props:
label,value,pts,onIncrement,onDecrement - Renders: − button | value display | + button | points hint
- Style: Flex layout, border-bottom separator
SectionHeader:
- Props:
label,accent(alliance color) - Renders: Colored accent stripe | uppercase label
- Style: Border-bottom underline
PARKING_OPTIONS:
const PARKING_OPTIONS: { label: string; value: ParkingState }[] = [
{ value: 0, label: "Không" },
{ value: 1, label: "Một phần" },
{ value: 2, label: "Toàn bộ" },
];useMatchScoresheet() (MatchScoresheetPage):
const { scoresheet, isLoading, error } = useMatchScoresheet(
eventCode,
matchType,
matchNumber,
token,
shouldFetch // Boolean to enable/disable fetch
);- Fetches scoresheet data from API
- Returns
MatchScoresheetwith red/blue scores - Handles loading and error states
Color Constants:
const ALLIANCE_COLOR: Record<"red" | "blue", string> = {
red: "#dc2626", // Tailwind red-600
blue: "#0284c7", // Tailwind sky-600
};-
Startup
- Load event schedule
- Display "Load Next Match" as primary action
- Show Schedule tab with all matches
-
Match Preparation
- Click "Load Next Match"
- System queues next match, displays team assignments
- "Show Preview" becomes active (green)
-
Randomization & Setup
- Click "Randomize Field"
- Field elements randomized per game rules
- "Show Match" and "Start Match" become active
-
Match Execution
- Click "Start Match"
- Timer begins (8:00 countdown)
- Scoring panels live-update as fouls/scores entered
- Tab automatically switches to "Active Match"
-
Score Finalization
- Timer expires; match state = COMPLETED
- "Commit & Post Last Match" becomes active
- Operator reviews scores for accuracy
-
Publication
- Click "Commit & Post"
- Scores locked and published
- Match state = COMMITTED
- Displays updated in real-time
- Schedule advances
-
Next Match
- Repeat cycle from step 2
| Scenario | Detection | Recovery |
|---|---|---|
| Network disconnect | Sync timer expires | Retry on reconnect; cache local scores |
| Score input incomplete | Validation on submit | Highlight missing fields; prevent commit |
| Timer desynchronization | Server vs. local mismatch | Force resync; display warning |
| Duplicate submit | Idempotency check | Server rejects; show confirmation modal |
| Match already in progress | State check | Disable "Load Next" if match running |
| No randomization | Field status check | "Start Match" disabled until randomized |
- Multi-field support: Manage multiple matches in parallel
- Remote scoring: Mobile tablet interface for on-field referees
- Match replay: Video replay integration with scoring overlays
- Analytics: Real-time match statistics and trend analysis
- Mobile app: Native iOS/Android for operator control
- Accessibility: Full screen reader support, voice commands
./.claude/rules/development-rules.md- Coding standards./docs/design-guidelines.md- UI/UX patterns./docs/code-standards.md- TypeScript conventions./README.md- Project overview