Update 2023.11.14 2017.03.03

Python「コンウェイのライフゲーム」
オブジェクト指向プログラミングOOPにより配列,不等号がなくなります
tkinterアニメーションの試行が人間の思考と同じに見える
Python3(3.10)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

実際に動く Python3 ソースコード(.pyファイル,ipynbファイル)があります。左上隅からダウンロードできます。ダウンロードされるファイルの解説は,巻末のソースコードの手前にあります。

◆◆コンウェイのライフゲームとは◆◆

コンウェイのライフゲームとは,
左図の配置のセルラー細胞が下の枠内の規則によって世代ごとに生命が誕生したり死滅したりします。50世代後には右図のように成長します。
最初のセルラー細胞の配置によって成長の仕方は劇的に変化します。
成長過程をアニメーションによりシミュレーションします。 セルラー細胞の初期値を別ファイルにたくさん用意しました。メインプログラムにコピペしてください。

(2023-11-14)Python3.10で動作確認済み

【重要な注意】「PyCharm」では,ConwayGameOfLifeBoard.py②だけをrunします。ConwayGameOfLife.py①は②にimportされます。①の自己テスト(if __name__=='__main__':)は無視され,①のdefとclassが②に呼ばれます。

「Jupyter Notebook」では,①を上に②を下に並べます。②のimport文はコメントアウトされていて,①と②が順にrunします。①の自己テストもrunして①のdefとclassもrunしてその定義が記憶され②に引き継がれます。

このように「Jupyter Notebook」と「PyCharm」とでは,ファイルの参照関係が違うので注意が必要です。

◆◆コンウェイのライフゲーム・誕生と死滅の規則◆◆

たくさんの格子の中にいるセルラーが周囲のセルラーの影響を受けて生命が誕生したり死滅したり生き残ったりします。その規則は次のとおりです。

◆◆シミュレーションプログラムと描画プログラムを分けます◆◆

格子の中のセルラーの変化のシミュレーションプログラムとアニメーション描画プログラムを分けてつくります。シミュレーションプログラムは単独でも run できます。

◆◆tkinter は紫藤氏のものを参考にしました◆◆

http://www.shido.info/py/life.html
『Set を用いたライフゲームの実装』

ただ,シミュレーションのアルゴリズムやアニメーションの構造などは私のものとはまったく違います。私のプログラムは set を使わずクラスを使っています。

◆◆クラスの使い方は Java meets Python を参考にしました◆◆

ボードゲームのプログラムはどうしても配列を使いたくなります。盤面が配列そのものだからです。配列を止め,クラスを使ったオブジェクト指向プログラミングを薦めているのが次のWebです。

http://codezine.jp/article/detail/2252?mode=print
『配列と別れる50の方法(3) ライフゲーム』

◆◆シミュレーションプログラムのクラスは盤面とセルラーの2つ◆◆

巻末のシミュレーションプログラム【ConwayGameOfLife.py】のソースコードを見てもらうと,クラスは13行目の CellulaSet()と125行目 Cellula()である。階層にはなっていません。

ちなみにこのプログラムには不等号記号が一切ないです。配列を使ったら盤面の境界を表現するのに不等号は使わざるを得ない。そして例外エラーに悩まされることになる。クラスを上手く使えばそれをなくすことができるようになります。

◆◆クラス図もどき◆◆

クラスに階層はないので,クラス図は必要ないが,シミュレーションプログラムのメソッドの参照関係を図に示す。


                                        main
【CellulaSet】                     ↑                    ↑
                    initialize_board                    next_generation
              ↑            ↑           ↑                  ↑        ↑
listing_cellula  initial_cellula_life  cellula_life_to_hash  update_future_life
listing_neighbor                                             update_present_life
        ↑                  ↑
listing_neighbor1   grid_to_hash

check_border
                                                【Cellula】       ↑
                                                            update_future_life
                                                            update_present_life
                                                                  ↑
                                                            No_neighbors

◆◆シミュレーションプログラムのコードの詳細な解説◆◆

上の図では,下流のメソッドほど上にあります。

◆◆描画プログラムには関数mainの戻り値を渡す◆◆

描画プログラムはシミュレーションプログラムをimportします(Jupyter Notebookではコメントアウト)。シミュレーションプログラムの自己テストはモジュールが分けられていればrunしないので,描画プログラムでインスタンスをつくり関数mainを呼び出して戻り値を受取る(Jupyter Notebookでは自己テストはrunする)。

関数mainがメソッドinitialize_boardnext_generationから受取る戻り値のうち,1つ目は座標付きのグリッド図を受取りこのモジュール内でグリッド図をprintします。 2つ目は1列のグリッド図でありそれを貯めて関数mainの戻り値として自己テストと描画プログラムに渡します。自己テスト何もしません。関数mainを呼ぶ手段です。

上の図から判るように,メソッドinitialize_boardは,4つのメソッドlisting_cellulalisting_neighborinitial_cellula_lifecellula_life_to_hashを参照しています。

メソッドnext_generationは,3つのメソッドupdate_future_lifeupdate_present_lifecellula_life_to_hashを参照しています。

この2つのメソッドの2つ目の戻り値は,メソッドcellula_life_to_gridであり,座標付きのグリッド図を1列に直しています。関数mainではこれを受取り貯めてから描画プログラムに渡しています。

◆◆関数mainに渡す初期盤面をつくる◆◆

格子1つ1つの自分の座標を32行目のメソッドlisting_cellulaでオブジェクトにしてそれを座標から呼べるようにハッシュ(辞書)に格納する。次に38行目のメソッドlisting_neighborで隣接座標をオブジェクトにしておく。

境界の処理のために予め171行目でセルラーオブジェクトの代わりになるヌルオブジェクト border をインスタンス化しておく。

35行目では,クラスCellula(x, y)をインスタンス化して座標をオブジェクトにしている。次の行でdict(zip(でハッシュ(辞書)に座標と座標オブジェクトを取り込んでいる。

初期盤面は盤面の真ん中あたりの20×20の部分だけとしている。生存セルラーを「*」とすればよい。いくつかの初期盤面が左上隅からダウンロードできます。初期盤面の例が173行目にあるように文字列です。

76行目のメソッドinitial_cellula_lifeで大きくされた初期盤面がセルラーオブジェクトに登録される。

84行目のメソッドcellula_life_to_hashで先のセルラーオブジェクトをハッシュ(辞書)に変換しておきます。

◆◆世代ごとに関数mainに渡す◆◆

68行目のメソッドupdate_future_lifeは世代更新である。クラスCellulaの同名メソッドを格子の数だけ呼んでいる。

冒頭で紹介したライフゲームのライフ(誕生・生存・死滅)の規則は138行目のメソッド update_future_lifeにある。メソッドNo_neighborsは隣接の生存セルラーを数えている。このメソッドの戻り値は1行だけであることに注目してほしい。配列ではこうはいかないだろう。

72行目のメソッドupdate_present_lifeは先のメソッドで1世代あとを算出したのでそれを現在地に代入している。クラスCellulaの147行目の同名メソッドupdate_present_lifeを格子の数だけ呼んでいる。

◆◆モジュールを呼び自己テストをrunすればシミュレーションプログラムだけdebugできる◆◆

世代の更新回数は 10行目で設定できる。

描画プログラム「ConwayGameOfLifeBoard.py」から起動するときは,このシミュレーションプログラムの159行目以降の print文をコメントアウトした方が良い。そのままではスクリプトにしばらくプリントしてからボードが表示されるのでかなり待つことになってしまう。

◆◆tkinterによるアニメーション◆◆

冒頭で紹介したWebのライフゲームとはアニメーションの実現方法がまったく違う。冒頭でも述べたが,
  ライフの更新のアルゴリズムを判りやすくしたい
  ライフのクラスとアニメーションのクラスの干渉をなくしたい
という理由で本Webサイトの別記事で使っているアニメーションの構造を採用した。

描画プログラムはtkinterによるアニメーションを使っている。少々長いプログラムだが構造がしっかりしているので難しいところはない。

クラス LifeGameBord が11行目で tkinter.Canvasを継承している。17行目のコンストラクタも25行目で親コンストラクタを継承していて盤面を描いている。

この Canvas でセルラーのリバースを描くので,コンストラクタの43行目でシミュレーションプログラムから盤面データを受け取る。このクラスで重要な関数は,繰り返し盤面を描く67行目の関数 refresh() である。関数 refresh() はもう1つのクラスからアニメーションをつくるため繰り返し呼ばれている。

実際の描画は49行目の関数 put_a_cellula()でおこなっている。リバースするセルラーを決めているのが59行目の関数 draw_hash()である。

もう1つのクラス LifeGameFrame が77行目で tkinter.Frameを継承している。Canvas の枠でありタイトルなどを設定する。

この Frame で「start」ボタンを設定して,そのボタンから関数 show_next() を呼び出す。そしてこの関数から盤面を描画する関数 refresh() を再帰的に呼び出す。

123行目からのプログラムはこのモジュールが起動されたとき自動的に走るプログラムであり,クラスで tkinter を使っているときはこの決まり文句になる。

少々長いので複雑にみえるが,いろいろ応用しても決まった構造は変えないので理解しやすいと考える。

アニメーションのコマ送り間隔は,8行目で設定できる。1以上を設定すること。単位はミリ秒であるが「1」に設定しても描画のためかそれほど速くない。1秒(設定値は1000)程度であると試行が人間の思考のように見える。

◆◆ソースコード◆◆

このWebの左上隅からダウンロードできます。

ソースファイルは4つです。
・ConwayGameOfLife.py;シミュレーションプログラム
・ConwayGameOfLifeBoard.py;描画プログラム
・ConwayGameOfLifeGrid.py;初期盤面集(シミュレーションプログラムへコピペして利用してください)
・ConwayGameOfLifeBoard.ipynb;Jupyter Notebook用,
                 ConwayGameOfLife + ConwayGameOfLifeBoard
・ConwayGameOfLifeBoard.ipynb2;print文をコメントアウト,アニメーションのみ表示

以上

【ConwayGameOfLife.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# -*- coding: utf-8 -*-
"""
Conway's Game of Life
"""
""" No_XY = No_space + No_XYgrid + No_space """
No_XY = 64
No_XYgrid = 20
No_space = 22

No_repeat = 50


class CellulaSet():

    def __init__(self):
        """コンストラクタ"""
        self.cellula_list = [(x, y)
                             for y in range(No_XY)
                             for x in range(No_XY)]
        self.cellula_object = []
        self.cellula_hash= {}
        self.life_hash= {}
        self.neighbor_list = []

    def initialize_board(self):
        self.listing_cellula()
        self.listing_neighbor()
        self.initial_cellula_life()
        self.cellula_life_to_hash()
        return self.life_hash, self.cellula_life_to_grid()

    def listing_cellula(self):
        """すべてのセルラーをインスタンス化する"""
        for x, y in self.cellula_list:
            self.cellula_object.append(Cellula(x, y))
        self.cellula_hash = dict(zip(self.cellula_list, self.cellula_object))

    def listing_neighbor(self):
        """すべてのセルラーの隣接座標とそのインスタンスを抽出する"""
        for e in self.cellula_object:
            self.listing_neighbor1(e)

    def listing_neighbor1(self, cell):
        ix = cell.x
        iy = cell.y
        self.neighbor_list = [(x, y)
                        for y in (iy-1, iy, iy+1)
                        for x in (ix-1, ix, ix+1)]
        for x, y in self.neighbor_list:
            if (x, y) == (ix, iy): continue
            cell.neighbor_object.append(self.check_border(x, y))

    def check_border(self, x, y):
        obj = border
        for e in self.cellula_object:
            if (e.x, e.y) == (x, y):
                obj = e
                break
        return obj

    def next_generation(self):
        """盤面を更新する"""
        self.update_future_life()
        self.update_present_life()
        self.cellula_life_to_hash()
        return self.life_hash, self.cellula_life_to_grid()

    def update_future_life(self):
        for e in self.cellula_object:
            e.update_future_life()

    def update_present_life(self):
        for e in self.cellula_object:
            e.update_present_life()

    def initial_cellula_life(self):
        """最初のセルラーライフをセルラーオブジェクトに登録する"""
        hash = self.grid_to_hash(given_grid)
        for e in self.cellula_object:
            if hash[(e.x, e.y)] == '*':
                e.present_life = True
            # print e.x, e.y, e.present_life

    def cellula_life_to_hash(self):
        """セルラーライフをハッシュに変換する"""
        for e in self.cellula_object:
            if e.present_life == True:
                self.life_hash[(e.x, e.y)] = '*'
            else:
                self.life_hash[(e.x, e.y)] = '.'

    def cellula_life_to_grid(self):
        """セルラーライフを直線グリッドに変換する"""
        return ''.join('*' if e.present_life == True else '.'
                for e in self.cellula_object)

    def grid_to_hash(self, grid):
        """グリッド図を拡張してハッシュ(辞書)に変換する"""
        if No_XYgrid == No_XY:
            chars = [c for c in grid if c in '*. ']
            assert len(chars) == No_XY * No_XY
            return dict(zip(self.cellula_list, chars))
        else:
            assert No_XY == No_XYgrid + No_space + No_space
            chars = ''.join(c for c in grid if c in '*. ')
            assert len(chars) == No_XYgrid * No_XYgrid
            str_dot = ''.join('.' for s in range(No_space))
            str_dot_all = ''.join('.' for s in range(No_space * No_XY))
            new_chars = ''
            for n in range(No_XYgrid):
                n0 = No_XYgrid * n
                n1 = No_XYgrid * (n + 1)
                new_chars = new_chars + str_dot + chars[n0:n1] + str_dot
            new_chars = str_dot_all + new_chars + str_dot_all
            new_chars_list = [c for c in new_chars if c in '*. ']
            return dict(zip(self.cellula_list, new_chars_list))

    def linegrid_to_hash(sefl, grid):
        """直線グリッドをハッシュ(辞書)に変換する"""
        chars = [c for c in grid if c in '*. ']
        assert len(chars) == No_XY * No_XY
        return dict(zip(self.cellula_list, chars))


class Cellula():

    def __init__(self, x, y):
        """コンストラクタ"""
        self.x = x
        self.y = y
        self.present_life = False
        self.future_life  = False
        self.neighbor_object = []

    def No_neighbors(self):
        return len([None for e in self.neighbor_object if e.present_life])

    def update_future_life(self):
        n = self.No_neighbors()
        if n == 2:
            self.future_life = self.present_life
        elif n == 3:
            self.future_life = True
        else:
            self.future_life = False

    def update_present_life(self):
         self.present_life = self.future_life



def print_life(hash):
    """ハッシュ(辞書)をグリッド図に変換してprintする"""
    for y in range(No_XY):
        print(''.join('*' if hash[(x, y)] in '*' else ' '
                for x in range(No_XY)))
    print()

def main():
    grid_list = []
    a = CellulaSet()
    hash, grid = a.initialize_board()
    grid_list.append(grid)
    print_life(hash)
    for no in range(No_repeat):
        hash, grid = a.next_generation()
        grid_list.append(grid)
        print_life(hash)
    return grid_list

border = Cellula(0, 0)

given_grid = '\
....................\
....................\
....................\
....................\
....................\
....................\
....................\
....................\
......*.............\
........*...........\
.....**..***........\
....................\
....................\
....................\
....................\
....................\
....................\
....................\
....................\
....................'


if __name__=='__main__':
    glist = main()

【ConwayGameOfLifeBoard.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# -*- coding: utf-8 -*-
"""
Conway's Game of Life using Tkinter
"""
import tkinter as Tk
from ConwayGameOfLife import *

Timer = 500    # ミリ秒;アニメーションのコマ送り間隔(1以上を設定)


class LifeGameBord(Tk.Canvas):
    """Tk.Canvas for Life Game
    """
    """set initial value"""
    grid_size = 8

    def __init__(self, master):
        """コンストラクタ
        """
        self.master = master
        """set initial value"""
        endline = self.grid_size * (No_XY + 1)
        offset = self.grid_size * 0.36
        """親コンストラクタ"""
        Tk.Canvas.__init__(self, master, relief=Tk.RAISED, bd=4, bg="#000000",
                            width=(No_XY+2)*self.grid_size,
                            height=(No_XY+2)*self.grid_size,
                            highlightthickness=0)
        """set initial value"""

        """drawing lines and coordinate"""
        for i in range(No_XY+1):
            x = (i + 1) * self.grid_size
            """グリッド細線を描く"""
            """drawing X axes"""
            self.create_line(self.grid_size, x, endline, x, width = 1, \
                             fill = '#666666')
            """drawing y axes"""
            self.create_line(x, self.grid_size, x, endline, width = 1, \
                             fill = '#666666')
        """read solution"""
        a = CellulaSet()
        glist = main()
        self.glist = glist
        self.len_list = len(glist)
        self.draw_hash(linegrid_to_hash(self.glist[0]))
    """コンストラクタ終"""

    def put_a_cellula(self, xg, yg):
        """draw a cellula
        """
        x0 = self.grid_size * (xg + 1)
        y0 = self.grid_size * (yg + 1)
        x1 = x0 + self.grid_size
        y1 = y0 + self.grid_size
        self.create_rectangle(x0, y0, x1, y1, fill = '#FFFF00', tag = 'cellula')


    def draw_hash(self, hash):
        """ハッシュからセルラー座標を取得する
        """
        for y in range(No_XY):
            for x in range(No_XY):
                if hash[(x, y)] == '*':
                    self.put_a_cellula(x, y)

    def refresh(self, no):
        """アニメーション
        """
        if no == self.len_list:
            return True
        self.delete('cellula')
        self.draw_hash(linegrid_to_hash(self.glist[no]))
        return False


class LifeGameFrame(Tk.Frame):
    """ Tk.Frame for Life Game """
    def __init__(self, master=None):
        """コンストラクタ
        """
        """親コンストラクタ"""
        Tk.Frame.__init__(self, master, relief=Tk.FLAT, bd=2)
        self.master.title("Life Game")
        """title"""
        l_title = Tk.Label(self, text='Life Game',
                           font=('Times', '24', ('italic', 'bold')),
                           fg='#191970', bg='#E8B77D', width=12)
        l_title.pack(padx=10, pady=10)
        self.LGBord = LifeGameBord(self)
        self.LGBord.pack(padx=10,pady=10)
        self.f_footer = Tk.Frame(self)
        self.f_footer.pack()
        self.a_button = Tk.Button(self.f_footer, text="start",
                        font = ("Times", 14), command = self.show_next)
        self.a_button.pack(side=Tk.LEFT, padx=5,pady=5)
        """set initial value"""
        self.No_step = 0

    def show_next(self):
        """アニメーションのコマ送り
        """
        sol = self.LGBord.refresh(self.No_step)
        if sol:
            """終了"""
            return
        self.No_step += 1
        self.after(Timer, self.show_next)


cellula_list = [(x, y)
        for y in range(No_XY)
        for x in range(No_XY)]

def linegrid_to_hash(grid):
    """直線グリッドをハッシュ(辞書)に変換する"""
    chars = [c for c in grid if c in '*. ']
    assert len(chars) == No_XY * No_XY
    return dict(zip(cellula_list, chars))


if __name__ == '__main__':
    app = LifeGameFrame()
    app.pack()
    app.mainloop()

トップページに戻る
inserted by FC2 system