Skip to content

Latest commit

 

History

History
115 lines (89 loc) · 8.1 KB

File metadata and controls

115 lines (89 loc) · 8.1 KB

CHroS 技術要件 — 対人戦の試作

実装日: 2026-09-18。ユーザーが操作して評価できる、会場LAN向けの単一Next.jsアプリ。 要求は 要件定義、試用手順と自主判断は 試作提案 を参照。 従来のノードグラフ・PostgreSQLを中心とする詳細設計は technical-roadmap.md に保存した。 現在の実装を説明する文書は本書であり、将来設計の実装完了を意味しない。

構成

配置 責務
packages/shared/src/control.ts 状態・コマンド・表示ラベル・Zod検証
packages/scoring/src/match.ts、standings.ts 各戦・2戦合計の判定と予選順位。I/Oを持たない
chros/lib/tournament.ts 総当たり・本戦の生成、入力保護、再試合、監査記録を含む状態遷移
chros/lib/store.ts JSON永続化、更新番号検証、保存後のSSE通知
chros/lib/use-control.ts ブラウザーでのSSE購読、コマンド送信、通信状態とエラー表示
chros/components/ConsoleApp.tsx 進行管理と画面選択
chros/components/MatchEditor.tsx 先攻登録、各戦の結果入力・訂正・再試合
chros/components/management/ 参加者、順位、本戦作成、大会設定、エクスポート、履歴
chros/components/Rundown.tsx 表示順の編集と手動進行
chros/components/Board.tsx 会場掲示とプレビューの共通表示

既存のNext.js 15 / React 19 / TypeScript / pnpm workspaceを継続し、新たな実行時依存は加えていない。 外部フォント・外部画像・CDNを必須にしない。旧ダミー計算と未接続の画面を置き換えた。

保存する状態

ControlState は大会ID、状態リビジョン、保存時刻、計算方式と規則版 2026-09-18.1、 参加者、試合、各戦の結果、過去の無効試合、操作履歴、進行区分、掲示画面・対象試合・ページ、 表示順、ロゴを持つ。1プロセスにつき1つの大会が現在の操作対象。

  • 参加者は人間のみ。IDは得点とは独立し、表示名の重複を防ぐ。
  • Match.a/b は先攻側ではなく固定の参加者枠。先攻は firstCool に記録し、第2戦は反転する。
  • games は番号1・2、A/Bの素点、勝因、特殊勝者、残りターン数を保持する。
  • 試合の状態と勝者は保存データから同じ計算関数で導出する。状態と集計の二重管理をしない。
  • previousAttempts に無効化した両戦・先攻・理由・時刻を保持し、現在の順位計算から除く。
  • 本戦の sources は各枠の元になる試合ID。確定勝者をラウンド順に伝播する。
  • 操作履歴に更新内容を残す。結果訂正では変更前の入力値と訂正理由も記録する。

計算

evaluateGame → evaluateMatch → standings の順に純粋関数で計算する。 方式を切り替えても素点・勝因の意味が変わらないようにする。

方式 各戦の扱い 2戦の比較 予選順位(試作判断)
kushiro 得点比較は特殊Pなし。それ以外の入力済み勝因は勝者に1特殊P 特殊P合計、同値なら得点合計 試合勝数、特殊P、得点
asahikawa 勝数を数え、敗因に応じて0または負の残ターンへ得点を換算。素点は残す ゲーム勝数、同値なら換算得点合計 試合勝数、換算得点

両方式とも2戦終了前は試合勝者を確定しない。完全同値は再試合待ちとし、順位に算入しない。 予選順位が同じ場合、表示上の名前順を順位差とみなさず同順位で示す。 本戦生成時に同順位のシード順・進出者を確定する理由を要求する。 下位者への無条件の繰上げや進出者の重複は拒否する。

シードの初期候補は各組1位→各組2位の順。一般的なシード配置に展開する。 2組×2名なら A1-B2 / B1-A2。2の累乗に不足する枠は上位シード側の不戦進出。 予選対戦順は組・参加者の登録順からの全ペア列挙。コート割当・連戦回避の最適化はしない。

通信切断は、審判が「その参加者に起因する敗北」と裁定した場合に選ぶ。 応答タイムアウト・サーバー故障などの再試合判断は「両戦を仕切り直す」で理由を記録する。 サーバー事象の自動判別・同時事象の優先順位判定は行わない。

API・配信

API 用途
GET /api/state 現在の保存済み全状態
POST /api/state { expectedRevision, command } を検証して適用
GET /api/stream snapshot SSEイベントで全状態を配信
GET /api/export 状態・履歴を含むJSONダウンロード
GET /api/export?format=csv 各戦の対戦者・先後・素点・勝因・勝者・無効理由をCSV出力
GET /api/screen 現在の画面・進行区分・更新番号。旧POSTは410で新APIへ案内

Zodでコマンドの形・得点範囲・文字数などを検証し、状態遷移側で対戦者・進行順・訂正条件を検証する。 更新番号が不一致なら409と最新状態を返す。クライアントは確認を促して自動再送しない。 別サイトからのOriginを伴う書込を拒否する。認証の代わりではない。

保存後にEventEmitterから全状態を送る。再接続時も必ず全状態を送り直すため、 イベント差分の再送履歴・サーバー再起動時の連番リセットに依存しない。 15秒おきにSSEのコメントを送り、切断時は購読とタイマーを解除する。 接続が切れた掲示は最終受信値を残し、再接続中であることを表示する。

永続化と復旧

データディレクトリは CHROS_DATA_DIR。未指定ならNext.jsプロセスの作業ディレクトリにある .chros。 通常の起動では chros/.chros となる。GitとDockerのビルドコンテキストから除外する。

  • state.json が現行状態。
  • state.previous.json は直前の変更前状態。
  • 大会切替時は archive-<時刻>-<revision>.json に大会を退避する。
  • 一時ファイルへ書き、ファイルをfsyncした後にrenameして現行ファイルを置き換える。
  • 読取・更新検証・保存・通知を単一プロセス内で同期的に行う。
  • 保存ファイルが壊れている場合、サンプルを生成して上書きしない。

復旧する場合はサーバーを停止し、現行ファイルを別名で保管したうえで、正常な退避JSONまたは エクスポートJSONを state.json にコピーして再起動する。復元画面は未実装。 ファイル保存は今回の試作判断であり、PostgreSQL設計の完成を意味しない。 複数Nodeプロセス・共有ディスクからの同時書込は非対応。これらが必要になればDBへ移行する。

表示

掲示は16:9。コンテナー単位でプレビューと会場表示を同じ比率にする。 予選順位は1カード6名・1ページ2カード。本戦が8名以下なら全体図、それ以上は各ラウンド4試合ずつ。 ページ番号は状態に保存し、運営と会場で一致させる。 長い名前の一部は会場カード上で省略表示となるため、登録名は会場で読める長さにする。

ロゴはPNG/JPEG/WebP・1 MBまで。外部URLは使用せず、画像データを状態に含める。 画面選択・表示順再生は進行区分と画面をまとめて保存する。 現在の試合は独立して選択し、得点保存で意図せず画面を切り替えない。

検証

判定、旭川換算、左右入替の対称性、訂正、再試合、予選順位、進出、同順位裁定、不戦枠、 開始後の修正保護を packages/scoring/src/index.test.ts で検証する。 実施済みの検証と既知の制約は 試作提案 に記録する。