見出し画像

ライフゲームの状態数を増やしてみた

注意:この記事には激しい点滅を含むGIF動画が複数使用されています。

どうも、108Hassiumです。

みなさんは「ライフゲーム」というものをご存知でしょうか。

ライフゲームはめちゃくちゃ単純化された人工生命シミュレーターで、正方形のマス目(セル)1つ1つが1匹の生物を表します。

セルには「生きたセル」と「死んだセル」の2種類があり、以下の規則によって変化します。

  • 死んだセルが3つの生きたセルに接していたら、生きたセルに変わる

  • 生きたセルに接している生きたセルの数が2つでも3つでもないなら、死んだセルになる(2つか3つ接していたら、変化しない)

☝動作例

※この記事では死んだセルを黒で表示します。

たったこれだけのルールにより、様々なパターンが生じ予測不能な動きを見せます。

よく見られるパターンとして、以下のようなものがあります。

  • 固定物体:世代が進んでも配置が変化しない集団

  • 振動物体:一定世代ごとに同じ場所で同じ形に戻る集団

  • 移動物体:移動しながら同じ形状を繰り返す集団

※wikipediaに載っている名前とは若干違いますが、この記事では私が普段使っている名前を使用します。

☝移動物体、固定物体、振動物体の例

さて、この記事ではライフゲームに生きたセルでも死んだセルでもない新しい状態のセルを追加したルールをいくつか紹介します。(前例の有無は基本的に確認していません)

なお、本来のライフゲームは生物の生死を模して造られたものですが、私自身は「四角形が動いて面白いやつ」程度にしか思っていません。

☝ライフゲームについて考えている私

なので、この記事で紹介するルールは自然の摂理を一切考慮せず、ただ見ていて面白いかどうかだけを考えて作られています。

Double Life

まずは、生きているセルを2種類にしたものを作ってみました。

2種類のセルにはそれぞれ1と-1という値が割り振られ(死んだセルは0)、あるセルの変化の仕方はそのセルの周りにあるセルの番号の総和$${m}$$によって以下のように決まります。

  • 0のセルは$${m=3}$$なら1に、$${m=-3}$$なら-1になる。

  • 1のセルは$${m=2}$$でも$${m=3}$$でもなければ0になる。

  • -1のセルは$${m=-2}$$でも$${m=-3}$$でもなければ0になる。

2つの生きたセルは誕生条件の計算で互いに効力を打ち消し合うので、元のライフゲームではありえない固定物体がよく出現します。

☝Double Lifeの固定物体4個

また、生きたセルの増殖を互いに邪魔し合うという性質により、ブロック(2×2の正方形の固定物体)にグライダー(斜めに動く小さい移動物体)が衝突してもブロックだけが無傷で残るのことがあるのも面白いです。

☝グライダーとブロックの衝突

あえて人工生命体シミュレーターとして例えるなら、敵対する2種類の生物が存在するルール、といったとこでしょうか。

Multi Life

Double Lifeは4状態以上に拡張しようとしても上手くいかないので、いくらでも拡張できるルールを考えました。

このルールではセルの番号は0、1、2という値が割り振られ、Double Lifeと同様に番号の総和$${m}$$で動作が決まります。

  • 0のセルは$${m=3}$$なら1に、$${m=6}$$なら2に変化。

  • 1のセルは$${m=2}$$でも$${m=3}$$でもなければ0に変化。

  • 2のセルは$${m=4}$$でも$${m=6}$$でもなければ0に変化。

Double Lifeの1と-1のセルは互いに等価(=1と-1のセルを入れ替えても同じ動作をする)でしたが、Multi Lifeは等価ではありません。

☝2番セルと1番セルが違う動きをする例

どうやら2番セルは普通のライフゲームと同じ動きをして、1番のセルは周りに2番のセルが出現することがあるので違う動きになるようです。

また、このルールでは以下のような振動物体が存在します。

☝Multi Lifeの振動物体(32周期)

500×500くらいのランダムな初期状態だと20~30回に1回くらいの確率で出現します。

さて、最初に言いましたがこのルールは4状態以上に拡張することを念頭に置いて設計(?)してあります。

例えば3番のセルを追加して4状態に拡張する場合、以下のようなルールになります。

  • 0のセルは$${m=3}$$なら1に、$${m=6}$$なら2、$${m=9}$$なら3に変化。

  • 1のセルは$${m=2}$$でも$${m=3}$$でもなければ0に変化。

  • 2のセルは$${m=4}$$でも$${m=6}$$でもなければ0に変化。

  • 3のセルは$${m=6}$$でも$${m=9}$$でもなければ0に変化。

一般に、$${0~n}$$番までのセルを持つ$${n+1}$$状態のMulti Lifeルールは以下のように定義できます。

  • 0のセルは$${m=3k}$$なら$${k}$$に変化。

  • $${k(>0)}$$のセルは$${m=2k}$$でも$${m=3k}$$でもなければ0に変化。

$${n=3}$$のときは、以下のようになります。

3-Multi Life

3番のセル(青)はすぐに数が減っていき、ほぼ1番のセルだけが生き残りました。

これは、0番のセルが3番のセル1個だけと接していると1番に変化し、3番の生存・繁殖を阻害するためだと思われます。

$${n>3}$$の場合も、同様に番号の小さいセルが生き残りやすい傾向が見られました。

ところでこれはライフゲームと関係ない余談なのですが、3状態Multi Lifeを少しだけ改変した以下のルールは全然違う動きをすることがあって面白いです。

  • 0のセルは$${m=3}$$なら1に、$${\underline{m=5}}$$なら2に変化。

  • 1のセルは$${m=2}$$でも$${m=3}$$でもなければ0に変化。

  • 2のセルは$${m=4}$$でも$${m=6}$$でもなければ0に変化。

Slow Life

このルールは、以下のページに載っていたものです。(初出は不明です)

このルールには生きたセルと死んだセルの中間、つまり「生まれたてのセル」と「死にかけのセル」が存在します。

正確なルールは不明ですが、以下のルールで再現できそうです。

  • 生きているセルも死んでいるセルも、生まれたて・死にかけのセルと1つでも接していたら変化しない。

  • 死んだセルが3つの生きたセルと接していたら、生まれたてのセルに変化する

  • 生きたセルに接している生きたセルが2個でも3個でもなければ、死にかけのセルに変化する。

  • 生まれたてのセルは生きたセルに変化する。

  • 死にかけのセルは死んだセルに変化する。

このルールでは、生きたセルと死んだセルのみからなる初期状態では2世代ごとにライフゲームと同じ動きをします。

しかし、生まれたて・死にかけのセルが混ざった初期状態だと普通のライフゲームでは起きないような現象が起き、先程のリンク先ではそのようなパターンがいくつか紹介されています。(自力では何も見つけられませんでした)

☝ライフゲームには存在しない振動物体

さて、このルールも状態数を増やして遊ぶこともできるのですが、あまり面白くないので他のアレンジを紹介します。

まずはこちらのルールをご覧ください。

  • 死んだセルに接している生きたセルの個数と死にかけのセルの個数の和が3なら生まれたてのセルに変化する。

  • 生きたセルに接している生きセルと死にかけのセルの個数の和が2でも3でもなければ死にかけのセルに変化する。

  • 死にかけのセルは死んだセルに変化する。

  • 生まれたてのセルは生きたセルに変化する。

このルールはSlow Lifeをうろ覚えで再現しようとしてできたもので、2世代ごとにライフゲームと同じ動きをするという仕様は(多分)再現できています。

しかし、死にかけ・生まれたてのセルが混ざり合った時の挙動は異なり、不安定なまま爆発的に増殖していきます。

☝爆発の様子(5倍速)

次に、以下のルールを紹介します。

  • 死んだセルに接している生きたセルの個数が3個なら生まれたてのセルに変化する。

  • 生きたセルに接している生きたセルの個数が2個か3個なら生まれたてのセルに、そうでなければ死んだセルに変化する。

  • 生まれたてのセルは生きたセルに変化する。

死にかけのセルが出てきませんが、このルールでも「2世代ごとにライフゲームと同じ動きをする」という性質が再現できます。

このルールの生きたセルと生まれたてのセルは互いの生存・死亡に直接干渉できない(誕生の阻害はできる)ので、固定物体が不安定なセルの成長を止めたり2つくっついた状態で安定したりします。

Flare Life

赤いセルがライフゲームと同じ動きをして、その周りに緑のセルが炎のように纏わりついています。

  • 0番のセルに接している1番のセルの個数が3個なら1番のセルに、そうでなければ接している2番のセルの個数が3個なら2番のセルに変化。

  • 1番のセルに接している1番のセルの個数が2個でも3個でもなければ2番のセルに変化。

  • 2番のセルに接している1番のセルの個数が3個なら1番に、そうでなければ接している2番のセルの個数が2個でも3個でもなければ0番のセルに変化。

正確な書き方をするとややこしそうに見えますが、大まかなコンセプトは以下の通りです。

  • 1番のセルは常にライフゲームと同じ動きをし、1番のセルが誕生条件を満たせば2番が生まれる位置だろうが既に2番が居ようが必ず1番のセルが生まれる。

  • 1番のセルが死ぬと2番のセルになり、2番のセルは1番の邪魔にならない限りライフゲームと同じ動きをする

このルールではいろいろな振動物体が自然発生します。

☝2周期
☝6周期


☝10周期
☝12周期
☝22周期

さて、このルールも4状態以上に拡張可能です。

$${n}$$状態Flare Lifeのルールは大体以下の通りです。

  • $${k}$$番($${0≤k≤n-1}$$)のセルがより小さい特定の番号のセル丁度3個と接している場合、丁度3個接しているセルのうち最も番号が小さい(0番を除く)セルに変化する。

  • $${m}$$番目$${1≤m≤n-2}$$)のセルに3個接している番号の小さい(0を除く)セルが無く、なおかつ接している$${m}$$番目のセルの個数が2個でも3個でもなければ$${m+1}$$番のセルに変化する。

  • $${n-1}$$番のセルに接している$${n-1}$$番のセルの個数が2個でも3個でもなければ、0番のセルに変化する。

※うまく言語化できないので、詳しく知りたい人は記事の最後に乗せてあるソースコードを読んでください。

Pulse Life

最後に、この記事の趣旨とは若干ズレますが私が個人的に気に入っているルールを一つ紹介します。

※定義は複雑すぎて説明不能なので、気になる人は記事の最後に乗せてあるソースコードを読んでください。

このルールでは、生きたセルを1セルずつ間をあけて並べることで2世代ごとにライフゲームと同じ動きをします。

☝例:グライダーの動作

これだけだとライフゲームを模倣しただけですが、縦横1セルずつの間隔に対してズレたセルがあるとライフゲームとの対応関係が崩れます。

☝ライフゲームで対応する物体が存在しない振動物体

また、時間軸上のズレ(SLow Life全部のセルが混ざり合ったような状態)まで考慮すると、合計で8種類の異なる位相のライフゲームのセルが同時に混在できることになります。

☝位相が異なる8個のグライダー

なお、異なる位相のセルが混ざると以下のような状態になることがあります。

※「なることがある」といっても完全にランダムな初期配置だとほぼ100%発生します。グライダーの衝突などで人為的に発生させようとするとなかなか起きません。

Multi Lifeの項目の最後に紹介したやつと似たような自己複製物体が出現し、爆発的に増殖して空間を埋め尽くしてしまいます。

最後に

定義が説明できなかったもののソースコードを置いておきます。(言語はProcessingです)

4状態Flare life

int cell[][]=new int[500][500];
int pcell[][]=new int[500][500];
int n,i,j,k;

void setup(){
  size(1000,1000);
  background(0);
  noStroke();
  for(int a=0;a<500;a++){
    for(int b=0;b<500;b++){
      cell[a][b]=floor(random(4));
      pcell[a][b]=cell[a][b];
      if(cell[a][b]==1){
        fill(255,0,0);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==2){
        fill(0,255,0);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==3){
        fill(0,0,255);
        rect(a*2,b*2,2,2);
      }
    }
  }
}

void draw(){
  for(int a=0;a<500;a++){
    for(int b=0;b<500;b++){
      if(pcell[a][b]!=cell[a][b]){
        pcell[a][b]=cell[a][b];
        if(cell[a][b]==0){
          fill(0);
        }else if(cell[a][b]==1){
          fill(255,0,0);
        }else if(cell[a][b]==2){
          fill(0,255,0);
        }else if(cell[a][b]==3){
          fill(0,0,255);
        }
        rect(a*2,b*2,2,2);
      }
    }
  }
  for(int a=0;a<500;a++){
    for(int b=0;b<500;b++){
      i=neighbor(a,b,1);
      j=neighbor(a,b,2);
      k=neighbor(a,b,3);
      if(pcell[a][b]==0){
        if(i==3){
          cell[a][b]=1;
        }else if(j==3){
          cell[a][b]=2;
        }else if(k==3){
          cell[a][b]=3;
        }
      }else if(pcell[a][b]==1){
        if(!(i==2||i==3)){
          cell[a][b]=2;
        }
      }else if(pcell[a][b]==2){
        if(i==3){
          cell[a][b]=1;
        }else if(!(j==2||j==3)){
          cell[a][b]=3;
        }
      }else{
        if(i==3){
          cell[a][b]=1;
        }else if(j==3){
          cell[a][b]=2;
        }else if(!(k==2||k==3)){
          cell[a][b]=0;
        }
      }
    }
  }
}

int neighbor(int x,int y,int s){
  n=0;
  for(int a=-1;a<2;a++){
    for(int b=-1;b<2;b++){
      if(pcell[(x+a+500)%500][(y+b+500)%500]==s&&!(a==0&&b==0)){
        n++;
      }
    }
  }
  return n;
}

Pulse life

int cell[][]=new int[500][500];
int pcell[][]=new int[500][500];
int n,m;

void setup(){
  size(1000,1000);
  background(0);
  noStroke();
  for(int a=0;a<500;a++){
    for(int b=0;b<500;b++){
      cell[a][b]=floor(random(8));
      pcell[a][b]=cell[a][b];
      if(cell[a][b]==1){
        fill(255);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==2){
        fill(0,255,0);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==3){
        fill(0,0,255);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==4){
        fill(255,255,0);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==5){
        fill(255,170,0);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==6){
        fill(255,85,0);
        rect(a*2,b*2,2,2);
      }else if(cell[a][b]==7){
        fill(255,0,0);
        rect(a*2,b*2,2,2);
      }
    }
  }
}

void draw(){
  for(int a=0;a<500;a++){
    for(int b=0;b<500;b++){
      if(pcell[a][b]!=cell[a][b]){
        pcell[a][b]=cell[a][b];
        if(cell[a][b]==0){
          fill(0);
        }else if(cell[a][b]==1){
          fill(255);
        }else if(cell[a][b]==2){
          fill(0,255,0);
        }else if(cell[a][b]==3){
          fill(0,0,255);
        }else if(cell[a][b]==4){
          fill(255,255,0);
        }else if(cell[a][b]==5){
          fill(255,170,0);
        }else if(cell[a][b]==6){
          fill(255,85,0);
        }else if(cell[a][b]==7){
          fill(255,0,0);
        }
        rect(a*2,b*2,2,2);
      }
    }
  }
  for(int a=0;a<500;a++){
    for(int b=0;b<500;b++){
      m=neighbor(a,b,4)+2*neighbor(a,b,5)+3*neighbor(a,b,6)+4*neighbor(a,b,7)-(neighbor(a,b,2)+2*neighbor(a,b,3));
      if(pcell[a][b]==0){
        if(0<cneighbor(a,b,1)){
          cell[a][b]=cneighbor(a,b,1)+3;
        }else if(eneighbor(a,b,1)==1||eneighbor(a,b,1)==2){
          cell[a][b]=eneighbor(a,b,1)+1;
        }else if(m==3){
          cell[a][b]=1;
        }
      }else if(pcell[a][b]==1){
        if(neighbor(a,b,0)!=8&&!(m==2||m==3)){
          cell[a][b]=0;
        }
      }else{
        cell[a][b]=0;
      }
    }
  }
}

int neighbor(int x,int y,int s){
  n=0;
  for(int a=-1;a<2;a++){
    for(int b=-1;b<2;b++){
      if(pcell[(x+a+500)%500][(y+b+500)%500]==s&&!(a==0&&b==0)){
        n++;
      }
    }
  }
  return n;
}

int cneighbor(int x,int y,int s){
  n=0;
  for(int a=0;a<2;a++){
    for(int b=0;b<2;b++){
      if(pcell[(x+a*2+499)%500][(y+b*2+499)%500]==s){
        n++;
      }
    }
  }
  return n;
}

int eneighbor(int x,int y,int s){
  n=0;
  for(int a=0;a<2;a++){
    for(int b=0;b<2;b++){
      if(pcell[(x+a-b+500)%500][(y+b+a+499)%500]==s){
        n++;
      }
    }
  }
  return n;
}