PythonとTkinterでGUI三目並べ作ってみた

##初めに
python学習のために、そこらに転がっている「PythonでCUIオセロ！」みたいなやつを見様見真似でコピペしたはいいけれど、見返すと何がなんやら、ちんぷんかんぷん。一個一個理解しようにも初心者には量が多く（多くない）げんなり。

やはり、知識を自分のものにするには、手を動かして悩みながらコツコツ覚えるのが一番！
ということで、既存のコードに頼らずCUIオセロをGUIにしてみようと思い立ったのもつかの間。
何から手を付けていいのかさっぱりさっぱり・・・

初心者が身に余るものに挑戦して挫折するのはよくある話なので、とりあえず三目並べに方向転換。
少しレベルを下げてあげるだけで、なんだかできそうな気になる。単純なものである。

作る前からあれこれ考えるとやる気がなくなるので、必要に応じて調べて実装する、コード継ぎ足し方式で三目並べを作ってみました。

途中に出てくるコードは完成品を切り取っているので少々見づらいかもしれませんが悪しからず。
ページの一番下に全体像を載せています

出来上がり像
![Tictac.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/497538/f37b6145-5173-7f82-fa8d-dd546d7cf80d.gif)


##とりあえず作る
まずは画面を作る。
ボタンを搭載する。
ボタンを押す。
ここから始めていきます。

```python

from tkinter import *
from tkinter import ttk
from tkinter import messagebox

squares = 3

class TictacApp(ttk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()
        self.set_value()

    def create_widgets(self):
        for i in range(squares):
            for j in range(squares):
                button = ttk.Button(self, command=self.record(i, j))
                button.bind('<Button-1>', self.mark)
                button.grid(column=i, row=j, sticky=(N, S, E, W))

        for i in range(squares):
            self.columnconfigure(i, weight=1)
            self.rowconfigure(i, weight=1)

        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)

        self.grid(column=0, row=0, sticky=(N, S, E, W))


def main():
    root = Tk()
    root.title('三目並べ')
    TictapApp(root)
    root.mainloop()


if __name__ == '__main__':
    main()
```

どうやらTkinterというものをimportしてくればGUIの機能を搭載できるらしい。
buttonに色々な属性を与えてやれば見た目を様々に変更できそうですが、ここは電卓をマネつつ電卓よりボタンの少ない3×3の配置で。
main関数の中にmainloop()があれば、ウィンドウを閉じない限り操作が出来るそうな。
Buttonの中に関数を仕込むのが、一杉縄ではいかず、試行錯誤で色々試した結果上記に落ち着きました。

python使っていると嫌でも目に入るself。まだ、完全に理解はできていませんが、複数の関数で使う変数はselfで定義しとけばなんとかなる。くらいの認識です。

ちまちま実行しながら進めれば、何かが出来上がっていく感でモチベーションが保てるし、どのコードが何の役割かが体感的に理解できるので、いずれ別のものを作りたくなった時に、今回使った部品を出張セットとして応用できそうです。

```Python

def mark(self, event):
        if not event.widget['text']:
            if self.player == 1:
                event.widget['text'] = str('〇')
            else:
                event.widget['text'] = str('×')
```

画面が出来れば今度は機能を追加していきます。
ボタンを押したら押したところに〇か×を表示させたいということでmark関数をTictacクラスの中に追加。
Buttonのbindメソッドを使えばボタンが押されたときに関数を実行するということが出来るようなので、押したらボタン上に文字が出る関数を作成。
event.widgetのtextをstr('文字列')で上書きさせます。

```Python

    def record(self, i, j):
        def x():
            if not self.field[i][j]:
                self.field[i][j] = self.player
                self.line_check()
                self.change_player()
                self.clear()
        return x

    def set_value(self):
        self.player = 1
        self.field = []
        for i in range(squares):
            self.field.append(['' for i in range(squares)])
        self.finish = 0
```

ボタンを押せるようになったら、どのプレイヤーがどのボタンを押したかを記録する必要がありそうなので、record関数を作成。
記録を保持する入れ物もひつようになりそうなので、set_value関数を作成。
ついでに、現在のプレイヤー情報や決着判定のパラメーターも盛り込む。
record関数では入れ物にプレイヤーの取得情報をセットして、三目並んでるか判定して、プレイヤーを入れ替えて、決着がついていたら盤面を初期に戻す。と大忙しになりました。
コード継ぎ足し方式の悪いところですね。

```Python

    def line_check(self):
        cross = 0
        for i in range(squares):
            horizon = 0
            vertical = 0
            for j in range(squares):
                if self.field[i][j] == self.player:
                    horizon += 1
                if self.field[j][i] == self.player:
                    vertical += 1
            if self.field[i][i] == self.player:
                cross += 1
            if horizon == 3 or vertical == 3 or cross == 3:
                self.game_end()
        if self.field[0][2] == self.field[1][1] == self.field[2][0] == self.player:
            self.game_end()
````

三目並んでるかを判定する必要が出たので、line_check関数を作成。
縦横斜めに中身を覗いてプレイヤーと同じ値が三つあればゲーム終了となります。
最後の斜め一本の判定は力業で解決しました。

```Python

    def game_end(self):
        if self.player == 1:
            messagebox.showinfo('決着', '先行プレイヤーの勝利です！')
        else:
            messagebox.showinfo('決着', '後攻プレイヤーの勝利です！')
        self.finish = 1
```

三つ並んだら勝敗を知らせてあげないといけないですね。game_end関数の実装です。
game_end関数が呼び出されるとメッセージボックスが出現して、勝敗を知らせるとともに決着フラグを立てます。

```Python

    def change_player(self):
        if self.finish == 0:
            self.player = -self.player

    def clear(self):
        if self.finish == 1:
            self.create_widgets()
            self.set_value()
```

決着がついていなければプレイヤーを交代させて、ついていれば盤面をもう一度作成して初期化を行います。
そんなこんなで一人三目並べが完成です。

##作ってみた感想
その場その場でコードを継ぎ足したので最終的には、ものすごく見にくいものになりました。。。
完成したのでよし！
全体像考えながら、綺麗に作り上げようとするとげんなりしてしまいますが、作りながら必要に応じて機能を追加していけば、それなりにモチベーションを維持しながら完成までもっていきやすい気がします。
一個一個追加していけばその度に調べるので、消化もしやすかったです。

今回はボタンを押して関数を実行するという機能を覚えたので、次はそれを組み込みながら機能を増やしたものを作っていきたいと思います。

##出来上がりの全体コード
```Python

from tkinter import *
from tkinter import ttk
from tkinter import messagebox

squares = 3


class TictacApp(ttk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.create_widgets()
        self.set_value()

    def set_value(self):
        self.player = 1
        self.field = []
        for i in range(squares):
            self.field.append(['' for i in range(squares)])
        self.finish = 0

    def create_widgets(self):
        for i in range(squares):
            for j in range(squares):
                button = ttk.Button(self, command=self.record(i, j))
                button.bind('<Button-1>', self.mark)
                button.grid(column=i, row=j, sticky=(N, S, E, W))

        for i in range(squares):
            self.columnconfigure(i, weight=1)
            self.rowconfigure(i, weight=1)

        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)

        self.grid(column=0, row=0, sticky=(N, S, E, W))

    def mark(self, event):
        if not event.widget['text']:
            if self.player == 1:
                event.widget['text'] = str('〇')
            else:
                event.widget['text'] = str('×')

    def record(self, i, j):
        def x():
            if not self.field[i][j]:
                self.field[i][j] = self.player
                self.line_check()
                self.change_player()
                self.clear()
        return x

    def change_player(self):
        if self.finish == 0:
            self.player = -self.player

    def line_check(self):
        cross = 0
        for i in range(squares):
            horizon = 0
            vertical = 0
            for j in range(squares):
                if self.field[i][j] == self.player:
                    horizon += 1
                if self.field[j][i] == self.player:
                    vertical += 1
            if self.field[i][i] == self.player:
                cross += 1
            if horizon == 3 or vertical == 3 or cross == 3:
                self.game_end()
        if self.field[0][2] == self.field[1][1] == self.field[2][0] == self.player:
            self.game_end()

    def game_end(self):
        if self.player == 1:
            messagebox.showinfo('決着', '先行プレイヤーの勝利です！')
        else:
            messagebox.showinfo('決着', '後攻プレイヤーの勝利です！')
        self.finish = 1

    def clear(self):
        if self.finish == 1:
            self.create_widgets()
            self.set_value()


def main():
    root = Tk()
    root.title('三目並べ')
    TictacApp(root)
    root.mainloop()


if __name__ == '__main__':
    main()

```