見出し画像

センター試験「情報関係基礎」2020年の問3 宝探しをプログラミングの教材にする

初めはこちら。センター試験の問題のようにコーディングしてみたが動かない,という話。
つぎがこちら。動かない理由と,ひとまず問題の図6まで作って動かすことのできるプログラムを示した。読者への宿題は,そのあと,図7とその他の課題をやって完成させること。
今回はその完成品と,教材にするための修正。

あ〜,念のため書いておくが,これ,高校生向けではなく,Pythonを学ぼうとする一般の人向けでもなく,情報Iを教える高校教員向けだから。

まず,元の問題にある課題を再掲しよう。

画像1

画像2

図7のプログラムを修正した上で,図6の「罠のマス判定手続き」の直後に挿入することになっている。図6はこうなっている。

画像3

10-28行で図5のプログラムを実行し,その次に図4,図3とつづく。この中には空欄はないが,図7をこの中に入れなければならない。
この図6のプログラムを while ループで書くということはしない。Tkinter の root.mainloop() がその役割を果たしているからだ。すると,図2から図5まで関数として書いてきたものをどこに入れるのか。これを実習時の課題とするには難しい。したがって,これを入れたものを作って,実習では図5までに相当するものを完成させることになる。初期設定や,画面表示などはすべて作り込んでおく。したがって,全体としては次のような構造になる。

      ライブラリの import
      図2のプログラム 適当に空欄を設けて完成させる
      図3のプログラム   同上
      図4のプログラム   同上
      図5のプログラム   同上
      画面の設定,諸変数の準備と初期化(以下は変更しない)
      方向指定ボタンをクリックしたときの処理をはじめ画面表示など

では,問題になかったものも含め,ゲームとしてちゃんと動くようにコードを追加していく方法を示そう。

(1) up() down() などをまとめて作る
(2) 方向指定ボタンを押したとき,進む方向を表示する

この2つをまとめてやってしまおう。問題は,dx,dy がグローバル変数なので,global で宣言する必要があったことだ。

def up():
  global dx
  global dy
  dx = 0
  dy = -1

このようなものが4つ続くのが煩わしい。そこで,ここを書かせるのはやめてしまう。そうすれば,効率的な書き方もできる。
これらの関数は,方向指定ボタンをクリックすると呼ばれる。そのしかけはこうだ

btn1 = tk.Button(text="上", command = up) 
btn2 = tk.Button(text="下", command = down) 
btn3 = tk.Button(text="左", command = left) 
btn4 = tk.Button(text="右", command = right)

として4つのボタンを作る。command オプションでそれぞれの関数を実行するのだ。これを次のように変える。

btn1 = tk.Button(text="上") 
btn2 = tk.Button(text="下") 
btn3 = tk.Button(text="左") 
btn4 = tk.Button(text="右")
btn1.bind("<ButtonPress>", direction)
btn2.bind("<ButtonPress>", direction)
btn3.bind("<ButtonPress>", direction)
btn4.bind("<ButtonPress>", direction)

 bind メソッドを使って,ボタンが押されたら direction() を呼ぶようにする。どれをクリックしても direction() だ。
前回,「どのボタンをクリックしたかの情報がない」と書いたが,bind を使うとこれができる。direction()を呼び出したとき,自動的に引数を引き渡すのだ。その引数を direction() 側で受け取る。それを event とすると,どのボタンがクリックされたかが event.widget に入っている。

def direction(event):
   global dx
   global dy
   global direc
   if event.widget == btn1:
       direc = 1
       dx = 0
       dy = -1
   elif event.widget == btn2:
       direc = 2
       dx = 0
       dy = 1
   elif event.widget == btn3:
       direc = 3
       dx = -1
       dy = 0
   else:
       direc = 4
       dx = 1
       dy = 0
   info0.config(text=direcstr[direc]+"方向に進む")

最後の info0 というのは,方向指定ボタンの上に表示するラベルで,初めは次のように作ってある。

info0 = tk.Label(text="方向をクリック", font=("",16))

これが,方向指定ボタンをクリックすることによって「左方向に進む」などに変わる。direcstr はリストで,初めに次のように定義されている。

direcstr = ["","上","下","左","右"]

(3) ゲームの状態を表す変数 zyotai を配置する

ここから,プログラムの完成に入る。
なお,変数名を,元の問題文のように,アンダースコア付きのものに戻している

図7の前に書かれている課題の zyotai 。宝の位置にたどりつければ,zyotaiを1 にして終了する。罠に3回落ちるか,規定回数以内に宝にたどりつけなければ zyotaiを-1 にして終了する。したがって,移動と罠探知ボタンが有効なのは,zyotai == 0 の間だけにする。
図2のプログラム(関数 move)に if zyotai == 0 を追加する。宝を見つけたときの zyotai = 1 も追加。
これが最初にくるので,その前の import 文もあわせて載せておこう。

import numpy as np
import tkinter as tk
   
# 移動ボタンが押されたときの手続き (図2)
def move():
   global robo_x
   global robo_y
   global nokori
   global zyotai
   global message

   if zyotai == 0:
       message = ""
       if (robo_x+d_x > 0 and robo_x+d_x <= YOKO and
             robo_y+d_y > 0 and robo_y+d_y <= TATE):
           robo_x = robo_x + d_x
           robo_y = robo_y + d_y
       nokori = nokori - 1
       if takara_x == robo_x and takara_y == robo_y:
           message = "宝を見つけた!\n宝探し成功!"
           zyotai = 1
       hantei()

次の図3は変更なし。

# 最小歩数の計算 (図3)
def calc():
   global hosuu
   
   sa_x = takara_x - robo_x
   sa_y = takara_y - robo_y
   if sa_x < 0:
       sa_x = sa_x * (-1)
   if sa_y < 0:
       sa_y = sa_y * (-1)
   hosuu = sa_x + sa_y

 問題には,罠に落ちると「罠にかかった!ダメージを受けた!」と表示することになっているが,実際にはダメージがない。やはり,ダメージとして動ける回数を1回減らすべきだろう。それも含めて,図4の罠のマス判定(関数 hantei)に,zyotai = -1 を追加する。

# 罠のマス判定(図4)
def hantei():
   global miss
   global nokori
   global message
   global zyotai

   for i in range(WANASUU):
       if Wana_x[i] == robo_x and Wana_y[i] == robo_y:
           message = "罠にかかった!\nダメージを受けた!"
           miss = miss + 1
           Wana_hyoji[i] = 1
           # ダメージを受けると1回減る
           if nokori > 0:
               nokori = nokori - 1
   if miss == 3:
       message = "3回目だ!\nついに壊れた・・宝探し失敗"
       zyotai = -1

(4) 罠探知で,罠がなかったときのメッセージを追加

 問題には,罠探知ボタンで罠を見つけたら「罠を発見した」と表示することになっているが,罠ではなかったときのことはかかれていない。罠がなかったとき,「罠はなかった」というメッセージを出すようにする。
 図5(関数 action) に,図7とともに追加する。

# 行動(図5)
def action(event):
   global message
   global nokori
   global zyotai
   
   if event.widget == btn5:
       if direc > 0:
           move()
   else:
       message = "罠ではなかった"
       for i in range(WANASUU):
           if Wana_x[i] == robo_x+d_x and Wana_y[i] == robo_y+d_y:
               message = "罠を発見した"
               Wana_hyoji[i] = 1
       nokori=nokori-1;
   if nokori == 0 and zyotai == 0:
       message = "動きすぎて疲れた!\n宝探し失敗!"
       zyotai = -1
   banmen()

(5) ゲームが終わったとき,「もう一度やる」ボタンでやり直せるようにする

これが少しやっかいだ。ともかくボタンを用意して,必要な変数を初期化するとともに,表示されている罠の状態をもとに戻す。

def reset(even):
   global zyotai
   global miss
   global nokori
   global robox
   global roboy
   global message
   global direc
   global hosuu
   global dx
   global dy
   
   zyotai = 0
   miss = 0
   nokori = Shokikaisuu
   robox = 6
   roboy = 2
   message = ""
   direc = 0
   hosuu = 0
   dx = 0
   dy = 0
   for i in range(WANASUU):
       if Wanahyoji[i] == 1:
           canvas.delete("wana" + str(i))
           Wanahyoji[i] = 0
   btn7.place(x=800, y=0)
   info0.config(text="方向をクリック")
   banmen()

この関数で変数の初期化ができるなら,初めに置いて,直後に reset() を実行すればよいように思える。ところが,そうすると,ラベル info0 がまだできていないのでエラーになる。
では,ボタン類を作ったあとで実行すればいいかというと,こんどはボタン類の設定時に変数を使うので,ここで「変数が未定義」というエラーが出てしまう。
したがって,二度手間に思えるが,変数の初期化はこの関数の外でも一度行っておく。

以上。図2〜図5をどう教材化するか。元の問題に則して空欄にしたものを配布するか,テキストに空欄つきのコードを書いて全部打ち込ませるか,そこは生徒の状況と授業時間との兼ね合いになる。

(6) 裏方を作る

「以下は変更しない」部分:裏方 を示す。前に書いた direction() , reset() が重複するが,全文を示そう。

# === 以下は変更しない(実習用のコメント)=======
root = tk.Tk()
root.title("宝探しゲーム")
root.geometry("700x350")
canvas = tk.Canvas(root, width=402, height=252, bg="white")
# 変数の初期化 マスの番号は1からカウント
YOKO = 8
TATE = 5
Shokikaisuu = 9
WANASUU = 8
Wana_hyoji = np.zeros(WANASUU)
Wana_x = [2, 3, 3, 4, 5, 5, 6, 6]
Wana_y = [3, 2, 4, 2, 4, 5, 4, 5]
direcstr = ["","上","下","左","右"]
wana = tk.PhotoImage(file="wana.png")
robot = tk.PhotoImage(file="robot.png")
takara = tk.PhotoImage(file="takara.png")
unitw = 50
takara_x = 2
takara_y = 5
zyotai = 0
miss = 0
nokori = Shokikaisuu
robo_x = 6
robo_y = 2
message = ""
direc = 0
hosuu = 0
d_x = 0
d_y = 0

def banmen():
   global zyotai
   global message
   calc()
   for i in range(WANASUU):
       if Wana_hyoji[i] == 1:
           canvas.create_image(
               Wana_x[i]*50, Wana_y[i]*50, 
               image=wana, 
               anchor=tk.SE,
               tag = "wana" + str(i)
           )
   canvas.moveto("robo", robo_x*50-38, robo_y*50-45)
   info1.config(text="宝までの最小歩数 "+str(hosuu))
   info2.config(text="残り操作回数 "+str(nokori))
   info3.config(text="罠にかかった回数 "+str(miss))
   info4.config(text=message)
   if zyotai != 0:
       btn7.place(x=550, y=280)
       
# 盤面の描画
for x in range(YOKO):
   for y in range(TATE):
       px = x * unitw + 3
       py = y * unitw + 3
       canvas.create_rectangle(
           px, py, px + unitw, py + unitw
       )
       
# 開発時の image に関するエラーが出たら 1==0 にして対処
if 1 == 1:
   # robot の基準位置は,moveto のときが NW なのでそれに合わせる
   canvas.create_image(
       robo_x*50-38, robo_y*50-45, 
       image=robot, 
       anchor=tk.NW,
       tag = 'robo'
   )
   canvas.create_image(
       takara_x*50, takara_y*50, 
       image=takara, 
       anchor=tk.SE
   )

def direction(event):
   global d_x
   global d_y
   global direc
   if event.widget == btn1:
       direc = 1
       d_x = 0
       d_y = -1
   elif event.widget == btn2:
       direc = 2
       d_x = 0
       d_y = 1
   elif event.widget == btn3:
       direc = 3
       d_x = -1
       d_y = 0
   else:
       direc = 4
       d_x = 1
       d_y = 0
   info0.config(text=direcstr[direc]+"方向に進む")
def reset(even):
   global zyotai
   global miss
   global nokori
   global robo_x
   global robo_y
   global message
   global direc
   global hosuu
   global d_x
   global d_y
   
   zyotai = 0
   miss = 0
   nokori = Shokikaisuu
   robo_x = 6
   robo_y = 2
   message = ""
   direc = 0
   hosuu = 0
   d_x = 0
   d_y = 0
   for i in range(WANASUU):
       if Wana_hyoji[i] == 1:
           canvas.delete("wana" + str(i))
           Wana_hyoji[i] = 0
   btn7.place(x=800, y=0)
   info0.config(text="方向をクリック")
   banmen()
btn1 = tk.Button(text="上") 
btn2 = tk.Button(text="下") 
btn3 = tk.Button(text="左") 
btn4 = tk.Button(text="右")
btn5 = tk.Button(text="移 動", font=("",16))
btn6 = tk.Button(text="罠探知", font=("",16))
btn7 = tk.Button(text="もう一度やる", font=("",16),command=reset)
info0 = tk.Label(text="方向をクリック", font=("",16))
info1 = tk.Label(text="宝までの最小歩数 "+str(hosuu))
info2 = tk.Label(text="残り操作回数 "+str(nokori))
info3 = tk.Label(text="罠にかかった回数 "+str(miss))
info4 = tk.Label(text=message, justify=tk.LEFT, font=("",18))
canvas.place(x=50, y=50)
btn1.place(x=500, y=75)
btn2.place(x=500, y=125)
btn3.place(x=470, y=100)
btn4.place(x=525, y=100)
btn5.place(x=600, y=80)
btn6.place(x=600, y=120)
info0.place(x=470, y=50)
info1.place(x=50, y=315)
info2.place(x=200, y=315)
info3.place(x=350, y=315)
info4.place(x=470, y=225)
btn1.bind("<ButtonPress>", direction)
btn2.bind("<ButtonPress>", direction)
btn3.bind("<ButtonPress>", direction)
btn4.bind("<ButtonPress>", direction)
btn5.bind("<ButtonPress>", action)
btn6.bind("<ButtonPress>", action)
btn7.bind("<ButtonPress>", reset)
root.mainloop()

空行も含めて243行になった。今までで一番長い。GUIが入るとそれだけコード量が増すわけだ。
 同じことができるCindyscriptのコードは152行。図形のファイルは中に取り込めてしまうので,「同じディレクトリに置く」といったことも不要。Cinderellaのファイル一つを渡せばすむ。
 こうして作ってみると,授業でPythonを使うのが本当にいいのかどうか疑問になる。生徒が書く分には Python でも Cindyscript でも JavaScript でもたいして変わらない。センター試験の過去問演習などやらないのであればGUIで苦労する必要もない。その過去問演習にしても,紙の上で解くだけなら,準備も何もいらない。
 しかし,何のためのプログラミング演習なのかを考えてみよう。教科書にあるからでも,大学入試に出るからでもないだろう。将来(大学で)使うからだ。文系であってもこれからはコンピュータを使った解析はどんどん取り入れられるだろうからプログラミングの知識は必要になる。
 それともうひとつ。プログラミングは模型作りと同じ。部品を組み合わせて完成した時に満足考えられる。プログラムが動いたときに満足感が得られるのだ。穴埋めだけのプログラミング,紙の上だけのプログラムングの何が楽しいだろう。

 生徒の「動いた!」という声を聞くために,面倒なGUIの準備をするのだ。