読者です 読者をやめる 読者になる 読者になる

$ service ssh0 start

from everything import *

Pythonでライフゲーム【つくってみた】(端末上 & Tkinter)

今回は,数値シミュレーションではなくて,コンウェイライフゲーム(Conway's Game of Life)を作ってみました.

息抜きのつもりで始めたものの,作ってみると意外とハマるところがあって,かなり試行錯誤しました.ですので,完成品もようやく動く形になった程度の出来です.仕様を理解していれば,もっとうまいものが作れると思いますが,そこまで読み込む気にはなれませんでした...

Life Game_007

ライフゲームとは?

まず,ライフゲームについて軽く説明を.ライフゲームとは,セル・オートマトンの一種で,格子点が「生きている」「死んでいる」という情報をもち,時間(世代)が1進むと,周囲の状況によって,与えられた規則に従い内部の状態を変化させていくものです.代表的なものとしては,正方格子上で,周囲8マスの点に関し,2個あるいは3個生きていれば生き残り,周囲に生存格子点が3個あるときは新たに誕生する,というルール23/3などがあるかと思います.ライフゲームチューリング完全なので,ライフゲームの内部で,コンピュータを再現することが出来ます.詳しくは

ライフゲーム - Wikipedia:

ライフゲームの世界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として分けたところです.これによってそれぞれの格子点を独立したものとして扱うことができ,格子点を押せば「生」「死」を入れ替える,という操作ができるようになりました.実際に実行してみた様子をキャプチャしたので,ご覧ください(後半はコードのつながりを追うだけの内容なので,興味のない場合は飛ばしてもらっても結構です).

まとめ

今回はほとんどお遊びなんですが,新しいことも覚えられたし,これからのシミュレーションの幅も広がるんではないかと思います.あと,動画には字幕とか解説付けるべきですね...もう少し触ってみたいと思います.