OpenScore
Universal sports scoring and scheduling engine. Pure functional, offline-first, cross-language.
One-liner
A scoring and scheduling engine for any sport, built on one contract: apply(state, action, ruleset) → {state, events}.
Why
Every sports app reinvents scoring logic, but scoring is fundamentally a pure function: given a state and an action, produce a new state and events. Extract that layer and the engine becomes reusable across sports and platforms.
Core Design
Action-sourced, not event-sourced. The engine is passive — it doesn’t track time, only reacts to actions. History is an action sequence, not an event log. Undo = replay from scratch minus the last action, not a reversal.
State bubbling: Point scored → recursively check win conditions upward. game_won → set_won → match_won, events bubble bottom-up.
Declarative rulesets: Win conditions, display formats, special behaviors are all config, not code. Same engine, different ruleset, different sport.
Supported Sports
9 sports, 18+ rulesets:
| Sport | Rulesets |
|---|---|
| Tennis | standard, grand slam, no-ad |
| Badminton | standard, doubles |
| Basketball | NBA, FIBA, 3x3 |
| Padel | standard, golden point |
| Pickleball | rally scoring, side-out |
| Table Tennis | standard, short |
| Volleyball | standard |
| Baseball | MLB |
| Softball | fastpitch |
Cross-Language Conformance
The engine is implemented in 4 languages: Elixir (all 9 sports), JavaScript, Swift, and Kotlin. All implementations run the same JSON conformance test suite (609 cases) to guarantee identical behavior.
Three-Layer Architecture
Platform Layer — Phoenix API + WebSocket + Astro frontend + iOS (SwiftUI)
Schedule Layer — Tournament engine (knockout, double elimination, round robin, swiss, group knockout)
Match Layer — Single-match scoring engine
The schedule engine handles winner_of / loser_of dependency resolution: match completed → resolve downstream brackets → emit match_ready when both sides are determined.
Realtime Sync
Phoenix Channels handle live scoring sync. Match Channel and Session Channel let multiple viewers watch the same match’s live score. The iOS app stays in sync via WebSocket.