楽しいカードゲームを作るまでの記録～その２～

## CUIからGUIに変更
### 【ゲームの仕様】
1．プレイヤーとエネミーの手札（１～１２）をランダムに３つ生成する
2．プレイヤーは自分の手札を選択（画像をクリック）する
3．選択された手札とエネミーがランダムに選択する手札で勝負する
4．高い数字の手札を出した方に、（自分の数字ー相手の数字）をスコアとして加算する
5. １は１２に勝利し、その際のスコアは１１とする
6．勝負を３回繰り返し、スコアの高い方がゲームの勝者となる
※手札に数字を記載してもよかったけど、記載しないとどのカードが強いのか探りながらプレイできるので、ゲーム性が向上すると思う。
### 【ゲーム画面のキャプチャ】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763806/1509f1df-4ebf-cb2c-38e2-d7d50a8f57b3.png)
### 事前準備
実行ファイルと同階層にimgフォルダを作成して、使いたい画像を入れる
例） ./img/sample/1.png
カードの大きさは「125×198」
カードの拡張子は「png」
基本のカードはファイル名がそのまま点数になる（1.pngなら1点のカード）
選択後のカードはファイル名に「_gray」をつける（1.pngなら1_gray.png）
### 実行ファイル
Tkinterを使うにあたりクラス化。
初期化はコンポーネントごとにまとめる。
手札クリック時の処理はなるべく定義した関数を呼び出すだけにする。
終了判定の画面切り替えもparts2.pyに持っていきたかったが、フレームの更新がうまくいかず断念。
```py
import tkinter as tk
import tkinter.ttk as ttk
import parts2

class Application(tk.Frame):
	
	# 画像のディレクトリ
	img_dir = "sample"
	
	# 待機時間（ミリ秒）
	sleep = 2000
	
	# 手札がとりうる値の最大値
	max_number = 12
	
	"""
	画面の初期表示
	"""
	def __init__(self,base):
		super().__init__(base)
		
		base.title("【楽しいカードゲームを作るまで】")
		base.geometry("450x570")
		base.grid_rowconfigure(0, weight=1)
		base.grid_columnconfigure(0, weight=1)

		# 最初のフレーム
		self.frame = ttk.Frame(base)
		self.frame.grid(row=0, column=0, sticky="nsew", pady=20)

		# ゲーム画面のフレーム
		self.frame_app = ttk.Frame(base)
		self.frame_app.grid(row=0, column=0, sticky="nsew", pady=20)
		
		# リザルト画面のフレーム
		self.frame_result = ttk.Frame(base)
		self.frame_result.grid(row=0, column=0, sticky="nsew", pady=20)

		# ラベル情報を初期化
		self.label_p = tk.Label(self.frame_app, text="【プレイヤーの手札】")
		self.label_p.place(x=0, y=0)
		
		# ラベル情報を初期化
		self.p_score = 0
		self.label_p_score = tk.Label(self.frame_app, text="スコア："+str(self.p_score))
		self.label_p_score.place(x=100, y=0)
		
		# ラベル情報を初期化
		self.label_e = tk.Label(self.frame_app, text="【エネミーの手札】")
		self.label_e.place(x=0, y=220)
		
		# ラベル情報を初期化
		self.e_score = 0
		self.label_e_score = tk.Label(self.frame_app, text="スコア："+str(self.e_score))
		self.label_e_score.place(x=100, y=220)
		
		# ラベル情報を初期化
		self.text_result = "Draw...\r\nTry Again!"
		self.foreground_result = "#000000"
		self.x_result = 140
		self.label_result = tk.Label(self.frame_result, text=self.text_result, foreground=self.foreground_result, font=("", 20))
		self.label_result.place(x=self.x_result, y=140)
		
		# プレイヤーの手札を初期化
		self.card_list_p = parts2.initializeNumber(self.max_number)
		print(self.card_list_p)
		
		# エネミーの手札を初期化
		self.card_list_e = parts2.initializeNumber(self.max_number)
		print(self.card_list_e)

		# プレイヤーの手札画像を初期化（左）
		self.canvas_p_1 = tk.Canvas(self.frame_app, bg="#FFFFFF", height=197, width=125)
		self.canvas_p_1.place(x=0, y=20)
		self.img_p_1 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{self.card_list_p[0]}.png', width=125, height=197)
		self.canvas_p_1.create_image(0, 0, image=self.img_p_1, anchor=tk.NW, tag='pc_1')
		self.canvas_p_1.bind("<ButtonPress-1>", self.canvasPress)
		self.canvas_p_1.extra = self.card_list_p[0]

		# プレイヤーの手札画像を初期化（真ん中）
		self.canvas_p_2 = tk.Canvas(self.frame_app, bg="#FFFFFF", height=197, width=125)
		self.canvas_p_2.place(x=150, y=20)
		self.img_p_2 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{self.card_list_p[1]}.png', width=125, height=197)
		self.canvas_p_2.create_image(0, 0, image=self.img_p_2, anchor=tk.NW, tag='pc_2')
		self.canvas_p_2.bind("<ButtonPress-1>", self.canvasPress)
		self.canvas_p_2.extra = self.card_list_p[1]

		# プレイヤーの手札画像を初期化（右）
		self.canvas_p_3 = tk.Canvas(self.frame_app, bg="#FFFFFF", height=197, width=125)
		self.canvas_p_3.place(x=300, y=20)
		self.img_p_3 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{self.card_list_p[2]}.png', width=125, height=197)
		self.canvas_p_3.create_image(0, 0, image=self.img_p_3, anchor=tk.NW, tag='pc_3')
		self.canvas_p_3.bind("<ButtonPress-1>", self.canvasPress)
		self.canvas_p_3.extra = self.card_list_p[2]
		
		# エネミーの手札画像を初期化（左）
		self.canvas_e_1 = tk.Canvas(self.frame_app, bg="#FFFFFF", height=197, width=125)
		self.canvas_e_1.place(x=0, y=240)
		self.img_e_1 = tk.PhotoImage(file=f'img\\{self.img_dir}\\uramen.png', width=125, height=197)
		self.canvas_e_1.create_image(0, 0, image=self.img_e_1, anchor=tk.NW, tag='ec_1')
		self.canvas_e_1.bind("<ButtonPress-1>", self.canvasPressDummy)
		self.canvas_e_1.extra = self.card_list_e[0]

		# エネミーの手札画像を初期化（真ん中）
		self.canvas_e_2 = tk.Canvas(self.frame_app, bg="#FFFFFF", height=197, width=125)
		self.canvas_e_2.place(x=150, y=240)
		self.img_e_2 = tk.PhotoImage(file=f'img\\{self.img_dir}\\uramen.png', width=125, height=197)
		self.canvas_e_2.create_image(0, 0, image=self.img_e_2, anchor=tk.NW, tag='ec_2')
		self.canvas_e_2.bind("<ButtonPress-1>", self.canvasPressDummy)
		self.canvas_e_2.extra = self.card_list_e[1]

		# エネミーの手札画像を初期化（右）
		self.canvas_e_3 = tk.Canvas(self.frame_app, bg="#FFFFFF", height=197, width=125)
		self.canvas_e_3.place(x=300, y=240)
		self.img_e_3 = tk.PhotoImage(file=f'img\\{self.img_dir}\\uramen.png', width=125, height=197)
		self.canvas_e_3.create_image(0, 0, image=self.img_e_3, anchor=tk.NW, tag='ec_3')
		self.canvas_e_3.bind("<ButtonPress-1>", self.canvasPressDummy)
		self.canvas_e_3.extra = self.card_list_e[2]

		# ボタンの初期化
		self.start = tk.Button(self.frame, text='ゲームを\r\n開始する', font=("", 20), command=lambda:parts2.changeAppFrame(self))
		self.start.place(x=160,y=180)

		# ボタンの初期化
		self.end = tk.Button(self.frame_app, text='ゲームを終了する', font=("", 10), command=base.destroy)
		self.end.place(x=160,y=470)
		
		# ボタンの初期化
		self.end2 = tk.Button(self.frame_result, text='ゲームを終了する', font=("", 10), command=base.destroy)
		self.end2.place(x=170,y=470)
		
		# 最初のフレームを表示
		self.frame.tkraise()
	
	"""
	プレイヤーの手札がクリックされたときのイベント処理
	"""
	def canvasPress(self,event):
		# 手札情報
		p = event.widget.extra
		e = parts2.selectRandamNumber(self.card_list_e, 3)
		print(p,e)
		
		# 選択済みなら以降の処理はスキップ
		if parts2.checkSelected(p):
			return
		
		# 手札の画像を更新
		parts2.updateImages(self,p,e)
		
		# 選択した手札を削除
		parts2.deleteNumber(p,self.card_list_p)
		parts2.deleteNumber(e,self.card_list_e)
		
		# スコア情報を更新
		parts2.setResult(self, p, e)
		
		# ラベル情報を更新
		parts2.setLabelInfo(self)
		
		# 終了判定
		if parts2.isFinished(self):
			print("finish")
			# 待機時間(ミリ秒)後にフレーム切り替え
			self.frame_app.after(
				self.sleep,
				self.changeResultFrame,
			)
		
	"""
	エネミーの手札がクリックされたときのイベント処理
	"""
	def canvasPressDummy(self,event):
		e = event.widget.extra
		print(e)
	
	"""
	ゲーム終了時にリザルト画面に切り替え
	"""
	def changeResultFrame(self):
		self.frame_result.tkraise()

def main():
	root = tk.Tk()
	app = Application(base=root)
	app.mainloop()

if __name__ == "__main__":
    main()
```
### 関数定義ファイル（parts2.py）
[その１](https://qiita.com/isukaka/items/b70979c1f046b162d2f2 "楽しいカードゲームを作るまでの記録～その１～")で作成した関数を流用。
一部関数の仕様を変更。
```py
"""
関数定義
"""
import tkinter as tk
import tkinter.ttk as ttk
import random

"""
ゲーム開始時にフレームを切り替える
"""
def changeAppFrame(self):
	self.frame_app.tkraise()

"""
ゲーム終了判定
return boolean True:終了 False:継続
"""
def isFinished(self):
	# 論理削除の個数が３つなら処理終了とする
	if self.card_list_p.count(None) == 3:
		return True
	return False

"""
選択済みの手札をクリックしているか
p プレイヤーの手札
return boolean True:選択済み False:未選択
"""
def checkSelected(p):
	if p == None:
		return True
	return False

"""
選択された手札を選択済みに更新する
p プレイヤーの手札
e エネミーの手札
"""
def updateImages(self,p,e):

	# 手札の場所を取得
	pc_idx = self.card_list_p.index(p)
	ec_idx = self.card_list_e.index(e)
	print(p,e,ec_idx)
	
	if pc_idx == 0:
		# 手札が左の場合
		self.canvas_p_1.extra = None
		self.canvas_p_1.delete('pc_1')
		self.img_p_1 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(p)}_gray.png', width=125, height=197)
		self.canvas_p_1.create_image(0, 0, image=self.img_p_1, anchor=tk.NW, tag='pc_1')
	elif pc_idx == 1:
		# 手札が真ん中の場合
		self.canvas_p_2.extra = None
		self.canvas_p_2.delete('pc_1')
		self.img_p_2 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(p)}_gray.png', width=125, height=197)
		self.canvas_p_2.create_image(0, 0, image=self.img_p_2, anchor=tk.NW, tag='pc_2')
	elif pc_idx == 2:
		# 手札が右の場合
		self.canvas_p_3.extra = None
		self.canvas_p_3.delete('pc_1')
		self.img_p_3 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(p)}_gray.png', width=125, height=197)
		self.canvas_p_3.create_image(0, 0, image=self.img_p_3, anchor=tk.NW, tag='pc_3')
	else:
		# ありえないが念のため
		self.canvas_p_1.extra = None
		self.canvas_p_1.delete('pc_1')
		self.img_p_1 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(p)}_gray.png', width=125, height=197)
		self.canvas_p_1.create_image(0, 0, image=self.img_p_1, anchor=tk.NW, tag='pc_1')
	
	if ec_idx == 0:
		# 手札が左の場合
		self.canvas_e_1.delete('ec_1')
		self.img_e_1 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(e)}_gray.png', width=125, height=197)
		self.canvas_e_1.create_image(0, 0, image=self.img_e_1, anchor=tk.NW, tag='ec_1')
	elif ec_idx == 1:
		# 手札が真ん中の場合
		self.canvas_e_2.delete('ec_2')
		self.img_e_2 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(e)}_gray.png', width=125, height=197)
		self.canvas_e_2.create_image(0, 0, image=self.img_e_2, anchor=tk.NW, tag='ec_2')
	elif ec_idx == 2:
		# 手札が右の場合
		self.canvas_e_3.delete('ec_3')
		self.img_e_3 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(e)}_gray.png', width=125, height=197)
		self.canvas_e_3.create_image(0, 0, image=self.img_e_3, anchor=tk.NW, tag='ec_3')
	else:
		# ありえないが念のため
		self.canvas_e_1.delete('ec_1')
		self.img_e_1 = tk.PhotoImage(file=f'img\\{self.img_dir}\\{str(e)}_gray.png', width=125, height=197)
		self.canvas_e_1.create_image(0, 0, image=self.img_e_1, anchor=tk.NW, tag='ec_1')

"""
手札情報を論理削除する
param number 削除する数字
param arr 削除先の配列
return arr 削除後の配列
"""
def deleteNumber(number, arr):
	del_idx = arr.index(number)
	arr[del_idx] = None
	return arr

"""
手札の初期化
max とりうる値の最大値
return arr 数字が３つ入った配列
"""
def initializeNumber(max):
	num_chk = 0
	arr = []
	while num_chk == 0:
		randam1 = random.randint(1, max)
		randam2 = random.randint(1, max)
		randam3 = random.randint(1, max)
		arr = [randam1,randam2,randam3]
		if max < 3:
			num_chk = 1
		if arr[0] != arr[1] and arr[0] != arr[2] and arr[1] != arr[2]:
			num_chk = 1
	return arr

"""
ラベル情報を更新する
"""
def setLabelInfo(self):
	
	# スコアを更新
	self.label_p_score['text'] = "スコア："+str(self.p_score)
	self.label_e_score['text'] = "スコア："+str(self.e_score)
	print(self.p_score,self.e_score)

	# スコアの文字色とリザルト表示を更新
	if self.p_score > self.e_score:
		self.label_p_score['foreground'] = '#0000ff' # 青
		self.label_e_score['foreground'] = '#ff0000' # 赤
		self.label_result['text'] = "Congratulations!\r\nYou Win!\r\nPlayer Score: "+str(self.p_score)+"\r\nEnemy Score: "+str(self.e_score)
		self.label_result['foreground'] = "#0000ff" # 青
		self.label_result.place(x=140,y=140)
	elif self.p_score < self.e_score:
		self.label_p_score['foreground'] = '#ff0000' # 赤
		self.label_e_score['foreground'] = '#0000ff' # 青
		self.label_result['text'] = "You Lose...\r\nTry Again!\r\nPlayer Score: "+str(self.p_score)+"\r\nEnemy Score: "+str(self.e_score)
		self.label_result['foreground'] = "#ff0000" # 赤
		self.label_result.place(x=140,y=140)
	else:
		self.label_p_score['foreground'] = '#000000' # 黒
		self.label_e_score['foreground'] = '#000000' # 黒
		self.label_result['text'] = "Draw...\r\nTry Again!\r\nPlayer Score: "+str(self.p_score)+"\r\nEnemy Score: "+str(self.e_score)
		self.label_result['foreground'] = "#000000" # 黒
		self.label_result.place(x=140,y=140)

"""
手札からランダムに選択
論理削除されたインデクスが選ばれたら再帰的に呼び出す
param e_number エネミー手札の配列
param range 選択肢の幅
return e_select 選択した数字
"""
def selectRandamNumber(e_number, range):
	r = random.randint(0, range-1)
	e_select = e_number[r]
	if e_select == None:
		e_select = selectRandamNumber(e_number, range)
	return e_select

"""
スコア情報をセットする
param p_input プレイヤーの手札
param e_select エネミーの手札
"""
def setResult(self, p_input, e_select):
	special = specialWin(p_input, e_select)
	if special == 1:
		self.p_score += 11
	elif special == 2:
		self.e_score += 11
	elif p_input > e_select and special == 0:
		self.p_score += p_input - e_select
	elif p_input == e_select and special == 0:
		self.p_score += 0
		self.e_score += 0
	elif p_input < e_select and special == 0:
		self.e_score += e_select - p_input
	else:
		self.p_score += 0
		self.e_score += 0

"""
特殊勝利の定義
param p_input プレイヤーの手札
param e_select エネミーの手札
return special 以下の値
	該当しない：0
	プレイヤーの勝利：1
	エネミーの勝利：2
"""
def specialWin(p_input, e_select):
	special = 0
	if int(p_input) == 1 and e_select == 12:
		special = 1
	elif int(p_input) == 12 and e_select == 1:
		special = 2
	else:
		special = 0
	return special
```