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


## ライフゲームとは

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

引用:[ライフゲーム](https://ja.wikipedia.org/wiki/ライフゲーム)

##  ライフゲームのルール

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

<b>誕生</b>

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

<b>生存</b>

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

<b>過疎</b>

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

<b>過密</b>

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

引用:[ライフゲーム](https://ja.wikipedia.org/wiki/ライフゲーム)

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


## 実行例

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

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

### 初期状態

![スクリーンショット 2019-03-11 13.41.49.png](https://qiita-image-store.s3.amazonaws.com/0/29415/d9cd4479-d5ba-dfe2-430d-23e66694ed78.png "スクリーンショット 2019-03-11 13.41.49.png")

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

### 途中の状態

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

![スクリーンショット 2019-03-11 13.42.23.png](https://qiita-image-store.s3.amazonaws.com/0/29415/2fb8728d-ae96-dd84-6ee6-1f1fbc3ddb41.png "スクリーンショット 2019-03-11 13.42.23.png")


## 作成した主要なクラス

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

### 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

```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をラッピングしたもの。当アプリで利用するものだけを定義

```js
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

ひとつのマス（セル）を表すクラスを定義

```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

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

```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 クラスを定義

```js
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クラスを定義

```js
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]画面になるので、それから、開始してください。

<p class="codepen" data-height="600" data-theme-id="0" data-default-tab="js,result" data-user="gushwell" data-slug-hash="VNGLQq" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="LifeGame">
  <span>See the Pen <a href="https://codepen.io/gushwell/pen/VNGLQq/">
  LifeGame</a> by Gushwell (<a href="https://codepen.io/gushwell">@gushwell</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>