やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

pyxelでライフゲーム作った(85行)

 動かすたびに変化して面白い。

成果物

ライフゲームとは

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

  • ライフゲームは1つの矩形をセル(細胞)と呼ぶ
  • セルは1つの生命体を模したものである
  • セルには生/死の2状態がある
  • セルが生きているなら, 死んでいるならで表した

生死ルール

ルール 概要
誕生 死セルは隣接する生セルが3つあれば、次の世代が誕生する
生存 生セルは隣接する生セルが2〜3つあれば、次の世代でも生存する
過疎 生セルは隣接する生セルが1つ以下なら、過疎により死滅する
過密 生セルは隣接する生セルが4つ以上なら、過密により死滅する

 隣接は、上下左右に加えて斜めも含まれる。たとえば以下。に隣接するセルはである。

□□□□□□□
□□■■■□□
□□■★■□□
□□■■■□□
□□□□□□□

興味深い点

興味深い点

 「過疎でも過密でも個体の生存に適さない」という[個体群生態学][]的な側面を持つ。

 個体群生態学

 現実の人間においても、このような性質が見られる。現代社会や日本にある社会問題を表しているとも見れる。たとえば以下のような。

過疎

 仲間が少なすぎると死ぬ。人は独りでは生きられない。生物は群れをなして協力することで生きれる。

  • 地方が過疎化
    • 生活が成り立たなくなる
      • 働けない
        • 食べられない
          • 餓死
    • 経済社会で生き延びられない
      • 公共交通機関が減る
      • 企業がない
      • 商業施設がない
      • スーパーなど食品小売業がない
    • 高齢化
      • 若人がいなくなる
        • 子供が生まれなくなる
          • 老人ばかりになる
            • 労働力がなくなる
              • 生活が成り立たなくなる
                • 死ぬ
    • 働き手が足りない
      • 生きるために最低限必要な労働力が得られず絶滅する

 人が少なすぎたら死ぬ。

過密

 仲間は多いほうがいいかといえば、そうではない。過密すぎても死ぬ。息苦しさ、生きにくさ、搾取、強奪、戦争などが勃発する。「最後には誰も残らない」という、よくある終末。

  • 他者を蹴落として生き残る競争が激化する
    • 経済では同業者同士で価格競争になる
      • 独占寡占が生じる
        • ごく一部だけが生き残り、大多数が奪われ殺される
          • 疲弊、過疎化して死ぬ
    • 芸能人でキャラが被ると強者だけが生き残る
  • 感染症が爆発的に蔓延しパンデミックになる
    • 人口密度の多さが仇となり、絶滅不可避

 もう少し生活に密接した考え方をすると以下。

  • 都会で密集した場所に住んでいると……
    • 隣人トラブルが起こりやすい
      • 騒音による安眠妨害
      • 日照条件が悪化する
    • 満員電車で痴漢や痴漢冤罪が起こりやすい
    • 交通事故が頻発する
      • 狭いため、わずかな操作ミスで接触事故などが起こる
      • 学校でサッカーをしていたらボールが道に飛んでバイク運転手に当たり事故が起こって死ぬ
      • お祭り時、陸橋で人々がドミノ倒しになって圧死する
      • 建物の看板が落ちてきて圧死する
    • 地球環境の悪化
      • 都市熱により暑くなりクーラーをかける
        • クーラーにより体調を崩す
        • 地球温暖化が進む
          • 気候変動により異常気象が起きて自然災害になる
            • 死ぬ
            • 食料生産に支障をきたす
              • 餓死する
              • 奪い合いのバトルになって死ぬ
  • 多数の人が集まって組織を組むと……
    • 主義主張・価値観・都合の違いが生じ、
      • 人間関係のトラブルに発展し、
        • バトルになる
          • 死ぬ
        • 派閥ができて、
          • 権力闘争になる
            • 死ぬ

 効率の良さばかり追求すると、人の死まで効率的に起こってしまう。

 これらを解決するために、人が集中しすぎぬよう以下のような対策が講じられる。

  • 人を分散するために……
    • 隙間産業で活路を見出す
    • 多様な価値観を模索する
    • 人と距離を置く

 ある程度の規模になったら、細分化して人口分散しないと過密で死ぬ。昨今、「多様化」して多数のグループに人を分散する動きも「過密」の中で生存しようとした結果かも?

ほどほどが一番

 何事もほどほどが一番。バランス大事。

今の日本

 超少子高齢化

 日本は閉鎖社会。多様性を認めず、排他的な世界で生存してきた。そんな中、西洋文化による大量生産・大量消費になり、人工が爆発的に増加。やがて「過密」になり、競争が激化。価格競争で疲弊し、生存するべく独占寡占に走る。賄賂や汚職が横行。企業だけでなく政治や市民までもが二極化。鬱の蔓延。自殺数増大。国は貧困化。競争が激化して一強化。ごく一部だけが生存し、多数が苦境に立たされたり死ぬようになった。

 ライフゲーム的に言えば「過密」の状態。このままだと死滅すると思われる。特に高齢者が多数いるのが問題。彼らは労働力にもならず、自己保身に走り、変化を拒む。そんな老人たちが権力をもって集団を支配しているため、種の存続において害悪となっていると思われる。

 個が死を受け入れ、世代交代せねば、種として滅びる。自然の摂理。

 自分の死と向き合えない個体は、社会を崩壊させる因子である。もし人類の存続を願うなら、自己保存欲の塊である権力者を排除する必要がある。いつの時代も同じことを繰り返す。創造と破壊。

コード

#!/usr/bin/env python3
# coding: utf8
import numpy, random, pyxel

class App:
    def __init__(self):
        self.window = Window()
        self.world = World(self.window.Width, self.window.Height)
        pyxel.run(self.update, self.draw)
    def update(self):
        self.world.update()
    def draw(self):
        self.window.draw()
        self.world.draw()

class Window:
    def __init__(self):
        pyxel.init(self.Width, self.Height, border_width=self.BorderWidth, caption=self.Caption, fps=5)
    @property
    def Width(self): return 64
    @property
    def Height(self): return 48
    @property
    def Caption(self): return "Life game"
    @property
    def BorderWidth(self): return 0
    def update(self): pass
    def draw(self): pyxel.cls(0)

class World:
    def __init__(self, w=32, h=24):
        self.__w = w
        self.__h = h
        self.__colors = (0, 11)
        self.__init_cells()
    @property
    def Width(self): return self.__w
    @property
    def Height(self): return self.__h
    @property
    def Cells(self): return self.__cells
    @property
    def Colors(self): return self.__colors
    def __init_cells(self):
        self.__cells = numpy.array([(1 if random.randint(0, 10) == 0 else 0) for x in range(self.Width * self.Height)]).reshape(self.Height, self.Width)
    def update(self): self.proceed()
    def draw(self):
        pyxel.cls(0)
        for y in range(self.Height):
            for x in range(self.Width):
                pyxel.rect(x, y, 1, 1, self.Colors[1] if self.Cells[y][x] else self.Colors[0])
    def proceed(self):
        next_cells = numpy.zeros((self.Height, self.Width))
        for y in range(self.Height):
            for x in range(self.Width):
                cnt = self.__count_adjacent_cells(x, y)
                if self.__is_birth(x, y, cnt): next_cells[y][x] = 1
                elif self.__is_alive(x, y, cnt): next_cells[y][x] = 1
                elif self.__is_depopulation(x, y, cnt): next_cells[y][x] = 0
                elif self.__is_overcrowding(x, y, cnt): next_cells[y][x] = 0
                else: next_cells[y][x] = self.Cells[y][x]
        self.__cells = next_cells
    def __is_birth(self, x, y, adjs_alive_cells_num):
        if 0 == self.Cells[y][x] and 3 == adjs_alive_cells_num: return True
        else: return False
    def __is_alive(self, x, y, adjs_alive_cells_num):
        if 1 == self.Cells[y][x] and 2 <= adjs_alive_cells_num <= 3: return True
        else: return False
    def __is_depopulation(self, x, y, adjs_alive_cells_num): # 過疎
        if 1 == self.Cells[y][x] and adjs_alive_cells_num <= 1: return True
        else: return False
    def __is_overcrowding(self, x, y, adjs_alive_cells_num): # 過密
        if 1 == self.Cells[y][x] and 4 <= adjs_alive_cells_num: return True
        else: return False
    def __count_adjacent_cells(self, x, y):
        return numpy.count_nonzero(self.__get_adjacent_cells(x, y))
    def __get_adjacent_cells(self, x, y):
        adjs = []
        relatives = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1))
        for r in relatives:
            if -1 < x + r[1] < self.Width and -1 < y + r[0] < self.Height:
                adjs.append(self.Cells[y+r[0], x+r[1]])
        return adjs

App()

要点

セルの初期値

def __init_cells(self):
    self.__cells = numpy.array([(1 if random.randint(0, 10) == 0 else 0) for x in range(self.Width * self.Height)]).reshape(self.Height, self.Width)
  • セルを二次元配列で生成する
    • [y][x]の順である
  • 生存セルは1/10の確率で生じる(ランダム)
  • セルの値に生死をセットする
    • 0=死, 1=生

セルの生死ルール

 wikipediaのルールをそのままコード化した。

誕生

def __is_birth(self, x, y, adjs_alive_cells_num):
    if 0 == self.Cells[y][x] and 3 == adjs_alive_cells_num: return True
    else: return False

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

生存

def __is_alive(self, x, y, adjs_alive_cells_num):
    if 1 == self.Cells[y][x] and 2 <= adjs_alive_cells_num <= 3: return True
    else: return False

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

過疎

def __is_depopulation(self, x, y, adjs_alive_cells_num): # 過疎
    if 1 == self.Cells[y][x] and adjs_alive_cells_num <= 1: return True
    else: return False

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

過密

def __is_overcrowding(self, x, y, adjs_alive_cells_num): # 過密
    if 1 == self.Cells[y][x] and 4 <= adjs_alive_cells_num: return True
    else: return False

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

隣接セルの取得

def __get_adjacent_cells(self, x, y):
    adjs = []
    relatives = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1))
    for r in relatives:
        if -1 < x + r[1] < self.Width and -1 < y + r[0] < self.Height:
            adjs.append(self.Cells[y+r[0], x+r[1]])
    return adjs

 今回のキモ。一番苦労した。隣接セルは自セルの周囲3 * 3セルのうち自セルを省いたもの。

□□□□□□□
□□■■■□□
□□■★■□□
□□■■■□□
□□□□□□□

 自セルを中心(0, 0)としたとき、各隣接セルの相対的な座標は以下。

[(-1, -1), (-1, 0), (-1, 1),
 (0, -1), (0, 0), (0, 1),
 (1, -1), (1, 0), (1, 1)]

 自セル(0, 0)は隣接セルでないため対象外として省く。以下8つになる。

relatives = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1))

 隣接セルは必ず8つとは限らない。最大8つである。8つ以下の場合もある。具体的には、隣接セルのパターンは以下9パターンある。(が自セルのときが隣接セル)

★■□■★■□■★
■■□■■■□■■
□□□□□□□□□
■■□■■■□■■
★■□■★■□■★
■■□■■■□■■
□□□□□□□□□
■■□■■■□■■
★■□■★■□■★

 これをif条件式で制御した。

if -1 < x + r[1] < self.Width and -1 < y + r[0] < self.Height:

失敗した他の方法

失敗した他の方法

 最初はもっと簡単にできる気がしていた。隣接セル3 * 3は配列のスライスで取得できるはず。以下のように。

self.Cells[y+-1:y+1+1, x+-1:x+1+1]

 だが、自セルの排除ができない。上記9パターンのうち、自セルの配列位置が異なる。これを算出する方法がわからなかった。なのでスライスによる隣接セル取得はできなかった。

 考えてみよう。隣接セルは上記だと3 * 39個取得できる。このうち必ず自セルが1つ含まれる。これを排除するために、自セルのインデックス位置を特定したい。だが、パターンによって自セルのインデックス位置が代わってしまう。このインデックス値を算出する方法がわからない。

 たとえば左上隅なら以下。このとき自セルのインデックスは0番目である。

★■
■■

 たとえば左右中央で上端なら以下。このとき自セルのインデックスは1番目である。

■★■
■■■

 たとえば中央パターンなら以下。このとき自セルのインデックスは4番目である。

■■■
■★■
■■■

 計算する方法はあるのかもしれない。だが、パッと思いつかないし、書いたコードよりも複雑になるだろうから、やめた。

生存している隣接セル数の取得

def __count_adjacent_cells(self, x, y):
    return numpy.count_nonzero(self.__get_adjacent_cells(x, y))

 これはnumpyAPIcount_nonzero()を使えば一発。引数で渡されたnumpy配列のうち、値がゼロでない要素の数を返す。

 ポイントはnumpyAPIを使わねばならないということ。今回セルに使っている配列は、Python標準配列ではなく、numpyライブラリが生成する配列である。それらは別の型である。

 もしPython標準の配列を使っていたら、以下のようなコードで解決していただろう。

len([x for x self.__get_adjacent_cells(x, y) if 1 == x])

 だが、今回はnumpy配列を使っているため、エラーになる。count_nonzer()APIを使うことでしか解決できない。これはnumpyの仕様である。

注意点

  • ESCキーで終了する
    • 何度が押下する必要があるかもしれない
      • FPSが遅くて受け付けられないため

所感

ライフゲームは興味深い

ライフゲームについて

 ライフゲームは奥が深そう。生存に適した戦略を探すのは面白いかも。それを現実の社会問題と関連づけて分析・予想したり。妄想がはかどりそう。シムシティのようなシミュレーション好きにはたまらないだろう。

 たとえば二大派閥を作って趨勢をシミュレートするとか。よくある左翼と右翼のようなわかりやすい対立者をつくってその生存状況を再現できるかも? どっちのほうが生き残りやすいかとか分析したら面白いかも。

 でも、結局は初期値がすべて。なにもかもが「運」による。きっとこれが真理。宇宙における地球の配置もただの運。偶発的に生物が発生する条件が整っただけにすぎないのだろう。

 運ゲークソゲーといわれるが、リアルはクソゲーとも言われる。つまりリアルは運ゲーなのでは? 努力がムダとは言わないが、運によって未来が決まっているのも真理なのだろう。

 どう読み取るかにも違いが出てきて面白そう。画面上はただの矩形にすぎないし、2つの状態しかなく、4つの単純なルールしかないのだが、奥が深い。

 なぜこんなにも興味をそそるのか? それが「生死」だからだと思われる。人は創作物にやたらと「殺人事件」を求める。TVでは病気など健康にまつわる脅迫の内容で注目を集めようとする。

 人は「死」に敏感なのだろう。「どうすれば自分は生き残れるか」の答えを常に求めている。私たちは「生き残ろうとする」ルールに縛られたセルである。完全体セルに、私はなりたい。

コーディングは苦労した

コーディングについて

 何度も書き直して試行錯誤しまくった。

 最初からキレイに書こうとしてもムリだった。まずは最小コードを書いて動かすのがいい。規模が大きくなっていくので、その都度、関数化、クラス化、抽象化していく。

 作り直すと効率が悪いので最初からフレームワーク的に書きたいのだが、それだけの能力がなかった。そもそもアルゴリズム自体が難しいため、抽象化どころではない。

 現実をみて、身の程をわきまえると、「ググってコピペする」という結果になった。悔しい。だから必死で考えた。図を書いて日本語で書いてイメージして試行錯誤した。

 算数ができない私としては、妥当な結果だった。偶然うまくいったみたいな感覚が非常に気持ち悪い。もっとこう、最初から一発でバシっと決めた。こういうアルゴリズムを書きまくる必要がありそう。きっと筋トレと同じで日々の積み重ねが大事系。

参考

前回まで

対象環境

$ uname -a
Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux