Python (NumPy) でLifeGame を作る
Python (NumPy) でLifeGame を作る
NumPyの使い方わかってきたし、せっかくなのでLifeGame を作ってみます。
環境
LifeGameって何?
wikipedia からの引用
ライフゲーム (Conway's Game of Life[1]) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。 単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。
動画を見たほうが早いすね。
実際に以下のサイトで試せる(jsで書かれている)。
パターンや詳細については、こちらがよく書かれている。
ライフゲームのルールと実装
ルール解説しながら実装していく。
セル
ライフゲームの1つのマスはセル(細胞)と呼ぶ。 セルには「生」と「死」の2つの状態があります。 「生」の状態は「1(黒)」。「死」の状態は「0(白)」などと決めておく。
なので、 セル配列として、NumPy配列 を使うのがよさそうです。 numpy には零配列を作る関数があるので、それで初期の(どのセルも生きていない)セル配列は作れそうです。
また、今回は簡単化のために行数、列数が同じセル配列にしておきます。 コードに落とすと以下のようになる。
import numpy as np
CELLS_WIDTH = 8
cells = np.zeros((CELLS_WIDTH, CELLS_WIDTH), 'int')
cells
[[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]]
8×8の0行列ができる。
ルール1:生存
自分(セル)が生きていて、周囲8マス以内のセルが2つか3つ生きていれば、そのセルは「生存」となる。
自分の周りに、ほどよく他のセルがいれば、生き残るという考え方のよう。
今回は状態として、「2」とする。
実際にコードに落としていく。 まず、セルが生きているかをチェックする関数をつくる。 セル配列のindexをもらって, 0 かそれ以外かを見ればいい。
# 条件:セルが生きている
def isAlive(cell_idx, cells):
if cells[cell_idx] == 0:
return False
return True
セルの周りに、どれくらいの数のセルが生きているかをカウントする関数も必要。
-
周囲8マスになるようにindex範囲を決定
-
そのindex範囲でスライス
-
スライスした配列を
numpy.count_nonzero()
でカウントする。
セル配列は、2次元なので、indexは、長さ2のタプルを想定している。 また、はみ出した場合は、indexの始め(0) または最後を使うようにする。
def count_alive_surrounding(cell_idx, cells):
'''
note:自分のセル分も含まれる
'''
range_idx = [(idx - 1 if idx - 1 >= 0 else 0, idx + 2 if idx + 2 <= CELLS_WIDTH else CELLS_WIDTH) for idx in cell_idx]
return np.count_nonzero(cells[range_idx[0][0]:range_idx[0][1], range_idx[1][0]:range_idx[1][1]])
次に実際の「生存」の条件のルールをコード化。 自分の周りに生きているセルの数(2か3)を変更できるように、 関数ファクトリにしておく。
def rule_alive_condition_setting(min_cell_num:int, max_cell_num:int):
def rule_alive_condition(cell_idx, cells_in):
if isAlive(cell_idx, cells_in) and (min_cell_num <= count_alive_surrounding(cell_idx, cells_in) - 1 <= max_cell_num):
return True
return False
return rule_alive_condition
生存条件が適用されたときの処理を書く。 生存なので、状態「2」に書き換える。
def rule_alive_apply(cell_idx, cells_out):
# そのcellはそのまま生存 なにもしない
cells_out[cell_idx] = 2
ルール2:誕生
生きてないセルで、周囲8マス以内にちょうど3セルが生存ならば、 そのセルに「誕生」する。
今回は「生存」と分けたいの状態として、「1」とする。
「誕生」の条件のルールをコード化。 「生存」のとき同様、自分の周りに生きているセルの数を変更できるように、 関数ファクトリにしておく。 自分のセルが生きているか、 周囲8マス以内の生存数については、ルール1で作った関数が使える。
def rule_birth_condition_setting(birth_required_cell_num:int):
def rule_birth_condition(cell_idx, cells_in):
# 周囲8マス以内に他のセルがbirth_required_cell_numだけ生きていれば
if not isAlive(cell_idx, cells_in) and (count_alive_surrounding(cell_idx, cells_in) == birth_required_cell_num):
return True
return False
return rule_birth_condition
「誕生」ルールが適用されたときの処理を書く。 今回は状態「1」に書き換える。
def rule_birth_apply(cell_idx, cells_out):
## そのcellに誕生
cells_out[cell_idx] = 1
ルール3:死亡
ルール1、ルール2以外であれば、そのセルは、「死亡」とする。
以下のように死亡にも、複数の状態があるが、 今回は「生存、誕生」以外は、すべてまとめて「0」 とする。
-
周りのセルが多すぎた場合は、「過密」による「死亡」
-
周りのセルが少なすぎた場合は、「過疎」による「死亡」
上記2つの条件が当てはまらなかったら、常に適用するので、 True を返しておく。引数は、ほかの条件と同じようにループで回せるようにダミーにしておく。
def default_condition(cell_idx, cells_in):
return True
「死亡」ルールが適用された場合は、 セルの状態は「0」 に書き換える。
def rule_death_apply(cell_idx, cells_out):
cells_out[cell_idx] = 0
ルールをまとめる
今までに作ったルールとその処理、それぞれ3つをタプルでまとめる。
rules = (
(rule_alive_condition_setting(2, 3), rule_alive_apply), # alive
(rule_birth_condition_setting(3), rule_birth_apply), # birth
(default_condition, rule_death_apply) # death
)
これで、for文で上から順に条件をチェックしていく。 最後の「死亡」条件はTrueなので必ずどれかが適用される。
処理の実行
実際に処理を実行するを書いてく。
初期のセル配列を入力引数とする。 ジェネレータで実行し、 返り値は、何回目の実行かとルール適用後のcell配列にする。
今回は可視化は行わず、セル配列を直接表示する。
def play_gen(cells):
def count(i):
x = [i]
def count_up():
x[0] += 1
return x[0]
return count_up
countup = count(0)
cells_in = np.copy(cells)
while True:
# copy先のcell
cells_out = np.copy(cells_in)
# index の取得
for idx in np.ndindex(cells.shape):
for condition, apply in rules:
if condition(idx, cells_in):
apply(idx, cells_out)
break
yield countup(), cells_out
cells_in = np.copy(cells_out)
実際にルール適用の処理を書いているのは、 Whileループ後。
-
セル配列の出力先を作成
-
セル配列のindex取得
-
ルールタプルから各ルール条件と適用処理を抽出
-
条件に当てはまれば適用 を繰り返す。
-
すべてのセルに適用したら、カウントとセル配列をyeild で返す。
実行例
実行例示す。 可視化してないので見づらい。。
初期のセル配列をつくる。
CELLS_WIDTH = 8
cells = np.zeros((CELLS_WIDTH, CELLS_WIDTH), 'int')
cells[:3, :3] = 1
print(cells)
[[1 1 1 0 0 0 0 0]
[1 1 1 0 0 0 0 0]
[1 1 1 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]]
あとはgenerater を生成して、nextで回す。
gen = play_gen(cells)
next(gen) #1回目
(1, array([
[2, 0, 2, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[2, 0, 2, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]))
gen = play_gen(cells)
next(gen) #1回目
(1, array([
[2, 0, 2, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[2, 0, 2, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]))
next(gen) # 2回目
(2, array([
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 0, 0, 0, 0],
[0, 1, 2, 0, 0, 0, 0, 0],
[0, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]))
next(gen) # 2回目
(2, array([
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 0, 0, 0, 0],
[0, 1, 2, 0, 0, 0, 0, 0],
[0, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]))
next(gen) # 3回目
(3, array([
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 2, 2, 0, 0, 0, 0],
[0, 2, 0, 1, 0, 0, 0, 0],
[0, 2, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]))
コード
まとめたコードを載せておく。