マガジン

最短経路を競うゲーム:グラフの上で道を選ぶ

遊びをつくる

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

紙の飛行機が、海辺の小さな街と山の上を飛んでいく絵。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

「月夜の配達」は、小包3つ、バッグ2枠、燃料は1区間で1ずつ減る、という条件の経路パズルです。ヒントには「残り最短距離」と「次の一歩を照らす表示」があり、一歩戻ることもできます。地図の見た目を取り払うと、中身は「点と線の上を、少ない歩数で目的地へ進む」遊びです。この骨格だけを取り出すと、最短経路を競うゲームは短いコードで作れます。 ただし、作れることと、遊びとして成り立つことは別の問題です。画面に「あと3」と出ていれば、プレイヤーは数字の小さい方へ進むだけで済みます。表示がなければ、頭の中で道を数え直すことになります。残り最短距離のヒントは、迷う楽しさを助けるのか、それとも奪うのか。この記事は、その問いを自分で確かめるための実験の設計書です。結果は書きません。準備、手順、記録、読み方を順に示します。

この記事の目次
  1. 点と線にすると何が残るのか
  2. 実験の問い:残り最短距離の数字は、選び方を変えるのか
  3. 準備するもの:60行以内の最小プロトタイプ
  4. 手順と記録の取り方
  5. 結果の読み方と、起こりやすい誤り
  6. 次の行動

点と線にすると何が残るのか

グラフとは、点(ノード)と、点どうしをつなぐ線(辺)の集まりです。経路を選ぶゲームをグラフで表すと、地形の描写はすべて落ち、「どの点から、どの点へ行けるか」だけが残ります。

月夜の配達の燃料は「1区間で燃料1」です。各区間の費用が同じなので、道の長さは区間の数そのものになります。この性質があると、最短経路を求める計算は幅優先探索(BFS)で済みます。目的地から外側へ、隣の点、そのまた隣の点と順に距離を振っていく方法です。各点に振られた数字が、そのまま「残り最短距離」になります。

ここに設計上の面白さがあります。この数字は目的地から一度計算すれば、どの点からでも使えます。プレイヤーがどこにいても「残りいくつ」を答えられるので、ヒントとして出しやすい形をしています。

もう一つ注目したいのは、画面上の見た目の距離と、辺を数えた距離が一致しないことです。ゴールが画面の近くにあっても、辺をたどると遠回りになる配置は作れます。プレイヤーの直感は見た目の距離に引っ張られるので、そのずれが「迷い」を生みます。この記事の実験は、そのずれを数字のヒントがどこまで補うかを測る設計になっています。

実験の問い:残り最短距離の数字は、選び方を変えるのか

問いを一つに絞ります。

各点に「ゴールまでの残り最短距離」を表示すると、プレイヤーが選ぶ道は最短に近づくのか。

この問いには、次の二つの読み方が両立します。

  • 数字があれば、誰でも最短で着ける。ヒントは強すぎるかもしれない。
  • 数字があっても、戻る回数や遠回りが残る。数字の意味が伝わっていないか、グラフの側に罠がある。

どちらになるかは、実際に遊んでもらわないとわかりません。そこで、変える条件を一つだけにします。

  • 条件A:表示なし。 点には何も書かない。
  • 条件B:表示あり。 各点に残り最短距離を書く。

グラフの形、操作方法、ゴールの位置は同じにします。条件が二つ以上変わると、違いの原因を特定できなくなります。

準備するもの:60行以内の最小プロトタイプ

必要なのは、ブラウザとテキストエディタです。次のコードを index.html として保存し、ブラウザで開きます。外部ライブラリは使いません。

html
<!doctype html><meta charset="utf-8">
<canvas id="c" width="400" height="300"></canvas>
<p id="info"></p>
<label><input type="checkbox" id="hint"> 残り最短距離を表示</label>
<button id="u">一歩戻る</button>
<script>
const P=[[40,150],[130,60],[130,240],[230,60],[230,240],[360,150]];
const E=[[0,1],[0,2],[1,3],[2,4],[3,5],[4,5],[1,2]];
const GOAL=5,G=P.map(()=>[]);
E.forEach(([a,b])=>{G[a].push(b);G[b].push(a);});
const D=P.map(()=>Infinity),q=[GOAL];D[GOAL]=0;
while(q.length){const u=q.shift();
  G[u].forEach(v=>{if(D[v]==Infinity){D[v]=D[u]+1;q.push(v);}});}
const log=[0];let undo=0;
const $=id=>document.getElementById(id),c=$("c"),x=c.getContext("2d");
function draw(){
  x.clearRect(0,0,400,300);
  E.forEach(([a,b])=>{x.beginPath();x.moveTo(...P[a]);x.lineTo(...P[b]);x.stroke();});
  const now=log[log.length-1];
  P.forEach((p,i)=>{
    x.beginPath();x.arc(p[0],p[1],14,0,7);
    x.fillStyle=i==now?"#fc0":i==GOAL?"#8cf":"#fff";
    x.fill();x.stroke();x.fillStyle="#000";
    if($("hint").checked)x.fillText(D[i],p[0]-3,p[1]+4);
  });
  $("info").textContent="区間数 "+(log.length-1)+" / 戻した回数 "+undo
    +" / 経路 "+log.join("→")+(now==GOAL?" / 到着":"");
}
c.onclick=e=>{
  const r=c.getBoundingClientRect(),now=log[log.length-1];
  if(now==GOAL)return;
  G[now].forEach(v=>{
    if(Math.hypot(P[v][0]-(e.clientX-r.left),P[v][1]-(e.clientY-r.top))<16)log.push(v);});
  draw();};
$("u").onclick=()=>{if(log.length>1){log.pop();undo++;draw();}};
$("hint").onchange=draw;
draw();
</script>

遊び方は、黄色の点(現在地)から線でつながった隣の点をクリックして進み、水色の点(ゴール)を目指すだけです。画面には区間数、戻した回数、たどった経路が出ます。

この例では、点0から点5までの最短は3区間です。0と5の両方に隣り合う点はないので、2区間では着けません。一方、0→1→3→5と0→2→4→5の二通りが3区間になります。1と2をつなぐ線は、遠回りへの誘い道として入れてあります。

実験の前に、次の二点を決めておきます。

  • 参加者に見せる画面から、条件を固定する。 チェックボックスは実験者が操作し、参加者には触らせません。条件Aは外した状態、条件Bは入れた状態で渡します。
  • 最短区間数は、実験者が事前に手で数えておく。 画面には最短の数を出していません。表示すると、それ自体が条件Bと同じヒントになってしまうためです。

手順と記録の取り方

グラフを3種類用意する

同じグラフを二度解くと、道を覚えてしまいます。そこで、性質の違うグラフを3つ作り、一人が解くのは1条件だけにします。コードの P(点の座標)と E(線の組)を書き換えて作ります。

  • グラフ1: 上のコードそのまま。最短が二通りあり、短絡の線が一本ある。
  • グラフ2: 最短が一本だけで、途中の分岐の片方が行き止まりになっている。
  • グラフ3: 画面上ではゴールに近く見える点を通る道が、実は辺の数では遠回りになる。見た目の距離と辺の距離をずらした配置。

各グラフで、GOAL と開始点(log の初期値)を決め、最短区間数を手で数えて記録しておきます。

参加者を条件に割り当てる

条件は、一人に一つだけ割り当てます。同じ人にAとBの両方をやらせると、先に解いた方の知識が後に響きます。

  • 条件AとBに、できるだけ同じ人数を割り当てる。
  • 条件の割り当てはくじなどで決め、慣れている人が片方に偏らないようにする。
  • グラフの出題順は、参加者ごとにずらす。全員が同じ順で解くと、グラフ1の影響が最後まで残ります。

人数は多いほど傾向を読みやすくなりますが、まず各条件に数人ずつ集める小さな規模で構いません。少人数では傾向の目安しか得られないことを、前提に置きます。

一回ごとに記録する項目

1回の挑戦ごとに、1行ずつ記録します。

  • 参加者ID、取り方:氏名ではなく番号
  • 条件、取り方:A または B
  • グラフ、取り方:1、2、3
  • 区間数、取り方:到着時に画面に出る数
  • 最短との差、取り方:区間数から事前に数えた最短を引いた値
  • 戻した回数、取り方:到着時に画面に出る数
  • 所要秒、取り方:ストップウォッチで、最初のクリックから到着まで
  • 経路、取り方:画面の「経路」の文字列を写す

到着の画面を見たら、すぐ写します。「一歩戻る」のボタンは到着後も押せるので、到着した時点で記録し、その後の操作は記録に含めません。ページを再読み込みすると、各回の状態は消えます。

参加者には「できるだけ少ない区間数でゴールに着いてください」とだけ伝えます。条件の違いや、仮説は伝えません。

結果の読み方と、起こりやすい誤り

数字を並べたあとの見方

まず、条件ごと・グラフごとに「最短との差」を並べます。平均だけでなく、全員分の値を一覧で見てください。少人数では、一人の値が平均を大きく動かします。次の観点で読み比べます。

  • 条件Bで差がほぼ0になり、所要秒も短い。 数字が道案内として機能している。次に確かめるのは、数字なしでも解ける簡単なグラフだったのではないか、という点。条件Aでも差が小さいなら、グラフが易しすぎます。
  • 条件Aで差が大きく、条件Bで小さい。 ヒントが選択を変えている。その上で、ヒントが強すぎて考える余地を消していないかは、遊んだ感想と合わせて判断する。
  • 条件Bでも差が残る、または戻す回数が多い。 数字の見せ方(位置、大きさ、意味の説明)が伝わっていない可能性がある。グラフ3のように、見た目の距離と辺の距離がずれる配置で特に残るなら、直感の側が強く働いている。
  • 条件Aで戻す回数が多く、Bでは少ない。 行き止まりに入ってから気づく動きが、数字で減っている。グラフ2の経路欄で、行き止まりの点を通った回数を数えると裏づけられる。

こうなれば結論、という閾値はここでは決めません。人数も少なく、数字の大小だけで言い切れる規模ではないためです。条件AとBの分布を並べて、「どのグラフで差が出たか」を言葉で説明できることを目標にします。

起こりやすい誤り

  • 最短の定義が曖昧になる。 グラフ1のように最短が複数あるときは、どれも「最短」とします。経路の一致ではなく、区間数で判定してください。
  • 途中の差と到着時の差を混ぜる。 記録は到着時の区間数で統一します。途中経過を見たいときは、経路の文字列から別に起こします。
  • グラフが小さすぎる。 点が少なすぎると、条件Aでも全員が最短で着きます。差が出ないときは、点と線を増やして再実験する前に、まずグラフを大きくします。
  • 参加者に目的を話してしまう。 「数字を見てね」と言うと、条件Bの動きが変わります。伝えるのは課題の文だけにしてください。
  • 知り合いだけで集める。 ゲームに慣れた人ばかりだと、条件の差が出にくくなります。実験前に、各条件の顔ぶれがおおまかに似ているか確かめます。

月夜の配達は、このプロトタイプよりも要素が多く、小包を3つ運び、バッグは2枠、燃料は有限です。どの順で何を拾うかという判断が加わるため、道を選ぶ悩みの種類も増えます。こうした仕様が実際にどんな盤面を作るのかは、作品を開いて確かめてください。この記事の実験は、その土台になる「点と線の上の選択」だけを切り出したものです。

次の行動

まず上のコードを保存し、グラフ1を自分で遊んで、条件AとBで感じ方がどう違うかを確かめてください。次に、グラフ2とグラフ3を作り、最短区間数を手で数えて記録用紙に書き込みます。そのうえで数人に条件を割り当てて遊んでもらえば、「数字は迷いを助けるか、奪うか」に自分のデータで答えられます。実験を終えて形になった作品は、index.html を直下に置いて(20MiB以内)、アソバースに投稿できます。