前回、オセロ盤を作るのみで対戦はできないまま終わってしまいました。
今回は対戦できるようにプログラムを書いていきます。
下に置いたプログラムは前回書いたプログラムになります。
import tkinter as tk class App(tk.Tk): def __init__(self): super(App, self).__init__() self.title("オセロ") self.geometry("{}x{}+{}+{}".format(550, 550, 300, 100)) self.resizable(0, 0) self.iconbitmap("othello.ico") self.configure(bg="white") self.set_widgets() def set_widgets(self): # オセロ盤 self.board = tk.Canvas(self, bg="lime green", width=350, height=350) self.board.pack(pady=(30, 0)) # オセロ盤の設定 self.board2info = [-1] * 10 + [[0, -1][i in [0, 9]] for i in range(10)] * 8 + [-1] * 10 # tag self.numstr = '12345678' self.alpstr = "abcdefgh" # tagからクリックされたtagの位置を得る self.tag2pos = {} # 1次元の座標からtagを得る self.z2tag = {} # 長方形を配置する。 for i, y in zip(self.numstr, range(15, 336, 40)): for j, x in zip(self.alpstr, range(15, 336, 40)): pos = x, y, x+40, y+40 tag = i + j self.tag2pos[tag] = pos self.board.create_rectangle(*pos, fill="lime green", tags=tag) self.z2tag[self.z_coordinate(tag)] = tag # 初期設定 for p, i in zip([1, 2], "45"): for q, j in zip([2, 1], "de"): color = ["black", "white"][p-q] tag = i + j self.board2info[self.z_coordinate(tag)] = [1, 2][p-q] self.board.create_oval(*self.tag2pos[tag], fill=color, tags=tag) self.get_board_info() # Label self.turn = 1 self.info = tk.Canvas(self, bg="white", width=300, height=100) self.info.pack(pady=(30, 0)) self.var = tk.StringVar() self.update_label() self.label = tk.Label(self.info, bg="white", font=("Helvetica", 15), textvariable=self.var) self.label.pack() def get_board_info(self): board_format = " {:2d} " * 10 print("", *[board_format.format(*self.board2info[i:i+10]) \ for i in range(0, 100, 10)], sep='\n') def z_coordinate(self, tag): x = self.alpstr.index(tag[1])+1 y = self.numstr.index(tag[0])+1 return y*10 + x def update_label(self): self.var.set("{}のターン".format(["AI", "あなた"][self.turn])) def run(self): self.mainloop() if __name__ == "__main__": app = App() app.run()
このプログラムをいくつか変更して対戦できるようにします。
今回やることは、
1. オセロ盤に配置された長方形(マス)がクリックされたとき、白もしくは黒の石がクリックされたところに配置するようにする。
2. 石を置いたら、相手(AI)の手番に変更し、石を置く。
※今回のプログラムでは先手番が自分、後手番がAIとし、手番の設定はできません。AIは打てるところを探してランダムに打ちます。いずれ、ちゃんとしたものを実装します。
クリックしたら石を置けるようにする
前回、create_rectangleメソッドを使って長方形(マス)をつくりました。この長方形にバインディングの設定をして、クリックしたら石を置けるようにします。
前回のプログラムの長方形を配置するところの最終行にtag_bindメソッド(紫色のとこ)を加えて、バインディングの設定をしています。
前回の記事で説明しましたが、作ったすべての長方形に対して、それぞれ固有のtagを設定しています。(長方形の別名のようなもの?)tag_bindメソッドを使い、あるtagが<ButtonPress-1>というイベントが発生したとき、self.pressedメソッドが呼ばれるように設定しています。
ここで、<ButtonPress-1>はマウスの左クリックのことです。
つまり、オセロ盤の長方形(マス)が左クリックされると、self.pressedメソッドが呼ばれるようになります。
# 長方形を配置する。 for i, y in zip(self.numstr, range(15, 336, 40)): for j, x in zip(self.alpstr, range(15, 336, 40)): pos = x, y, x+40, y+40 tag = i + j self.tag2pos[tag] = pos self.board.create_rectangle(*pos, fill="lime green", tags=tag) self.z2tag[self.z_coordinate(tag)] = tag self.board.tag_bind(tag, "<ButtonPress-1>", self.pressed)
例えば、pressedメソッドを下のようにして、プログラムを実行してみます。
オセロ盤の一番左上を左クリックすると Tag 1a pressedとでるはずです。
def pressed(self, event): id = self.board.find_closest(event.x, event.y) tag = self.board.gettags(id[0])[0] print("Tag {} pressed".format(tag))
石を置く処理はこのpressedメソッドに書きます。
そのために、まず他のメソッドの内容を少し変更します。
__init__
class App(tk.Tk): def __init__(self): super(App, self).__init__() self.title("オセロ") self.geometry("{}x{}+{}+{}".format(550, 550, 300, 100)) self.resizable(0, 0) self.iconbitmap("othello.ico") self.configure(bg="white") self.turn = 1 self.num_pass = 0 self.candidates = {} self.flag = True self.tmp = [] self.set_widgets() self.search_candidate() self.color_candidate()
いくつかのインスタンス変数を加えています。これらは石がどこに置けるのかを探索するときとかに使います。
widgetの設定をした後に、置けるところ(候補手)を探索して、候補手のマスの色を変えます。
そして、石を置く処理pressedメソッドは下のような感じになります。
はじめにどのマスがクリックされたのかを調べ、tag(符号)で手に入れます。それを1次元の座標に変換して、クリックされたところが空白ではない、もしくは置けない場所ならば何もしません。
置ける場所ならupdate_boardメソッドで石を置きます。
def pressed(self, event): id = self.board.find_closest(event.x, event.y) tag = self.board.gettags(id[0])[0] #print("Tag {} pressed".format(tag)) # 符号から1次元の座標に変換 z = self.z_coordinate(tag) if self.board2info[z] != 0: # 空白でなければ return if tag not in self.candidates: # クリックされたところが、置けない場合 return # 候補手の色を元に戻す self.back_candidate() # 盤の更新 self.update_board(tag) # 手番の変更 self.turn = 0 # ラベルの更新 self.update_label() self.get_board_info()
以下はupdate_boardメソッドの中身です。
def update_board(self, tag): for z in self.candidates[tag]: ctag = self.z2tag[z] self.board.create_oval(*self.tag2pos[ctag], fill=["black", "white"][self.turn]) self.board2info[z] = [1, 2][self.turn] self.board.create_oval(*self.tag2pos[tag], fill=["black", "white"][self.turn]) self.board2info[self.z_coordinate(tag)] = [1, 2][self.turn]
self.candidatesは__init__で辞書として作りました。この辞書のkeyには候補手(クリックされた場所の符号)、valueにはリストをいれています。そのリストにはkey(候補手)に石を置いたときにひっくり返る石が1次元の座標として入っています。候補手の探索はself.search_candidateメソッドで行います。
ここでは、始めにひっくり返す石の色を変えています。
そのあと、クリックされたところに新しく円を描いています。
相手の手番にする
自分が石を置いた後、相手(AI)の手番にします。やり方は簡単で手番を表すself.turnを0にします。(1は自分の手番)その後、候補手を探すself.search_candidateメソッドを呼ぶだけです。先ほどのpressedメソッドの最終行でself.search_candidateを使います。
def pressed(self, event): id = self.board.find_closest(event.x, event.y) tag = self.board.gettags(id[0])[0] #print("Tag {} pressed".format(tag)) # 符号から1次元の座標に変換 z = self.z_coordinate(tag) if self.board2info[z] != 0: # 空白でなければ return if tag not in self.candidates: # クリックされたところが、置けない場合 return # 候補手の色を元に戻す self.back_candidate() # 盤の更新 self.update_board(tag) # 手番の変更 self.turn = 0 # ラベルの更新 self.update_label() self.get_board_info() ##### AI ##### self.search_candidate()
おわり
ここまで、オセロ盤を作ってきましたが、tkinterの説明を中心にしてきました。
候補手の探索についての説明は割愛します。
最後に全体のコードを晒しときます。
全体のコード
import random import tkinter as tk from tkinter import messagebox class App(tk.Tk): def __init__(self): super(App, self).__init__() self.title("オセロ") self.geometry("{}x{}+{}+{}".format(550, 550, 300, 100)) self.resizable(0, 0) self.iconbitmap("othello.ico") self.configure(bg="white") self.turn = 1 self.num_pass = 0 self.candidates = {} self.flag = True self.tmp = [] self.set_widgets() self.search_candidate() self.color_candidate() def set_widgets(self): # オセロ盤 self.board = tk.Canvas(self, bg="lime green", width=350, height=350) self.board.pack(pady=(30, 0)) # オセロ盤の設定 self.board2info = [-1] * 10 + [[0, -1][i in [0, 9]] for i in range(10)] * 8 + [-1] * 10 # tag self.numstr = '12345678' self.alpstr = "abcdefgh" # tagからクリックされたtagの位置を得る self.tag2pos = {} # 1次元の座標からtagを得る self.z2tag = {} # 長方形を配置する。 for i, y in zip(self.numstr, range(15, 336, 40)): for j, x in zip(self.alpstr, range(15, 336, 40)): pos = x, y, x+40, y+40 tag = i + j self.tag2pos[tag] = pos self.board.create_rectangle(*pos, fill="lime green", tags=tag) self.z2tag[self.z_coordinate(tag)] = tag self.board.tag_bind(tag, "<ButtonPress-1>", self.pressed) # 初期設定 for p, i in zip([1, 2], "45"): for q, j in zip([2, 1], "de"): color = ["black", "white"][p-q] tag = i + j self.board2info[self.z_coordinate(tag)] = [1, 2][p-q] self.board.create_oval(*self.tag2pos[tag], fill=color, tags=tag) self.get_board_info() # Label self.turn = 1 self.info = tk.Canvas(self, bg="white", width=300, height=100) self.info.pack(pady=(30, 0)) self.var = tk.StringVar() self.update_label() self.label = tk.Label(self.info, bg="white", font=("Helvetica", 15), textvariable=self.var) self.label.pack() def get_board_info(self): board_format = " {:2d} " * 10 print("", *[board_format.format(*self.board2info[i:i+10]) \ for i in range(0, 100, 10)], sep='\n') def z_coordinate(self, tag): x = self.alpstr.index(tag[1])+1 y = self.numstr.index(tag[0])+1 return y*10 + x def update_label(self): self.var.set("{}のターン".format(["AI", "あなた"][self.turn])) def pressed(self, event): id = self.board.find_closest(event.x, event.y) tag = self.board.gettags(id[0])[0] #print("Tag {} pressed".format(tag)) # 符号から1次元の座標に変換 z = self.z_coordinate(tag) if self.board2info[z] != 0: # 空白でなければ return if tag not in self.candidates: # クリックされたところが、置けない場合 return # 候補手の色を元に戻す self.back_candidate() # 盤の更新 self.update_board(tag) # 手番の変更 self.turn = 0 # ラベルの更新 self.update_label() self.get_board_info() ##### AI ##### self.search_candidate() def search_candidate(self): self.candidates = {} for y in self.numstr: for x in self.alpstr: tag = y + x if self.board2info[self.z_coordinate(tag)] != 0: continue self._search(tag) res = self.color_candidate() if self.num_pass == 2: print("Finish") I = sum(1 for v in self.board2info if v == 2) AI = sum(1 for v in self.board2info if v == 1) messagebox.showinfo("Result", "あなた: {}\nAI: {}".format(I, AI)) return if res == -1: print("Pass") self.turn = self.turn ^ 1 self.update_label() self.search_candidate() if self.turn: return self.after(1000, self.next_turn) def next_turn(self): self.back_candidate() try: tag = random.choice(list(self.candidates.keys())) except: return self.update_board(tag) self.turn = self.turn ^ 1 self.update_label() self.get_board_info() self.search_candidate() def _search(self, tag): z = self.z_coordinate(tag) for num in [-10, 10, 1, -1, -11, 11, -9, 9]: self.flag = False self.tmp = [] res = self._run_search(z+num, num) if res == -1: continue if tag in self.candidates: self.candidates[tag] += self.tmp else: self.candidates[tag] = self.tmp def _run_search(self, z, num): v = self.board2info[z] if v in [-1, 0]: return -1 if v == (self.turn+1): return z if self.flag else -1 self.flag = True self.tmp.append(z) return self._run_search(z+num, num) def color_candidate(self): if len(self.candidates) == 0: self.num_pass += 1 return -1 for tag in self.candidates.keys(): self.board.itemconfig(tag, fill="lawn green") self.num_pass = 0 return 1 def back_candidate(self): if len(self.candidates) == 0: return -1 for tag in self.candidates.keys(): self.board.itemconfig(tag, fill="lime green") def update_board(self, tag): for z in self.candidates[tag]: ctag = self.z2tag[z] self.board.create_oval(*self.tag2pos[ctag], fill=["black", "white"][self.turn]) self.board2info[z] = [1, 2][self.turn] self.board.create_oval(*self.tag2pos[tag], fill=["black", "white"][self.turn]) self.board2info[self.z_coordinate(tag)] = [1, 2][self.turn] def run(self): self.mainloop() if __name__ == "__main__": app = App() app.run()