Skip to content

feat(spectator): 観戦者が任意のタイミングで途中参加できるようにする - #23

Open
progre wants to merge 21 commits into
mainfrom
claude/funny-lamport-c1rav2
Open

progre wants to merge 21 commits into
mainfrom
claude/funny-lamport-c1rav2

Conversation

@progre

@progre progre commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

概要

これまで観戦者は、難易度選択画面(カード未選択時)でしか参加できませんでした。この PR により、観戦者はいつ接続しても観戦を始められるようになります。

仕組み

  • 接続時: ホストは観戦者にプレイヤー名と設定を送ります。
  • 選択画面で接続した場合: 観戦者は、難易度選択画面またはキャラクター選択画面で、キャラクターを決定せずに待ちます。
    • ホストは試合開始時(キャラクター選択画面から読み込み画面に移ったとき)に、難易度、両プレイヤーのキャラクター・カード、乱数シードを送ります(InitGame)。
    • 観戦者は難易度・キャラクターのカーソル・カードを合わせ、一致していることを確認してから両プレイヤーを決定します。読み込み画面に移った時点で乱数シードを設定し、以降はホストの試合中の入力を再生します。
  • 試合中に接続した場合: ホストは、進行中の試合の InitGame とそれ以降の入力の記録をまとめて送ります(記録は1試合分で、試合ごとに作り直します)。観戦者は試合の最初から早送りで追いかけます。
  • ホストとの差の解消: 観戦者の試合がホストより遅れて始まった分は、溜まった入力を no_wait で早送りして追いつきます。
  • ずれたときの扱い:
    • 観戦者の試合中にホストの次の試合が始まった場合は、今の試合を打ち切り、次の試合から同期し直します。
    • 試合開始時の状態を合わせられない場合や、合わせた結果が一致しない場合は、観戦を終了します。
  • 観戦用のデータチャネルプロトコル: JUNOWEN/1.2 に上げ、旧バージョンの観戦者は接続の時点で拒否します。対戦は JUNOWEN/1.1 のままで、互換性を保ちます。

主な変更

  • junowen-lib: PeerConnection / SignalingSocket / ChannelSocket で、プロトコルを用途ごとに指定できるようにしました(BATTLE_PROTOCOL / SPECTATOR_PROTOCOL)。
  • ホスト側(SpectatorHostState):
    • 試合開始を待つ観戦者と、観戦中の観戦者を分けて管理するようにしました。
    • 進行中の試合の記録(GameLog)を追加しました。
    • 選択画面の入力は観戦者に送らなくなりました。
  • 観戦者側:
    • 状態遷移を Prepare → Standby → GameLoading → Game → BackToSelect → Standby にしました。
    • 待機中は、キーボードの ESC か P1 の PAUSE で中断できます。

確認状況

  • i686-pc-windows-msvc ターゲットで cargo check(ワークスペース全体)/ clippy / fmt が通ることを確認しました。clippy の警告は main より減っています。
  • 実機での動作は未確認です。 特に次の点の確認をお願いします。
    1. 読み込み画面に移った時点で、selection.pX.character/card/difficulty が確定した値になっているか
    2. キャラクター選択画面で、カーソル(Menu::pX_cursor)とカード(selection.pX.card)を書き込んで反映されるか。カーソルの値がキャラクター番号と一致するか
    3. 観戦者がキャラクター選択画面で難易度の違いを検知したとき、PAUSE の入力で難易度選択画面に戻れるか
    4. 読み込み画面に移った最初のフレームで乱数シードを設定して、ホストと同期するか
    5. 試合中に no_wait で早送りしても問題ないか。また、試合中に接続した観戦者がホストに追いつけるか

🤖 Generated with Claude Code

https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa

これまで観戦者は難易度選択画面 (カード未選択時) でしか参加できなかった。
ホストが直近の同期ポイント (難易度選択画面・キャラクター選択画面に入った時点) の
状態と乱数シード、およびそれ以降の入力を記録しておき、途中参加した観戦者には
それらを送信して早送りで再生させることで、キャラクター選択中や対戦中でも
参加できるようにする。

- 観戦者の受け付けを画面に関わらず行う (F1 も対戦中に受け付ける)
- 観戦者側はホストからの初期化情報を非ブロッキングで待つ
- キャラクター選択画面から同期する場合は難易度を決定して画面遷移し、
  到達時にカード選択状態と乱数シードを再現する
- 対戦中に未処理の入力が溜まっている場合は早送りで追いつく

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa

@progre progre left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可読性の観点でいくつか気になった点をインラインコメントしました。機能自体は成立していそうですが、状態管理(initializing_state)と sync_screen/ready の意味の乖離が特に追いにくいと感じたポイントです。


Generated by Claude Code

Comment thread junowen/src/state/battle_session/spectator_host.rs Outdated
Comment thread junowen/src/state/spectator_session/spectator_select.rs Outdated
Comment thread junowen/src/state/spectator_session/spectator_select.rs Outdated
Comment thread junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs
Comment thread junowen/src/state/battle_session/battle_game.rs Outdated
Comment thread junowen/src/state/battle_session/spectator_host.rs Outdated
- sync_screen を bool を返す is_sync_point に変更
- SpectatorSelect の initializing_state を enum 化し、
  キャラクター選択画面経由の同期でのみ到達時に乱数シードを設定するよう整理
- WaitingForPureP2pSpectator の ready を実態に合わせて show_hint に改名
- pushed_f1 の戻り値の変数名を f1_pushed に改名
- SyncPoint のメッセージ蓄積量についてコメントを追記

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa

@progre progre left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

設計レビュー

方針は妥当です。 「同期ポイントの状態+それ以降の入力ログを再生する」方式は、既存の決定論的な入力同期と噛み合っており、通信メッセージの型も新設せずに済んでいます。送信経路を broadcast() にまとめて記録漏れを防いでいる点、initializing_state を enum にした点も良い改善です。

一方で、増えた複雑性は観戦者側で「キャラクター選択画面に合わせる」処理に集中しており、整理の余地があります。指摘は重要度の高い順に次のとおりです(詳細は各インラインコメントを参照してください)。

  1. pending_sessions は実質的に到達しない経路で、関連する表示やカウントも不要なコードになっている
  2. 同期ポイントまで移動する処理を SpectatorSelect から独立した状態に切り出す
  3. 入力ログの記録量に上限がない(コメントの「1 対戦分」は実態と合わない)
  4. 対戦中の F1 受け付けは不要で、差分を減らせる
  5. Screen::Game の unimplemented!() と、旧バージョンの観戦者との互換性
  6. (軽微)buffered_len の名前と副作用が一致していない

PR 本文に挙がっている実機での確認事項(カード選択状態の再現、入力タイミングの一致、no_wait による早送り)は、コードからは判断できません。2 の方針はその確認結果を踏まえて決めるのがよいと思います。


Generated by Claude Code

Comment thread junowen/src/state/battle_session/spectator_host.rs Outdated
Comment thread junowen/src/state/battle_session/spectator_host.rs Outdated
Comment thread junowen/src/state/spectator_session/spectator_select.rs Outdated
Comment thread junowen/src/state/spectator_session/spectator_select.rs Outdated
Comment thread junowen/src/state/battle_session/battle_game.rs Outdated
Comment thread junowen/src/session/spectator.rs Outdated
- 同期ポイントへの移動処理を SpectatorSelect から SpectatorSync 状態に切り出し、
  SpectatorSelect を従来に近いシンプルな形に戻す
- 実質到達しない pending_sessions と関連表示を削除し、
  同期ポイントが無い場合は途中参加を拒否する
- 同期ポイント以降の記録量に上限 (約 10 分) を設け、超えたら次の同期ポイントまで
  途中参加を受け付けない
- 対戦中の F1 受け付けをやめ、ready の改名も元に戻す
- Screen::Game を削除して unimplemented!() によるパニック経路をなくす
- buffered_len を副作用がわかる poll_buffered_len に改名

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
入力ログを記録・再生する方式をやめ、観戦者が接続した時点の状態を送る方式にする。

- キャラクター選択画面では、カーソル位置・カード・各プレイヤーの進行段階
  (キャラクター選択/カード選択/準備完了) を送る。進行段階はメモリ上の値が
  未解析のため、ホストが決定キーとキャンセルキーの押下から推定する
- 観戦者は決定キーを自動で押して進行段階を再現してから、ホストの入力を再生する
- 対戦中など状態を送れない間に接続した観戦者は、次に状態を送れるまで待機させる
- 入力ログとその上限を削除
- 観戦用の接続のデータチャネルプロトコルを JUNOWEN/1.2 にし、
  旧バージョンの観戦者を接続時に拒否する (対戦は JUNOWEN/1.1 のまま)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
キャラクター選択とカード選択は同時に進み、準備完了後もカードを変更できるため、
キャラクター選択画面の途中状態を送る方式をやめる。

- ホストは観戦者の接続時にプレイヤー名と設定を送り、試合開始時 (読み込み画面に
  移ったとき) に難易度・キャラクター・カード・乱数シードを送る
- 観戦者は難易度選択画面またはキャラクター選択画面で待機し、試合開始時の状態を
  受け取ったら難易度・カーソル・カードを合わせ、一致を確認してから決定する
- 選択画面の入力は観戦者に送らず、試合中の入力のみを送る
- 対戦中に接続した観戦者は次の試合から観戦する
- SpectatorSync / SpectatorSelect を SpectatorStandby にまとめる

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa

@progre progre left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再レビュー(95f9cd2)

前回の指摘6件は、f8b7908 ですべて対応済みです。その後、方式が「試合開始時に InitGame を送る」形に変わったので、新しい方式を前提に見直しました。

設計は大きく簡素になり、方針として妥当です。

  • 入力ログ、同期ポイント、記録量の上限、進行段階の推定がすべてなくなりました。
  • ホスト側は「試合開始を待つ観戦者」と「観戦中の観戦者」の2つのリストを持つだけになりました。
  • 観戦者側の同期処理は spectator_standby.rs の1か所に集まっています。
  • プロトコルの分け方は SignalingSocket::protocol() の既定実装で済ませており、変更が最小限です。

変更ファイルは26に増えましたが、その多くはプロトコルを渡すための配線です。ロジックの複雑性は前回より下がっています。

新たな指摘は次のとおりです(詳細は各インラインコメントを参照してください)。

  1. 古い方式を前提にしたコメントが4か所に残っている
  2. 状態が一致しないときは warn ログを出すだけで、一致が成立しないと待機画面から抜け出せない
  3. 試合中に InitGame を受け取るとセッションが切れる。次の試合から同期し直す形にできる
  4. (軽微)待機中にゲームパッドから抜けられない
  5. (軽微)try_recv_init_game にリセットの副作用がある

「試合途中からの観戦」については、実装側からの質問(ラウンド間で持ち越す状態があるか)が回答待ちのままです。


Generated by Claude Code

Comment thread junowen/src/signaling/waiting_for_match/waiting_for_spectator.rs Outdated
Comment thread junowen/src/state/spectator_session.rs
Comment thread junowen/src/session/spectator.rs Outdated
Comment thread junowen/src/state/spectator_session.rs Outdated
Comment thread junowen/src/session/spectator.rs Outdated
- 古い方式を前提にしたコメントを直す
- 試合開始時の状態が一致しない場合は観戦を終了し、状態を合わせられないまま
  一定フレームを超えた場合も諦める
- 観戦者の試合中に次の試合の開始を受け取ったら、今の試合を打ち切って
  次の試合から同期し直す
- 待機中もゲームパッドの PAUSE で中断できるようにする (自動で入力した PAUSE は除く)
- try_recv_init_game の副作用をなくし、待機状態に入るときに明示的に破棄する

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa

@progre progre left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再々レビュー(0fe6dbd)

前回の指摘5件への対応を確認しました。

指摘 結果
古いコメント 解決。4か所とも修正済み
不一致時・待機上限 方針は同意。軽微な追記あり(スレッドを未解決に戻しました)
試合中の InitGame 方針は同意。簡素化の提案を追記(スレッドを未解決に戻しました)
ゲームパッドで中断 解決
try_recv_init_game の副作用 解決

設計上の大きな問題はもうありません。未解決に戻した2件は、どちらも簡素化や読みやすさに関するもので、マージを止めるほどの内容ではありません。

PR 本文にある実機での確認事項は、引き続き確認をお願いします。


Generated by Claude Code

ホストは進行中の試合の開始時の状態と、それ以降のラウンド初期化情報・入力を
記録する (1試合分で、試合ごとに作り直す)。試合中に接続した観戦者には
これらをまとめて送り、観戦者は試合の最初から早送りでホストに追いつく。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
- next_game_initial フィールドをやめ、試合中に受け取った InitGame は
  受信バッファの先頭に戻す
- 待機の上限を超えたときに RecvError を中断の合図として流用していることをコメントに明記

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
観戦者側で selection.card に書き込んでも、試合では常に初期位置のカードが
使われることがわかった。ゲームが実際に使うカードの値の場所を特定するため、
試合開始時にホストと観戦者の両方で候補の値をログに出力する。
原因がわかったら削除する。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
カードは入力で選ばせる方式にするため、診断ログは不要になった。

This reverts commit e7ef63c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
selection.card への書き込みはキャラクター選択画面の表示にのみ影響し、
試合で使われるカードには反映されなかった。通常の操作と同じく
「低速移動を押しながら左右」の入力でカードを切り替える。

- 観戦者側で現在のカードを把握する。タイトル画面を経由すると初期位置に戻り、
  以降は前の試合の選択が引き継がれる
- キャラクター選択画面に入ってから少し待ち、目標との差の分だけ入力してから決定する

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
画面遷移中などに左右を押すと入力が無視され、カードが切り替わらなかった。
押す前の selection.card を覚えておき、値が変わったら入力が受け付けられたとみなして
次へ進む。一定フレーム変わらなければ押し直す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
selection.card は v1.10c で構造体の配置が変わった後、正しい値を指していない
可能性が高く、ホストが常に 0 を送っていたため観戦者はカードを切り替えなかった。

- ホストは前回の選択を保持している vs_mode のカードを送り、観戦者も同じ値で照合する
- selection.card の変化で入力の受け付けを確認する方式は、値があてにならないため
  回数を数えて入力する方式に戻し、開始前の待ち時間を 60 フレームに延ばす
- 原因を確かめるため、試合開始時に両方の候補の値をログに出す (一時的)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
vs_mode のカードに 37 という、カードの番号としてありえない値が入っていたため、
正しい値の場所を改めて調べる。試合開始時にホストと観戦者の両方で、
selection・vs_mode のカード周辺・キャラクター選択カーソルの生のバイト列をログに出力する。
原因がわかったら削除する。

This reverts commit 718716e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
v1.10c 対応で Player 構造体が大きくなった際、カードの位置が +90h のまま
残っており、常に別の値 (0 や 100) を読んでいた。診断ログで、カードの値が
+94h にあり vs_mode のカードと一致することを確認した。

カードの値を正しく読み書きできるようになったため、観戦者のカードは
入力ではなくキャラクター選択画面で直接書き込む方式に戻す。

- selection.card の位置を +94h に修正
- ホストは selection.card を送り、観戦者はキャラクター選択画面で書き込んで照合してから決定する
- 入力でカードを切り替える処理 (CardMover) と、観戦者側のカードの把握を削除
- 診断ログを削除

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
試合中に no wait を切り替えても反映されない可能性があり、読み込み画面で
no wait を強制的にオフにしていたため、試合全体が通常速度で進んでいた。
読み込み画面でも溜まった入力の量で判定し、溜まっていれば早送りで試合を始める。
早送り中も入力が届くまで待つので、追いついた後はホストの速度で進む。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
試合中に観戦を始めて10秒以上遅れても早送りが発生しなかった。
溜まった入力の量が条件を満たしていないのか、no wait を設定しても
ゲームに反映されていないのかを切り分けるため、約60フレームごとに
溜まった入力の数と no wait の設定前後の値をログに出力する。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
診断ログで、溜まった入力が 964 フレーム分あり no wait を有効にしても、
試合中は早送りされないことがわかった。試合中は表示の垂直同期待ちで
60fps に制限されていると考え、早送り中は Present を8回に1回だけ行う。
早送りの要求が途絶えたら2フレームで通常の表示に戻す。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qhChXocxUoUmMqt9cLXQa
_unknown2: [u8; 0x80],
/// Available on player select screen
_unknown2: [u8; 0x84],
/// Available on player select screen (+94h)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

何を根拠に位置を変えたのですか?(+94h は自明なので書かないでください)

Comment thread junowen-lib/src/connection/signaling/socket/channel_socket.rs
Comment thread junowen-lib/src/connection/peer_connection.rs Outdated
Comment thread junowen/src/state/spectator_session/spectator_game.rs Outdated
Comment thread junowen/src/present_skipper.rs Outdated
protocol は SignalingSocket が決めて渡す値のため、PeerConnection は保持しない。
データチャネルの作成と相手との照合で使う箇所だけ引数で受け取る。

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
progre and others added 2 commits October 8, 2026 09:14
早送り中は表示を間引く対応を入れたため、原因調査用の一時的な診断ログを削除する。

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants