どん底から這い上がるまでの記録

どん底から這い上がりたいけど這い上がれない人がいろいろ書くブログ(主にプログラミング)

作ったオセロ盤で対戦できるようにする

前回、オセロ盤を作るのみで対戦はできないまま終わってしまいました。

pytry3g.hatenablog.com

 

 

今回は対戦できるようにプログラムを書いていきます。

下に置いたプログラムは前回書いたプログラムになります。

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()