マガジン

30分で作る「数を当てる」ゲーム:高い・低いで絞り込む

遊びをつくる

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

試作の段ボールから入口やゴールを備えた完成形へつながるゲーム模型の写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

1から100までの数を一つ思い浮かべてもらい、「高い」「低い」の答えだけを頼りに当てる場面を考えてみます。ひらめきで1回目に「37」と言う人もいれば、「1、2、3」と順に数える人もいます。同じゲームでも、予想の選び方で必要な回数が大きく変わります。 この差を画面に出せば、数当てはただの運試しではなくなります。毎回の予想で候補がどれだけ減ったかを見せ、最後に「何回で当てたか」と「理屈の上での目安」を並べるのです。プレイヤーは「真ん中を言えば早い」と自分で気づき、次は回数を縮めたくなります。 この記事では、1ファイルのHTMLでそのゲームを作ります。候補の範囲と個数の表示、回数の記録、最少回数の更新まで入れて、30分で終わる分量です。

この記事の目次
  1. 完成形と30分の配分
  2. 手順1:HTMLを一枚置く
  3. 手順2:動かして、絞り込みを確かめる
  4. 手順3:回数の記録を「目安」とセットで読む
  5. 手順4:遊びとして一つ調整する
  6. 起こりやすい誤り
  7. 次の行動

完成形と30分の配分

作るのは、次の要素を持つ画面です。

  • 答えは1〜100の整数で、ゲーム開始のたびに決まります。
  • 予想を入れると「もっと高い」「もっと低い」が履歴に残ります。
  • 画面には「候補: 1〜100(100個)」のように、まだ答えがありうる範囲と個数が出ます。
  • 範囲外の予想は回数に数えません。絞り込みを無視した予想をその場で止めるためです。
  • 正解すると回数と目安を比べ、このページを開いている間の最少回数を更新します。

時間の目安は次のとおりです。

  • 1、内容:HTMLファイルを作ってコードを貼る、目安:5分
  • 2、内容:動かして挙動を確かめる、目安:10分
  • 3、内容:回数の目安と記録を読み解く、目安:5分
  • 4、内容:遊びの調整を一つ入れる、目安:7分
  • 5、内容:公開の準備を確認する、目安:3分

手順1:HTMLを一枚置く

テキストエディタで新しいファイルを作り、index.html という名前で保存します。次のコードをそのまま貼ります。外部ライブラリは使わず、ブラウザで開くだけで動きます。

html
<!doctype html>
<meta charset="utf-8">
<title>数当て</title>
<h1>1〜100の数を当てます</h1>
<p id="range"></p>
<input id="g" type="number">
<button id="ok">予想する</button>
<button id="mid">中央を入れる</button>
<button id="again">新しい数</button>
<p id="msg"></p>
<ol id="log"></ol>
<p id="best"></p>
<script>
const MAX = 100, LIMIT = Math.ceil(Math.log2(MAX));
const $ = id => document.getElementById(id);
let answer, lo, hi, log, best = null, done;
function start() {
  answer = 1 + Math.floor(Math.random() * MAX);
  lo = 1; hi = MAX; log = []; done = false;
  $("msg").textContent = "予想を入力してください。";
  show();
}
function show() {
  $("range").textContent = "候補: " + lo + "〜" + hi + "(" + (hi - lo + 1) + "個)";
  $("log").innerHTML = log.map(l => "<li>" + l + "</li>").join("");
  $("best").textContent = best === null ? "" : "このページでの最少: " + best + "回(目安は最大" + LIMIT + "回)";
}
$("mid").onclick = () => { $("g").value = Math.floor((lo + hi) / 2); };
$("again").onclick = start;
$("ok").onclick = () => {
  const g = Number($("g").value);
  if (done) return;
  if (!Number.isInteger(g) || g < lo || g > hi) {
    $("msg").textContent = lo + "〜" + hi + "の整数を入れてください。この予想は回数に数えません。";
    return;
  }
  if (g === answer) {
    done = true;
    const n = log.length + 1;
    log.push(g + " → 正解");
    if (best === null || n < best) best = n;
    $("msg").textContent = n + "回で当たりました。" + (n <= LIMIT ? "目安以内です。" : "目安は" + LIMIT + "回です。");
  } else if (g < answer) { log.push(g + " → もっと高い"); lo = g + 1; }
  else { log.push(g + " → もっと低い"); hi = g - 1; }
  show();
};
start();
</script>

ファイルをブラウザで開き、「候補: 1〜100(100個)」と表示されれば準備完了です。

手順2:動かして、絞り込みを確かめる

次に、画面を見ながら次の順で操作してみます。コードの理解より、画面が何を伝えているかを読むことが目的です。

  1. 「中央を入れる」を押します。欄に50が入るので「予想する」を押します。
  2. 履歴の1行目と、「候補」の表示を見ます。「もっと高い」なら候補は51〜100、「もっと低い」なら1〜49になります。
  3. 同じ操作を繰り返し、候補の個数が毎回どれだけ減るかを見ます。
  4. 候補の外の数、たとえば絞り込み後に「1」を入れてみます。回数に数えない旨のメッセージが出ることを確かめます。
  5. 当たったら表示される回数を見ます。「新しい数」で再開し、もう一度同じ手順を繰り返します。

ここで見てほしいのは、候補の個数が一回ごとにおよそ半分になる点です。中央を予想して外れた場合、残る候補は元の個数の半分以下です。コードの中央値は Math.floor((lo + hi) / 2) で、個数が n のとき、外れて残るのは最大でも n を2で割って切り捨てた個数です。

100個から始めた最悪の流れは次のとおりです。

  • 1回目に外れると、残りは最大50個
  • 2回目に外れると、25個
  • 3回目に外れると、12個
  • 4回目に外れると、6個
  • 5回目に外れると、3個
  • 6回目に外れると、1個
  • 7回目で、残った1個を言えば当たる

つまり、中央を言い続ければ、1〜100なら遅くとも7回で当たります。2を7回かけると128で、100以上になるためです。

手順3:回数の記録を「目安」とセットで読む

コードの LIMIT は Math.ceil(Math.log2(MAX)) で、100なら7になります。これは「中央を選び続ければ最悪でもこの回数で当たる」という上限です。幸運で4回目に当たることはありますが、7回を超えることはありません。

したがって、記録は次のように読めます。

  • 7回以内:絞り込みが成立しています。大きく外れた回があっても、中央に近い予想ができていた可能性が高いです。
  • 8回以上:どこかで候補の外側や端寄りを選んでいます。履歴を読み返し、どの予想で候補があまり減らなかったかを探します。
  • 最少回数が小さい:運の影響が大きい指標です。実力を見るなら、最少より「毎回7回以内に収まったか」を見るほうが適切です。

この区別を、自分で遊びながら確かめられます。「中央を入れる」だけを使って10回ほど遊び、毎回7回以内に収まるかを見ます。その後、ボタンを使わず勘で予想して10回遊び、回数の散らばり方を比べます。それぞれの結果は、実際に遊んだ人の画面にだけ出ます。

手順4:遊びとして一つ調整する

このままでも動きますが、遊びとしては「中央を入れる」ボタンが答えを教えすぎです。調整の候補を三つ挙げます。いずれも数行の変更で済みます。

範囲を広げる

先頭の const MAX = 100 を 1000 に変えます。見出しの「1〜100」という文字は手で直します。LIMIT は自動で10になります。2を10回かけると1024で、1000以上になるためです。範囲が10倍になっても、必要な回数の上限は3回しか増えません。この手応えは、二分探索の面白さをいちばん伝えやすい場面です。

中央ボタンを消す

<button id="mid"> の行を削除し、$("mid").onclick の行も消します。すると、プレイヤーは自分で中央を考える必要が出ます。ボタンありとなしで、プレイヤーの回数がどう変わるかを見比べるのも一案です。

目安との差を出す

正解時のメッセージに、n - LIMIT を使った差を足します。「目安より2回少ない」のように表示すると、運のよさも実力のように見えすぎるので、表示は慎重に決めます。目安以内か外かの2択のほうが、読み違いが少ない設計です。

起こりやすい誤り

動かないときや、遊びがぼやけるときは、次の点を確認します。

  • 履歴が増えない:show() を呼ぶ位置を確認します。予想の処理の最後に必要です。
  • 範囲外の予想で回数が増える:範囲外の判定が、回数を数える処理より前にあるかを見ます。
  • 一度当たった後も入力できる:done のフラグで止めています。「新しい数」で start() を呼べば解除されます。
  • 候補が逆転して負の個数になる:lo と hi の更新に g + 1 と g - 1 を使っているかを確認します。g そのものを入れると、同じ数を再び予想できてしまいます。
  • 最少回数が残らないと思った:ここでの記録は変数に置いただけなので、ページを閉じると消えます。それで問題ありません。長く残す設計にするなら、保存の方法を別に決める必要があります。

次の行動

まずはコードを貼って、中央だけを予想する10回を遊んでみてください。毎回7回以内に収まるかを見れば、記事の核は自分で確かめられます。

そのうえで、範囲を1000に広げるか、中央ボタンを外すかを一つ選んで直すと、自分の作品になります。投稿の準備ができたら、アソバースの投稿画面で index.html をそのままアップロードし、「入力→表示の確認→公開」の順で進めます。公開にはメールアドレスの確認が必要です。説明文に迷う場合は、投稿画面の「紹介文と遊び方を作成」で下書きを作り、内容を読んで「この文章を使う」を押したときだけ反映されます。