前から気になっていた、ライフゲームをpythonで実装してみた。
参考 : https://ja.wikipedia.org/wiki/ライフゲーム
細胞たちの存在する空間(縦何マス、横何マスかの2次元空間)をフィールドと呼ぶことにする。
フィールド(field)はTrue, Falseの2重配列で表現する。(True = 生, False = 死)
周囲の細胞たちの情報
ある細胞が次にどうなるかは、周りの8つの細胞の状態で決まるので、とりあえず周りの8つの細胞の状態を取得するジェネレータを書く。
def get_env(x, y, field):
"点(x,y)の周囲8つの状態を取得"
height = len(field) # フィールドの縦幅
width = len(field[0]) # フィールドの横幅
for i in [-1,0,1]:
for j in [-1,0,1]:
if (i,j)!=(0,0):
yield field[(x+i)%height][(y+j)%width]
ここで、局面の上端(x=0)は下端(x=-1)と隣接し、左端は右端と隣接していると考える。
(画面を右に進むと左端から登場するというイメージ。同様に下に潜ると、空から降ってくるみたいな。)
次の状態
周囲の情報を得たので、それを元に次の時間での状態を決定する。
def next_state(x, y, field):
"周囲と自分の状態から、次の状態を決定する"
env = sum(get_env(x,y,field))
return env == 3 or (field[x][y] and env == 2)
各細胞は、周囲に3つの生存細胞があると、その細胞の状態によらずに次は生存(または誕生)し、その細胞が現在生きている場合に限ると、周囲に2つの生存細胞がある場合でも、生存する。(それ以外は死亡)
注意点が2つ
1. Bool型は足し算できる(Trueが1, Falseが0にキャストされる)
2. 組み込み関数sumにはイテレータを入れて良い
フィールド全体を進める
各細胞の次の状態がわかったので、それに基づきフィールドを更新する
def tick(field):
"フィールドを1ステップ進める"
for x,row in enumerate(field):
yield [next_state(x,y,field) for y in range(len(row))]
このようにジェネレータを使うとスッキリかけるのだが、これだと使うときに毎回
list(tick(field))
としてリストにしておかないとバグにつながったりするので、ジェネレータをリストに変換するデコレータを作り、適用した。
def gen_to_func(gen):
"ジェネレータを、リストを返す関数に変換する"
def wrapper(*args, **kwargs):
return list(gen(*args, **kwargs))
return wrapper
@gen_to_func
def tick(field):
"フィールドを1ステップ進める"
for x,row in enumerate(field):
yield [next_state(x,y,field) for y in range(len(row))]
以上でロジックは実装できた。あとは表示するだけである。
フィールドの初期化と表示
フィールドをいちいち
field = [[False, False, False, True, False], [False, True, True, False, False], [False, True, False, False, False]]
のように書いて初期化するのは面倒なので、文字列で表現して作れるようにする。
同時に、逆にBool2重配列を文字列に表現する関数も作る。
@gen_to_func
def field_initiator(description):
"文字列でフィールドを初期化"
for row in description.strip().split('\n'):
yield [c=='*' for c in row]
def field_printer(field):
for row in field:
print(*('*' if c else '_' for c in row), sep='')
(イテレータもアスタリスクでアンパッキングできるというのは便利である。)
こうしておけば、
field = field_initiator("""
___*_
_**__
_*___""")
のようにフィールドを初期化できる。
コマンドラインから実行
以上をまとめてlife.pyというファイルにした。
def gen_to_func(gen):
"ジェネレータを、リストを返す関数に変換する"
def wrapper(*args, **kwargs):
return list(gen(*args, **kwargs))
return wrapper
def get_env(x, y, field):
"点(x,y)の周囲8つの状態を取得"
height = len(field) # フィールドの縦幅
width = len(field[0]) # フィールドの横幅
for i in [-1,0,1]:
for j in [-1,0,1]:
if (i,j)!=(0,0):
yield field[(x+i)%height][(y+j)%width]
def next_state(x, y, field):
"周囲と自分の状態から、次の状態を決定する"
env = sum(get_env(x,y,field))
return (field[x][y] and env == 2) or env == 3
@gen_to_func
def tick(field):
"フィールドを1ステップ進める"
for x,row in enumerate(field):
yield [next_state(x,y,field) for y in range(len(row))]
def field_printer(field):
for row in field:
print(*('*' if c else '_' for c in row), sep='')
@gen_to_func
def field_initiator(description):
"文字列化でフィールドを初期化"
for row in description.strip().split('\n'):
yield [c=='*' for c in row]
if __name__=='__main__':
import sys
field = field_initiator(sys.argv[1])
print('')
while True:
field_printer(field)
field = tick(field)
input() # 任意のキー入力(とenter)で次に進む。
コマンドラインから実行すると、最後のif __name__=='__main__':
いかが実行される。
コマンドライン引数に初期状態を入れると、その遷移を観れるようにした。
python -i life.py '
___*___
__*_*__
_*___*_
__*_*__
___*___'
とすると、エンターキーを押すことでステップを進めてパターンの遷移を観察できる。
(ちなみにこのパターンは4世代目で死滅してしまった)
あとがき
今日の昼、本屋で『Effective Python ―Pythonプログラムを改良する59項目』を立ち読みしていたら、コルーチンの説明としてライフゲームの実装が掲載されていた。コルーチンはイテレータに値を送り込めるものだと理解しているが、いまいち使い方が理解できていない。立ち読みで頑張って理解しようとしたが、そのライフゲームのコードは結局よく理解できなかった。そこでとりあえず自分だったらこうするというので実装してみたのが以上のコードである。
今度はコルーチンで実装してまた記事にしてみたい。
追記
用語の理解が誤っていたので訂正したい。ジェネレータはdef ... yield ....
を使って宣言され、イテレータを返す関数の一種かと思っていたが、正しくは関数ではなくイテレータの一種であった。
つまり、def ... yield ....
を使って宣言した関数そのものではなく、それをcallした時に返ってくるものである。(または(x*x for x in range(10))
のようなジェネレータ式が表すもの)
例えばgen_to_func
というデコレータについて、「ジェネレータを、リストを返す関数に変換する」と説明していたが、正しくは「ジェネレータを返す関数を、リストを返す関数に変換する」となる。