三目並べを作る

tkinterを使って三目並べを作ってみました。
完成したものはこんな感じです。

f:id:pytry3g:20180626132530p:plain

これの作り方の核となる部分を書いてみます。

 

メインウィンドウを作る

はじめに、メインウィンドウ作ります。

import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        super(App, self).__init__()
        # Window
        self.title("三目並べ")
        self.geometry("{}x{}+{}+{}".format(360, 400, 450, 100))

    def run(self):
        self.mainloop()

        
if __name__ == "__main__":
    app = App()
    app.run()

このプログラムを実行すると、下のウィンドウが立ち上がります。

このウィンドウに盤面とボタンを貼り付けていきます。

f:id:pytry3g:20180626180833p:plain

 

前準備

    def set_variables(self):
        self.result = 0
        self.playerTurn = 1
        self.board2info = [0] * 9
        self.symbol = " ox"
        self.alpstr = "abcdefghi"
        self.winner = ["", "あなた", "引き分け", "CPU"]

self.playerTurnが1ならユーザのターン、-1ならCPUのターン。

self.board2infoには盤面の情報が入る。0は空白、1は〇、-1は✖。

盤面の作成

Canvasを使って盤面を作成します。
長方形のCanvasのオブジェクトを作った後に、create_rectangleメソッドで3x3の9個の正方形を作成し、Canvasのオブジェクト上に作っていきます。

    def set_board(self):
        self.board = tk.Canvas(self, bg="white", width=340, height=340)
        self.tag2pos = {}
        position = [(20, 20, 120, 120), (120, 20, 220, 120), (220, 20, 320, 120),
                    (20, 120, 120, 220), (120, 120, 220, 220), (220, 120, 320, 220),
                    (20, 220, 120, 320), (120, 220, 220, 320), (220, 220, 320, 320)]

        for tag, pos in zip(self.alpstr, position):
            self.tag2pos[tag] = pos[:2]
            self.board.create_rectangle(*pos, fill='green yellow', outline='green yellow', tags=tag)
            self.board.tag_bind(tag, "<ButtonPress-1>", self.pressed)
        # Vertical line
        for x in range(120, 320, 100):
            self.board.create_line(x, 20, x, 320)
        # Horizontal line
        for y in range(120, 320, 100):
            self.board.create_line(20, y, 320, y)
        self.board.place(x=10, y=0)

バインディングの設定

tag_bindを使ってバインディングの設定をします。先ほど正方形を作るときにtagの設定をしていました。
このtagは作成した正方形を識別するために必要なものです。
今回は以下のようにtagの設定をしました。

f:id:pytry3g:20180626193346p:plain

これらの正方形にtag_bindを使って、9個の正方形と左クリックを紐つけています。
こうすることにより、正方形が左クリックされたとき、self.pressedメソッドが呼ばれるようになります。

 

ゲームの流れ

人間とCPUが交互に〇と✖を書き込んでいくように実装しました。

プログラムを動かすと、ウィンドウが立ち上がりすでにゲームができる状態です。

この状態で盤面のマスをクリックをすると、〇がクリックしたマスに書き込まれます。

この処理はself.pressedメソッドで行われます。

    def pressed(self, event):
        if self.thread.keylock:
            return
        item_id = self.board.find_closest(event.x, event.y)
        tag = self.board.gettags(item_id[0])[0]
        #print("Tag {} pressed...".format(tag))
        action = self.alpstr.index(tag)
        state = self.board2info[action]
        if state in [-1, 1]:
            return
        self.update_board(action, tag)
        if self.result:
            self.thread.keylock = 1
            return
        self.thread()

盤面がクリックされると、self.pressedメソッドが呼ばれます。
はじめのself.thread.keylockの役割はユーザがマスをクリックできるかを判定しています。CPUのターンやゲームが終了したときにクリックできないようにしています。

次に、どこがクリックされたのかを見ています。クリックされたところにすでに〇か✖が書き込まれていた場合、何もしません。

クリックされたところが空白ならば盤面の更新をします。

    def update_board(self, action, tag):
        self.board2info[action] = self.playerTurn
        self.draw_symbol(tag)
        self.check_result()
        self.playerTurn = -self.playerTurn

ここでは盤面の情報を更新した後、記号をCanvas上に描いてゲームの決着がついたかを見ています。

 

ソースコード

import threading
import time
import random
import tkinter as tk


class Thread(threading.Thread):
    def __init__(self):
        super(Thread, self).__init__()
        self.is_running = True
        self.thlock = 1
        self.keylock = 0
        self.parent = None
        self.start()

    def __call__(self):
        self.keylock = 1
        self.unlock()

    def set_parent(self, parent):
        self.parent = parent

    def lock(self):
        self.thlock = 1

    def unlock(self):
        self.thlock = 0

    def is_lock(self):
        return self.thlock

    def random_choice(self):
        time.sleep(0.5)
        i = random.choice([i for i, v in enumerate(self.parent.board2info) if v == 0])
        tag = self.parent.alpstr[i]
        self.parent.update_board(i, tag)
        if self.parent.result:
            self.lock()
            return
        self.parent.playerTurn = 1
        self.lock()
        self.keylock = 0

    def alpha_zero(self):
        pass

    def run(self):
        while self.is_running:
            if self.is_lock():
                continue
            self.random_choice()


table = """
Turn {}
        |{:^3s}|{:^3s}|{:^3s}|
        -------------
        |{:^3s}|{:^3s}|{:^3s}|
        -------------
        |{:^3s}|{:^3s}|{:^3s}|
"""

table_r = """
        結果: {}
        |{:^3s}|{:^3s}|{:^3s}|
        -------------
        |{:^3s}|{:^3s}|{:^3s}|
        -------------
        |{:^3s}|{:^3s}|{:^3s}|
"""

def check(board):
    wins = [[0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6]]
    for i in range(len(wins)):
        if board[wins[i][0]]==board[wins[i][1]]==board[wins[i][2]]==1:
            return 1
        elif board[wins[i][0]]==board[wins[i][1]]==board[wins[i][2]]==-1:
            return 1
    return [2, 0][0 in board]

class App(tk.Tk):
    def __init__(self):
        super(App, self).__init__()
        # Window
        self.title("三目並べ")
        self.geometry("{}x{}+{}+{}".format(360, 400, 450, 100))
        # Set up some variables
        self.set_variables()
        # Set up game board
        self.set_board()
        # Set up some buttons
        self.set_button()
        # Set threading
        self.thread = Thread()
        self.thread.set_parent(self)

    def set_variables(self):
        self.result = 0
        self.playerTurn = 1
        self.board2info = [0] * 9
        self.symbol = " ox"
        self.alpstr = "abcdefghi"
        self.winner = ["", "あなた", "引き分け", "CPU"]

    def set_board(self):
        self.board = tk.Canvas(self, bg="white", width=340, height=340)
        self.tag2pos = {}
        position = [(20, 20, 120, 120), (120, 20, 220, 120), (220, 20, 320, 120),
                    (20, 120, 120, 220), (120, 120, 220, 220), (220, 120, 320, 220),
                    (20, 220, 120, 320), (120, 220, 220, 320), (220, 220, 320, 320)]

        for tag, pos in zip(self.alpstr, position):
            self.tag2pos[tag] = pos[:2]
            self.board.create_rectangle(*pos, fill='green yellow', outline='green yellow', tags=tag)
            self.board.tag_bind(tag, "<ButtonPress-1>", self.pressed)
        # Vertical line
        for x in range(120, 320, 100):
            self.board.create_line(x, 20, x, 320)
        # Horizontal line
        for y in range(120, 320, 100):
            self.board.create_line(20, y, 320, y)
        self.board.place(x=10, y=0)

    def set_button(self):
        self.reset = tk.Button(self, text="reset", relief="groove", command=self.clear)
        self.reset.place(x=170, y=360)
        self.quit_program = tk.Button(self, text="quit", relief="groove", command=self.close)
        self.quit_program.place(x=320, y=360)

    def clear(self):
        self.board.delete("all")
        self.set_variables()
        self.set_board()
        self.thread.keylock = 0

    def draw_symbol(self, tag):
        symbol = self.symbol[self.playerTurn]
        x, y = self.tag2pos[tag]
        self.board.create_text(x+50, y+50,
                               font=("Helvetica", 60),
                               text=symbol)

    def pressed(self, event):
        if self.thread.keylock:
            return
        item_id = self.board.find_closest(event.x, event.y)
        tag = self.board.gettags(item_id[0])[0]
        #print("Tag {} pressed...".format(tag))
        action = self.alpstr.index(tag)
        state = self.board2info[action]
        if state in [-1, 1]:
            return
        self.update_board(action, tag)
        if self.result:
            self.thread.keylock = 1
            return
        self.thread()

    def update_board(self, action, tag):
        self.board2info[action] = self.playerTurn
        self.draw_symbol(tag)
        self.check_result()
        self.playerTurn = -self.playerTurn

    def check_result(self):
        result = check(self.board2info)
        winner = "Turn {}".format("?")
        if result:
            winner = self.winner[result] if result == 2 else self.winner[self.playerTurn]
            print(table_r.format(winner, *[[" ", "o", "x"][i] for i in self.board2info]))
        else:
            print(table.format("?", *[[" ", "o", "x"][i] for i in self.board2info]))
        self.result = result

    def close(self):
        self.thread.is_running = 0
        self.quit()

    def run(self):
        self.mainloop()


if __name__ == "__main__":
    app = App()
    app.run()

ちなみに今回はThreadを使っています。
なぜThreadを使ったかというと、思考に時間が少しかかるAIを実装するためです。
AIを実装した三目並べはThreadを使わなくてもtkinterのafterやupdateメソッドを使えば
実装できますが、AIの思考時間が長いとアプリケーションが固まってしまい停止するので、回避策としてThreadを使います。
自分はあまりThreadについての知識がないので、今回書いたプログラムがいいのかわかりませんが、うまくいったので、これでよしとします。

続き

AlphaZeroを使って三目並べを学習させたAIと対戦できるプログラムを書いたので、近日中に書く予定です。

www.pytry3g.com