LoginSignup
24
17

More than 3 years have passed since last update.

JavaScriptでライフゲームを作成する

Last updated at Posted at 2019-04-24

ライフゲームとは

Wikipediaより引用します。


ライフゲーム (Conway's Game of Life) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。

引用:ライフゲーム

ライフゲームのルール

これもWikipediaより引用します。


ライフゲームでは初期状態のみでその後の状態が決定される。碁盤のような格子があり、一つの格子はセル(細胞)と呼ばれる。各セルには8つの近傍のセルがある (ムーア近傍) 。各セルには「生」と「死」の2つの状態があり、あるセルの次のステップ(世代)の状態は周囲の8つのセルの今の世代における状態により決定される。
セルの生死は次のルールに従う。

誕生

死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。

生存

生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。

過疎

生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。

過密

生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

引用:ライフゲーム

つまり、ひとつのセルに注目した場合、そのセルの周りの8つのセルの状態によって、自分自身のセルの生死が決まるということです。

実行例

マウスのドラッグでセルをなぞり生存の状態にします。シフトキーを押しながらドラッグすると死滅の状態にします。
通常のクリックでは、生死を反転させます。
使い勝手はあまりよくないですが、マウス操作で以下のような初期状態を作りました。

このページの最後に、CodePenのページを埋め込んでいますので、実際に動かしてみることができます。

初期状態

スクリーンショット 2019-03-11 13.41.49.png

ちなみに、Randomボタンを押せば、初期状態をランダムで作成します。

途中の状態

Startボタンを押して、しばらく経過した状態が以下のスクリーンショットです。

スクリーンショット 2019-03-11 13.42.23.png

作成した主要なクラス

このプログラムは、以下の6つのクラスから構成されています。

MyCanvas クラス

HTML5のCanvasAPIをラッピングしているクラスで、セルの描画を担当します。

Cellクラス

ひとつのセル上の生物を表すクラス。
surviveメソッドが、引数で周囲の「生存数」の数を受け取り、その数により次の世代の状態を決めます。このメソッドを呼び出しただけでは、実際の状態は更新されません。nextStageを呼び出されることで、surviveで得られた状態に更新されます。

使い方として、すべてのCellに対してsurviveを呼び出した後に、すべてのCellに対してnextStageを呼び出すことになります。

Boardクラス

ライフゲームの空間(盤)を表すクラスです。
このクラスは複数のCellオブジェクトを管理します。一つの升目にひとつのCellオブジェクトが割り当てられます。初期値はすべてのCellが死んだ状態です。

このクラスにもsurviveメソッドがあります。このメソッドは、すべてのCellオブジェクトに対して、周りの生存数をカウントし、その値を引数にして、Cell.Survive メソッドを呼び出した後に、CellごとにnextStageメソッドを呼び出しています。

なお、状態が変化した場合、変化したCellの数分、onChangeコールバックメソッドを呼び出します(いわゆるイベントの発行です)。これにより、UI上にCellが変化したことを通知し、描画をさせています。このイベントを受け取るのは、次の LifeWorldクラスです。

LifeWorldクラス

このライフゲームのコアのクラスになります。startメソッドが呼ばれると、BoardクラスのSurviveメソッドを繰り返し呼び出し、世代が進んでいきます。生存数が0になるとゲームが終わります。
前述のように、描画処理も受け持ちます(実際の描画機能は、MyCanvasが行います)。

Programクラス

当プログラムのメインクラス。
ボタンのクリックイベントの処理などを担当。
LifeBoardオブジェクトを保持し、プログラムを統括しています。

HTML/JavaScriptのコード

HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>life game</title>
  <style>
      canvas {
          border: solid 1px #666666;
          margin: 11px;
      }
  </style>
</head>
<body>
  <div>
      <canvas id="mycanvas"></canvas>
  </div>
  <div>
    <input id="startButton" type="button" value="Start" >
    <input id="stopButton" type="button" value="Stop" >
    <input id="clearButton" type="button" value="Clear" >
    <input id="randomButton" type="button" value="Random" >
  </div>
  <script type="module">
      import { main } from './app.js';
      main();
  </script>
</body>
</html>

canvas.js

HTML5のCanvasをラッピングしたもの。当アプリで利用するものだけを定義

export class MyCanvas {
    constructor(id, width, height) {
        this.cellsize = 12;
        this.canvas = document.getElementById(id);
        this.ctx = this.canvas.getContext('2d');
        if (width) {
            this.ctx.canvas.width = width * this.cellsize;
        }
        if (height) {
            this.ctx.canvas.height = height * this.cellsize;
        }
        this.width = this.ctx.canvas.width;
        this.height =  this.ctx.canvas.height;
        this.clearAll();

        this.onClick = function(x, y) {};
        this.onMouseDown = function(x, y) {};
        this.onMouseMove = function(x, y) {};
        this.onMouseUp = function(x, y) {};

        this.canvas.onclick = (e) => {
            var pt = this.getPoint(e)
            this.onClick(pt.x, pt.y);
        };

        this.canvas.onmousedown = (e) => {
            var pt = this.getPoint(e)
            this.onMouseDown(pt.x, pt.y, e.shiftKey);
        };

        this.canvas.onmousemove = (e) => {
            var pt = this.getPoint(e)
            this.onMouseMove(pt.x, pt.y, e.shiftKey);
        };

        this.canvas.onmouseup = (e) => {
            var pt = this.getPoint(e)
            this.onMouseUp(pt.x, pt.y, e.shiftKey);
        };
    }

    getPoint(e) {
        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - Math.floor(rect.left);
        var y = e.clientY - Math.floor(rect.top);
        x = Math.floor(x / this.cellsize);
        y = Math.floor(y / this.cellsize);
        return { x: x, y: y };
    }
    // // 点を打つ
    drawPoint(x, y, color) {
        this.ctx.fillStyle = color;
        this.ctx.fillRect(x*this.cellsize+1, y*this.cellsize+1, this.cellsize-1, this.cellsize-1);
    }

    // 縦罫線
    drawVRuleLine(x) {
        this.ctx.strokeStyle = '#aaaaaa';
        this.ctx.lineWidth = 0.1;
        this.ctx.beginPath();
        this.ctx.moveTo(x+0.5, 0);
        this.ctx.lineTo(x+0.5, this.height);
        this.ctx.closePath();
        this.ctx.stroke();
    }

    // 横罫線
    drawHRuleLine(y) {
        this.ctx.strokeStyle = '#aaa';
        this.ctx.lineWidth = 0.1;
        this.ctx.beginPath();
        this.ctx.moveTo(0, y+0.5);
        this.ctx.lineTo(this.width, y+0.5);
        this.ctx.closePath();
        this.ctx.stroke();
    }

    // 指定場所の色を取得
    getColor(x, y) {
        var pixel  = this.ctx.getImageData(x*this.cellsize, y*this.cellsize, 1, 1);
        var data = pixel.data;
        return Canvas.toRgbaStr(data[0], data[1], data[2], data[3]);
    }

    // 指定位置をクリア
    clearPoint(x, y) {
        this.ctx.clearRect(x*this.cellsize, y*this.cellsize, this.cellsize, this.cellsize);
    }

    // すべてをクリア
    clearAll() {
        this.ctx.clearRect(0, 0, this.width, this.height);
        for (var x = 0; x < this.width; x += this.cellsize) {
            this.drawVRuleLine(x);
        }
        for (var y = 0; y < this.height; y += this.cellsize) {
            this.drawHRuleLine(y);
        }
    }

    // ひとつのセルをその状態によって描画
    drawPiece(loc, piece) {
        var cell = piece;
        if (cell.IsAlive) {
            this.DrawLife(loc, '#666666');
        } else {
            this.clearPoint(loc.x, loc.y);
        }
    }

    // 四角形を生成する
    drawLife(loc, color) {
        this.drawPoint(loc.x, loc.y, color);
    }

    static  toRgbaStr(r, g, b, a){
        return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
    };
}


call.js

ひとつのマス(セル)を表すクラスを定義


export class Cell {
    constructor() {
        this.IsAlive  = false;
        this._nextStatus = false;
    }


    // 生死を反転する
    toggle() {
        this.IsAlive = !this.IsAlive;
    };

    // trueならば生、falseならば死
    judge(count) {
        if (this.IsAlive)
            return (count === 2 || count === 3);
        else
            return (count === 3);
    };

    // 次の世代の状態を決める。変化があるとtrueが返る。
    survive(around) {
        this._nextStatus = this.judge(around);
        return this._nextStatus !== this.IsAlive;
    };

    // 次の状態にする
    nextStage() {
        var old = this.IsAlive;
        this.IsAlive = this._nextStatus;
        return this.IsAlive !== old;
    };
}


board.js

ライフゲームの空間(盤)を表すクラスを定義

import { Cell } from './cell.js';

// Cellを管理する
export class Board {
    // コンストラクタ
    constructor(w, h) {
        this.width = w;
        this.height = h;
        this.map = new Array((this.width) * (this.height));
        this.clearAll();
        this.onChange = {};
    }


    // Location から _coinのIndexを求める
    toIndex(x, y) {
        return x + y * (this.width);
    };

    // IndexからLocationを求める
    toLocation(index) {
        return { x: index % (this.width),  y: Math.floor(index / (this.width)) };
    };

    // 盤上のすべての位置(index)を列挙する
    getAllIndexes() {
        var list = [];
        for (var y = 0; y < this.height; y++) {
            for (var x = 0; x < this.width; x++) {
                list.push(this.toIndex(x, y));
            }
        }
        return list;
    };

    // 全てのPieceをクリアする
    clearAll() {
        this.getAllIndexes().forEach(ix => {
            this.map[ix] = new Cell();
        });
    };

    // 反転する
    reverse(index) {
        var cell = this.map[index];
        cell.toggle();
        this.onChange(index, cell);
    };

    // 生成する
    set(index) {
        var cell = this.map[index];
        cell.IsAlive = true;
        this.onChange(index, cell);
    };

    // 消滅させる
    clear(index) {
        var cell = this.map[index];
        cell.IsAlive = false;
        this.onChange(index, cell);
    };

    // 位置の補正 はみ出たらぐるっと回る
    corectIndex(x, y) {
        x = (x < 0) ? this.width - 1 : x;
        y = (y < 0) ? this.height - 1 : y;
        x = (x >= this.width) ? 0 : x;
        y = (y >= this.height) ? 0 : y;
        return this.toIndex(x,y);
        // { this.ToIndex(x % (this.width + 1), y % (this.height + 1));
    };

    // 周りの生存者の数を数える
    countAround(index) {
        var loc = this.toLocation(index);
        var arounds = [
            { x: loc.x-1, y: loc.y-1 },
            { x: loc.x-1, y: loc.y },
            { x: loc.x-1, y: loc.y+1 },
            { x: loc.x, y: loc.y-1 },
            { x: loc.x, y: loc.y+1 },
            { x: loc.x+1, y: loc.y-1 },
            { x: loc.x+1, y: loc.y },
            { x: loc.x+1, y: loc.y+1 },
        ];
        return arounds
            .map(loc => this.corectIndex(loc.x, loc.y))
            .filter(ix => this.map[ix].IsAlive)
            .length;
    };

    // 生死を決める
    survive() {
        var count = 0;
        this.getAllIndexes().forEach(ix => {
            var cell = this.map[ix];
            if (cell.survive(this.countAround(ix)))
                count++;
        });
        if (count > 0) {
            this.getAllIndexes().forEach(ix => {
                var cell = this.map[ix];
                if (cell.nextStage()) {
                    this.onChange(ix, cell);
                }
            });
        }
        return count;
    };
}

lifeworld.js

このライフゲームのコアのクラスである LifeWorld クラスを定義

var random = function(max) {
    return Math.floor(Math.random() * max) + 1 ;
};

export class LifeWorld {
    constructor(board, canvas) {
        this._canvas = canvas;
        this._board = board;

        this.timer = {};

        // 状態が変化した時の処理 Boardオブジェクトが呼び出す
        this._board.onChange = (index, cell) => {
            var loc = this._board.toLocation(index);
            var color = cell.IsAlive ? '#508030' : '#FFFFFF';
            this._canvas.drawPoint(loc.x, loc.y, color);
        };
    }

    //  開始
    start() {
        this.timer = setInterval(() => {
            var count = this._board.survive();
            if (count == 0)
                this.stop();
        }, 200);
    };

    // 停止
    stop() {
        clearInterval(this.timer);
    };

    // クリア
    clear() {
        this._canvas.clearAll();
        this._board.clearAll();
    };

    // ランダムに点を打つ
    random() {
        this.clear();
        var count = random(200) + 100;
        for (var i = 0; i < count; i++) {
            var x = random(this._board.width-1);
            var y = random(this._board.height-1);
            var ix = this._board.toIndex(x, y);
            this._board.reverse(ix);
        }
    };
}


app.js

Programクラスを定義

import { Board } from './board.js';
import { MyCanvas } from './canvas.js';
import { LifeWorld } from './lifeWorld.js';

export class Program {
    constructor(width, height) {
        var board = new Board(width, height);
        var canvas = new MyCanvas( 'mycanvas', width, height);
        this.world = new LifeWorld(board, canvas);

        this.dragState = 0;

        canvas.onMouseDown = (x, y,shiftKey) => {
            this.dragState = 1;
        };

        canvas.onMouseMove = (x, y,shiftKey) =>  {
            if (this.dragState >= 1) {
                if (shiftKey)
                    board.clear(board.toIndex(x, y));
                else
                    board.set(board.toIndex(x, y));
                this.dragState = 2;
            }
        };

        canvas.onMouseUp = (x, y,shiftKey) =>  {
            if (this.dragState === 1) {
                board.reverse(board.toIndex(x, y));
            }
            this.dragState = 0;
        };

    }

    run() {
        document.getElementById('startButton')
            .addEventListener('click', () => this.start(), false);
        document.getElementById('stopButton')
            .addEventListener('click', () => this.stop(), false);
        document.getElementById('clearButton')
            .addEventListener('click', () => this.clear(), false);
        document.getElementById('randomButton')
            .addEventListener('click', () => this.random(), false);
    };

    start() {
        this.world.start();
    };

    stop() {
        this.world.stop();
    };

    clear() {
        this.world.stop();
        this.world.clear();
    };

    random() {
        this.world.random();
    };
}

export function main() {
    window.onload = function() {
        var program = new Program(50, 35);
        program.run();
    };
}

CodePenのページを埋め込む

CodePenのページを埋め込んでみました。

左ペインの[JS]ボタンをクリックすると、全体が[Result]画面になるので、それから、開始してください。

See the Pen LifeGame by Gushwell (@gushwell) on CodePen.

24
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
17