ふしぎな動き!JavaScript+HTML5で50行で『ラングトンのアリ』を作ろう

おっ立ち野郎が2015年1月18日12:34:25に投稿しました。

JavaScript

6

お気に入り

準備中

Views

このレシピで作るもの

ラングトンのアリについて

ラングトンのアリを知っていますか?ラングトンのアリは以下の単純なルールに従って動くアリのことです。

  • 黒いマスにアリがいた場合、90°右に方向転換し、そのマスの色を反転させ、1マス前進する。
  • 白いマスにアリがいた場合、90°左に方向転換し、そのマスの色を反転させ、1マス前進する。

たったこれだけのルールでラングトンのアリさんはとても複雑な動きを見せ、まるで生きているかのようです。そしてこの複雑な動きを大体1万回繰り返すとなぜか一直線の道を作ってどっかにいってしまいます。

詳しくはデモを見てください。

今回はこれをJavaScriptで作っていきます。


目次

今回作るラングトンのアリの動作の流れ

書く前にどんなものか少し考えてみましょう。

  1. アリの場所のマスの色によってアリの向きの変更
  2. アリの場所のマスの色を反転
  3. アリを一マス進める
  4. 1へ。

めちゃめちゃシンプルですね。では早速書いていきましょう。

プログラミングの下準備

今回のラングトンのアリに必要なファイルは以下。

  • index.html
  • main.js

今回はmain.jsをメインに書いていくので、main.jsをちゃちゃっと書いておきましょう。

index.htmlを作る

index.htmlを以下のように記述してください。

<!DOCTYPE html>
<meta charset="utf-8">
<title>50行ラングトンのアリ</title>
<script src="main.js"></script>
<style>body{margin:0;padding:0;background:black;}</style>
<canvas id="world" style="margin:auto;display:block;"></canvas>

HTML5の宣言、文字コード、タイトルとJavaScriptの読み込みをし、キャンバス要素を記述します。

htmlタグやheadタグ、bodyタグは省略できます。詳しくは→Google HTML/CSS Style Guide [http://google-styleguide.googlecode.com/svn/trunk/htmlcssguide.xml:bookmark]

早速ラングトンのアリを書いていきましょう。

main.jsを書いていく

コード全体の構成を考える

いきなり白紙を渡されても書きづらいので、引数や内容は考えずに抽象的に構成だけ考えてみましょう。

こんな感じでラングトンのアリは完成します。

/* グローバル変数の宣言 */

window.onload = function() {
    // 初期化
    simulate();  // シミュレート開始
};

function simulate() {
    setTimeout(simulate, ミリ秒後);  // 再帰処理でループ
}
グローバル変数の宣言

定数などを代入しておきます。

window.onload

window.onloadではページが読み込まれた時に代入された関数が実行されます。今回はゲームの初期化とループのトリガーとして使います。

simulate関数

実際に以下の処理をしていきます。

  1. アリの場所のマスの色によってアリの向きの変更
  2. アリの場所のマスの色を反転
  3. アリを一マス進める
  4. 1へ

setTimeoutで自身を呼び出してループしていきます。

グローバル変数を宣言する

まず定数などをグローバル変数として宣言しましょう。

var SCREEN_SIZE = 500;                    // キャンバスのサイズ
var SIDE_CELLS = 200;                     // 一辺のマスの数
var CELL_SIZE = SCREEN_SIZE / SIDE_CELLS; // 1マスの幅
var FPS = 200;                            // フレームレート
var canvas;                               // キャンバス
var context;                              // コンテキスト
var dirs = [                              // アリの方向用配列
    {'row': -1, 'col': 0},
    {'row': 0, 'col': 1},
    {'row': 1, 'col': 0},
    {'row': 0, 'col': -1},
];

基本的にはコメントの通りです。

  • SCREEN_SIZE: キャンバスの横幅と縦幅(px)
  • SIDE_CELLS: 一辺のマスの数
  • CELL_SIZE: マスの幅
  • FPS: 一秒間に何回シミュレートするか(Frames Per Second)
  • canvas: 後にキャンバス要素を代入
  • context: 後にコンテキストを代入(コンテキストはキャンバスの筆のような概念です)
  • dirs: アリの進む方向の全ての組み合わせです。つまり順に上右下左を表しています。

ページが読み込まれた時の処理を書こう

それではページが読み込まれた時の処理を書いていきましょう。ここではゲームの初期化と開始の処理を行います。

まず始めにキャンバスを設定します。

次にフィールドとアリを作ってsimulate関数に渡しています。

window.onload = function () {
    canvas = document.getElementById('world'); // キャンバスの取得
    context = canvas.getContext('2d');         // コンテキストの取得
    canvas.width = canvas.height = SCREEN_SIZE; // キャンバスの画面サイズ設定
    var scaleRate = Math.min(window.innerHeight/SCREEN_SIZE, window.innerHeight/SCREEN_SIZE); // 画面引き伸ばし率
    canvas.style.height = canvas.style.width = SCREEN_SIZE*scaleRate + 'px'; // 画面引き伸ばし
    var field = new Array(SIDE_CELLS); // フィールド情報
    for (var i=0; i<SIDE_CELLS; i++) { // マス全てに0を格納して初期化
        field[i] = new Array(SIDE_CELLS);
        for (var j=0; j<SIDE_CELLS; j++) {
            field[i][j] = 0;
        }
    }
    var ant = {'dir': 0, 'row': SIDE_CELLS/2-1, 'col': SIDE_CELLS/2-1} // アリ
    simulate(field, ant);       // シミュレート開始
};
キャンバスの設定
  • canvasをgetElementByIdで取得します。そしてcanvas.getContext('2d')でコンテキスト情報を取得します。コンテキストはキャンバスの筆のような概念です。
  • canvas.width = canvas.height = SCREEN_SIZE; はキャンバスの横幅と縦幅を設定しています。それぞれpxです。
  • canvasの横幅と縦幅をcssで設定すると画面を引き伸ばすことができます。
フィールド情報
  • フィールド情報は2次元配列で管理します。はじめのマスは全て黒なので、全てに0を格納しています。
アリ
  • アリは方向情報と位置情報を持っています。それぞれdirとrow,colで表しています。始めのアリの向きは0、つまり上です。SIDE_CELLS/2-1でキャンバスのちょうど真ん中に現れるように初期化していきます。

さあ、初期化処理は書けましたのでsimulate関数を作っていきましょう。

メインループ処理を書こう

ではsimulate関数を書いていきましょう。

アリは今いるマスが白か黒かによって方向を変え、一歩進みます。そしてアリが居たマスの色は反転されます。たったこれだけの処理です。今回は画面外にアリがいってしまったらゲームオーバということにしました。

function simulate(field, ant) {
    if (field[ant.row][ant.col]) { // アリの現在地が白の場合
        ant.dir --;                // マスが白の時は左へ向き変更
        context.fillStyle = 'rgb(0, 0, 0)'; // 白を反転すると黒
    } else  {                               // アリの現在地が黒の場合
        ant.dir ++;             // マスが黒の時は右へ向き変更
        context.fillStyle = 'rgb(0, 255, 255)'; // 黒を反転すると白
    }
    field[ant.row][ant.col] = 1 - field[ant.row][ant.col]; // アリのいるマスを反転
    context.fillRect(ant.col*CELL_SIZE, ant.row*CELL_SIZE, CELL_SIZE, CELL_SIZE); // アリの居るマスの色を描画
    ant.dir = (ant.dir+4) % 4;    // アリの向きを修正(5=>1, -1=> 3)
    ant.row += dirs[ant.dir].row; // アリを移動
    ant.col += dirs[ant.dir].col; // アリを移動
    context.fillStyle = 'rgb(0, 0, 255)'; // アリの色
    context.fillRect(ant.col*CELL_SIZE, ant.row*CELL_SIZE, CELL_SIZE, CELL_SIZE); // アリを描画
    if (ant.row<0 || ant.row>=SIDE_CELLS || ant.col<0 || ant.col>=SIDE_CELLS) { // 壁判定
        alert("アリはそそくさと逃げていきました。"); // ゲームオーバ処理
    } else {
        setTimeout(simulate, 1000/FPS, field, ant); // 帰納処理でループ
    }
}

はじめにアリのいるマス目が何色かを判断してアリの方向の変更処理と塗りつぶす色の変更処理をしています。なぜアリが白いマスにいた時にコンテキストの色の設定を黒に、
黒の時は白にするのかというと、if文の後で反転するからです。

field[ant.row][ant.col] = 1 - field[ant.row][ant.col]; // アリのいるマスを反転

context.fillRect(x座標, y座標, 横幅, 縦幅)は矩形を描きます。

あとは基本的にコメントの通りです。シンプルなのであまり説明することがありません。。

ポイント

今回は高速化のため、以下のような処理はしていません!

  1. キャンバスを全てクリア
  2. フィールド情報をfor文で回して要素を全て取得して描画
  3. アリを描画

イメージ的にはアリのいる場所に色を上塗りしていく形となります。

ラングトンのアリ完成!!

いかがでしたでしょうか。これで『ラングトンのアリ』が完成しました。お疲れ様でした。ちゃんとアリさんは一定時間経った後に直線を作り出しましたかね?

このレシピを書いたシェフ

おっ立ち野郎

@ottatiyarou

コードレシピの管理人です。
「for文とか関数とかの基礎はやったけど、実際アプリとかどうやって作るのか分からない」を解決するようなサービスにしていきたいです。


このシェフが書いたレシピ