マガジン

スネークゲームの難しさは、どこから生まれるか

遊びを見つける

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

四つの仕掛けを巡って元へ戻る、赤いビー玉の円形コース
記事のテーマをもとに制作したAI生成イラスト。

スネークゲームを遊ぶ場面を考えてみます。序盤は壁際を回るだけで安全に進めます。ところが長さが増えるにつれ、同じ盤面、同じ操作、同じ速さなのに、急に判断が追いつかなくなります。ルールは何も変わっていないのに、難しさだけが上がっていきます。 この記事の問いは、その難しさがルールのどこに由来するのかです。結論を先に言うと、有力な候補は「自分の体が障害物になる構造」です。ただし、これは仮説です。この記事では結果を書きません。ルールを最小単位に分け、読者が自分で数えて確かめるための手順を示します。

この記事の目次
  1. ルールを最小単位に分ける
  2. 体だけが「動く障害物」になる理由
  3. 記録の設計:何を、どの単位で数えるか
  4. 試作で記録を取る
  5. 結果の読み方と、起こりやすい誤り
  6. 次の行動

ルールを最小単位に分ける

スネークのルールは、次の5つに分けられます。

  1. 盤面は格子状のマスでできています。
  2. 頭は一定の間隔で必ず1マス進み、止まれません。
  3. 向きは直進か直角にしか変えられず、真後ろには曲がれません。
  4. 餌のマスに入ると、体が1マス長くなります。
  5. 壁か体のマスに入ると終了します。

この5つのうち、難しさの候補になるのは「止まれない」(2)、「反転できない」(3)、「終了条件」(5)です。さらに時間の要素として、進む速さがあります。

終了条件の中の障害物には、性質の違う2種類があります。壁は固定で、最初から最後まで同じ場所にあります。体は、自分の動きが作る障害物です。頭が通った跡が体になり、餌を取るたびに体は増えます。尾は毎手ごとに後ろから消えていきます。

つまり壁は盤面の性質で、体はプレイヤー自身の履歴です。難しさの出どころを分けるなら、まずこの2種類を区別することが出発点になります。

体だけが「動く障害物」になる理由

体には、他の障害物にない性質が3つあります。

  • 増やすほど得点が増え、同時に危険も増えます。 報酬を取る行為が、そのまま障害物を増やす行為になっています。
  • 障害物の位置は、自分の過去の判断で決まります。 3手前に曲がった場所が、今の通れない場所を作っています。
  • 障害物には寿命があります。 長さが $L$ なら、体のマスはおよそ $L$ 手後に空きます。これは実装によって違い、食べない手で尾のマスが先に空くものと、頭の移動を先に判定するものがあります。この違いは終了の判定を変えるので、確かめる価値があります。

3つ目は特に重要です。長い体は、広い範囲を長い間ふさぎます。そのため、短いうちは今の一手を考えれば足りますが、長くなると数手先から、尾が空く時刻まで見通す必要が出てきます。必要な先読みの深さが、長さに比例して増える構造です。

ここから、確かめたい仮説が2つ出てきます。

  • 仮説A:難しさは長さそのものではなく、盤面の占有率(長さ÷マス数)と連動します。
  • 仮説B:終了の直接の原因は、頭の周りに進めるマスが残っていない局面です。

どちらも、記録を取れば反証できる形にしておきます。

記録の設計:何を、どの単位で数えるか

分析で大切なのは、数える単位を先に決めることです。おすすめは次の3つです。

  • 1回の食事ごと、記録する項目:長さ、前の食事からの手数、食べた直後の頭の周りの空きマス数(0〜3)、何がわかるか:長さに応じて餌までの手数と空きがどう変わるか
  • 1ゲームごと、記録する項目:最終の長さ、終了の原因(壁か体か)、何がわかるか:難しさの質が、壁由来か体由来か
  • 1条件ごと、記録する項目:上の2つを複数ゲーム分まとめたもの、何がわかるか:条件を変えたときの差

「空きマス数」は、頭の上下左右のうち、盤面の内側で体のないマスの数です。後ろは体なので、最大でも3になります。0なら、次の一手で詰みが確定した局面です。

ゲームの回数は、1条件につき10ゲーム以上を目安にします。1回では運の影響が大きすぎるためです。餌の出る場所は乱数なので、同じ腕前でもゲームごとに結果が散らばります。

比較する条件は、一度に1つだけ変えます。

  • 盤面の大きさ、切り分けられること:占有率と長さのどちらが効くか(同じ長さでも占有率が変わります)
  • 進む速さ、切り分けられること:反応時間による失敗と、構造による失敗の区別
  • 壁の扱い(壁で終了/反対側へつながる)、切り分けられること:壁由来の難しさの大きさ
  • 尾のマスの扱い、切り分けられること:判定の流儀が終了に与える影響

試作で記録を取る

記録は手で数えることもできますが、テキストエディタに貼って保存するだけで動く最小のスネークを使えば、食事ごとのログが自動で取れます。矢印キーで操作し、Enterで開始します。終了すると死因、最終の長さ、食事ごとの記録が表示されます。外部のライブラリは使いません。

html
<canvas id="c" width="240" height="240"></canvas>
<pre id="o">Enterで開始。矢印キーで操作します。</pre>
<script>
const N=12,S=20,g=document.getElementById("c").getContext("2d");
const o=document.getElementById("o"),D=[[1,0],[-1,0],[0,1],[0,-1]];
let sn,dir=[1,0],food,steps,log,timer;
const on=(x,y)=>x>=0&&y>=0&&x<N&&y<N;
const hit=(x,y,b)=>b.some(p=>p[0]==x&&p[1]==y);
function place(){do{food=[Math.random()*N|0,Math.random()*N|0]}while(hit(food[0],food[1],sn))}
function start(){sn=[[5,6],[4,6],[3,6]];dir=[1,0];steps=0;log=[];place();
 clearInterval(timer);timer=setInterval(tick,150);draw();o.textContent="";}
function openCount(h,b){return D.filter(d=>on(h[0]+d[0],h[1]+d[1])&&!hit(h[0]+d[0],h[1]+d[1],b)).length}
function tick(){
 const h=[sn[0][0]+dir[0],sn[0][1]+dir[1]],eat=h[0]==food[0]&&h[1]==food[1];
 const b=eat?sn:sn.slice(0,-1); // 食べない手では尾のマスが空く
 if(!on(h[0],h[1])||hit(h[0],h[1],b)){clearInterval(timer);
  o.textContent="終了: "+(on(h[0],h[1])?"自分の体":"壁")+" / 長さ "+sn.length+"\n"+
  log.map(e=>"長さ"+e.len+" 手数"+e.steps+" 空き"+e.open).join("\n");return;}
 sn.unshift(h);steps++;
 if(eat){log.push({len:sn.length,steps,open:openCount(h,sn)});steps=0;place();}else sn.pop();
 draw();}
function draw(){g.clearRect(0,0,240,240);g.fillStyle="#d33";g.fillRect(food[0]*S,food[1]*S,S-2,S-2);
 g.fillStyle="#2a6";sn.forEach(p=>g.fillRect(p[0]*S,p[1]*S,S-2,S-2));}
onkeydown=e=>{const i=["ArrowRight","ArrowLeft","ArrowDown","ArrowUp"].indexOf(e.key);
 if(e.key=="Enter")start();
 else if(i>=0&&(D[i][0]+dir[0]||D[i][1]+dir[1]))dir=D[i];};
</script>

条件を変えるときは、N(盤面の大きさ。draw の 240 と canvas の幅・高さも N*S に合わせます)と、setInterval の 150(速さ。単位はミリ秒)を書き換えます。尾のマスの扱いは、b を決める行で変えられます。eat?sn:sn.slice(0,-1) を常に sn にすれば、尾のマスも「まだ埋まっている」扱いになります。

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

10ゲーム以上たまったら、次の順に読みます。

  1. 死因の内訳を見ます。 壁での終了が多いなら、体の構造より操作や速さの問題が前に出ています。体での終了が多いなら、仮説の方向に合っています。
  2. 長さと空きマス数を並べます。 長さが増えるにつれ、空きが0〜1の局面が増えるかを見ます。増え始める長さが、盤面の大きさを変えるとどう動くかが次の手がかりです。盤面を広げたとき、増え始める長さが大きくなるなら、仮説A(占有率との連動)と整合します。長さが同じなら、占有率ではなく長さそのものが効いていると読めます。
  3. 餌までの手数を見ます。 長さが増えると餌までの手数が増えるなら、遠回りを強いられる構造が現れています。最短距離との差まで取りたい場合は、食事のたびに、頭から餌までの縦横の差(マンハッタン距離)も記録に加えます。
  4. 速さを変えた条件と比べます。 遅くしても終了の原因と長さの傾向が変わらないなら、反応時間ではなく構造の問題です。遅くすると壁の死因が減るなら、操作の余裕が効いています。

誤りやすい点は次のとおりです。

  • 1ゲームの結果で結論を出すこと。 餌の位置が乱数なので、1回では偶然との区別ができません。
  • 条件を2つ同時に変えること。 盤面と速さを一緒に変えると、どちらが効いたのかわかりません。
  • 腕前の上達を条件の差と取り違えること。 後半のゲームが良くなったのは、慣れた効果かもしれません。条件の順序は入れ替えて回します。
  • 尾の扱いの違いに気づかないこと。 判定の流儀が違うと、同じ操作でも終了の局面が変わります。比較するゲームは、同じ流儀にそろえます。
  • 操作の取りこぼしを難しさと数えること。 1手の間に素早く2回キーを押すと、反転の禁止をすり抜ける実装があります。壁や体での終了が、操作の問題でないかを見直します。

次の行動

まず上のコードを index.html として保存し、盤面は12のまま、速さだけを変えて10ゲームずつ記録します。次に盤面を変えて同じことを繰り返します。死因の内訳と、空きが0〜1になる局面の増え方を並べれば、難しさの出どころが壁、体、速さのどれに近いかを自分で読み取れます。

この形はHTML1枚で動くので、アソバースに投稿する作品の土台にもなります。投稿は直下に index.html を置いたHTMLかZIPを選び、入力、表示の確認、公開の3段階で進みます。ログ表示を残したまま、自分の条件を足して公開してみるのも一つの方法です。