ステータス:ドラフト(2026-10-26)、コードの根拠。 動作は2026-10-25基準のリポジトリコードと設計ドキュメントから読み取りました。本番DBの「修正した記録」の件数は2026-10-18に読み取った値です。ユーザー数などの運用統計は掲載していません。 作成規則:実測データが基本であり、測定できなかったものは「未測定」、根拠が見つからないものは「確認できず」と表示します。
目次
- 1. 試合が終わると
- 2. 「この試合を記録する」
- 3. 記録画面
- 4. ライブ共有:サーバーがスコアを再計算する
- 5. 修正した記録:同意したものだけ、そしてまだ0件
- 6. 映像はデバイスの外に出ない、そして例外
- 7. 実名の代わりにニックネーム
- 8. サーバーにはどのようなテーブルがあるのか
- 9. まとめ
- 参考文献
- あわせて読みたい
1. 試合が終わると
5ゲームマッチの試合が3:1で終わりました。カメラのスコアボードには「試合終了」画面が表示され、ゲームスコア(11:7、9:11、11:5、13:11)が並んで表示されます。この試合は今どこにあるでしょうか?
スマホの中にだけあります。 第2部で見たように、カメラのスコアボードは試合履歴全体をブラウザの localStorage(ttplay:cam)に保存します。履歴の項目ごとに出所も付いています。画面を押したスコア(tap)、リモコンキー(key)、スコア調整(adj)、自動判定を受け入れたスコア(auto)です。サーバーはこの試合を知りません。
この状態でサーバーに何かを残す方法は、ユーザーが選ぶ3つのルートだけです。
| ルート | サーバーに行くもの | 残る場所 |
|---|---|---|
| この試合を記録する | ゲームスコア、試合時間、相手 | 試合記録、レーティング |
| ライブで共有(試合中) | 得点・やり直し・終了イベント | ライブマッチとイベント列 |
| 自動スコア(試合中) | フレーム(保存しない)、人間がチップに回答した記録 | 修正した記録(映像なし) |
2. 「この試合を記録する」
何が渡るのか
「試合終了」シートの「この試合を記録する」を押すと(backend/static/tt/play/lib/cam-sheets.mjs)画面は試合の概要 {best_of, games, source:'scorekeeper'} を sessionStorage に一時的に保存し、「マイ記録」画面(/tt/me#match)に移動します。ログイン前であればログインを経由します。記録画面で相手を選んで「記録する」を押すと、その時点でサーバーに送信されます(POST /api/tt/matches).
渡るのは ゲームスコアのみです。ラリーごとの履歴、誰がいつどのように得点したか、自動判定の痕跡は渡りません。いくつかのケースでは記録できません。
- 終わっていない試合(勝者がいない)。
- スコア調整で手動で上げたゲーム。第2部で見た通り、このゲームが何対何だったのかをスコアボードが把握していないため記録から除外し、人手で直接入力させます。作られた11:0がレーティングに入ってはいけません。
ログインと表示名
記録を保存するにはGoogleログインが必要です。ゲストは閲覧のみです。書き込み権限はサイトのコミュニティと同様の関門です。
ここで重要な設計が1つあります。 Googleアカウントの名前はどこにも出力しません。 Googleの名前は通常実名であり、対戦相手やランキングにそのまま表示されると、卓球場で会った人に本名を知られてしまいます。そのため、卓球エリアのいかなる応答・画面でもGoogleの名前を使用しません(backend/app/tt_records.py ヘッダーの「Googleの名前は出力しない」)。表示名を決める前の選手は「卓球人 #番号」であり、試合を記録するにはまず表示名(2〜20文字)を決める必要があります。相手の画面に番号だけで表示される試合は、誰が記録したのか判別できないためです。
カメラのスコアボードのニックネーム欄(12文字まで)にも「実名の代わりにニックネーム」という案内が付いています。
相手が確認して初めてレーティングが動く
1人で記録した勝利でレーティングを上げられる場合、ランキングはどちらがたくさん記録したかの表になってしまいます。そのため、アカウントを持つ相手との試合は 確認待ちに入り、相手が「確認」を押して初めて 確認済みになり、その時点で一度レーティングが変動します。相手が「異議」を申し立てることもあります。 7日以内に確認がない場合 レーティングには入らず、個人の記録としてのみ残ります。期限切れはバッチ処理で行を修正せず、読み取り時に作成された時刻で判定します(DBの1つの時計で測定)。
アカウントのない相手(近所の友達)との試合も記録できます。このとき、相手の名前は その試合を記録した人にだけ 表示され、レーティングは変動しません。
Eloレーティング
レーティングはチェス由来のElo方式を採用しています。
# backend/app/tt_records.py 에서 줄임
def k_factor(rating, rated_games):
if rated_games < 10: return 40 # 처음 10경기: 제자리를 빨리 찾게
if rating >= 2000: return 16
return 24
def expected(ra, rb): # A 가 이길 기대값
return 1 / (1 + 10 ** ((rb - ra) / 400))
# 변화량 = K × (실제 결과 − 기대값). 두 사람이 각자 자기 K 를 쓴다.
- 全員1500から始まります。
- レーティングは 試合の勝敗のみを確認します。3:0と3:2を区別しません。コードのコメントには「説明しやすい方を選んだ」と記載されています。
- 2人のKが異なる場合(片方だけ最初の10試合)、変動量の合計が0ではないため、試合の行に2人の変動量を別々に残します。
- ランキングにはプロフィールを 公開に設定した人 のうち、確認された試合が3つ以上の人のみが表示されます。プロフィールは 非公開がデフォルトです。
例えば、両者とも1500で最初の10試合以内であれば期待値は0.5、勝った人は40 × 0.5 = +20、負けた人は−20となります。同じK 40で1600が1500に勝った場合、期待値は約0.64なので+14.4程度しか上がらず、負けると−25.6程度下がります。
1日の上限と同時性
| 上限 | 値 | 理由 |
|---|---|---|
| 1日の試合記録 | 30 | まとめての入力を防止 |
| 同じ相手と1日に | 10 | 示し合わせてスコアをやり取りし、レーティングを上げる行為を遅らせる |
| 1日の練習記録 | 20 | — |
| 試合日 | 1年以内 | 過去の記録をまとめて入力してレーティングを作る抜け道を狭める |
| 1ゲームスコアの上限 | 50 | 入力ミス(110:9)を弾く |
上限はDBでカウントします。同じ人の2つのリクエストが同時に「29番目」としてカウントされて一緒に通過しないよう、人ごとにDBのロック(advisory lock)で順番を制御します。確認する際は試合の行を最初にロックし、2人の選手の行を 番号が小さい方から ロックします。アリスとボブが互いの異なる試合を同時に確認する際に「自分の行から」ロックすると、互いに相手の行を待ってデッドロックに陥るためです。
3. 記録画面
| 画面 | 表示される内容 | 対象 |
|---|---|---|
マイ記録 /tt/me |
自分の試合、確認待ち・異議、レーティング、練習日誌 | 本人 |
ランキング /tt/rank |
公開選手の順位、サークル・今月のフィルター、今週の急上昇 | 誰でも |
履歴 /tt/me/history |
申請・試合・スコアの変化を集約、CSVダウンロード | 本人 |
試合共有 /tt/m/{코드} |
共有を有効にした1つの試合 | リンクを受け取った人 |
履歴は新しい記録を蓄積せず、各機能がすでに残したものを読み込んで集約します(backend/app/tt_history.py)。試合記録、ライブスコアボードイベント(サーバーが再計算)、録画解析結果、自動スコアを修正した記録、レーティングの変動が1行に混ざって表示されます。ライブスコアボードと試合記録は互いにキーでつながっていないため、「終了スコアが同じで時間が近い」試合を 推測で つなぎます。誤ってつなぐ可能性があるため、画面にはフローの出所(ライブ・映像)を常に併記します。URLに人の番号が含まれず、すべてのクエリがログイン中の人のみに限定されるため、他人の履歴を見る方法はありません。CSVはExcelの数式インジェクション対策を施して出力されます。
試合共有は試合番号ではなく、別途作成した11文字のコードを使用します。試合番号は連番であるためURLに使用すると他人の試合を1つずつ総当たりで閲覧できるためです。確認された試合は両選手のどちらでも、個人の記録は記録した人のみが共有を有効にできます。確認待ちや異議が申し立てられている試合は、「実際にあった試合」であるかすら決まっていないため共有されません。共有画面でも公開していない選手は「卓球人」、アカウントのない相手の名前は最後まで表示されません。
4. ライブ共有:サーバーがスコアを再計算する
試合中に「ライブで共有」を有効にすると、観戦リンクを受け取った人がリアルタイムでスコアを閲覧できます(backend/app/tt_watch.py).
- イベントのみを送信する。 ブラウザが「現在7:5」を送信する場合、任意のスコアを中継できてしまいます。そのため、得点・やり直し・終了イベントのみを受け取り、スコアはそのライブのイベントをサーバーが 最初からルール通りに最初からやり直して 決定します。第2部の「状態ではなく履歴」がサーバーでもそのまま適用されます。このときに使用するルールが第1部で述べた3つ目のコピー(
LiveBoard)であり、同じケースファイルとエンジンとのランダムな照合で結び付けられています。 - 順序番号。 イベントごとに1から増加する番号を付与します。スマートフォンは得点直後に画面がロックされたり通信が途切れたりして同じ束を再度送信する場合があるため、すでに受信した番号を同じ内容で再送した場合は静かに受け入れ、異なる内容であったり番号が抜けたりしている場合は409とサーバーの最後の番号を返します。画面は409を受け取ると送信を停止します。
- 書き込みは所有者 + トークン。 ライブを作成したアカウントでログインしている必要があり、作成時に一度だけ取得できるトークンを ヘッダーで 送信する必要があります。URLや本文に含めないのは、ログや共有リンクに残らないようにするためであり、DBにはトークンのハッシュのみを保持します。
- 調整はイベントとして転送する。 手動のスコアボードでかなり前の項目を消去した場合、ライブにはその分のやり直しを送信して再送します。
- ダブルスは送信しません(第2部)。
- 3時間イベントがない場合は終了したものとみなします。応援絵文字はこの時間を延長しません。
名前のルールも同様です。リンクは記録者が直接共有したものであるためその試合で設定されたニックネームが表示されますが、誰でも閲覧できる ホームフィードでは、公開を有効にしていない所有者は「卓球人」、相手は「対戦相手」として隠されます。
5. 修正した記録:同意したものだけ、そしてまだ0件
第3部で、人間が自動判定のチップに回答した場合(「正解」、別の選手に修正、「無効」)に、その回答が 修正した記録になると説明しました。これがどこに向かうのかを追ってみます。
何を含むのか
自動スコア画面は、人間が回答したラリーごとに1行を作成して POST /api/tt/corrections に送信します。行には以下が含まれます(backend/app/tt_corrections.py).
- 元の判定(勝者側、理由、信頼度、確認必要性の有無)と修正後の判定。
- その瞬間にエンジンが確認したイベントの概要(16個以下、数・側・種類のみ)。
- 信号ごとの根拠(ボールの信頼度、何を判定したか、信号が食い違っていなかったか)。
- 学習利用の 同意の有無.
映像は含みません。 サーバーはブラウザが送信したものをそのまま格納するのではなく、既知の形状に新しく整形します。テキストが入る余地がほとんどないため、悪口・URL・スクリプトが蓄積される場所はありません。行のキーにはログイン中の人の番号が含まれるため、他人の行を上書きすることはできません。上限は1人あたり1分間に30リクエスト、1日に2,000行です。
時間が経ってそのまま取り込まれた判定には修正の記録を残しません。人間が確認したものではないからです。
誰が何を持ち出すのか
- 同意していない行も記録します。 「人間がどれだけ修正するか」を数えるサービス指標だからです。
- 学習や評価に出力されるエクスポートには、同意した行のみ が対象となります。運営者専用のエクスポート(
GET /api/tt/corrections/export)では、個人の番号の代わりに エクスポートごとに新しく生成したソルトで作成したマスキング値を載せます。1つのファイル内では同一人物を紐づけられますが、ファイル間、および当社のDBとは結合できません。 - 自分の記録はいつでも削除できます(
DELETE /api/tt/corrections/mine).
これまでに学習に使ったことはない
2026-10-18時点の運営DBの読み取りにおいて、修正の記録は 0件でした。したがって、 修正記録を学習や評価に使用したことはまだ一度もありません。 第5作の学習モデルはいずれも公開データと合成画像で学習しています。
計画はあります。第5作で見たように、学習型モジュールを商用利用可能なデータで再学習する際、学習の利用に同意した人の修正判定は、「確認必要」の補正といったモジュールの材料になり得ます。「人間が修正した判定」はすなわち「エンジンが間違えた箇所」の正解だからです。ただしそれには修正の記録が十分に蓄積される必要があり、学習材料として使う際の保管期間や同意に関する案内の再点検が先決です。
6. 映像は端末の外に出ない、そして例外
カメラ得点板の第一原則は カメラ映像は端末の外に出ないことです(backend/static/tt/play/cam.js まるがき)。録画もスマートフォン内で合成し、スマートフォンに保存します。得点読み上げもブラウザの音声合成を使用します。
例外となるのは、ユーザーが有効にする2つの機能です。
自動得点。 ボールを探すGPUがサーバーにあるため、フレームを送信します。代わりに範囲を絞りました(第1部)。
- 512×288に縮小したJPEGのみ、1秒あたり最大30枚。
- Googleログイン会員のみ、セッションの範囲内のみ。
- ウェブ中継はフレームを通過させるだけで、作業者はボールモデルが参照する3枚のウィンドウのみをメモリに保持します。 フレームは保存しません。
- 作業者ログには、映像、ボールの座標、個人情報を残しません。
- カメラを起動する際、音声の権限は要求しません。
録画ファイルのアップロードによる分析。 ユーザーが映像ファイルを直接選択してアップロードする別の機能です。ブラウザが事前に360p60 500kbpsに縮小してアップロードし、元ファイルは30日後に削除します。分析作業者はDBとストレージのキーを持たず、ウェブの内部APIのみで作業を行います。
7. 本名の代わりにニックネーム
プライバシー設計をひとことでまとめると 「卓球場で出会った人に本名を知られないようにする」 ことです。
| 場所 | 表示名 |
|---|---|
| カメラ得点板の画面 | その試合で設定したニックネーム(12文字、「本名の代わりにニックネーム」の案内) |
| 試合記録・対戦相手の画面 | 表示名(2〜20文字)。設定する前は「卓球人 #番号」 |
| ランキング・公開プロフィール | 公開を有効にした人のみ。デフォルトは非公開 |
| アカウントのない対戦相手 | その試合を記録した人のみに表示 |
| ライブホームフィード | 公開していない主人は「卓球人」、相手は「対戦相手」 |
| 修正記録のエクスポート | 名前なし、エクスポートごとに変更されるマスキング値 |
| Googleアカウント名 | どこにも出力されない |
デモ映像でも同じ原則を適用しています。元の画面に焼き込まれている名前(中継の字幕)はそのまま残しますが、こちらで描き加えた文字は「選手A・B」のみを使用します。
8. サーバーにはどのようなテーブルがあるか
記録に関連するサーバー側のテーブルをまとめると以下のようになります。テーブルごとにDBのマイグレーション番号が付与されており、1つのマイグレーションが1つの機能を導入しています。
| マイグレーション | テーブル | 格納するもの | 格納しないもの |
|---|---|---|---|
| 0036 | tt_players |
表示名(2〜20文字、空欄の場合は「卓球人 #番号」)、レーティング、レーティング対象となった試合数、公開設定(デフォルト非公開) | Google名 |
| 0036 | tt_matches |
試合時間、ゲームスコア、ソース(直接入力・得点アシスト・録画分析)、ステータス(確認待ち・確認済み・異議・期限切れ・個人記録)、2人のレーティング変動量 | ラリーごとの履歴 |
| 0036 | tt_practice |
練習日誌(本人のみ閲覧) | — |
| 0039 | tt_live_matches·tt_live_events |
ライブの試合とイベントの列(番号付き得点・取り消し・終了)。同じマイグレーションで試合共有用の11文字のコードを tt_matches に追加 |
スコア自体(サーバーがイベントから再計算)、映像 |
| 0040 | tt_corrections |
元の判定・修正された判定・イベントの要約・根拠・同意の有無 | 映像 |
| 0048 | tt_videos |
ユーザーがアップロードした映像の分析キューと結果 | — (元ファイルは30日後に削除) |
これらのテーブルで注目すべき点は、 「何を格納しないか」が設計の半分であるということです。試合記録はラリーを把握せず、ライブはスコアを保存せずにイベントのみを保存し、修正記録は映像を把握しません。各テーブルは自身の機能に必須なものだけを保持し、履歴画面は新しいテーブルを作成せずにこれらのテーブルを読み込んで集約します(第3節)。格納されていないデータは流出することもありません。
9. まとめ
- 終了した試合は基本的に スマートフォンにのみ 残ります。サーバーに送信されるのは、ユーザーが選択した記録、ライブ、自動得点のみです。
- 記録はゲームスコアのみを送信し、対戦相手が7日以内に確認して初めてEloレーティング(K=40・24・16)が変動します。1日の制限とDBのロック機能により、まとめての入力や同時リクエストを防ぎます。
- ライブはイベントのみを受け取り、サーバーがルールに従って再計算を行います。書き込みには、主人のアカウントとヘッダーのトークンが必要です。
- 修正記録は映像なしで保存され、同意したもののみがマスキングされてエクスポートされます。まだ0件であるため、学習に使用したことはありません。
- カメラ映像は端末の外に出ず、自動得点のフレームはサーバーを通過するだけです。Googleの本名はどこにも送信されません。
ティティの一言。 今日の試合はスマホが先に覚えています。サーバーには「何対何だったのか」だけを伝え、名前もニックネームだけで大丈夫です。相手に「その通り」と承認してもらって初めてスコアが上がるため、勝った日はぜひ確認をお願いします。
参考文献
- A. E. Elo. The Rating of Chessplayers, Past and Present. Arco, 1978. (Eloレーティングの原典。原文は今回参照せず、当社方式はコード
backend/app/tt_records.pyで確認) - International Table Tennis Federation. ITTF Statutes 2026, Chapter 2: The Laws of Table Tennis, §2.12(試合とゲーム数). https://db.ittf.com/document
- コードと設計: リポジトリ
backend/app/tt_records.py·tt_history.py·tt_watch.py·tt_corrections.py·tt_live.pyの冒頭、backend/static/tt/play/cam.js·lib/cam-sheets.mjs·lib/camauto.mjs.
あわせて読みたい
- 本シリーズの 第1部: 全体構造: 何が端末の外に出ていくのかの表。
- 本シリーズの 第2部: 手動得点板と試合ルールエンジン: 「状態ではなく履歴」と調整フィールド。
- 本シリーズの 第3部: 自動判定: 修正記録が生まれるチップの答え。
- 本シリーズの 第5部: データ・ライセンス・学習プロセス: 修正記録が学習の材料になり得る条件と、商用サービスに合わせた学習データの原則。