45
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-09-22

今回は,数値シミュレーションではなくて,コンウェイのライフゲーム(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 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より小さくすると,僕の環境ではチラツキがひどくなったので,これ以上速くすることはできなさそうです.

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

Pythonで簡単なLifeGameを作ってみた(numpy)
Pythonで簡単なLifeGameを作ってみた(numpy)

#Tkinterを使って表示

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

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

Pythonでライフゲームを作ってみた【Tkinter】
Pythonでライフゲームを作ってみた【Tkinter】

#まとめ

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

45
52
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?