マガジン

「待った」を作り込む:対戦相手がいるときの合意

遊びをつくる

執筆にClaudeを使用しています。

小さなゲーム卓の島々を橋で結び、仲間との交流と個人の居場所を表した立体イラスト。
記事のテーマをもとに制作したAI生成イラスト。

一人用のゲームで「一手戻す」を作るのは、難しくありません。手を記録した配列の末尾を一つ取り除き、盤面を描き直せば終わりです。戻した結果を見るのはプレイヤー本人だけなので、誰にも迷惑がかかりません。 ところが相手がいると、同じ処理がそのままでは使えなくなります。自分が指した手を相手はすでに見ていて、次の手を考え始めているかもしれません。そこで自分だけの判断で盤面を巻き戻せば、相手の考えた時間と、相手が得た情報が消えます。対戦での「待った」は巻き戻しの機能というより、相手の同意を取る手続きとして作るものです。 この記事では、その手続きを四つのルールとして先に決め、同じ画面で動く30行ほどの試作に落とし込み、最後に自分で確かめるテスト項目まで進みます。

この記事の目次
  1. 一人用と対戦で、取り消しは何が違うのか
  2. 手順1:先に決める四つのルール
  3. 手順2:最小の合意を動かす
  4. 手順3:自分で確かめる六つの項目
  5. 手順4:対戦に広げるときに崩れやすい点
  6. 手順5:自分の作品に当てはめる
  7. 次の行動

一人用と対戦で、取り消しは何が違うのか

違いは三点に整理できます。

  • 誰の履歴か:一人用では履歴は自分のものです。対戦では履歴の全体が二人の共有物なので、片方が勝手に書き換えられません。
  • 戻した後に何が起きるか:一人用では戻した後に自分が打ち直すだけです。対戦では相手が一度見た手を、相手が見なかったことにする必要があります。
  • 悪用の余地:一人用では何度戻しても自分の問題です。対戦では不利になるたびに戻す、相手の時間切れ間際に戻す、といった使い方が成り立ってしまいます。

アソバースの公式作品にも、この違いが設計に表れている箇所があります。「潮目のオセロ」では、ヒントと一手戻すはひとり対局のみの機能です。友達とのオンライン対局には、投了や再読み込みでの復帰はあっても、一手戻すは含まれていません。ここから読み取れるのは、一人用の取り消しを対戦にそのまま持ち込まない、という線の引き方です。

「灯りのチェス」にも一手戻すがあります。ただし、同じ画面のふたり対局や招待リンクのオンライン対局で、それがどう振る舞うかは、この記事では断定しません。作品を開いて、モードごとに一手戻すが使えるか、使えるなら相手の操作が必要かを確かめてみてください。この観察が、自作の仕様を決める比較材料になります。

手順1:先に決める四つのルール

コードを書く前に、次の四つを文章で決めます。これが曖昧なまま実装すると、後から必ず食い違いが出ます。

  1. 誰が要求できるか:直前に指した本人だけにするのが最も単純です。相手の手を戻させる要求は、そもそも成立させません。
  2. 何手戻すか:「自分の直前の一手だけ」にします。相手が既に次の手を指した後の二手戻しは、拒否や同時操作の扱いが複雑になるので、最初は入れません。
  3. 誰が答えるか:要求者ではない側だけが、承認か拒否を選べます。
  4. 答えを待つ間の盤面:保留中は両者とも着手できません。ここを許すと、「要求したのに相手が先に指した」状態が生まれます。

四つ目が、一人用との最も大きな差です。一人用には「保留」という状態がありません。対戦では、通常、要求中、結果の反映、の三つの状態を持つことになります。

手順2:最小の合意を動かす

ルールを実装します。9マスに交互に石を置くだけの、ゲームとして成立しない骨組みです。目的は、合意の流れだけを取り出して確かめることです。次のコードをHTMLファイルとして保存し、ブラウザで開くとそのまま動きます。外部ライブラリは使っていません。

html
<!doctype html>
<meta charset="utf-8">
<p id="s"></p><div id="b"></div>
<button id="ask">待った</button>
<button id="yes">承認</button><button id="no">拒否</button>
<script>
const g={moves:[],pending:null};
const mover=k=>k%2?"白":"黒";
function draw(){
 const p=g.pending,len=g.moves.length;
 b.innerHTML="";
 for(let i=0;i<9;i++){
  const j=g.moves.indexOf(i),c=document.createElement("button");
  c.textContent=j<0?"・":(j%2?"○":"●");
  c.disabled=j>=0||!!p;
  c.onclick=()=>{g.moves.push(i);draw();};
  b.append(c);
 }
 ask.disabled=!len||!!p;
 yes.hidden=no.hidden=!p;
 s.textContent=p
  ?`${p.by}が「待った」を要求中。${p.by==="黒"?"白":"黒"}が答えます。`
  :`${mover(len)}の番です`;
}
ask.onclick=()=>{const len=g.moves.length;
 g.pending={by:mover(len-1),to:len};draw();};
function respond(ok){
 if(ok&&g.moves.length===g.pending.to)g.moves.pop();
 g.pending=null;draw();
}
yes.onclick=()=>respond(true);
no.onclick=()=>respond(false);
draw();
</script>

注目してほしい箇所は三つあります。

  • pending(保留中の要求)は、要求者 by と、要求した時点の手数 to を持ちます。
  • respond は、承認されても moves.length === to が成り立つときだけ巻き戻します。要求した時点から盤面が動いていたら、何も戻さずに終わります。通信を挟む対戦では、この「古い要求は無効」という確認が重要になります。
  • pending が空でない間は、すべてのマスが disabled になります。ルール4の実装です。

この試作は同じ画面を二人で使うので、承認ボタンを要求者自身が押せてしまいます。「答えるのは要求者でない側」というルール3は、この段階では口頭の約束に頼っています。

手順3:自分で確かめる六つの項目

作ったものは、次の順に操作して確かめます。結果は各自の画面で確認してください。期待する動きを先に書いておくので、食い違ったらその行が直す場所です。

  1. 黒が一手置き、「待った」を押す。→ 要求者が「黒」と表示され、すべてのマスが押せなくなるはずです。
  2. 白が「拒否」を押す。→ 手数は変わらず、白の番が続くはずです。
  3. もう一度要求し、「承認」を押す。→ 黒の石が消え、黒の番に戻るはずです。
  4. 一手も置いていない状態で「待った」を押そうとする。→ ボタンが押せないはずです。
  5. 黒、白の順に置いたあと、「待った」を押す。→ 要求者は「白」になるはずです。直前に指した人が要求できる、というルール1の確認です。
  6. 保留中にブラウザの再読み込みをする。→ 状態が消えて初期状態に戻ります。これは欠点として気づくための項目です。次の節で扱います。

六番目は「通らないのが正しい」操作ではなく、保存や再接続を考えるきっかけとして試すものです。

手順4:対戦に広げるときに崩れやすい点

試作を実際のオンライン対戦に広げる場面を考えてみます。ここから先は、確認済みの機能ではなく、設計上の注意点として読んでください。

判断は片側の画面に任せない

要求や承認の可否を、各プレイヤーのブラウザだけで判断してはいけません。アソバースの公式の対戦は、手番・合法手・勝敗をサーバー側で確認する作りだと案内されています。取り消しも同じで、「誰が要求でき、誰が答えられ、いつ反映されるか」は、双方から見て唯一の正本となる側が決めます。手順2の respond 内の手数チェックは、その縮小版です。

なお、アソバースには現在、投稿作品向けの共通対戦APIがありません。アップロードした作品にサーバー用コードを含めることも対象外です。投稿作品で合意の設計を試すなら、まずは同じ画面でのふたり対局に絞るのが現実的です。オンライン対戦そのものを提供したい場合は、現在の案内で何が使えるかを先に確認してください。

保留をどう扱うか

保留中の要求が増えると、扱いが難しくなります。決めておくとよいのは次の三点です。

  • 放置されたとき:相手が答えない場合に、一定時間で自動的に拒否扱いにするか、要求を取り下げられるようにします。承認されるまで盤面が止まり続けるのは避けてください。
  • 接続が切れたとき:保留中に片方が離脱して戻ってきた場合、保留状態を復元して続きを答えられるか、破棄して元の盤面から再開するかを決めます。「潮目のオセロ」は再読み込みで同じ部屋に復帰できると案内されています。その復帰の際に、途中の合意がどう扱われるべきかも、同じ設計課題です。
  • 同時に操作したとき:要求した瞬間に、相手が次の手を送っているかもしれません。手順2の to のように、要求の根拠となる手数を一緒に送り、一致しなければ無効にします。

回数と時間の扱い

時間制限や手数に価値があるゲームなら、「待った」の回数制限を設けるかも決めます。たとえば一局につき一人何回までか。承認された場合に、持ち時間は戻すのか、そのままにするのか。どれが正解というものはなく、ゲームの性格で変わります。重要なのは、ルールを画面上で相手に見せることです。見えないルールでの拒否は、相手に不満を残します。

手順5:自分の作品に当てはめる

最後に、作品に合わせて次の順で仕様を一枚にまとめます。

  1. 取り消しの対象を一つ選ぶ(直前の自分の一手、など)。
  2. 手順1の四つのルールを、自分のゲームの言葉で書き直す。
  3. 保留状態を、画面のどこにどう表示するか決める(相手にも見えること)。
  4. 放置・再接続・同時操作の三つについて、それぞれ「どうなるか」を一行ずつ書く。
  5. 手順3のような確認項目を、自分のゲーム用に六つ以上作る。

手順4まで書けた時点で、実装の半分は終わっています。コードより先に、状態の遷移を紙に書く習慣をつけると、後から合意の穴が見つけやすくなります。

次の行動

まず手順2のコードを保存し、六つの確認項目を順に操作してみてください。そのうえで「潮目のオセロ」と「灯りのチェス」を開き、取り消しがどのモードで使えるかを見比べると、自作の仕様を決める手がかりになります。次に、放置された要求を自動で拒否扱いにする処理を、試作に足してみましょう。