マガジン

閉じた道を作るパズル:一周して戻る線を引く

遊びをつくる

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

迷路の一角から出口までを段階的に照らす三つの灯りと、小さな旅人の駒
記事のテーマをもとに制作したAI生成イラスト。

格子状に並んだ点を線でつなぎ、一周して出発点に戻る輪を作る。これだけのルールでも、遊ぶ人は意外なほど迷います。迷いの正体は「ループができる条件」を頭の中で同時に守らなければならないことにあります。この記事の問いは、その条件をプレイヤーにどこまで見せると、作業が「パズル」になるのか、です。 ループの条件は、数学的には3つに分けられます。どの点にも線が0本か2本だけ集まること。線が1本以上あること。線のある点が1つの塊としてつながっていること。この3つを満たせば、点と線は1本の輪になります。条件が3つあるので、見せ方も何通りも考えられます。 この記事は、そのうちどれが面白さを生むかを自分で確かめる「実験の設計書」です。結果は書きません。短い判定コードを動かし、友人やご自身で数回遊び、記録を読むところまでを順に説明します。

この記事の目次
  1. 確かめる問いと、比較する3つの条件
  2. 準備するもの:最小の判定コード
  3. 手順:条件ごとに複数回ずつ遊ぶ
  4. 記録の取り方
  5. 結果の読み方と、3×3で起きること
  6. 起こりやすい誤り
  7. 次の行動

確かめる問いと、比較する3つの条件

最初に問いを具体化します。「判定のメッセージを出す時機と内容を変えると、遊び手の線の引き方はどう変わるか」です。変えるものは次の3点に絞ります。

  • 条件A:判定を常には出さず、「確認」を自分で押したときだけ結果を表示する
  • 条件B:線を引くたびに、いまの状態を文章で表示する(「行き止まりがあります」「ループが複数に分かれています」など)
  • 条件C:Bに加えて「すべての点を通る」を成立条件にする

AとBの違いは「条件の守り方を誰が管理するか」です。Aでは遊び手が頭の中で数え、Bでは画面が代わりに数えます。BとCの違いは目標の強さです。Cでは輪を作るだけでは足りず、全点を通る一筆書きの輪を探すことになり、探索の性格が変わります。

比較の軸を絞るため、盤の大きさは条件ごとに固定します。まず点が4×4の盤を使い、Cだけは後で3×3の盤も試します。理由は次の節で説明します。

準備するもの:最小の判定コード

必要なのはブラウザと、下の1ファイルだけです。外部ライブラリは使いません。点を順にクリックすると、隣り合う点の間に線が引け、同じ線をもう一度引くと消えます。check 関数が先ほどの3条件を調べます。

html
<canvas id="c" width="240" height="240"></canvas>
<p id="msg"></p>
<script>
const N=4, S=60, E=new Set();
let sel=null;
const c=document.getElementById('c'), g=c.getContext('2d');
const key=(a,b)=>a<b?a+','+b:b+','+a;
const pos=i=>[i%N*S+30, Math.floor(i/N)*S+30];
function check(){
  const deg=Array(N*N).fill(0), adj=deg.map(()=>[]);
  for(const k of E){const [a,b]=k.split(',').map(Number);
    deg[a]++;deg[b]++;adj[a].push(b);adj[b].push(a);}
  if(E.size===0) return '線を引いてください';
  if(deg.some(d=>d>2)) return '3本以上集まる点があります';
  if(deg.some(d=>d===1)) return '行き止まりがあります';
  const start=deg.findIndex(d=>d===2), seen=new Set([start]), st=[start];
  while(st.length){const v=st.pop();
    for(const w of adj[v]) if(!seen.has(w)){seen.add(w);st.push(w);}}
  const used=deg.filter(d=>d>0).length;
  return seen.size===used ? '一周のループです' : 'ループが複数に分かれています';
}
function draw(){
  g.clearRect(0,0,240,240); g.strokeStyle='#38b'; g.lineWidth=4;
  for(const k of E){const [a,b]=k.split(',').map(Number);
    g.beginPath(); g.moveTo(...pos(a)); g.lineTo(...pos(b)); g.stroke();}
  for(let i=0;i<N*N;i++){g.fillStyle=i===sel?'#e63':'#333';
    g.beginPath(); g.arc(...pos(i),6,0,7); g.fill();}
  document.getElementById('msg').textContent=check();
}
c.onclick=e=>{
  const r=c.getBoundingClientRect(),
    x=Math.round((e.clientX-r.left-30)/S), y=Math.round((e.clientY-r.top-30)/S);
  if(x<0||y<0||x>=N||y>=N) return;
  const i=y*N+x;
  if(sel!==null && Math.abs(sel%N-x)+Math.abs(Math.floor(sel/N)-y)===1){
    const k=key(sel,i); E.has(k)?E.delete(k):E.add(k);}
  sel=i; draw();
};
draw();
</script>

このままのコードが条件Bです。条件Aにするには、draw の最後の行(メッセージを書き込む行)を消し、代わりに「確認」ボタンから check() の結果を表示するようにします。条件Cにするには、最後の判定を seen.size===used && used===N*N に書き換えます。点の数を変えたいときは、先頭の N を変え、キャンバスの大きさも N*S に合わせて直します。

連結判定(seen を使った探索)は省かないでください。これを省くと、離れた2つの小さな輪を作っても「ループです」と表示されてしまいます。判定の誤りはそのまま遊びの誤解につながります。

手順:条件ごとに複数回ずつ遊ぶ

遊び手はご自身でも、数人の友人でも構いません。人数が少ないほど結論は弱くなります。ここでは「傾向を見る」ための実験と割り切ってください。

  1. 4×4の盤で、条件A・B・Cの3通りを用意します。同じファイルを3つ作って名前を変えておくと管理しやすくなります。
  2. 遊び手ごとに、条件を試す順番を入れ替えます。全員がA→B→Cの順だと、後の条件ほど慣れが効いてしまい、比較になりません。
  3. 各条件で、最低3回は遊びます。1回の終了は「成立のメッセージが出たとき」か「3分たったとき」にします。時間切れの場合も記録として残します。
  4. 条件Cだけは、4×4を遊んだ後に3×3の盤でも遊んでもらいます。ただし、先に成立しない理由を説明しないでください。

遊び手には、条件の中身を最初に説明しないほうが記録は読みやすくなります。ただし条件Aでは「確認ボタンで判定できる」ことだけは伝えます。

記録の取り方

1回の試行ごとに、次の項目をメモします。ストップウォッチと紙があれば足ります。画面を録画できる環境なら、後から数え直せるので確実です。

  • 成立までの時間、単位:秒(未成立は「打ち切り」)、見る目的:難しさの目安
  • 線を引いた回数と消した回数、単位:回、見る目的:試行錯誤の量
  • 確認ボタンを押した回数(Aのみ)、単位:回、見る目的:自分で管理できているか
  • 手が止まった回数、単位:回(5秒以上)、見る目的:迷った場所の特定
  • 一言の感想、単位:1行、見る目的:数字に出ない手触り

「消した回数」を「引いた回数」で割った値を試行ごとに出しておくと、条件の比較がしやすくなります。引き直しが多いほど、試行錯誤が大きかったと読めます。

結果の読み方と、3×3で起きること

数字がそろったら、条件ごとの中央値を並べます。平均より中央値のほうが、1回だけ極端に長引いた試行の影響を受けにくくなります。読み方の目安は次のとおりです。

  • Aの時間が長く、確認ボタンの回数も多いなら、条件を頭の中で守る負担が大きいと読めます。Bで時間が縮むなら、メッセージが負担を肩代わりしています。
  • Bで時間は縮んだのに「簡単すぎて退屈だった」という感想が多いなら、答えを教えすぎている可能性があります。メッセージを「行き止まりがあります」だけに減らす、点に色をつけるだけにするなど、見せる量を落とす案を次に試します。
  • AとBで時間がほとんど変わらないなら、盤が小さすぎて条件の管理が負担になっていないのかもしれません。盤を5×5に広げて再度比べます。

条件Cの3×3は、特に注意して見てください。この盤では、遊び手が何度挑戦しても成立しません。理由は盤を市松模様に塗ると分かります。隣り合う点は必ず別の色になるので、輪をたどると色が交互に並びます。すると輪に含まれる白と黒の点は同数になります。3×3は9点で、色の内訳は5対4です。全点を通る輪は作れません。4×4の16点なら、8対8なので可能性があります。

この性質を知らない遊び手が、どの時点で「無理かもしれない」と気づくかは、記録する価値があります。手が止まった回数と感想欄に注目し、気づいた後にどう行動したか(諦める、盤の端だけを使う、ルールを疑う)を書き留めてください。解けない問題を出してしまったとき、遊び手がそれをどう扱うかは、そのままパズル設計の判断材料になります。

起こりやすい誤り

  • キャンバスの拡大でクリック位置がずれる:CSSで見た目だけ大きくすると、座標計算がずれて隣の点が選ばれます。大きさは width と height の属性で変えてください。
  • 同じ盤を繰り返して覚えてしまう:4×4で全点を通る輪の形は限られます。2回目以降は記憶で解けてしまうので、条件を入れ替えるだけでなく、遊び手ごとに最初の点をあえて指定するなどの工夫が要ります。
  • メッセージの文言が結論を左右する:同じ判定でも「行き止まりがあります」と「あと1本つなげます」では、与えるヒントの量が違います。文言を変える場合は、別の条件として扱い、記録に残してください。
  • 少数の結果で言い切る:3〜5人の記録では、個人差が結果を大きく左右します。「この条件のほうが良い」ではなく「次に試す方向が見えた」程度に受け止めるのが安全です。

次の行動

まず上のコードを1ファイルで保存し、ご自身で条件Bを5分遊んで、判定が意図どおりに動くか確かめてください。そのうえで条件A・Cを作り、友人に順番を入れ替えて遊んでもらいます。納得できる形になったら、index.html を入口にした1ファイルのゲームとして、アソバースへの投稿も検討できます(投稿の手順やサイズ制限は、投稿画面の案内で確認してください)。