OpenScore
通用運動計分與賽程引擎。Pure functional、離線優先、跨語言。
一句話
一個任何運動都能用的計分與賽程引擎,核心是一行合約:apply(state, action, ruleset) → {state, events}。
為什麼做這個
每個運動 app 都在重新發明計分邏輯,但計分的本質是純函數:給定當前狀態和一個動作,產生新狀態和事件。把這層抽出來,引擎就能跨運動、跨平台複用。
核心設計
Action-sourced,不是 event-sourced。 引擎是被動的 — 不追蹤時間,只對動作做反應。歷史是 action 序列,不是 event log。Undo = 從頭 replay 扣掉最後一個 action,不是反向操作。
State bubbling: 得分 → 遞迴向上檢查勝負條件。game_won → set_won → match_won,事件由下往上冒泡。
Ruleset 是宣告式的: 勝負條件、顯示格式、特殊行為全用 config 描述,不用程式碼。同一個引擎吃不同 ruleset 就能處理不同運動。
支援的運動
9 種運動、18+ 種規則集:
| 運動 | 規則集 |
|---|---|
| 網球 | standard, grand slam, no-ad |
| 羽球 | standard, doubles |
| 籃球 | NBA, FIBA, 3x3 |
| 板式網球 | standard, golden point |
| 匹克球 | rally scoring, side-out |
| 桌球 | standard, short |
| 排球 | standard |
| 棒球 | MLB |
| 壘球 | fastpitch |
跨語言一致性
引擎用 4 種語言實作:Elixir(完整 9 種)、JavaScript、Swift、Kotlin。所有實作跑同一組 JSON conformance test(609 cases),確保行為一致。
三層架構
Platform Layer — Phoenix API + WebSocket + Astro 前端 + iOS (SwiftUI)
Schedule Layer — 賽程引擎(淘汰賽、雙敗、循環賽、瑞士制、分組)
Match Layer — 單場計分引擎
賽程引擎處理 winner_of / loser_of 依賴解析:一場比賽結束 → 自動解析下游對陣 → 兩邊都到齊時 emit match_ready。
即時同步
Phoenix Channels 處理即時計分同步。Match Channel 和 Session Channel 讓多人同時觀看同一場比賽的 live score。iOS app 透過 WebSocket 與 server 保持同步。