ラングトンのアリをブラウザで動かしみたい。
そう思ってJavaScriptでプログラムを書きました。
動かし方とコードの解説をしていきます。
このページの目次
ラングトンのアリとは何か
ここは説明不要と思うけど念のため。
次の規則で動くプログラムのことです。
▼ こういうルールで動く
平面が格子状に構成され、各マスが白または黒で塗られる。ここで、1つのマスを「アリ」とする。アリは各ステップで前後左右のいずれかのマスに移動することができる。アリは以下の規則に従って移動する。
白いマスにアリがいた場合、90°右に方向転換し、そのマスの色を反転させ、1マス前進する。
黒いマスにアリがいた場合、90°左に方向転換し、そのマスの色を反転させ、1マス前進する。この単純な規則で驚くほど複雑な動作をする。当初でたらめな動作をしているが、アリはいずれ例外なく10000歩ほどうろついた後に真っ直ぐな「道」を作る動作に入る。
たった2つのルールしかないんです。
でもアリは超カオスな動きをしまくります。
実際のラングトンのアリの挙動がこちら
あと当チャンネルで解説動画も出しました。
そして上記動画でラングトンのアリを動かしてますが、これはJavaScriptでコードを書いてブラウザ上で実行してます。
そのプログラム例をここで解説です。
1.必要なHTMLとjsファイルを作成
ここでは次の2つを作成します。
- main.html
- main.js
次のように空ファイルを作成してください。
▼ 各自のPCで適当なフォルダに以下2つ作成
そしてmain.htmlに以下をコピペします。
▼ 以下をmain.htmlにコピペして保存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<html> <head> </head> <body> <div> <button onclick="start()">開始</button> <button onclick="pause()">停止</button> <button onclick="scale(2)">拡大</button> <button onclick="scale(.5)">縮小</button> </div> <canvas id="canvas" width="500" height="500" style="image-rendering: pixelated;"></canvas> <script src="./main.js"></script> </body> </head> |
これで下準備は完了
2.ラングトンのアリのJavaScript全体コード
そしたらmain.jsの内容について
ここにラングトンのアリのプログラムを書きます。
以下を全てコピペして貼り付けてください。
▼ ラングトンのアリ JavaScriptコード例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
/// 座標幅 const GRID_WIDTH = 100; /// 座標高さ const GRID_HEIGHT = 100; /// 座標を表す配列(ついでに幅×高さで初期化) const grid = Array(GRID_WIDTH*GRID_HEIGHT).fill(0); /// アリを表す配列 const ants = [new Ant(50, 50)]; /// ループタイマーのID let LOOP_TIMER_ID; /// 描画間隔(ミリ秒) let LOOP_INTERVAL = 10; /// 開始前に初期状態を描画 draw(); /// 描画を開始(再開)する関数 function start(){ LOOP_TIMER_ID = setInterval(function(){ ants.forEach(function(ant){ /// アリのX方向ベクトル const vx = ant.vec[0]; /// アリのY方向ベクトル const vy = ant.vec[1]; if(getGridStatus(ant.x,ant.y)===0){ /// アリが白のマスにいるなら… /// マス目を黒に変更 setGridStatus(ant.x,ant.y,1); /// アリを90度右に方向転換させる ant.vec[0] = 0*vx - 1*vy; ant.vec[1] = 1*vx + 0*vy; }else{ /// アリが白以外(黒)のマスにいるなら… /// マス目を白に変更 setGridStatus(ant.x,ant.y,0); /// アリを90度左に方向転換させる ant.vec[0] = 0*vx - (-1)*vy; ant.vec[1] = -1*vx + 0*vy; } /// アリを1歩だけ進ませる ant.x += ant.vec[0]; ant.y += ant.vec[1]; }); /// アリと座標を描画 draw(); }, LOOP_INTERVAL); } /// 描画を停止する関数 function pause(){ clearInterval(LOOP_TIMER_ID); } function scale(s){ const $canvas = document.getElementById('canvas'); const c = $canvas.getContext('2d'); c.scale(s,s); } /// アリと座標を描画する関数 function draw(){ const $canvas = document.getElementById('canvas'); const c = $canvas.getContext('2d'); const LINE_W = "0"; const CELL_W = 1; const CELL_H = 1; c.lineWidth=LINE_W; c.strokeStyle="#ddd"; for(let x=0; x<GRID_WIDTH; ++x){ for(let y=0; y<GRID_HEIGHT; ++y){ c.beginPath(); //c.rect(20+x*CELL_W,20+y*CELL_H,CELL_W,CELL_H); //c.stroke(); c.fillStyle = getGridStatus(x,y)===0?'#fff':'#000'; c.fillRect(20+x*CELL_W,20+y*CELL_H,CELL_W,CELL_H); } } ants.forEach((ant,i)=>{ c.fillStyle = '#f00'; if(CELL_W<=5){ c.fillRect(20+ant.x*CELL_W,20+ant.y*CELL_H,CELL_W,CELL_H); } }); } /// 座標の状態を取得する関数 function getGridStatus(x,y){ return grid[y*GRID_WIDTH+x]; } /// 座標の状態を変更する関数 function setGridStatus(x,y,status){ grid[y*GRID_WIDTH+x] = status; } /// アリの定義 function Ant(x,y,vec=[-1,0],color="#000"){ this.x = x; this.y = y; this.vec = vec; this.color = color; } |
大丈夫、これから解説していきます。
3.上記JavaScriptプログラムの解説
ということで上記コードの細かな解説です。
初めに以下の部分で必要な定数定義してます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// 座標幅 const GRID_WIDTH = 100; /// 座標高さ const GRID_HEIGHT = 100; /// 座標を表す配列(ついでに幅×高さで初期化) const grid = Array(GRID_WIDTH*GRID_HEIGHT).fill(0); /// アリを表す配列 const ants = [new Ant(50, 50)]; /// ループタイマーのID let LOOP_TIMER_ID; /// 描画間隔(ミリ秒) let LOOP_INTERVAL = 100; |
コメントを見れば大体分かるはず。
座標を表すのに配列を使っています。上記の Array(GRID_WIDTH*GRID_HEIGHT).fill(0) ですが幅×高さ分の要素を持つ配列をすべて 0 で初期化するって意味ですね。(参考 : Array() コンストラクター - JavaScript | MDN)
それから LOOP_INTERVAL の定数について
ラングトンのアリが1歩進むたびに描画処理するんですが、その描画間隔をミリ秒で指定できます。1000ミリ秒=1秒なので、100だと0.1秒間隔になります。
次にアリの定義がここです。
▼ Antを定義している箇所
1 2 3 4 5 6 |
function Ant(x,y,vec=[-1,0],color="#000"){ this.x = x; this.y = y; this.vec = vec; this.color = color; } |
アリの今いる座標を this.x this.y で表し、アリが向いている方向(ベクトル)は this.vec という配列で表します。アリが白マスを反転させる色は this.color で表しました。
ちなみにベクトルは次の定義とします。
- 値が [-1,0] なら左向き
- 値が [1,0] なら右向き
- 値が [0,-1] なら下向き
- 値が [0,1] なら上向き
ベクトルを (x, y) の配列で表現し、左右上下のどこを向いているか定義してるだけです。こうするとアリ移動の処理が楽に書けるので、便宜上こうしてます。
そして肝心のstart関数はこうなってます。
▼ 上記コードのstart関数だけ抜粋
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function start(){ LOOP_TIMER_ID = setInterval(function(){ ants.forEach(function(ant){ /// アリのX方向ベクトル const vx = ant.vec[0]; /// アリのY方向ベクトル const vy = ant.vec[1]; if(getGridStatus(ant.x,ant.y)===0){ /// アリが白のマスにいるなら… /// マス目を黒に変更 setGridStatus(ant.x,ant.y,1); /// アリを90度右に方向転換させる ant.vec[0] = 0*vx - 1*vy; ant.vec[1] = 1*vx + 0*vy; }else{ /// アリが白以外(黒)のマスにいるなら… /// マス目を白に変更 setGridStatus(ant.x,ant.y,0); /// アリを90度左に方向転換させる ant.vec[0] = 0*vx - (-1)*vy; ant.vec[1] = -1*vx + 0*vy; } /// アリを1歩だけ進ませる ant.x += ant.vec[0]; ant.y += ant.vec[1]; }); /// アリと座標を描画 draw(); }, LOOP_INTERVAL); } |
コメントを見れば大まかに分かると思う。
でも方向転換の部分は分かりにくいですね。
これは行列に対して回転操作を実行してます。
▼ 例えば90度右回転させる次のコード
1 2 3 |
/// アリを90度右に方向転換させる ant.vec[0] = 0*vx - 1*vy; ant.vec[1] = 1*vx + 0*vy; |
▼ これは次のコードを省略したもの
1 2 3 4 |
/// アリを90度右に方向転換させる const radian = -90*(Math.PI/180); ant.vec[0] = Math.cos(radian)*vx-Math.sin(radian)*vy; ant.vec[1] = Math.sin(radian)*vx+Math.cos(radian)*vy; |
行列の回転については次記事を見てください。
この知識をコードに落とし込んだだけ。三角関数なんか役に立たないと思う人は多いですが、こういう場面で意外に役立ったりします。
それから最後に描画関数を定義しました。
▼ 上記コードのdraw関数だけ抜粋
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/// アリと座標を描画する関数 function draw(){ const $canvas = document.getElementById('canvas'); const c = $canvas.getContext('2d'); const LINE_W = "0"; const CELL_W = 1; const CELL_H = 1; c.lineWidth=LINE_W; c.strokeStyle="#ddd"; for(let x=0; x<GRID_WIDTH; ++x){ for(let y=0; y<GRID_HEIGHT; ++y){ c.beginPath(); //c.rect(20+x*CELL_W,20+y*CELL_H,CELL_W,CELL_H); //c.stroke(); c.fillStyle = getGridStatus(x,y)===0?'#fff':'#000'; c.fillRect(20+x*CELL_W,20+y*CELL_H,CELL_W,CELL_H); } } ants.forEach((ant,i)=>{ c.fillStyle = '#f00'; if(CELL_W<=5){ c.fillRect(20+ant.x*CELL_W,20+ant.y*CELL_H,CELL_W,CELL_H); } }); } |
ここについては面倒なので解説しません。
何か不明点があればコメントから聞いてください。
4.ブラウザ上でラングトンのアリを実行
そしたらブラウザ上で動かしてみましょう。
単純にmain.htmlにブラウザからアクセスするだけ
▼ Windowsの場合はこういう感じ
▼ あとは開始ボタンを押すだけ
▼ こういう風にアリを眺められる
初期状態だと座標は1ドット単位です。
見にくいなら拡大ボタンを押してください。
複数のラングトンのアリを召喚する
初期プログラムではアリは1匹だけです。
でもJavaScriptコードを少し改変すれば、
アリをバリエーションを変えて複数匹召喚できます。
▼ 場所を変えて3匹召喚するコード例
1 2 3 4 5 6 |
/// アリを表す配列 const ants = [ new Ant(30, 70), new Ant(50, 30), new Ant(70, 30) ]; |
必要な分だけアリを召喚可能です。
それから向き変更もできます。
▼ 違う向きのアリを4匹召喚する例
1 2 3 4 5 6 7 8 9 10 11 |
/// アリを表す配列 const ants = [ /// 左向きのアリ new Ant(30, 30, [0,-1]), /// 右向きのアリ new Ant(30, 70, [0,1]), /// 下向きのアリ new Ant(70, 30, [-1,0]), /// 上向きのアリ new Ant(70, 70, [1,0]) ]; |
さて、ここで問題です。
アリに反転色(color)を持たせましたが、紹介したコードでは反転色はすべて黒です。この反転色をアレンジできるようにコードを改造してみてください。
答えはあえて載せません。
以上、JavaScriptでラングトンのアリ実装でした。
質問・指摘などはコメント欄からどうぞ。ではまた