マガジン

紙の迷路を画面にする:手で描いた線をデータにする

遊びをつくる

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

色紙の試作コース、小さな駒、ノートを並べたゲーム制作の作業机。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

方眼紙に迷路を描き終えたところを考えてみます。壁はペンで引いた線で、少し曲がり、太さもまちまちです。これを写真に撮ってブラウザに読み込ませ、矢印キーで動かせる迷路にするには、何を決めておけばよいのでしょうか。この記事は、その手順を自分で試して確かめるための設計書です。 先に答えの方向だけ示すと、手描きの線をそのままデータにするのは難しい作業です。線の太さも位置もあいまいで、「どこからが壁か」をプログラムが決められません。そこで「線を引く」のをやめ、方眼のマスを塗りつぶして壁にする描き方に変えます。紙をマス目の集まりとして読めば、画像を「壁か通路か」の0と1の表に置き換えられます。 確かめたいのは、紙と画面で壁の位置がどの程度ずれるかと、そのずれが何で決まるかです。マス数、ペンの濃さ、撮影条件、しきい値(明るさをどこで壁と通路に分けるかの境目)のどれが効くのかを、1回に1つずつ変えて記録する形で進めます。結果はこの記事には書きません。

この記事の目次
  1. 先に決めること:線ではなくマスにする
  2. 準備するもの
  3. 手順:読み込み、判定、遊ぶ
  4. 記録の取り方と結果の読み方
  5. 起こりやすい誤り
  6. 次の行動

先に決めること:線ではなくマスにする

迷路のデータは、横W×縦Hの表で表せます。各マスは壁なら1、通路なら0です。この表ができれば、移動判定も描画も、ゴールに着けるかの検査も同じ表から作れます。

紙の側のルールは次の3つに絞ります。

  • 壁にしたいマスは、枠からはみ出さないように黒のペンで塗りつぶします。
  • 左上のマスをスタート、右下のマスをゴールとし、どちらも塗りません。
  • 通路は何も書かず、白いままにします。

この描き方なら、線の細かい形を読む必要がなくなります。読むのは「このマスは全体として暗いか」だけです。画像を横W、縦Hに縮小すれば、1ピクセルが1マスに対応します。縮小の仕方はブラウザの実装に左右されることがあるため、これも後で比較する条件にします。

準備するもの

  • 方眼紙、または定規で引いたマス目の紙。最初は12×12マスほどを目安にします。
  • 黒の太めのペン。細すぎると塗りにむらが出ます。鉛筆とも比べるなら、別の条件として扱います。
  • スマートフォンなどのカメラ。真上から撮れる場所を選びます。
  • テキストエディタとブラウザ。外部ライブラリもサーバーも使いません。

写真は、紙の端が画像の端に来るようにトリミングします。余白が残ると、横幅から縦のマス数を割り出す計算がずれ、すべてのマスが少しずつずれて読まれます。

手順:読み込み、判定、遊ぶ

次のコードを index.html として保存し、ブラウザで開きます。ファイルを選ぶと、画像を指定した列数のマス目に縮小し、暗いマスを壁にします。矢印キーで赤い丸を動かし、右下の緑のマスがゴールです。ゴールに着けるかどうかも、表示の上に出ます。

html
<input type="file" id="f" accept="image/*">
<label>列<input id="n" type="number" value="12" min="4" max="40"></label>
<label>しきい値<input id="t" type="range" min="40" max="220" value="128"></label>
<p id="m"></p>
<canvas id="c"></canvas>
<script>
const $=i=>document.getElementById(i);
let img,g,W,H,p;
function build(){
  if(!img)return;
  W=+$('n').value;H=Math.round(W*img.height/img.width);
  const s=document.createElement('canvas');s.width=W;s.height=H;
  const x=s.getContext('2d');x.drawImage(img,0,0,W,H);
  const d=x.getImageData(0,0,W,H).data;g=[];
  for(let i=0;i<W*H;i++){
    const v=0.3*d[i*4]+0.59*d[i*4+1]+0.11*d[i*4+2];
    g.push(v<$('t').value?1:0);
  }
  g[0]=0;g[W*H-1]=0;p=0;
  $('m').textContent=W+'×'+H+'マス / '+(solvable()?'ゴールに着けます':'ゴールに着けません');
  draw();
}
function solvable(){
  const seen=new Set([0]),q=[0];
  while(q.length){const a=q.shift(),ax=a%W,ay=(a-ax)/W;
    for(const[dx,dy]of[[1,0],[-1,0],[0,1],[0,-1]]){
      const nx=ax+dx,ny=ay+dy;if(nx<0||ny<0||nx>=W||ny>=H)continue;
      const b=ny*W+nx;if(g[b]||seen.has(b))continue;seen.add(b);q.push(b);}}
  return seen.has(W*H-1);
}
function draw(){
  const k=20,c=$('c'),x=c.getContext('2d');c.width=W*k;c.height=H*k;
  g.forEach((v,i)=>{x.fillStyle=v?'#222':'#fff';x.fillRect(i%W*k,Math.floor(i/W)*k,k,k);});
  x.fillStyle='#2a9d8f';x.fillRect((W-1)*k,(H-1)*k,k,k);
  x.fillStyle='#e76f51';x.beginPath();x.arc(p%W*k+k/2,Math.floor(p/W)*k+k/2,k/3,0,7);x.fill();
}
$('f').onchange=e=>{const im=new Image();im.onload=()=>{img=im;build()};im.src=URL.createObjectURL(e.target.files[0]);};
$('n').oninput=$('t').oninput=build;
addEventListener('keydown',e=>{
  const m={ArrowRight:[1,0],ArrowLeft:[-1,0],ArrowDown:[0,1],ArrowUp:[0,-1]}[e.key];
  if(!g||!m)return;e.preventDefault();
  const px=p%W+m[0],py=Math.floor(p/W)+m[1];
  if(px<0||py<0||px>=W||py>=H||g[py*W+px])return;
  p=py*W+px;draw();if(p==W*H-1)$('m').textContent='ゴール!';
});
</script>

使い方は次のとおりです。

  1. 紙のマス数と、画面の「列」の値を一致させます。12列の紙なら12と入れます。
  2. 写真を選ぶと、迷路が表示されます。
  3. スライダーで、しきい値を動かします。表示がどう変わるかを見て、紙の意図に最も近い値を探します。
  4. 矢印キーで実際に歩き、ゴールまで行けるかを確かめます。

記録の取り方と結果の読み方

最初に1つの問いを立てます。たとえば「しきい値を変えると、紙と画面で食い違うマスの数は変わるか」です。1回の実験で動かす条件は1つだけにし、ほかは固定します。

記録の単位と回数

記録するのは「紙の意図と画面で壁の有無が違うマスの数」です。これを誤読マス数と呼びます。数え方は次のとおりです。

  1. 紙の迷路を、画面と同じ向きで見ます。
  2. 画面の表示と、紙のマスを1つずつ見比べます。
  3. 壁の有無が違うマスに印をつけ、その数を数えます。

同じ紙を、条件ごとに3回撮影して測ります。1回だけでは、たまたま影が入ったかどうかを区別できません。表は次のような形が使いやすいです。

  • A、マス数:12、ペン:太い黒、撮影:真上・室内灯、しきい値:128、誤読マス数(1/2/3回目):空欄、ゴール判定:空欄
  • B、マス数:12、ペン:太い黒、撮影:真上・室内灯、しきい値:90、誤読マス数(1/2/3回目):空欄、ゴール判定:空欄

結果の読み方

数値が入ったら、次の基準で判断します。

  • 3回の誤読マス数がほぼ同じなら、原因は撮影のばらつきではなく、条件の設定にあります。しきい値やペンの濃さを疑います。
  • 3回で大きくばらつくなら、原因は撮影にあります。影、角度、紙の反りを疑います。
  • しきい値を下げると、壁が欠けるマスが増える一方、通路が壁になる誤りは減るはずです。逆に上げると、その反対になります。誤読の種類を「壁が消えた」「通路が壁になった」に分けて数えると、片寄りが見えます。
  • 画面上は「ゴールに着けます」と出ても、紙では着けない迷路だった場合は、壁が消えた誤読があるということです。逆に着けないと出たなら、通路に影や汚れが出ている可能性があります。紙の迷路を事前にゴールまで解いておくと、この比較ができます。

マス数を変える実験では、12、20、30のように段階を取り、誤読の割合(誤読マス数÷全マス数)で比べます。マスが小さくなるほど、塗りのむらが相対的に大きくなります。ただし、どの程度のマス数から問題が出るかは、ペンや紙によって変わります。自分の道具で確かめてください。

縮小方式の比較には、改良を1つ足します。画像を一度大きなキャンバスに描き、各マスの中央付近の平均の明るさを読む方法です。これを条件Cとして、同じ写真で誤読マス数を比べると、ブラウザ任せの縮小に問題があったかどうかが分かります。

起こりやすい誤り

  • 列数と紙のマス数が合っていません。 12マスの紙に10と入れると、すべてのマスが少しずつずれます。表示が全体にぼやけて見えるなら、最初に疑う点です。
  • 紙の余白をトリミングしていません。 縦のマス数は画像の縦横比から割り出すため、余白があると縦方向にずれが蓄積します。
  • 影が通路を暗くします。 手や端末の影が紙に落ちると、白いマスが壁になります。撮影時は光の向きを揃えます。
  • スタートとゴールを塗ってしまいます。 コードは左上と右下を強制的に通路にしますが、隣のマスが塗られていると、そこから出られません。
  • 条件を2つ同時に変えます。 しきい値とペンを同時に変えると、どちらが効いたのか分からなくなります。
  • 動いたかどうかだけを見ています。 「ゴールに着けた」は合格の目安にすぎません。誤読があっても、遠回りして着けることがあります。誤読マス数も必ず数えます。

次の行動

まず12×12マスの紙を1枚描き、条件Aと条件Bを各3回撮影して、誤読マス数を表に入れてください。しきい値の違いだけで差が出るかどうかが分かれば、次に動かす条件を決められます。遊べる形になったら、アソバースへ投稿することもできます。その場合は、写真を読み込む仕組みごとではなく、確定した壁の表をHTMLに埋め込んだ単独の index.html にまとめる方法が扱いやすいでしょう。アップロードの条件は、投稿画面の案内で確認してください。