30分で作る早押しクイズ:二人で同じキーボードを使う
遊びをつくる執筆にClaudeを使用しています。

一台のキーボードを二人で囲み、問題文が出た瞬間に左の人はAキー、右の人はLキーを叩く場面を考えてみます。ほぼ同時に押したとき、ゲームはどちらを「先」と決めるのでしょうか。この問いに答えが出ていないと、勝ったはずの人が納得できない早押しクイズになります。 結論を先に言うと、ブラウザは押されたキーを一つずつ順番にプログラムへ渡します。最初に処理したほうを勝者として状態を「確定」に切り替えれば、後から届いた入力は無視できます。難しいのは、その順番を信じてよい範囲です。キーボードの仕組みやブラウザの時刻の丸めによって、数ミリ秒の差は信頼できないことがあります。この記事では、30分で動く最小の早押しクイズを作り、「ほぼ同時」をどう扱うかを自分で決める方法まで進めます。
先着判定の考え方:順番、確定、同時扱い
判定の骨格は次の三つです。
- 順番に任せる:キーを押すと keydown というイベントが発生します。二人が同時に押しても、プログラムには片方ずつ届きます。
- 確定させる:最初のイベントで locked という状態に切り替えます。以後の押下は、状態を見て無視します。
- 近すぎる差は別扱いにする:イベントには timeStamp(ページを開いてからの経過ミリ秒)が付いています。二人目の押下がしきい値より近ければ「ほぼ同時」として、同じ問題をやり直します。
三つ目は、ゲームの設計として決める部分です。差が数ミリ秒でも先着を採用すれば、公平かどうかは別にしてルールは単純になります。反対に、しきい値を広げすぎると、明らかに先に押した人の権利を消してしまいます。このしきい値は後の節で、自分たちのキーボードで測って決めます。
キーは e.key ではなく e.code(KeyA のような物理キー位置を表す値)で見ます。日本語入力の状態に左右されにくく、キー配列が違っても位置が変わらないからです。押しっぱなしで繰り返し発生するイベントは e.repeat で除外します。
手順:30分の進め方
目安は、準備に5分、実装に15分、動作確認に10分です。
手順1:キーを決める(5分)
左の人は KeyA、右の人は KeyL にします。二つを離すのは、手が干渉しにくいことと、キーボードが同時押しを取りこぼす現象(後述)を避けやすいことが理由です。答えるときは、数字キー1〜4を使います。
手順2:HTMLを1枚書く(15分)
次のコードを index.html として保存し、ブラウザで開きます。外部ライブラリもサーバーも不要です。
<!doctype html>
<meta charset="utf-8">
<title>早押しクイズ</title>
<body style="font:20px sans-serif;text-align:center">
<h1 id="msg">ページをクリックしてからスペースで開始</h1>
<div id="ch"></div>
<p id="sc">左 0 - 0 右</p>
<script>
const Q=[{q:"日本一高い山は?",c:["富士山","北岳","槍ヶ岳","立山"],a:0},
{q:"1週間は何日?",c:["5","6","7","8"],a:2}];
const KEY={KeyA:0,KeyL:1}, TIE=30; // TIE: ほぼ同時とみなす差(ms)
let st="idle", i=-1, score=[0,0], who=-1, t1=0, tk=0;
const $=id=>document.getElementById(id);
const show=m=>{$("msg").textContent=m;
$("sc").textContent="左 "+score[0]+" - "+score[1]+" 右";};
function next(redo){
if(!redo) i=(i+1)%Q.length;
const my=++tk; st="wait"; $("ch").textContent=""; show("構えて…");
setTimeout(()=>{ if(st==="wait"&&my===tk){ st="open"; show(Q[i].q); } },
1500+Math.random()*2500);
}
function end(m){ st="idle"; show(m+"(スペースで次へ)"); }
addEventListener("keydown",e=>{
if(e.repeat) return;
const p=KEY[e.code];
if(st==="idle" && e.code==="Space") return next();
if(p!==undefined && st==="wait"){
score[p]--; end("お手つき:"+(p?"右":"左")+"が-1点");
} else if(p!==undefined && st==="open"){
st="locked"; who=p; t1=e.timeStamp;
$("ch").textContent=Q[i].c.map((c,k)=>(k+1)+":"+c).join(" ");
show((p?"右":"左")+"が先。1〜4で答えて");
} else if(p!==undefined && st==="locked" && p!==who
&& e.timeStamp-t1<TIE){
st="redo"; show("ほぼ同時。同じ問題をやり直し");
setTimeout(()=>next(true),1500);
} else if(st==="locked" && /^Digit[1-4]$/.test(e.code)){
const ok=Number(e.code.slice(5))-1===Q[i].a;
score[who]+=ok?1:-1; end(ok?"正解":"不正解");
}
});
</script>状態は idle(待機)、wait(問題が出る前)、open(受付中)、locked(確定)、redo(やり直し待ち)の5つです。ゲーム全体が、この状態を見て入力を捌くだけの作りになっています。
手順3:お手つきの扱いを確認する(5分)
問題文が出る前に押すと、押した人が1点減って終わります。問題が出る前の待ち時間をランダムにしているのは、リズムで先読みする押し方を防ぐためです。tk という番号は、お手つきの後に古いタイマーが新しい問題を勝手に開いてしまう事故を防ぐ目印です。
手順4:問題を増やす(5分)
Q の配列に {q, c, a} の形で足すだけです。a は正解の選択肢の位置を0始まりで書きます。選択肢を押した後にだけ表示しているのは、問題文を読む段階で選択肢まで見えると、早押しの意味が薄れるためです。
同時押しを自分で確かめる:実験の設計
しきい値 TIE の30は、仮置きの値です。適切かどうかは、使うキーボードで決まります。次の手順で、二人で確かめます。
準備:ハンドラの先頭に console.log(e.code, e.timeStamp.toFixed(1)); を1行足し、開発者ツールのコンソールを開きます。
手順:「せーの」の合図で、二人が同時にキーを叩く試行を行います。1セットを10回とし、押す前に状態が open になっていなくても構いません。ログだけを見るためです。
記録の単位:1回ごとに、次の3項目を表にします。
- 両方の keydown がログに出たか(有・無)
- 二つの timeStamp の差(ミリ秒)
- 先に記録されたのは左右どちらか
結果の読み方:
- 片方の keydown が出ない回がある、判断:キーボードが同時押しを取りこぼしている可能性があります。キーの組み合わせを変えて再試験します
- 先着がいつも同じ側になる、判断:手の速さの差か、叩き方の癖かもしれません。左右の席を入れ替えて、偏りが席に付くか人に付くかを見ます
- 差がいつも0か、決まった刻みの値になる、判断:時刻が粗く丸められている可能性があります。その場合、ミリ秒単位の差で勝敗を決めるのは危険です
- 「同時のつもり」の差が、しきい値の近くに多く分布する、判断:TIE を、その分布を覆う値に見直します
同じ手順を、キーの組み合わせごとに繰り返します。組み合わせの候補は、AとL、QとP、左Shift(ShiftLeft)と右Shift(ShiftRight)などです。Shiftが安定するかどうかはキーボード次第なので、使う機種で確かめるのが確実です。
起こりやすい失敗
- キーが取りこぼされる:安価なキーボードでは、特定のキーを同時に押すと片方が反応しないことがあります。これは、キーの配線の都合による現象(ゴースト、ロールオーバー制限)として知られています。コードでは直せないので、前節の試験でキーの組み合わせを選び直します。
- 画面にフォーカスがない:ページを開いた直後や、別の枠をクリックした後は、キー入力が届かないことがあります。開始前に一度、ゲームの画面をクリックしてもらうようにします。
- e.key で判定してしまう:日本語入力中は e.key が別の値になることがあります。位置で見る e.code のほうが安定します。
- e.repeat を忘れる:押しっぱなしの繰り返し入力が、次の判定として扱われてしまいます。
- やり直しの無限ループ:TIE を大きくしすぎると、毎回「ほぼ同時」になります。やり直しに上限を設けるか、同点なら次の問題へ進むなど、ルールを一つ決めておきます。
アソバースに投稿するとき
このゲームは、index.html 1枚で完結します。アソバースへのHTMLの直接アップロードがそのまま使える形で、入力、表示の確認、公開の3段階で進みます。ゲームはsandbox付きのiframeで動くため、遊ぶ人にも最初に画面をクリックしてもらう案内を紹介文に書いておくと親切です。
なお、投稿作品向けの共通対戦APIは現在ありません。この早押しクイズは、同じ端末を二人で使う方式のため、その点とは無関係に動きます。投稿画面の「遊び方・最大参加人数」は作者の申告欄で、対戦機能を有効にするものではありません。「二人で一台のキーボード」と明記しておくのが適切です。スコアの共通ランキングも未提供なので、点数は画面内の表示だけで完結させます。
次の行動
まず手順2のコードを保存して開き、二人で「せーの」の試験を10回ずつ行ってみてください。ログの差と取りこぼしの有無を表にすると、自分たちのキーボードに合うキーの組み合わせと TIE の値が見えてきます。その後に問題を足し、選択肢の見せ方やお手つきの罰則を調整すれば、そのまま遊べる作品に近づきます。

