マガジン

ヒントの作り方:答えを全部出さずに一歩だけ示す

遊びをつくる

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

一つだけ空いた場所へ、丸い駒を置こうとする初めての一手。
記事のテーマをもとに制作したAI生成の写真風イメージ。

迷路パズルの途中で、プレイヤーが「ヒント」を押す場面を考えてみます。画面に解答の経路が最後まで線で描かれたら、パズルはそこで終わってしまいます。「そっちに進めばよい」という矢印が一つだけ出れば、次の判断はプレイヤーに残ります。どちらも同じ「正解」を知っているのに、渡し方で体験が変わります。 この記事が確かめるのは、その渡し方がプログラムでどう作れるかです。核になるのは、最適解を最後まで求めたうえで、最初の一手だけを取り出す処理です。解を全部持っていながら、見せるのは先頭の一歩だけにします。仕組みは短いコードで書けるので、後半で動かせる形にします。 公式作品の中にもヒントを持つものがあります。確認済みの機能説明だけを手がかりに、設計から読み取れることを整理します。内部の実装は公開されていないので、確かめられない部分は「作品を開いて確かめる」と書きます。

この記事の目次
  1. 「答えを渡す」と「一歩を示す」は何が違うのか
  2. 距離マップから一歩だけ取り出す
  3. 公式作品のヒントから読み取れること
  4. 一歩のヒントを設計するときの判断
  5. 起こりやすい誤り
  6. 次の行動

「答えを渡す」と「一歩を示す」は何が違うのか

ヒントには、情報量の違う段階があります。

  1. 解の存在だけを教える(「この局面は解けます」)
  2. 残りの距離を教える(「あと何手」)
  3. 次の一歩を教える(「右へ」)
  4. 解の全体を教える(経路を最後まで表示する)

最適解を求めるアルゴリズムは、本来4番まで出せます。3番は、4番の出力の先頭だけを使うことで作れます。2番も、同じ計算の途中経過から得られます。段階の違いは探索の違いではなく、出力のどこまでを見せるかの違いです。

公式作品の説明にも、この段階の違いが表れています。月夜の配達には「残り最短距離」と「次の一歩を照らすヒント」があります。かたちのあいだには「次の一手のヒント」があります。プリズムの道には「一歩だけのヒント」があります。どれも解の全体ではなく、一歩を示す形です。

距離マップから一歩だけ取り出す

迷路のようにゴールが決まっている問題では、「ゴールからの距離マップ」を使うと一歩のヒントが簡単に作れます。手順は次の2段階です。

  1. ゴールから幅優先探索をして、各マスに「ゴールまでの残り歩数」を書き込みます。幅優先探索とは、近いマスから順に外へ広げていく探索のことです。
  2. 現在地の隣のマスのうち、残り歩数がいちばん小さいマスへの一歩を返します。

ゴールから計算する点が肝心です。スタートから探索すると、現在地が変わるたびにやり直しになります。ゴールからの距離マップは一度作れば、どのマスにいるプレイヤーにも使い回せます。

次のコードは、HTMLファイルとして保存すればそのまま動く最小構成です。外部ライブラリは使いません。

html
<pre id="out"></pre>
<script>
const maze = [
  "S..#....",
  ".#.#.##.",
  ".#...#..",
  ".####.#.",
  "......#G",
];
const H = maze.length, W = maze[0].length;
const DIRS = [[0,-1,"上"],[0,1,"下"],[-1,0,"左"],[1,0,"右"]];
const find = ch => {
  for (let y = 0; y < H; y++) {
    const x = maze[y].indexOf(ch);
    if (x >= 0) return [x, y];
  }
};
const open = (x, y) =>
  x >= 0 && y >= 0 && x < W && y < H && maze[y][x] !== "#";

// ゴールから幅優先探索して、各マスの「残り歩数」を作る
function distanceMap(goal) {
  const d = Array.from({ length: H }, () => Array(W).fill(Infinity));
  const queue = [goal];
  d[goal[1]][goal[0]] = 0;
  for (let i = 0; i < queue.length; i++) {
    const [x, y] = queue[i];
    for (const [dx, dy] of DIRS) {
      const nx = x + dx, ny = y + dy;
      if (open(nx, ny) && d[ny][nx] === Infinity) {
        d[ny][nx] = d[y][x] + 1;
        queue.push([nx, ny]);
      }
    }
  }
  return d;
}

// 現在地から、残り歩数が最も小さい隣のマスへの一歩だけを返す
function hint(pos, d) {
  let best = null;
  for (const [dx, dy, name] of DIRS) {
    const nx = pos[0] + dx, ny = pos[1] + dy;
    if (open(nx, ny) && (!best || d[ny][nx] < best.cost)) {
      best = { name, cost: d[ny][nx], to: [nx, ny] };
    }
  }
  return best;
}
const goal = find("G"), d = distanceMap(goal);
const pos = find("S");
const h = hint(pos, d);
document.getElementById("out").textContent =
  "残り " + d[pos[1]][pos[0]] + " 歩 / ヒント: " + h.name + " へ一歩";
</script>

distanceMap が「最適解を求める」部分です。hint は、その結果から先頭の一歩だけを取り出す部分です。経路全体を配列にして返すことも、hint を繰り返し呼ぶだけで簡単にできます。しかし画面に出すのは一歩分にとどめます。出す量は表示側で決めるものです。

コードを動かして確かめられることがあります。

  • 迷路の文字列を書き換えて、行き止まりの近くにある pos を指定したとき、ヒントが行き止まりから離れる向きになるか確かめます。
  • maze の G を壁で囲んで到達不能にしたとき、距離が Infinity のまま残ります。この場合 hint は意味のない向きを返しかねません。「ヒントを出せない」状態の扱いを自分で決めておきます。

公式作品のヒントから読み取れること

迷路以外のゲームでも、「状態」を一つの点と見なせば同じ考え方が使えます。スライドパズルなら、盤面の配置が一つの点で、一手動かすことが隣の点への移動です。ゴールは「数字がそろった配置」です。次の表は、確認済みの機能説明から読み取れる範囲を整理したものです。

  • 月夜の配達、確認済みのヒントの形:残り最短距離と、次の一歩を照らすヒント、設計から読み取れること:距離の表示と一歩の提示が別の機能として用意されている、確かめ方:ヒントを使わず残り最短距離だけを見て進めるかを試す
  • プリズムの道、確認済みのヒントの形:一歩だけのヒント、設計から読み取れること:解の全体ではなく、一歩単位で示す設計です、確かめ方:同じステージでヒントを連続して使い、毎回一歩分だけ進むかを見る
  • かたちのあいだ、確認済みのヒントの形:次の一手のヒント(最短とは限らない)、設計から読み取れること:最短保証より、次の一手を示すことを優先している可能性がある、確かめ方:同じ配置で手数を数え、ヒントに従った場合との差を自分で記録する
  • 潮目のオセロ、確認済みのヒントの形:理由付きヒント(ひとり対局のみ)、設計から読み取れること:一手に理由が添えられ、オンライン対局には出さない、確かめ方:ひとり対局とオンライン対局の画面を見比べる
  • 灯りのチェス、確認済みのヒントの形:ヒント、一手戻す、設計から読み取れること:対局が主体の作品で、ヒントは補助機能として置かれている、確かめ方:ヒントの出る場面と、CPUの応手の速さを見比べる

かたちのあいだは、4×4でも遊べます。参考として、3×3のスライドパズルの到達可能な配置は 9!÷2 で181,440通りです。4×4では16!÷2で約10兆通りになり、桁が大きく変わります。ヒントが最短とは限らないと明記されているのが、この規模の差によるものかどうかは、案内からは判断できません。この部分は作品を開いて確かめるしかありません。

オセロやチェスは事情が異なります。終局まで読み切れないため、迷路のような「ゴールまでの最短距離」は使えません。一般には、局面ごとに評価値をつけ、数手先まで読んで最も良い手を選びます。灯りのチェスはCPUの思考時間が約450msに制限されていると案内されています。このCPUの手とヒントが同じ処理かどうかは、確認済み事項にはありません。作品を開いて確かめてください。

一歩のヒントを設計するときの判断

一歩を取り出す処理自体は短くても、決めることはいくつか残ります。

最短を保証するかどうか

迷路の距離マップは、最短であることを保証できます。ただし、状態が巨大なゲームでは保証が重くなります。探索を途中で打ち切れば、「最短ではないが前進にはなる手」を返せます。かたちのあいだのヒントが「最短とは限らない」と明記しているように、保証の有無は仕様として書いておくのが誠実です。保証しないなら、そう書きます。

同点のときの選び方

隣のマスが同じ残り歩数なら、どちらも最適です。コードでは先に見つかった方向を返します。この選び方が固定されていると、同じ場所で何度押しても同じ向きが出ます。ばらつかせたいなら乱数を使えますが、テストしにくくなります。

理由を添えるか

潮目のオセロには、理由付きヒントがあります。「右へ」だけより、「ここが近道です」と添えた方が、プレイヤーは次から自分で判断できるようになります。ただし理由の文を作るには、評価の内訳(なぜその手が良いか)を記録しておく必要があります。距離マップだけでは、「近い」以上の理由は言えません。

使える回数と手数の扱い

ヒントを無制限にすると、パズルが作業になります。かたちのあいだの「一手戻す」は手数が減らないと案内されています。ヒントや戻す操作と記録の関係を決めておくと、プレイヤーは納得して使えます。公式作品の記録がどう扱われるかは、各作品の画面で確かめてください。

起こりやすい誤り

  • 壁や取れない手の判定を、距離計算とヒント選択で別々に書いてしまいます。open のような判定を一つにまとめないと、ヒントが壁へ向かうバグが出ます。
  • 距離マップを、プレイヤーが動くたびに作り直します。ゴールが変わらないなら一度で足ります。ゴールが動くゲームでは、動くたびの再計算が必要です。
  • 到達不能の場合を考えていません。ヒントが null や無意味な向きを返す場面を、先に決めておきます。
  • ヒントの出力を、そのまま解答として見せてしまいます。連続で押せる設計なら、実質的に解答表示と同じです。回数制限か待ち時間を検討します。
  • スライドパズルで、状態の表現が定まっていません。盤面を文字列にして重複を判定すると、訪れた配置を見分けやすくなります。

次の行動

上のコードを自分の迷路に差し替えて動かし、スタート位置を変えながら、ヒントが毎回最短の向きを指すか確かめてみてください。そのうえで、hint の戻り値を使って「残り歩数だけ」「向きだけ」「向きと理由」の3通りの表示を作り、どれが遊びを邪魔しないかを比べます。公式作品の月夜の配達、プリズムの道、かたちのあいだでヒントを使い、自分の設計と見比べるのも有効です。