追記20130928
本題の非同期版を書いた。
非同期的 (async) life game (JavaScript) http://c4se.hatenablog.com/entry/2013/09/28/004018
前回のは未だ開発を続けてるけど。Ruby 2.1に成って、keyword引数の仕様に機能が追加されるから、多分修正が必要だと思ふ。12/25には2.1でちゃうし。
cf. Dart風のautomatic field initializationをRubyで http://c4se.hatenablog.com/entry/2013/09/23/075129
まぁissue呉れ(〃l _ l) (乞食
cf. Issues · ne-sachirou/AutoAttrInit.rb https://github.com/ne-sachirou/AutoAttrInit.rb/issues
(〃l _ l) 其れでJavaScriptで普通のlife gameをやった訳だが
何人目だよ (l _ l〃)
(〃l _ l) 此れが目的なのではなくて、此れを元に改造する計画が有るの。
HTMLのtitleに"Sync"って付いてるのが其れかね (l _ l〃)
(〃l _ l) 黙れよ
demo
実装
Opera 16 (Chromium, V8), Firefox 23, IE 10 で目視で動作確認をした。
index.html
<!DOCTYPE html> <!-- lisence: Public Domain --> <meta charset="UTF-8"> <title>Sync Life game</title> <style> #game { position: relative; -webkit-transform-origin: 0 0; transform-origin: 0 0; -webkit-transform: scale(2, 2); transform: scale(2, 2); } </style> <div> <button id="spawn0.01">spawn 1%</button> <button id="spawn0.05">spawn 5%</button> <button id="spawn0.1">spawn 10%</button> <button id="spawn0.3">spawn 30%</button> <button id="spawn0.5">spawn 50%</button> </div> <div> <canvas id="game" width="200" height="200"></canvas> </div> <script src="lifegame.js"></script> <script> (function(global) { var view = new ViewLifegame; document.getElementById('spawn0.01'). addEventListener('click', function(evt) { view.cells.spawn(0.01); }); document.getElementById('spawn0.05'). addEventListener('click', function(evt) { view.cells.spawn(0.05); }); document.getElementById('spawn0.1'). addEventListener('click', function(evt) { view.cells.spawn(0.1); }); document.getElementById('spawn0.3'). addEventListener('click', function(evt) { view.cells.spawn(0.3); }); document.getElementById('spawn0.5'). addEventListener('click', function(evt) { view.cells.spawn(0.5); }); view.init(document.getElementById('game')).run(); document.getElementById('spawn0.1').click(); }(this.self || global)); </script>
lifegame.js
/** * @license Public Domain */ (function(global) { 'use strict'; if (! global.requestAnimationFrame) { if (global.mozRequestAnimationFrame) global.requestAnimationFrame = global.mozRequestAnimationFrame; } /** * @constructor */ function ViewLifegame() { /** @type {HTMLCanvasElement} */ this.canvas = null; /** @type {CanvasRenderingContext2D} */ this.context = null; /** @type {Cells} */ this.cells = null; } ViewLifegame.prototype = { /** * @param {HTMLCanvasElement} canvas * @return {ViewLifegame} */ init: function(canvas) { this.canvas = canvas; this.context = canvas.getContext('2d'); this.cells = new Cells(canvas.width, canvas.height); return this; }, /** * @return {ViewLifegame} */ run: function() { function doNext(tick) { this.cells.doNextAll(); this.draw(); requestAnimationFrame(doNext); } doNext = doNext.bind(this); this.draw(); requestAnimationFrame(doNext); return this; }, /** * @private * @return {ViewLifegame} */ draw: function() { this.context.fillStyle = 'black'; this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); this.context.fillStyle = 'rgb(255, 100, 200)'; this.cells.forEach(function(v, x, y) { if (v) this.context.fillRect(x, y, 1, 1); }, this); return this; } }; /** * @param {number} width * @param {number} height */ function Cells(width, height) { var x = 0, y = 0; /** @type {number} */ this.width = width; /** @type {number} */ this.height = height; /** @type {Array.<Array.<number>>} */ this.field = [ ]; for (x = 0; x < width; ++x) { this.field[x] = [ ]; for (y = 0; y < height; ++y) this.field[x][y] = 0; } } Cells.prototype = { /** * @param {number} ratio * @return {Cells} */ spawn: function(ratio) { var i = 0, iz = 0; for (i = 0, iz = Math.floor(this.width * this.height * ratio); i < iz; ++i) this.set(random(0, this.width), random(0, this.height), 1); return this; }, /** * @return {Cells} */ doNextAll: function() { var requests = [ ]; this.forEach(function(v, x, y) { var next = 0; next = this.next(x, y); if (this.at(x, y) !== next) requests.push([x, y, next]); }, this); requests.forEach(function(elm) { this.set(elm[0], elm[1], elm[2]); }, this); return this; }, /** * @param {function(this:Object,0|1,x:number,y:number,Cells):(boolean|undefined)} fn * @param {Object} obj * @return {Cells} */ forEach: function(fn, obj) { var x = 0, y = 0, width = this.width, height = this.height, result; if (obj === void 0) obj = this; loop: for (x = 0; x < width; ++x) { for (y = 0; y < height; ++y) { result = fn.call(obj, this.field[x][y], x, y, this); if (result === false) break loop; } } return this; }, /** * @private * @param {number} x * @param {number} y * @return {0|1|null} */ at: function(x, y) { var column; x = (x + this.width) % this.width; y = (y + this.height) % this.height; column = this.field[x]; if (! column) return null; return column[y]; }, /** * @private * @param {number} x * @param {number} y * @param {0|1} value * @return {Cells} */ set: function(x, y, value) { this.field[x][y] = value; return this; }, /** * This method has no effect, just show the next state of the place. * * @private * @param {number} x * @param {number} y * @return {0|1} */ next: function(x, y) { var adjacent_count = 0; adjacent_count = this.adjacent(x, y).reduce(function(sum, v) { return sum + v; }); if (this.at(x, y)) { switch (adjacent_count) { case 0: case 1: return 0; case 2: case 3: return 1; case 4: case 5: case 6: case 7: case 8: return 0; default: throw new Error('Adjacent count is ' + adjacent_count + '.'); } } else { if (adjacent_count === 3) return 1; return 0; } }, /** * @private * @param {number} x * @param {number} y * @return {Array.<0|1|null>} * [ 0, 1, 2, * 3, v, 4, * 5, 6, 7 ] */ adjacent: function(x, y) { return [ this.at(x - 1, y - 1), this.at(x, y - 1), this.at(x + 1, y - 1), this.at(x - 1, y), this.at(x + 1, y), this.at(x - 1, y + 1), this.at(x, y + 1), this.at(x + 1, y + 1) ]; } }; /** * @param {number} min * @param {number} max * @return {number} */ function random(min, max) { return Math.floor(Math.random() * (max - min)) + min; } global.ViewLifegame = ViewLifegame; }(this.self || global));