ライフゲームを作るの1

ほとんどの人は、プログラミングを、誰かの書いたソースコードを入力して実行してみることから始めただろう。 いわゆる写経。

遥か遠い昔、俺もそんなところからプログラミングを始めた。 X68000 で、主に Oh!X からCやBASICのソースコードを拾ってきて。 今ならネットで検索してコピペだろうが、まだインターネットなんてなかった時代。 誌面のソースコードを手入力してた。

そんな中にライフゲームがあった。

シンプルなルールの繰り返しなのに、わずかな初期値の違いで、思いがけない変化を生み出すのがライフゲーム。 大きく広がって最終的に全滅とか、周期的な変動とか、周期的に変動しつつ位置も変えていくとか。

おっと、思いがけないってのは人の能力の問題か。 ライフゲームって実はチューリング完全で、頑張れば計算機も作れるし、その上で動くプログラムも作れたりする。 まあ作れるってだけで、全然実用的じゃ無いんだろうけどさ。 でもそこに偶然の入る余地は無い。

それはそれとして。 小さなライフゲームのソースコードを雑誌を見ながら入力して、コンパイルが通って、初めて動いた時は、結構な達成感と感動があったんだよな。 まあ達成感の方は、主に入力の大変さ由来なんだけどさ。 0とOの入力ミスとか、ねえ。

そんなこんなをしばらく前に思い出して、久し振りに作ってみたくなった。

なので作ってみる。 今度は、誰かの書いたものの丸写しではなく、自分で考えて。 とは言っても、ルールの実装だけなら、あんまり考えることもないだろう。

と、軽く考えて、まずはルールの確認。 以下、Wikipediaから引用。

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

セルの生死は次のルールに従う。

誕生

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

生存

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

過疎

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

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

これを実装した結果。 ChromeとFirefoxとSafariで動くはず。 現時点ではIEとEdgeは駄目。

盤面内のセルをクリックすると、セルの生死が反転する。 白が死んでる状態。 黒が生きている状態。

start ボタンをクリックすると、現状のセルの状態から世代を一つだけ進める。

clear ボタンをクリックすると、盤面の全てのセルを死んだ状態にする。 つまり真っ白になる。

盤面の端は、そこで切れるのではなく逆側と繋がるようにしている。 上下、左右、どちらも。

ソースコードは以下の通り。

まずはスタイル定義。

#board { border-collapse: collapse; cursor: crosshair; } #board td { width: 8pt; height: 8pt; font-size: 0; } #board td.dead { background: white; } #board td.alive { background: black; }

前半は割とどうでもよくて、大切なのは後半。 .dead が死んでるセルで .alive が生きてるセルのクラス。 違うのは背景色。 これを切り替えることで生死を表現する。

次にセルの置き場とボタンの定義。

<table id="board"></table> <input type="button" id="btnStart" value="start" /> <input type="button" id="btnClear" value="clear" />

セルは、 table の子要素 tr の更に子要素 td とする。 その置き場としてのテーブル。

canvasでもよかったのだが、セルをクリックして生死を設定するためのイベントの取り回しが楽なのでtableにしている。 セルの数が多くなると厳しいだろうが 40 * 40 ぐらいならtableでも余裕でいけるだろう。

中身はスクリプトで作るので、この時点では空。

そしてJavaScript。

addEventListener( "load", () => { const R = 20; const C = 20; const bd = document.getElementById( "board" ); bd.innerHTML = ( "<tr>" + ( "<td class='dead'></td>" ).repeat( C ) + "</tr>" ).repeat( R ); bd.addEventListener( "click", ( ev ) => { ev.target.className = ev.target.className === "alive" ? "dead" : "alive"; }, false ); const lg = new LifeGame( Array.from( bd.querySelectorAll( "td" ) ), R, C ); document.getElementById( "btnStart" ).addEventListener( "click", () => lg.update(), false ); document.getElementById( "btnClear" ).addEventListener( "click", () => lg.clear(), false ); }, false ); class LifeGame { constructor( cells, R, C ) { this.R = R; this.C = C; this.cells = cells.map( ( cell, idx, lst ) => ( { myCell : cell, around : this._getAroundCells( idx, lst ), } ) ); } _wrap( i, size ) { return ( i + size ) % size; } _toIdx( r, c ) { return this._wrap( r, this.R ) * this.C + this._wrap( c, this.C ); } _toRC( idx ) { return [ Math.floor( idx / this.C ), idx % this.C ]; } _getAroundCells( idx, lst ) { const [ ir, ic ] = this._toRC( idx ); const rng = [ -1, 0, 1 ]; return rng.map( r => rng.map( c => this._toIdx( ir + r, ic + c ) ) ).flat() .filter( i => i != idx ) // 自分を除外 .map( i => lst[i] ); } update() { this.cells.map( cell => ( { isAlive : cell.myCell.className === "alive", around : cell.around.filter( i => i.className === "alive" ).length, myCell : cell.myCell, } ) ).forEach( cell => { if ( cell.isAlive && ( cell.around <= 1 || 4 <= cell.around ) ) { cell.myCell.className = "dead"; } else if ( !cell.isAlive && cell.around === 3 ) { cell.myCell.className = "alive"; } } ); } clear() { this.cells.forEach( cell => cell.myCell.className = "dead" ); } }

処理は大きく二つ、 load イベントと LifeGame クラスの定義とに分けて書いている。

load イベント処理では以下を行う。

  1. 盤面となるテーブルを作る。

  2. テーブルのセルをクリックしたら生死が反転するようにイベントを設定する。

  3. LifeGame オブジェクトを生成する。

  4. start と clear の二つのボタンのクリックイベントを設定する。

LifeGame クラスは盤面の更新を行う。 ライフゲームの中核となる処理をここに実装しているので LifeGame という名前にしているが、この名前が最適なのか自分でも微妙な感じ。

各メソッドの処理は以下の通り。

constructor

盤面を構成するセルのリスト、列数、行数を受け取り、盤面の更新に都合のいい形に再構成したリストに変換しておく。 具体的には、以下を構成要素とするオブジェクトのリスト。

  • セル自身。

  • 隣接するセルのリスト。

update

コンストラクターで作ったリストの各要素から、現在の状況として以下を構成要素とするリストを作る。

  • 自身の生死。

  • 隣接する生きているセルの数。

これにライフゲームのルールを適用して生死を決定し、盤面に反映する。

clear

全てのセルを死んだ状態にする。

盤面が小さめなのも、startボタンで1世代しか進まないのも、テストを楽にするため。 まずはこの段階でテストして問題を潰し、それからもっと大きくして次世代への遷移も周期実行しようと考えていたのだが、大して難しいところも無かったためにあっさり動いてしまった。

まあ問題無く動くのはいいことなんだけどさ。 でもこの仕事を長くやっていると、一つもエラーが出ないと逆に不安になったりするんだよなぁ…。

あと、しばらく前にソースコードに色をつけるのを作ったので、それを使って色をつけてみようと思ったのだが、Firefoxが未だに正規表現のsフラグに対応していなくて駄目だった。 アップデートがリリースされていたので適用してみたのだが、それでも駄目。 Edgeももちろん駄目。 Chromeで使えるようになっていたからもういいような気もするが、とりあえず今回は採用見送り。

明日は使い勝手をちょっと改善する予定。

燕の巣

近所の橋の下に作られた燕の巣。 長屋状態。

蛇苺

たぶん蛇苺。 食えなくは無いが不味いらしい。