Python
GUI
Tk
将棋

tkinterを使ってはさみ将棋の将棋盤を作る

tkinterを使ってはさみ将棋の将棋盤を作ってみました。
この記事ではtkinterを使ったはさみ将棋の将棋盤の作り方を書いてみます。

はさみ将棋

はさみ将棋とは、その名の通り相手の駒を自分の駒ではさんで取り合うゲームです。
使う駒は歩だけで、初期配置は以下のような感じです。
一方は歩を使い、もう一方は歩を裏返してとを使います。

new_img.png

詳細は下のリンクを参照してください。
https://www.shogi.or.jp/knowledge/hasami_shogi/

tkinter

tkinterとはGUIの開発で用いられるTcl/Tkをpythonで使うためのライブラリです。tkinterは標準で入っているので他のGUIライブラリと違ってインストールに手間取ることはありません。

tkinterでのGUIの作成は以下のステップになります。
  1. tkinterをインポート
  2. メインウィンドウの作成
  3. Widget(ボタンやラベル等の部品)を作成し、メインウィンドウに配置する
  4. イベントループの開始

プログラムにすると下のようになります。(※Widgetの作成と配置はしていません。)

import tkinter as tk
root = tk.Tk()
root.mainloop()

1行目でtkinterをtkとしてインポート。
2行目でメインウィンドウの作成。
3行目でイベントループが始まり、メインウィンドウが立ち上がります。イベントループが始まると、ユーザからの処理(イベント)待ちの状態になります。

ここで、メインウィンドウの設定を少し紹介します。
タイトルの設定
titleメソッドを使ってタイトルの設定ができます。
root.title("はさみ将棋")

メインウィンドウのサイズと位置の設定
geometryメソッドを使うとメインウィンドウのサイズと立ち上がり時の位置を指定できます。
root.geometry("{}x{}+{}+{}".format(width, height, x, y))
サイズだけ設定する。
root.geometry("{}x{}".format(width, height))
位置だけ設定する。
root.geometry("+{}+{}".format(x, y))

メインウィンドウの拡大・縮小の設定
デフォルトではメインウィンドウの拡大・縮小はできますが、resizableメソッドを使えば、拡大・縮小ができなくなります。
root.resizable(width=0, height=0)

アイコンの設定
iconbitmapメソッドにアイコンファイルを渡すことで、タイトル左とタスクバーのアイコンの設定ができる。
root.iconbitmap("hasami.ico")

いくつかの設定方法を書きましたが、まとめると下のプログラムになります。

import tkinter as tk
root = tk.Tk()
root.title("はさみ将棋")
root.geometry("{}x{}+{}+{}".format(400, 400, 300, 100))
root.resizable(width=0, height=0)
root.iconbitmap("hasami.ico")
root.mainloop()

このプログラムを実行すると下のウィンドウが出てきます。
(※アイコン画像は各自用意してください。)

new_root.png

ここからは、上で書いたプログラムをAppクラスに書き直して進めていきます。

import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        super(App, self).__init__()
        self.title("はさみ将棋")
        self.geometry("{}x{}+{}+{}".format(400, 400, 300, 100))
        self.resizable(width=0, height=0)
        self.iconbitmap("hasami.ico")

    def run(self):
        self.mainloop()


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

この時点で、ステップ1と2は完了しています。
1. tkinterをインポート
2. メインウィンドウの作成
3. Widget(ボタンやラベル等の部品)を作成し、メインウィンドウに配置する
4. イベントループの開始

Widget

ここから、widgetの作成と配置をしていきます。widgetのひとつCanvasを使って将棋盤を作ります。1

Canvas

Canvasを使うと、図形や文字が描けたり、画像の貼り付けなどができます。
Canvasを使って将棋盤を作ります。
先ほどのAppクラスにset_widgetsメソッドを追加して、ここにCanvasの作成をします。
Canvasの作り方は、
  widget = Canvas(parent, **options) で作ります。
parentはwidgetの配置先です。optionでCanvasのサイズや色などいろいろ設定できます。

set_widgets()
    def set_widgets(self):
        ### 将棋盤を作る ###
        self.board = tk.Canvas(self, width=400, height=400, bg="Peach Puff3")
        self.board.pack()

上でしていることは、Canvasのwidgetを作りその配置先にself(メインウィンドウ)を指定して、optionでサイズと背景の設定をしています。
widgetを生成しただけではメインウィンドウには表示されないのでpackメソッドを使いCanvasをメインウィンドウに貼り付けています。
プログラムを実行すると、以下のウィンドウがでるはずです。

new_canvas.png

create_rectangle

create_rectangleメソッドで長方形を作りCanvas上に配置していきます。
この長方形を駒とマスの代わりとして使います。
使い方は
  create_rectangle(bbox, **options)
bboxにはバウンディングボックスの設定をして、optionでfill(背景)とtagの設定をします。
例えば、
バウンディングボックスを(20, 20, 60, 90), 背景を赤色にすると

set_widgets()
    def set_widgets(self):
        ### 将棋盤を作る ###
        self.board = tk.Canvas(self, width=400, height=400, bg="Peach Puff3")
        self.board.pack()

        # 長方形
        self.board.create_rectangle((20, 20, 60, 90), fill="red")

new_rectangle_explanation.png

こんな感じのウィンドウがでるはずです。
将棋盤は9x9の合計81個のマスがあるので、create_rectangleを使って81個のマスを作っていきます。

set_widgets()
    def set_widgets(self):
        ### 将棋盤を作る ###
        self.board = tk.Canvas(self, width=400, height=400, bg="Peach Puff3")
        self.board.pack()

        ### 長方形を作る ###
        # 将棋盤の情報
        # -1 -> 盤の外, 0 -> 空白
        self.board2info = [-1] * 11 + [[0, -1][i in [0, 10]] for i in range(11)] * 9 + [-1] * 11
        # {tag: position}
        self.tag2pos = {}
        # 座標からtagの変換
        self.z2tag = {}
        # 符号
        self.numstr = "123456789"
        self.kanstr = "一二三四五六七八九"
        for i, y in zip(self.kanstr, range(20, 380, 40)):
            for j, x in zip(self.numstr[::-1], range(20, 380, 40)):
                pos = (x, y, x+40, y+40)
                tag = j + i
                self.tag2pos[tag] = pos[:2]
                self.board.create_rectangle(*pos, fill="Peach Puff3", tags=tag)
                self.z2tag[self.z_coordinate(tag)] = tag
        self.get_board_info()
        """ self.board2info
         -1  -1  -1  -1  -1  -1  -1  -1  -1  -1  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1   0   0   0   0   0   0   0   0   0  -1
         -1  -1  -1  -1  -1  -1  -1  -1  -1  -1  -1
        """

board2infoに盤面の情報を入れています。将棋は9 x 9の9路盤ですが盤外や挟まれたときの判定をしやすくするために11x11にして1次元のリストにしました。
-1は盤外、0は空白、1はと、2は歩です。この時点で駒の配置は完了していません。
上のコードにしてプログラムを実行すると下のウィンドウがでるはずです。

new_create_line.png

長方形を作るときにoptionのtagsにtagを入れています。ここでのtagとは生成した長方形を識別するための名前のようなものだと思ってください。
tagには符号を入れています。
符号は、2つの数字を使って駒の位置や動きを表すために使われます。詳しくはこちら
tagsに符号を設定することにより、長方形がクリックされたときに、どの長方形がクリックされたのかを符号で取得できるようになります。
例えば、左上の隅がクリックされたときに取得できる符号は9九です。
(※今の段階では長方形をクリックしても何の反応もないです。)

tag2posにはすべての符号の位置を入れています。テキストを長方形に描くときに使います。
z2tagは1次元の座標を符号に変換するために使います。

z_coordinateはtag(符号)から1次元の座標に変換します。

z_coordinate()
    def z_coordinate(self, tag):
        x, y = self.numstr[::-1].index(tag[0])+1, self.kanstr.index(tag[1])+1
        return y*11 + x

get_board_infoでboard2infoから盤面の情報を取得します。

get_board_info()
    def get_board_info(self, a=None, b=None):
        tags = "" if a is None else "\n{} -> {}".format(a, b)
        board_format = " {:2d} " * 11
        print(tags, *[board_format.format(*self.board2info[i:i+11]) \
                                    for i in range(0, 121, 11)], sep='\n')

create_text

初期配置をします。
draw_textメソッドを作りtag(符号)とturn(手番)を渡し、長方形に文字を描いて駒とします。
上段は,下段にはを描きます。
手番の0は相手、1は自分の手番とします。
文字を描いた後、z_coordinateに符号を渡して1次元の座標に変換し、盤面の情報を更新します。
-1は盤外、0は空白、1はと、2は歩です。

set_widgets()
        ### 文字を描く ###
        # 初期配置
        for turn, i in zip([0, 1], ["一", "九"]):
            for j in self.numstr[::-1]:
                tag = j + i
                self.draw_text(tag, turn)
                self.board2info[self.z_coordinate(tag)] = [1, 2][turn]
        self.get_board_info()
        """self.board2info
        -1  -1  -1  -1  -1  -1  -1  -1  -1  -1  -1
        -1   1   1   1   1   1   1   1   1   1  -1
        -1   0   0   0   0   0   0   0   0   0  -1
        -1   0   0   0   0   0   0   0   0   0  -1
        -1   0   0   0   0   0   0   0   0   0  -1
        -1   0   0   0   0   0   0   0   0   0  -1
        -1   0   0   0   0   0   0   0   0   0  -1
        -1   0   0   0   0   0   0   0   0   0  -1
        -1   0   0   0   0   0   0   0   0   0  -1
        -1   2   2   2   2   2   2   2   2   2  -1
        -1  -1  -1  -1  -1  -1  -1  -1  -1  -1  -1
        """

文字を描くには
  create_text(position, **options)
positionには座標が与えられ、テキストの中心が与えられた座標に位置するように描かれます。

draw_text()
    def draw_text(self, tag, turn):
        x, y = self.tag2pos[tag]
        self.board.create_text(x+20, y+20,
                               font=("Helvetica", 10, "bold"),
                               angle=[180, 0][turn],
                               text=["と", "歩"][turn],
                               tags=tag)

これで、下のような盤面になるはずです。
今、駒をクリックしても何の反応もしません。駒をクリックしたとき、何か反応を起こすために次でバインディングの設定をします。

new_create_text.png

tag_bind

バインディングの設定を作った長方形にすると、クリックしたときに指定した関数が呼ばれるようになります。
bindingメソッド内でtag_bindを使いバインディングの設定をします。
  tag_bind(item, event, callback)
item : 設定したtagもしくは長方形を作成したときの返り値。
event : イベント名。
callback : イベントが発生したときに呼ばれる関数

set_widgets()
        # バインディングの設定
        self.binding()
binding()
    def binding(self):
        for tag in self.tag2pos.keys():
            self.board.tag_bind(tag, "<ButtonPress-1>", self.board_pressed)

<ButtonPress-1>はマウスの左クリックに対応します。tag_bindを使って長方形をクリックしたとき、反応(イベント)が起きるように設定をしています。
これにより、長方形が左クリックされたとき、board_pressedメソッドが呼ばれます。

board_pressed
    def board_pressed(self, event):
        _id = self.board.find_closest(event.x, event.y)
        tag = self.board.gettags(_id[0])[0]
        print("Tag {} pressed".format(tag))

bindingメソッドで生成したすべての長方形に対して、それらが左クリックされたとき、board_pressedメソッドが呼ばれるようにバインディングの設定をしました。

board_pressedはイベントが発生すると呼ばれ、イベントクラスのオブジェクトが渡されます。
ここでしていることはfind_closestメソッドに発生したイベントの座標を渡し、どの長方形が左クリックされたのかを見つけてidとして手に入れています。
gettagsに見つけたidを渡し、その長方形のtag(符号)を手に入れ、どの符号が選択されたのかを出力しています。

ここまでざっくりと将棋盤の作り方を書いてきましたが、ここで書いたコードをまとめてみます。

import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        super(App, self).__init__()
        self.title("はさみ将棋")
        self.geometry("{}x{}+{}+{}".format(400, 400, 300, 100))
        self.resizable(width=0, height=0)
        self.iconbitmap("hasami.ico")

        self.flag = False
        self.turn = 1
        self.unpressed = 1
        self.previous_tag = None
        self.current_tag = None
        self.tmp = []
        self.candidates = []
        self.retrieves = []
        self.rtmp = []
        self.rflag = False
        self.result = [0, 0]
        self.lock = 0
        self.enlock = 0

        # Canvasの設定
        self.set_widgets()

    def set_widgets(self):
        ### 将棋盤を作る ###
        self.board = tk.Canvas(self, width=400, height=400, bg="Peach Puff3")
        self.board.pack()

        ### 長方形を作る ###
        # 将棋盤の情報
        # -1 -> 盤の外, 0 -> 空白
        self.board2info = [-1] * 11 + [[0, -1][i in [0, 10]] for i in range(11)] * 9 + [-1] * 11
        # {tag: position}
        self.tag2pos = {}
        # 座標からtagの変換
        self.z2tag = {}
        # 符号
        self.numstr = "123456789"
        self.kanstr = "一二三四五六七八九"
        for i, y in zip(self.kanstr, range(20, 380, 40)):
            for j, x in zip(self.numstr[::-1], range(20, 380, 40)):
                pos = (x, y, x+40, y+40)
                tag = j + i
                self.tag2pos[tag] = pos[:2]
                self.board.create_rectangle(*pos, fill="Peach Puff3", tags=tag)
                self.z2tag[self.z_coordinate(tag)] = tag

        ### 文字を描く ###
        # 初期配置
        for turn, i in zip([0, 1], ["一", "九"]):
            for j in self.numstr[::-1]:
                tag = j + i
                self.draw_text(tag, turn)
                self.board2info[self.z_coordinate(tag)] = [1, 2][turn]
        self.get_board_info()
        # バインディングの設定
        self.binding()

    def get_board_info(self, a=None, b=None):
        tags = "" if a is None else "\n{} -> {}".format(a, b)
        board_format = " {:2d} " * 11
        print(tags, *[board_format.format(*self.board2info[i:i+11]) \
                                    for i in range(0, 121, 11)], sep='\n')

    def draw_text(self, tag, turn):
        x, y = self.tag2pos[tag]
        self.board.create_text(x+20, y+20,
                               font=("Helvetica", 10, "bold"),
                               angle=[180, 0][turn],
                               text=["と", "歩"][turn],
                               tags=tag)

    def z_coordinate(self, tag):
        x, y = self.numstr[::-1].index(tag[0])+1, self.kanstr.index(tag[1])+1
        return y*11 + x

    def binding(self):
        for tag in self.tag2pos.keys():
            self.board.tag_bind(tag, "<ButtonPress-1>", self.board_pressed)

    def board_pressed(self, event):
        _id = self.board.find_closest(event.x, event.y)
        tag = self.board.gettags(_id[0])[0]
        print("Tag {} pressed".format(tag))

    def run(self):
        self.mainloop()


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

駒を動かして対戦する。

駒が動かせなければゲームはできないので、動かせるようにします。
駒を動かす処理はboard_pressedメソッドに書きます。
今回は先手が自分(歩)で、後手がAI(と)とします。
実際のはさみ将棋のルールとは違い、先に相手の駒を3つ取ったほうを勝ちとします。

対戦は次のように実装しました。

 1. 自分の手番。将棋盤をクリック。クリックされた駒が自分の駒かどうか確かめる
 2. 自分の駒ならば、動ける範囲のところ(候補手)の色を変える。
 3. 候補手をクリックし、駒を動かして盤面の更新。
 4. 挟んでいるか確認。相手の駒を3つ取っていたら終了。
 5. 相手の手番に変わる。
 6. 打てるところを探しランダムに打つ
 7. 挟んでいるか確認。相手の駒を3つ取っていたら終了。
 8. 1.に戻る

1~3をプログラムになおすと下のような感じになります。

    def board_pressed(self, event):
        # 自分のターンでなければ何もしない
        if self.lock:
            return
        _id = self.board.find_closest(event.x, event.y)
        tag = self.board.gettags(_id[0])[0]
        #print("Tag {} pressed".format(tag))
        #print("Z {} pressed".format(self.z_coordinate(tag)))

        # クリックされた長方形のtagから1次元の座標に変換し、
        # それをもとに盤面の情報を手に入れる。
        state = self.board2info[self.z_coordinate(tag)]
        # クリックされたのが自分の歩ならば色を変える
        # かつ、自分の歩が他に選択されていないとき
        if state == 2 and self.unpressed:
            self.board.itemconfig(tag, fill="Peach Puff2")
            # 文字が消えるので再度文字を書く
            self.draw_text(tag, 1)
            # クリックされた状態
            self.unpressed = 0
            self.previous_tag = tag
            # 候補手の探索と表示
            self.show(tag)
        elif state == 2:
            # 既に自分の歩が選択されていて、
            # 自分の他の歩を選択したとき、
            # 既に選択されているものを元に戻す。
            # その後、新しく選択した歩の色を変える。
            self.board.itemconfig(self.previous_tag, fill="Peach Puff3")
            # 文字が消えるので再度文字を書く
            self.draw_text(self.previous_tag, 1)
            self.board.itemconfig(tag, fill="Peach Puff2")
            self.draw_text(tag, 1)
            self.previous_tag = tag
            # 候補手の表示の前に、先の候補手の色を元に戻す。
            for z in self.candidates:
                ctag = self.z2tag[z]
                self.board.itemconfig(ctag, fill="Peach Puff3")
            # 候補手の探索と表示
            self.show(tag)
        elif state == 1 or self.unpressed:
            # とが選択されたとき, もしくは歩が選択されていない
            return
        else:
            # 歩が選択されていて、かつ空白をクリックしたときの処理
            # クリックされたところが、候補手にあるかどうか確認
            flag = self.click_is_valid(tag)
            if flag == 0:
                return
            self.current_tag = tag
            # クリックされたところが、候補手にあるので盤面の更新。
            self.update_board(tag)

    def update_board(self, tag):
        if self.turn:
            self.lock = 1
        # 候補手の色を元に戻す
        for z in self.candidates:
            ctag = self.z2tag[z]
            self.board.itemconfig(ctag, fill='Peach Puff3')
        self.draw_text(tag, self.turn)
        # 盤面の更新
        self.board2info[self.z_coordinate(tag)] = self.turn + 1
        self.board2info[self.z_coordinate(self.previous_tag)] = 0
        self.board.itemconfig(self.previous_tag, fill="Peach Puff3")
        self.get_board_info(self.previous_tag, tag)
        self.unpressed = 1
        self.previous_tag = None
        self.candidates = []
        # 挟まれているかの確認
        self.after(1000, self.check)

    def show(self, tag):
        # 候補手の表示
        self.candidates = []
        z = self.z_coordinate(tag)
        self.search(z)
        for z in self.candidates:
            ctag = self.z2tag[z]
            self.board.itemconfig(ctag, fill="Peach Puff1")

    def search(self, z):
        # 候補手の探索
        for num in [-11, 11, 1, -1]:
            self.tmp = []
            self.run_search(z+num, num)
            if self.tmp:
                self.candidates += self.tmp

    def run_search(self, z, num):
        v = self.board2info[z]
        if v == 0:
            self.tmp.append(z)
            self.run_search(z+num, num)
        return -1

    def click_is_valid(self, tag):
        # クリックされたところが、候補手にあるかどうか確認
        ans = self.z_coordinate(tag)
        return 1 if ans in self.candidates else 0

    def check(self):
        self.retrieves = []
        z = self.z_coordinate(self.current_tag)
        # 挟んでるかの確認
        self.is_hasami(z)

        # とる
        for z in set(self.retrieves):
            tag = self.z2tag[z]
            self.board.itemconfig(tag, fill="skyblue")
            self.draw_text(tag, self.board2info[z]-1)
        if len(self.retrieves) > 0:
            self.after(500, self.get_koma)

        # 手番を変える
        if self.turn:
            self.after(1000, self.AI)
        else:
            self.after(1000, self.YOU)

コメントでプログラムの説明をしていますが、説明しきれてないところがあるので、補足として書いときます。
unpressed
 自分の駒(歩)が選択されているかどうか。
 選択されていないならTrue

itemconfig
 itemconfig(item, **options)
 長方形の色を変えるために使っています。

after
 after(delay_ms, callback=None, *args)
 afterを使って、指定した関数をdelay_ms秒後に実行するようにしています。
 afterを使わないと、自分が打つとほぼ同時にAIが駒を動かしてしまうので使っています。
 ちゃんとしたAIを使えば思考時間が長くなると思うので、これを使う必要はなくなるかもしれません。

盤面を更新した後は挟んでいるか確認してAIの手番にします。今回のAIは打てるとこを探してそこからランダムに打つようにしています。以下は4~8を実装したものです。

    def AI(self):
        if self.enlock:
            return
        self.turn = 0
        self.candidates = []
        while True:
            z = random.choice([i for i, v in enumerate(self.board2info) if v == 1])
            # 動かす駒の符号
            self.previous_tag = self.z2tag[z]
            self.search(z)
            if self.candidates:
                break

        # 候補手からランダムに選択
        z = random.choice(self.candidates)
        # 動かした後の符号
        self.current_tag = self.z2tag[z]
        self.update_board(self.current_tag)

    def YOU(self):
            self.turn = 1
            self.lock = 0

    def is_hasami(self, z):
        # 横と縦のチェック
        for num in [-11, 11, 1, -1]:
            self.rflag = False
            self.rtmp = [z]
            self.hasami_search(z+num, num)
            if self.rflag:
                self.retrieves += self.rtmp

        # 端の探索
        for edge in [(12, 100, 1, 11), (20, 108, -1, 11), (100, 12, 1, -11), (108, 20, -1, -11)]:
            flag = self.edge_check(*edge)
            if flag:
                break

    def edge_check(self, start, end, interval_0, interval_1):
        source = [2, 1][self.turn]
        target = [1, 2][self.turn]
        tmp = []
        cnt = interval_0
        if self.board2info[start] != source:
            return
        i = start + interval_0
        while self.board2info[i] == source:
            cnt = cnt + interval_0
            i = i + interval_0
        if self.board2info[i] in [-1, 0]:
            return
        tmp += [j for j in range(start, start+cnt+interval_0, interval_0)]
        start = start + interval_1
        while True:
            flag_0 = \
                all([1 if self.board2info[j] == source else 0 for j in range(start, start+cnt, interval_0)])
            if flag_0:
                tmp += [j for j in range(start, start+cnt+interval_0, interval_0)]
                start = start + interval_1
                continue
            flag_1 = \
                all([1 if self.board2info[j] == target else 0 for j in range(start, start+cnt, interval_0)])
            if flag_1:
                tmp += [j for j in range(start, start+cnt, interval_0)]
            break
        if flag_1:
            self.retrieves += tmp
        return flag_1

    def get_koma(self):
        for z in set(self.retrieves):
            tag = self.z2tag[z]
            self.board.itemconfig(tag, fill="Peach Puff3")
            if self.board2info[z] == [1, 2][self.turn]:
                self.draw_text(tag, self.board2info[z]-1)
            else:
                self.board2info[z] = 0
                self.result[self.turn] += 1
        # 結果の確認
        if self.result[self.turn] >= 3:
            self.enlock = 1
            self.end_game()

    def hasami_search(self, z, num):
        v = self.board2info[z]
        if v == [2, 1][self.turn]:
            self.rtmp.append(z)
            self.hasami_search(z+num ,num)
        if v == [1, 2][self.turn] and len(self.rtmp) > 1:
            self.rtmp.append(z)
            self.rflag = True
        return

    def end_game(self):
        self.board.unbind("<ButtonPress-1>")
        result = self.result[0] < self.result[1]
        print("Result: {} Win".format(["相手", "あなた"][result]))

おわり

課題として以下のようなものがあると思うので、できれば今後実装していきたいと思います。

  • ランダムに打つのではなく、ちゃんとしたAIを実装する。
  • 手番を決めれるようにする。
  • 棋譜を残せるようにする。

最後に、コードを上げときます。
https://github.com/pytry3g/tkapp/blob/master/hasami/app.py


  1. tkinterでGUIアプリケーションを作る際、Frameを使うのが一般的らしいですが今回は使っていません。