ライフゲームのすすめ

はじめに

この記事は「マイスター・ギルド:暑中見舞!夏のアドベントカレンダー2020」 14日目の記事です。

マイスター・ギルド 開発部のエンジニア 古川です。今回note初投稿になります。

さて今回、季節外れのAdvent Calendarというところですが、
昨今の情勢・逆境を有効活用できたらいいな、
という本企画の思い(?)を汲み取り、
普段仕事でしかコードを書かない方でも、
休日にプログラムを書くチャンスになるような記事にできたらと思います。

今回はHTML canvasを使ったライフゲームの実装について書いていきたいと思います。

ライフゲームとは

ライフゲームとは、「生命の誕生、進化、淘汰」 などのプロセスを簡単なモデルで再現したシミュレーションゲームです。

なお、ライフゲームは英語ではより正確には考案者の名前から取り、 Conway's Game of Lifeといいます。Game of Lifeだと人生ゲームの意味もあります。

今回、ライフゲームについての記事であり、これに触れないわけにはいかないのですが、考案者の John Horton Conway先生は昨今の新型ウィルスによって亡くなられております。
ご冥福をお祈りするとともに、この記事でJohn Horton Conway先生の功績を広めることになればと思っております。

ライフゲームについての理論的詳細は割愛しますが、
人工生命や計算理論の領域で昔から注目されてきたモデルの1つであり、
このモデルから生成される様々なパターンは見ているだけでも面白いです。そのため、研究者に限らない多くの人の人気を集めています。

画像3

画像はWikipediaより引用。パブリックドメイン)

通常のGUIのグラフィックコンポーネント上だけではなく、Minecraftのゲーム内・関数電卓など様々な環境の上で実装が試されており、動かすこと自体を目的に楽しまれる面もあります。
(中にはライフゲーム上で動くライフゲームなんてのもあります!)

面白いパターンについて知りたい方はこちらの方の記事がすごく良かったので、
あなたの知らないライフゲームの宇宙 《前編》
をぜひ見てください。

今回はこのライフゲームをHTML canvasで実装してみたいと思います。

実装

ライフゲームは2次元のマス目を決められたルールで計算し、次の時刻の状態をもとめていくモデルになっています。
各セルの状態には 「生」と「死」の2つの状態があります。

以下のルールを各セルに適用すると、次の時刻の各セルの状態がもとまります。

画像2

・誕生:「死」セルは周囲の「生」セルがちょうど3個→次の時刻では「生」
・生存:「生」セルは周囲の「生」セルが2個か3個→次の時刻では「生」
・過疎:「生」セルは周囲の「生」セルが1個以下→次の時刻では「死」
・過密:「生」セルは周囲の「生」セルが4個以上→次の時刻では「死」

このルールを実装したコード全体を以下に示します。
※class構文を使用しているためIEでは動きません。

<!DOCTYPE html>
<html>
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
   <title>lifegame</title>
   <script type="text/javascript" src="lifegame.js"></script>
 </head>
 <body>
   <canvas
     width="300"
     height="300"
     style="border: 1px solid;"
   ></canvas
   >
 </body>
</html>
class LifeGame {
   constructor(canvas) {
       this.canvas = canvas;
       this.fieldWidth = canvas.width / 4;
       this.fieldHeight = canvas.height / 4;
       this.field = new Array(this.fieldWidth * this.fieldHeight);
   }

   initField() {
       for (let y = 0; y < this.fieldHeight; ++y) {
           for (let x = 0; x < this.fieldWidth; ++x) {
               this.field[y * this.fieldWidth + x] = Math.random() > 0.7 ? 1 : 0;
           }
       }
   }

   draw() {
       const ctx = canvas.getContext("2d");
       ctx.fillStyle = "#00ff00"
       ctx.clearRect(0, 0, canvas.width, canvas.height);
       for (let y = 0; y < this.fieldHeight; ++y) {
           for (let x = 0; x < this.fieldWidth; ++x) {
               if (this.field[y * this.fieldWidth + x]) {
                   ctx.fillRect(4 * x, 4 * y, 4, 4);
               }
           }
       }
   }

   loop() {
       this.nextStep()
       this.draw()

       const self = this
       setTimeout(function () {
           self.loop()
       }, 100)
   }

   nextStep() {
       // 周囲のセル数の合計値を持つ配列
       const sumField = new Array((this.fieldWidth + 2) * (this.fieldHeight + 2));
       for (let y = 0; y < this.fieldHeight + 2; ++y) {
           for (let x = 0; x < this.fieldWidth + 2; ++x) {
               sumField[y * this.fieldWidth + x] = 0
           }
       }

       // 周囲のセル数の合計値を加算
       for (let y = 0; y < this.fieldHeight; ++y) {
           for (let x = 0; x < this.fieldWidth; ++x) {
               for (let dy = 0; dy < 3; ++dy) {
                   for (let dx = 0; dx < 3; ++dx) {
                       // 自分自身は足さない
                       if (dx == 1 && dy == 1) {
                           continue
                       }

                       sumField[(y + dy) * this.fieldWidth + x + dx] += this.field[
                           y * this.fieldWidth + x
                       ];
                   }
               }
           }
       }

       const nextField = new Array(this.fieldWidth * this.fieldHeight);
       for (let y = 0; y < this.fieldHeight; ++y) {
           for (let x = 0; x < this.fieldWidth; ++x) {
               const livingCells = sumField[(y + 1) * this.fieldWidth + x + 1];
               // ルールに従い、次の時刻の生死をもとめる
               if (livingCells <= 1) {
                   nextField[y * this.fieldWidth + x] = 0;
               } else if (livingCells == 2) {
                   nextField[y * this.fieldWidth + x] = this.field[
                       y * this.fieldWidth + x
                   ];
               } else if (livingCells == 3) {
                   nextField[y * this.fieldWidth + x] = 1;
               } else if (livingCells >= 4) {
                   nextField[y * this.fieldWidth + x] = 0;
               }
           }
       }
       this.field = nextField;
   }
}

window.onload = function () {
   canvas = document.querySelector("canvas");
   const life = new LifeGame(canvas);
   life.initField();
   console.log(life.field)
   life.loop();
};

今回、初期値については特定のパターンで初期化することは考えず、乱数での初期化としています。

しきい値0.7なので、初期状態では「生」のセルが30%程度、「死」のセルが70%程度になります。

また、実装した生存しているセルのカウント処理は、
注目しているセルが生存セルだったら周りのセルに生きている個数を足し算するアルゴリズムとしました。

こんな感じで動きます。

画像1


そのほか応用等

今回はHTML canvasを使って実装しましたが、実装方法は自由です。

プログラミングの実装チュートリアルとしても良いかもしれません。

実装した人によって言語・ライブラリ・アルゴリズムなどに違いが出てくるので、そこも面白いと思います。

生成されるパターンを楽しんでみるのもよし、
マウスでセルを入力できるように操作性を調整するのもよし、
HashLifeのような高速化アルゴリズムを実装するもよしです。

なお、純粋にライフゲームを研究したいならGollyが優秀です。

最後に

今回はHTML canvasを使用し、ライフゲームの実装を行いました。

みなさんもぜひ、自分の好きな言語での実装や、ライフゲームのディープな世界の探求にトライしてみてはいかがでしょうか。

この記事が気に入ったらサポートをしてみませんか?