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

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

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

<a href="http://yuzugosho.blog.fc2.com/img/201409042322239eb.png/" target="_blank"><img src="http://blog-imgs-64.fc2.com/y/u/z/yuzugosho/201409042322239eb.png" alt="Life Game_007" border="0" width="311" height="311" /></a>



#ライフゲームとは？

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

<a href="http://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0" target="_blank" title="ライフゲーム - Wikipedia:">ライフゲーム - Wikipedia:</a>
<a href="http://www.nicovideo.jp/watch/sm19347846" target="_blank" title="ライフゲームの世界１【複雑系】 - ニコニコ動画:GINZA:">ライフゲームの世界１【複雑系】 - ニコニコ動画:GINZA:</a>

などを参考にしてみてください．動画は特にオススメです．


#端末上でライフゲーム

Tkinterを使ってライフゲームを作ってみようと思ったのだけれど，案外難しそうだったので，まずはライフゲームのルールの部分を先に作り，端末上で確認できるようにしてみた．

それが次です．

```py:lifegame_console.py
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#
# written by ssh0, June 2014.

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<p
        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
        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]
                    self.lattice[0, m] = self.lattice[self.L, m]
                    self.lattice[self.L+1, m] = self.lattice[1, m]
                
                # 隣接格子点の判定
                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より小さくすると，僕の環境ではチラツキがひどくなったので，これ以上速くすることはできなさそうです．

動画にしているのでどうぞ

<a href="http://youtu.be/7KMluy0fRq8" target="_blank"><img src="https://i.ytimg.com/vi/7KMluy0fRq8/2.jpg" alt="Pythonで簡単なLifeGameを作ってみた(numpy)" border="0" width="145" height="109" /></a>
[Pythonで簡単なLifeGameを作ってみた(numpy)](http://youtu.be/7KMluy0fRq8)


#Tkinterを使って表示

続いて，TkinterのCanvasを用いて作ったものを紹介します．これを作ってて一番詰まったのは，格子一つ一つの「生」「死」をクリックで変更できるようにすることでした．Pygameなんかを使うとここらへんは簡単になるのかもしれませんし，調べた限りこちらを使っている人しか見かけませんでした．ですが基本は一緒だと思うので，これからも使うTkinterそのものを使うことにしました．以下にすべて載せます．

```py:lifegame.py
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#
# written by ssh0, 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 < p
            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):
        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]
                    self.lattice[0, m] = self.lattice[self.L, m]
                    self.lattice[self.L+1, m] = self.lattice[1, m]

                # 隣接格子点の判定
                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]:
                        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="#202020", fill=color, tag=tag)
        self.root.canvas.tag_bind(self.ID, '<Button-1>', self.pressed)

    def pressed(self, event):
        if self.live:
            self.live = False
            color = "black"
        else:
            self.live = True
            color = "green"
        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 = 100
        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)),
                             (('save', self.pr),),
                             (('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 pr(self):
        import tkinter.filedialog
        import os
        if self.DrawCanvas is None:
            return 1
        fTyp = [('eps file', '*eps'), ('all files', '*')]
        filename = tkinter.filedialog.asksaveasfilename(filetypes=fTyp,
                                                  initialdir=os.getcwd(),
                                                  initialfile='figure_1.eps')
        if filename is None:
            return 0
        self.DrawCanvas.canvas.postscript(file=filename)

    def quit(self):
        self.pause()
        sys.exit()

if __name__ == '__main__':

    app = Main()
```

先ほどのlifegame_console.pyとの差分は，LifeGame.__init__の細かい変数の扱いと，canvas_updateを別のクラスDraw_canvasにしたこと，その中で格子を描く部分をさらに別のクラスRectとして分けたところです．これによってそれぞれの格子点を独立したものとして扱うことができ，格子点を押せば「生」「死」を入れ替える，という操作ができるようになりました．実際に実行してみた様子をキャプチャしたので，ご覧ください（後半はコードのつながりを追うだけの内容なので，興味のない場合は飛ばしてもらっても結構です）．

<a href="http://youtu.be/1M9sc5TlWQg" target="_blank"><img src="https://i.ytimg.com/vi/1M9sc5TlWQg/3.jpg" alt="Pythonでライフゲームを作ってみた【Tkinter】" border="0" width="145" height="109" /></a>
[Pythonでライフゲームを作ってみた【Tkinter】](http://youtu.be/1M9sc5TlWQg)


#まとめ

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