見出し画像

ライフゲームを作る(4) 完成版

ライフゲームを作る(1) はCindyscriptで書いた。授業でもやった。
ライフゲームを作る(2) はPythonで,matplotlib を使ってやってみた。授業で扱うにはよさそう。
ライフゲームを作る(3) はPythonで,Tkinter を使ってやってみた。マウスクリックで生命体を置いて,ボタンで次世代に進む。Cindyscriptで作ったものと同様のものができたが,ひとつ問題点があった。それは,操作を繰り返すとだんだん反応が悪くなるというもの。その原因は何か,どう解決するか。それが今回の話題。

だんだん反応が遅くなる原因,そのヒントは本文に書いてある。画面に表示する drawboad() の説明の中にこういう部分がある。

長方形を描くのに,fill='white' としているのは,次世代になったとき,前の生命体の表示を白で上書きして消すためだ。fill なしで枠だけを描くと,再度この関数を実行したとき,前に生命体が描かれていた場合,それが残ってしまう。この関数では,図形を書き換えるのではなく,追加して同じ場所に上書きするので,枠線だけではだめなのだ。

セルの数の設定 N を N=20 とすると,セルは全部で400ある。このすべてに生命体が描かれている。長方形も描くので800個。生命体が生存していれば青,そうでなければ白で塗る。ボタンを押すたびに800個の図形を新たに作って上書きする。上書きといっても,それ以前のものも処理対象になるので,数が増えれば処理が遅くなるわけだ。

なぜだろう,と考えて,原因はほどなく思いついた。これには背景がある。
モンテカルロ法のシミュレーションをやったときのことだ。

for による 50000 回の繰り返しごとに点を打つのでは処理が遅いので,50000個の点データを作ってまとめて打つ方がよい

ということであった。これとの連想で,drawboad() に問題があると考えたわけだ。
 もうひとつある。mainloop() の動作だ。Tkinter では,最後に mainloop() を書く。これを書かないと実行されない。しかし「これを書くと実行される」という程度の理解ではやはり思い付かなかっただろう。
 mainloop() は,loop の名の通り,繰り返し。ウィンドウを閉じるか,終了の手続き(ボタンを作ってコマンドを実行)するまで無限ループになる。ただの無限ループではない。その間何をしているかというと,イベント待ち をしているのだ。マウスカーソルを動かす,クリックする,キーボードを打つなど。今回作ったライフゲームではマウスクリックだ。イベント処理は 

canvas.bind("<Button>", change)

で行っている。

という話は,「だえうホームページ」の「tkinterのmainloopについて解説」というページに懇切丁寧に書いてある。これを読んでいなかったら分からなかっただろう。

matplotlib であれ,Tkinter であれ,その他,Web上で検索するといろいろなページがみつかる。その中にサンプルコードもある。しかし,そのサンプルコードをコピーして動かしても,解説がなければなぜ動くのかが分からない。この,解説がないページがほとんどなのだ。本人は自分用メモか,他の人に知らせるつもりかのいずれかで書いているのだろうけど,自分ではわかっているのでわざわざ説明は書かない。筆者が「Pythonはわからん」と書いてきているのはそういう意味だ。

さて,解決方法。それも,「だえうホームページ」にある。「Tkinterの使い方:Canvasクラスで描画した図形を操作する」だ。

描画した図形にはすべて ID が自動的につく。自動ではなく,「タグ」をつけることもできる。この ID かタグを使って,その図形を操作することができる。「操作」は多岐にわたる。移動する,色を変える,そのほかいろいろ。そう,「色を変える」ことができる。
ならば,「書き換え」を,新しく作って上書き するのではなく,描いたものの色を変えればいいのだ。生命体をはじめは全部白にしておいて,誕生したら青にすれば良い。

描画を関数にするのではなく,色の変更を関数にする。したがって,描画は最初に一度だけおこなえばよい。このとき,タグをつけておく。長方形は白で塗らなくて良い。

# 表示画面を作る。生命体をすべて配置し,色を白,枠線なしにしておく
# 生命体にはタグで番号をつける
tagno = 0
for x in range(N):
   for y in range(N):
       px = x * unitw
       py = y * unitw
       canvas.create_rectangle(
           px, py, px + unitw, py + unitw, 
       )
       canvas.create_oval(
           px + unitw//5, py + unitw//5, 
           px + unitw*4//5, py + unitw*4//5, 
           width=0, 
           fill='white', 
           tag='s'+str(tagno)
       )
       tagno += 1

最後の行で二項演算子を使っている。授業で生徒に書かせるときは二項演算子はやらない方がよい。しかし,今やっていることは,もう生徒にやらせるレベルのものではないから使っている。もし生徒に書かせるなら tagno = tango + 1 とするほうがよい。理由は,大学入試共通テストの擬似言語では使わないということだ。

いままで drawboad() としていたものは,名前を変えて,リストの要素が1なら青,0なら白に変えるものにする。

# 生命体の色 Life の値 0/1 に対応する
color = ["white", "blue"]  

# Life の値に応じて生命体の色を変える
def drawlife():
   for x in range(N):
       for y in range(N):
           tag = 's' + str(x*N+y)
           canvas.itemconfig(tag, fill=color[int(Life[x][y])])

Life の値は0か1だから,リスト color を用意して色を選択している。このあたりも,生徒実習だったら,if 文でまとも?にやるほうがいいだろう。生徒実習では効率より基本優先。

nextLife() などで,最後に drawboad() としていたのは drawLife() に変更。

クリックした位置の生死を変える change() は,その箇所の色を変えるように追加。

# クリックした位置の生死を変える。周囲ひとマスは対象外
def change(ev):
   x = ev.x // unitw
   y = ev.y // unitw
   if 0 < x < N-1 and 0 < y < N-1:
       Life[x][y] = 1 - Life[x][y]
       tagno = x * N + y
       canvas.itemconfig('s'+str(tagno), fill=color[int(Life[x][y])])

こんなところかな,修正点は。

これで重くならずに何世代でも行けるようになる。

ということで,これで完成なんだけど,このあたりはもはや生徒に書かせるレベルではなくなってしまっている。「メソッド」も「イベント」も授業で扱わないほうがいい。どこかを穴あきで,と思っても,そんな箇所はない。
 ここまで調べて書いてくると,授業で書かせるとしたら最初の matplotlib で作ったところまでかなとも思うが,やはり出来上がりとしてはこの方がいいだろう。したがって,生徒に書かせるのは,世代を進める nextLife() くらいで,適当に穴あけにしてやるくらい。Cindyscriptではマウスクリックで1/0を変えるところも欠かせたが,それもなし。目標をルールのコーディングに絞ればTkinterのややこしいあれこれは知らなくてもよい。大学入試共通テストの形式・レベルとも合う。
 しかし,「正しくできれば動く」教材を作るには,教員側にはこれだけのスキルが必要ということだ。