Pythonでライフゲーム【つくってみた】(端末上 & Tkinter)
今回は,数値シミュレーションではなくて,コンウェイのライフゲーム(Conway's Game of Life)を作ってみました.
息抜きのつもりで始めたものの,作ってみると意外とハマるところがあって,かなり試行錯誤しました.ですので,完成品もようやく動く形になった程度の出来です.仕様を理解していれば,もっとうまいものが作れると思いますが,そこまで読み込む気にはなれませんでした...
ライフゲームとは?
まず,ライフゲームについて軽く説明を.ライフゲームとは,セル・オートマトンの一種で,格子点が「生きている」「死んでいる」という情報をもち,時間(世代)が1進むと,周囲の状況によって,与えられた規則に従い内部の状態を変化させていくものです.代表的なものとしては,正方格子上で,周囲8マスの点に関し,2個あるいは3個生きていれば生き残り,周囲に生存格子点が3個あるときは新たに誕生する,というルール23/3などがあるかと思います.ライフゲームはチューリング完全なので,ライフゲームの内部で,コンピュータを再現することが出来ます.詳しくは
ライフゲームの世界1【複雑系】 - ニコニコ動画:GINZA:
などを参考にしてみてください.動画は特にオススメです.
端末上でライフゲーム
Tkinterを使ってライフゲームを作ってみようと思ったのだけれど,案外難しそうだったので,まずはライフゲームのルールの部分を先に作り,端末上で確認できるようにしてみた.
それが次のコードです.
lifegame_console.py
#! /usr/bin/env python# -*- coding:utf-8 -*-
#
# written by yuzugosho, August 2014.
from Tkinter import *
import numpy as np
import sys
import time
import os
class LifeGame:
def __init__(self, L=50, rule="2 3/3"):
self.L = L # lattice size
p = 0.2
self.survive = [int(i) for i in rule.split("/")[0].split()]
self.birth = [int(i) for i in rule.split("/")[1].split()]
lattice = np.random.random([self.L+2, self.L+2])
self.lattice = lattice
self.lattice[0,:] = self.lattice[self.L+1,:] = False
self.lattice[:,0] = self.lattice[:,self.L+1] = False
def canvas_update(self):
os.system("clear")
print "\n"
l = ""
for y in range(1,self.L+1):
for x in range(1,self.L+1):
if self.lattice[x,y]:
l += u" ■"
else:
l += u" □"
l += "\n"
print l
print "\n"
time.sleep(0.1)
def progress(self):
L = self.L
self.survive = (2,3)
self.birth = (3,6)
Tmax = 2000
t = 0
while t < Tmax:
try:
self.canvas_update()
nextsites = []
# 周期境界条件
self.lattice[0,0] = self.lattice[self.L,self.L]
self.lattice[0,self.L+1] = self.lattice[self.L,1]
self.lattice[self.L+1,0] = self.lattice[1,self.L]
self.lattice[self.L+1,self.L+1] = self.lattice[1,1]
for m in range(1, self.L+1):
self.lattice[m,self.L+1] = self.lattice[m,1]
self.lattice[m,0] = self.lattice[m,self.L]
for n in range(1, self.L+1):
self.lattice[0,n] = self.lattice[self.L,n]
self.lattice[self.L+1,n] = self.lattice[1,n]
# 隣接格子点の判定
for m in range(1,self.L+1):
for n in range(1,self.L+1):
if self.lattice[m,n]:
neighber = np.sum(self.lattice[m-1:m+2, n-1:n+2])-1
if neighber in self.survive:
nextsites.append((m,n))
else:
neighber = np.sum(self.lattice[m-1:m+2, n-1:n+2])
if neighber in self.birth:
nextsites.append((m,n))
# latticeの更新
self.lattice[:] = False
for nextsite in nextsites:
self.lattice[nextsite] = True
t += 1
except KeyboardInterrupt:
print "stopped."
break
if __name__ == '__main__':
lg = LifeGame()
lg.progress()
初期条件は,乱数を割り当てた行列で閾値以下の点を生存としました.表示は,まず端末をclearして,生存サイトは■,そうでないサイトは□としてprintすることでなんとかしました.遅延時間を0.1より小さくすると,僕の環境ではチラツキがひどくなったので,これ以上速くすることはできなさそうです.
Tkinterを使って表示
続いて,TkinterのCanvasを用いて作ったものを紹介します.これを作ってて一番詰まったのは,格子一つ一つの「生」「死」をクリックで変更できるようにすることでした.Pygameなんかを使うとここらへんは簡単になるのかもしれませんが,基本は一緒だと思うので,これからも使うTkinterそのものを使うことにしました.以下にコードの全文を載せます.
lifegame.py
#! /usr/bin/env python# -*- coding:utf-8 -*-
#
# written by yuzugosho, September 2014.
from Tkinter import *
import numpy as np
import sys
import time
class LifeGame:
def __init__(self, L=30, rule="2 3/3", p=None, pattern=None):
self.L = L # lattice size
self.survive = [int(i) for i in rule.split("/")[0].split()]
self.birth = [int(i) for i in rule.split("/")[1].split()]
if p:
lattice = np.random.random([self.L+2, self.L+2])
self.lattice = lattice
self.lattice[0,:] = self.lattice[self.L+1,:] = False
self.lattice[:,0] = self.lattice[:,self.L+1] = False
else:
self.lattice = np.zeros([self.L+2, self.L+2], dtype=bool)
if pattern:
for x,y in pattern:
self.lattice[x,y] = True
def progress(self, canvas_update, update):
L = self.L
Tmax = 2000
t = 0
self.loop = True
while self.loop:
try:
past_lattice = self.lattice.copy()
nextsites = []
# 周期境界条件
self.lattice[0,0] = self.lattice[self.L,self.L]
self.lattice[0,self.L+1] = self.lattice[self.L,1]
self.lattice[self.L+1,0] = self.lattice[1,self.L]
self.lattice[self.L+1,self.L+1] = self.lattice[1,1]
for m in range(1, self.L+1):
self.lattice[m,self.L+1] = self.lattice[m,1]
self.lattice[m,0] = self.lattice[m,self.L]
for n in range(1, self.L+1):
self.lattice[0,n] = self.lattice[self.L,n]
self.lattice[self.L+1,n] = self.lattice[1,n]
# 隣接格子点の判定
for m in range(1,self.L+1):
for n in range(1,self.L+1):
if self.lattice[m,n]:
neighber = np.sum(self.lattice[m-1:m+2, n-1:n+2])-1
if neighber in self.survive:
nextsites.append((m,n))
else:
neighber = np.sum(self.lattice[m-1:m+2, n-1:n+2])
if neighber in self.birth:
nextsites.append((m,n))
# latticeの更新
self.lattice[:] = False
for nextsite in nextsites:
self.lattice[nextsite] = True
# 描画の更新
changed_rect = np.where(self.lattice!=past_lattice)
for x, y in zip(changed_rect[0], changed_rect[1]):
if self.lattice[x,y] == True:
color = "green"
else:
color = "black"
canvas_update(x,y, color)
update()
time.sleep(0.1)
t += 1
if t > Tmax:
self.loop = False
except KeyboardInterrupt:
print "stopped."
break
class Draw_canvas:
def __init__(self, lg, L):
self.lg = lg
self.L = L
default_size = 640 # default size of canvas
self.r = int(default_size/(2*self.L))
self.fig_size = 2*self.r*self.L
self.margin = 10
self.sub = Toplevel()
self.sub.title("Life Game")
self.canvas = Canvas(self.sub, width=self.fig_size+2*self.margin,
height=self.fig_size+2*self.margin)
self.c = self.canvas.create_rectangle
self.update = self.canvas.update
self.rects = dict()
for y in range(1,self.L+1):
for x in range(1,self.L+1):
if self.lg.lattice[x,y]:
live = True
else:
live = False
tag = "%d %d" % (x,y)
self.rects[tag] = Rect(x, y, live, tag, self)
self.canvas.pack()
def canvas_update(self, x, y, color):
v = self.rects["%d %d" % (x,y)]
v.root.canvas.itemconfig(v.ID, fill=color)
class Rect:
def __init__(self, x, y, live, tag, root):
self.root = root
self.x = x
self.y = y
self.live = bool(live)
if live: color = "green"
else: color = "black"
self.ID = self.root.c(2*(x-1)*self.root.r+self.root.margin,
2*(y-1)*self.root.r+self.root.margin,
2*x*self.root.r+self.root.margin,
2*y*self.root.r+self.root.margin,
outline='grey', fill=color, tag=tag)
self.root.canvas.tag_bind(self.ID, '
', self.pressed) def pressed(self, event):
if self.live == 0:
self.live = True
color = "green"
else:
self.live = False
color = "black"
self.root.lg.lattice[self.x, self.y] = self.live
self.root.canvas.itemconfig(self.ID, fill=color)
class TopWindow:
def show_window(self, title="title", *args):
self.root = Tk()
self.root.title(title)
frames = []
for i, arg in enumerate(args):
frames.append(Frame(self.root, padx=5, pady=5))
for k, v in arg:
Button(frames[i],text=k,command=v).pack(expand=YES, fill='x')
frames[i].pack(fill='x')
self.root.mainloop()
class Main:
def __init__(self):
L = 60
rule = "2 3/3"
self.top = TopWindow()
c = L/2
# ダイハード
pattern = [(c-1,c+1),(c,c+1),(c,c+2),(c+4,c+2),
(c+5,c),(c+5,c+2),(c+6,c+2)]
self.lg = LifeGame(L, rule, p=None, pattern=pattern)
self.top.show_window("Life game", (('set', self.init),),
(('start', self.start),
('pause', self.pause)),
(('quit', self.quit),))
def init(self):
self.DrawCanvas = Draw_canvas(self.lg, self.lg.L)
def start(self):
self.lg.progress(self.DrawCanvas.canvas_update, self.DrawCanvas.update)
def pause(self):
self.lg.loop = False
def quit(self):
self.pause()
sys.exit()
if __name__ == '__main__':
app = Main()
先ほどのlifegame_console.pyとの差分は,LifeGame.__init__の細かい変数の扱いと,canvas_updateを別のクラスDraw_canvasにしたこと,その中で格子を描く部分をさらに別のクラスRectとして分けたところです.これによってそれぞれの格子点を独立したものとして扱うことができ,格子点を押せば「生」「死」を入れ替える,という操作ができるようになりました.実際に実行してみた様子をキャプチャしたので,ご覧ください(後半はコードのつながりを追うだけの内容なので,興味のない場合は飛ばしてもらっても結構です).
まとめ
今回はほとんどお遊びなんですが,新しいことも覚えられたし,これからのシミュレーションの幅も広がるんではないかと思います.あと,動画には字幕とか解説付けるべきですね...もう少し触ってみたいと思います.