読者です 読者をやめる 読者になる 読者になる

エンジニアの頭の中

フリーランスエンジニアが書く技術系ブログです。

新しいプログラミング言語の学習でライフゲームを作る(Conway’s Game of Life)

ライフゲーム(Conway’s Game Of Life)とは

ライフゲームをご存知でしょうか? ライフゲームは、ボード上にセル(細胞)を表示して、それぞれのセルがあるルールに従い、誕生、淘汰、生存を繰り返していく様子を眺めるシミュレーションゲームのことです。

https://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/Game_of_life_boat.svg/82px-Game_of_life_boat.svg.png

知らない人は、これだけじゃ何のことかさっぱりわからないと思います。詳細な説明はWikipediaにお任せします。

ライフゲーム - Wikipedia

※ちなみに日本ではライフゲームと呼びますが、英語では「Conway’s Game of Life」と言います。頭にConway’s (コンウェイの)と付いているのは、人生ゲーム(The Game of Life )と区別するために、先頭にライフゲームの発案者である John Horton Conwayの名前を付けているようです。(ってWikipediaにも書いてあります)

新しいプログラミング言語を覚える時にライフゲームを書く

私はライフゲームが好きです。他にも似たようなものだと群れをシミュレーションするBoidsなども好きです。何と無く眺めているのが面白いのです。 そんなライフゲームですが、私は新しいプログラミング言語に触れてみる際の入門的なプログラムとして、よくライフゲームを書いています。

必要なコードは基本的な条件分岐や、繰り返し処理、配列に加え、ちょっとした乱数、スリープの扱いなどがあるくらいなので、軽くコード書いてみるには、丁度良いのです。ただし、セル(細胞)の描画処理は、標準出力だと寂しいので、GUI付きで書く場合もあります。(ここが若干面倒な場合がありますが)

ライフゲームソースコードJavaScript

セルの描画処理が簡単に書けるので、JavaScriptとHTMLを使用して、WEBブラウザ上でライフゲームを描画するコードを載せておきます。

このコードを起動すると以下のような画面が表示されます。 f:id:mitsu3204:20170320000950p:plain

画面の下部に描画のコントローラがあります。各役割は以下のとおりです。

  • Start ・・・クリックするとセルの描画を開始します。一定時間ごとにセルの世代交代が発生します。
  • Reset ・・・クリックするとセルの状態をリセットします。リセットすると、セルの位置をランダムに再描画します。
  • Speed ・・・横にスライドするとセルの世代交代の速度を変更します。

index.html ライフゲームを動かすためのCanvasを表示するhtmlファイルです。

<html>
<head>
<title>Conway's Game of Life</title>
<script src="./cell.js"></script>
<style>
.controll {
    background-color: #c0c0c0;
    width: 600px;
    border-top: 1px solid #c0c0c0;
}
.controll .label {
    font-size: 14px;
    font-weight: bold;
}
</style>
</head>
<body>

    <!-- セルの描画部分 -->
    <canvas id="cell" width="600" height="500"></canvas>

    <!-- 描画の開始、停止や、速度のコントローラ -->
    <div class="controll">
        <label for="speed" class="label">Speed</label>
        <input id="speed" type="range" min="1" max="100" value="50" onchange="changeSpeed();"/>
        <button id="random" onclick="resetCells();" class="label">Reset</button>
        <button id="start" onclick="start();" class="label">Start</button>
    <div>
</body>
</html>

cell.js index.htmlから読み込んでいるJavaScriptファイルです。

var COLOR_CELL_LIFE = '#00ff00'; // 生きているセルの色
var COLOR_CELL_DEAD = '#000000'; // 死んでいるセルの色
var CELL_SIZE = 14; // セルの大きさ
var interval = 500; // セル描画処理の一時停止止時間(ミリ秒)
var pause = true; // 描画を停止中かどうかを表すフラグ
var cells; // セルの配列
var canvas;
var context;
var timer;

onload = function() {
    init();
};

// 初期化
function init() {
    canvas = document.getElementById('cell');
    context = canvas.getContext('2d');
    cells = new Array(Math.ceil(canvas.height / CELL_SIZE));
    resetCells();
    canvas.onmousedown = mouseMoveListner;
    canvas.onmousemove = mouseMoveListner;
}

// セル描画を初期化する
function resetCells() {

    clearTimeout(timer);

    // ボタンを初期表示に戻す
    document.getElementById('start').innerHTML = 'Start';

    // セルの位置を初期化(ランダムに配置する)
    for (i = 0; i < cells.length; i++) {
        cells[i] = new Array(Math.ceil(canvas.width / CELL_SIZE));
        for (j = 0; j < cells[i].length; j++) {
            if (Math.random() < 0.4) {
                cells[i][j] = true;
            } else {
                cells[i][j] = false;
            }
        }
    }
    
    // セルをcanvasへ描画する
    drawCells();
}

// グリッドをクリックした場合にセルを描画する
function mouseMoveListner(e) {
    if (e.which == 1) {
        var rect = e.target.getBoundingClientRect();
        var mouseX = e.clientX - rect.left;
        var mouseY = e.clientY - rect.top;
        var rowIndex = Math.round(mouseY / CELL_SIZE);
        var colIndex = Math.round(mouseX / CELL_SIZE);
        cells[rowIndex][colIndex] = true;
        if (pause) {
            drawCells();
        }
    }
}

// セルの世代交代を再開する
function start() {
    var startButton = document.getElementById('start');
    if (startButton.innerHTML == 'Start') {
        pause = true;
        startButton.innerHTML = 'Pause';
        draw();
    } else {
        pause = false;
        startButton.innerHTML = 'Start';
        clearTimeout(timer);
    }
}

// セル描画のインターバルを短くして、世代交代の速度を変更する
function changeSpeed() {
    var speed = document.getElementById('speed').value;
    interval = 1000 - speed * 10;
}

var draw = function() {
    var tmpCells = new Array(cells.length);

    for (i = 0; i < cells.length; i++) {
        tmpCells[i] = new Array(cells[i].length);

        for (j = 0; j < cells[i].length; j++) {
            cnt = countRoundCells(i, j);
            if (cnt == 3) {
                tmpCells[i][j] = true;
            } else if (cnt ==2) {
                tmpCells[i][j] = cells[i][j];
            } else {
                tmpCells[i][j] = false;
            }
        }
    }
    cells = tmpCells;
    drawCells();

    clearTimeout(timer);
    timer = setTimeout(draw, interval);
}

// 周囲のセルの数を数えて返す
function countRoundCells(rowIndex, colIndex) {
    topIndex = rowIndex - 1;
    btm = rowIndex + 1;
    lft = colIndex - 1;
    rgt = colIndex + 1;
    count = 0;
    if (rowIndex != 0) {
        if (colIndex != 0 && cells[topIndex][lft]) {
            count++;
        }
        if (cells[topIndex][colIndex]) {
            count++;
        }
        if (colIndex != cells[0].length - 1 && cells[topIndex][rgt]) {
            count++;
        }
    }
    if (colIndex != 0 && cells[rowIndex][lft]) {
        count++;
    }
    if (colIndex != cells[0].length - 1 && cells[rowIndex][rgt]) {
        count++
    }
    if (rowIndex != cells.length - 1) {
        if (colIndex != 0 && cells[btm][lft]) {
            count++;
        }
        if (cells[btm][colIndex]) {
            count++;
        }
        if (colIndex != cells[0].length - 1 && cells[btm][rgt]) {
            count++;
        }
    }
    return count;
}

// セルを描画する
function drawCells() {

    // 現在の描画を消去
    context.clearRect(0, 0, canvas.width, canvas.height);

    // 枠描画
    context.rect(0, 0, canvas.width, canvas.height);
    context.stroke();

    // グリッドを描画
    drawLine();

    for (i = 0; i < cells.length; i++) {
        for (j = 0; j < cells[i].length; j++) {
            x = j * CELL_SIZE + 0.1
            y = i * CELL_SIZE + 0.1
            w = CELL_SIZE - 0.5
            h = CELL_SIZE - 0.5
            if (cells[i][j]) {
                context.fillStyle = COLOR_CELL_LIFE
                context.fillRect(x, y, w, h);
            } else {
                context.fillStyle = COLOR_CELL_DEAD
                context.fillRect(x, y, w, h);
            }
        }
    }
}

// グリッドを描画する
function drawLine() {
    context.linesize = 0.1;
    context.beginPath();

    // 横線
    for (i = 0; i <= cells.length; i++) {
        context.moveTo(0, i * CELL_SIZE);
        context.lineTo(canvas.width, i * CELL_SIZE);
    }

    // 縦線
    for (i = 0; i <= cells[0].length; i++) {
        context.moveTo(i * CELL_SIZE, 0);
        context.lineTo(i * CELL_SIZE, canvas.height);
    }
    context.closePath();
    context.stroke();
}

index.htmlとcell.jsを同じ階層に配置して、index.htmlをWEBブラウザで開くと、ライフゲームが起動します。