ライフゲームとは
Wikipediaより引用します。
ライフゲーム (Conway's Game of Life) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。
引用:ライフゲーム
ライフゲームのルール
これもWikipediaより引用します。
ライフゲームでは初期状態のみでその後の状態が決定される。碁盤のような格子があり、一つの格子はセル(細胞)と呼ばれる。各セルには8つの近傍のセルがある (ムーア近傍) 。各セルには「生」と「死」の2つの状態があり、あるセルの次のステップ(世代)の状態は周囲の8つのセルの今の世代における状態により決定される。 セルの生死は次のルールに従う。誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
引用:ライフゲーム
つまり、ひとつのセルに注目した場合、そのセルの周りの8つのセルの状態によって、自分自身のセルの生死が決まるということです。
実行例
マウスのドラッグでセルをなぞり生存の状態にします。シフトキーを押しながらドラッグすると死滅の状態にします。
通常のクリックでは、生死を反転させます。
使い勝手はあまりよくないですが、マウス操作で以下のような初期状態を作りました。
このページの最後に、CodePenのページを埋め込んでいますので、実際に動かしてみることができます。
初期状態
ちなみに、Randomボタンを押せば、初期状態をランダムで作成します。
途中の状態
Startボタンを押して、しばらく経過した状態が以下のスクリーンショットです。
作成した主要なクラス
このプログラムは、以下の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]画面になるので、それから、開始してください。