Python「コンウェイのライフゲーム」
オブジェクト指向プログラミングOOPにより配列,不等号がなくなります
tkinterアニメーションの試行が人間の思考と同じに見える
Python3(3.10)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」
実際に動く Python3 ソースコード(.pyファイル,ipynbファイル)があります。左上隅からダウンロードできます。ダウンロードされるファイルの解説は,巻末のソースコードの手前にあります。
◆◆コンウェイのライフゲームとは◆◆
コンウェイのライフゲームとは,(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_boardとnext_generationから受取る戻り値のうち,1つ目は座標付きのグリッド図を受取りこのモジュール内でグリッド図をprintします。 2つ目は1列のグリッド図でありそれを貯めて関数mainの戻り値として自己テストと描画プログラムに渡します。自己テスト何もしません。関数mainを呼ぶ手段です。
上の図から判るように,メソッドinitialize_boardは,4つのメソッドlisting_cellula,listing_neighbor,initial_cellula_life,cellula_life_to_hashを参照しています。
メソッドnext_generationは,3つのメソッドupdate_future_life,update_present_life,cellula_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できる◆◆
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()
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()