Reactのチュートリアルの三目並べをTkinterでやってみる

#今度はPythonで・・・
またまたくどいですが三目並べチュートリアルを勉強も兼ねてPythonでやってみます。
GUIのアプリを作成する場合に、[Tkinter](https://docs.python.org/ja/3/library/tkinter.html)を使用するのが一般的らしいです。こちらを使用していきます。

開発環境は[VSCode](https://azure.microsoft.com/ja-jp/products/visual-studio-code/)で、Pythonのインストールは[以前の記事](https://qiita.com/GodPhwng/items/82bf5ec87cf5e93bd53f#環境構築)で行いました。

#Tkinterのインストール
ターミナル上で以下を実行します。

```
python -m tkinter
```

#マス目に数値を表示させる
こちらの[Hello world](https://docs.python.org/ja/3/library/tkinter.html#a-simple-hello-world-program)を参考に、メイン処理を記述します。

マス目にはラベルWidget？を使用しました。
ttkの[ウィジェット](https://docs.python.org/ja/3/library/tkinter.ttk.html#widget)が主流らしいです多分。
今回はtkの方のウィジェット使ってます。

```python:tictactoe.py
import tkinter as tk
import tkinter.font as font


class Application(tk.Frame):

    def __init__(self, master=None):
        super().__init__(master)
        master.title('三目並べ')
        master.geometry('600x400')
        self.master = master
        self.pack(anchor=tk.W)
        self.create_squares()

    def create_squares(self):
        labelFont = font.Font(size=30, weight='bold')
        for i in range(0, 3):
            for j in range(0, 3):
                tblTxt = i * 3 + j
                square = tk.Label(self, text=tblTxt, bg='white', bd=2,
                                  font=labelFont, relief='groove', width=5, height=2)
                square.grid(column=j, row=i)


root = tk.Tk()
Application(master=root)
root.mainloop()
```
`grid`のcolumn,rowの指定でマス目を9つ配置しています。

**デバッグ**
デバッグしたかったので、以下のVSCodeのデバッグマークから`launch.json`を作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526982/fd7d751d-b1f1-8cb6-6da2-139adc4c574f.png)

```javascript:launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal"
        }
    ]
}
```
デフォルトのままです。

**実行結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526982/ddb3aa99-d41f-5dbb-6d9f-26cf2dd767cc.png)

#マス目にX／Oを表示させる

```py:tictactoe.py
import tkinter as tk
import tkinter.font as font


class Application(tk.Frame):

    def __init__(self, master=None):
        super().__init__(master)
        master.title('三目並べ')
        master.geometry('600x400')
        self.master = master
        self.pack(anchor=tk.W)
        self.xIsNext = True
        self.create_squares()

    def create_squares(self):
        labelFont = font.Font(size=30, weight='bold')
        for i in range(0, 3):
            for j in range(0, 3):
                square = tk.Label(self, text='', bg='white', bd=2,
                                  font=labelFont, relief='groove', width=5, height=2)
                square.grid(column=j, row=i)
                square.bind("<1>", self.btnClick)

    def btnClick(self, event):
        label = event.widget
        if label['text'] == '':
            label['text'] = 'X' if self.xIsNext else 'O'
            self.xIsNext = False if self.xIsNext else True


root = tk.Tk()
Application(master=root)
root.mainloop()
```
作成した`square`に`bind`を使用してイベントを設定しました。
第1引数と第2引数はお決まりのようです。
selfがおそらくApplicationのインスタンス。(自身)
eventがクリックイベント発生時のイベントオブジェクト。

#履歴ありの完成までもっていく

なぜこう冗長になるのか・・・

```py:tictactoe.py
import tkinter as tk
import tkinter.font as font


class Application(tk.Frame):

    def __init__(self, master=None):
        super().__init__(master)
        master.title('三目並べ')
        master.geometry('600x400')
        self.master = master
        self.pack()

        self.lFrame = tk.Frame(self)
        self.lFrame.grid(column=0, row=0)
        self.rFrame = tk.Frame(self)
        self.rFrame.grid(column=1, row=0)

        self.squares = []
        self.stepNumber = 0
        self.history = [{'squares': [''] * 9, 'message':'次の手番 ：X'}]

        # メッセージ
        self.message = tk.Label(self.rFrame, text='次の手番 ：X')
        self.message.pack(anchor=tk.N)

        # 履歴ボタン
        button = tk.Button(self.rFrame, text='Go to game start')
        button.bind('<1>', lambda e, args={'index': 0}: self.histBtnClick(e, args))
        button.pack()
        self.buttons = [button]

        # マス目
        self.create_squares()

        # 勝ちライン
        self.winnerLines = [
            [0, 1, 2],
            [3, 4, 5],
            [6, 7, 8],
            [0, 3, 6],
            [1, 4, 7],
            [2, 5, 8],
            [0, 4, 8],
            [2, 4, 6]
        ]

    def create_squares(self):
        labelFont = font.Font(size=30, weight='bold')
        for i in range(0, 3):
            for j in range(0, 3):
                square = tk.Label(self.lFrame, text='', bg='white', bd=2,
                                  font=labelFont, relief='groove', width=5, height=2)
                square.grid(column=j, row=i)
                square.bind("<1>", lambda e, args={'index': i * 3 + j}: self.sqClick(e, args))
                self.squares.append(square)

    def sqClick(self, event, args):

        # カレント
        current = self.history[self.stepNumber]

        label = event.widget

        if label['text'] == '' and not self.calculateWinner(current['squares']):
            label['text'] = 'X' if self.stepNumber % 2 == 0 else 'O'

            # シャローコピー
            squares = current['squares'].copy()
            squares[args['index']] = label['text']

            # 手番
            message = '次の手番 ：' + ('O' if self.stepNumber % 2 == 0 else 'X')
            if self.calculateWinner(squares):
                message = '勝者　:' + label['text']

            for i in range(len(self.history) - (self.stepNumber + 1)):
                self.history.pop()
                button = self.buttons.pop()
                button.destroy()

            self.stepNumber += 1
            self.message['text'] = message
            self.history.append({'squares': squares, 'message': f'{message}'})
            self.createButton(self.stepNumber)

    def createButton(self, index):
        button = tk.Button(self.rFrame, text=f'Go to move #{index}')
        button.bind('<1>', lambda e, args={'index': index}: self.histBtnClick(e, args))
        self.buttons.append(button)
        button.pack()

    def histBtnClick(self, event, args):
        self.stepNumber = args['index']

        history = self.history[self.stepNumber]
        self.message['text'] = history['message']

        # マス目を上書き
        for i, square in enumerate(history['squares']):
            self.squares[i]['text'] = square

    # reactのチュートリアルから拝借
    def calculateWinner(self, squares):
        for line in self.winnerLines:
            if squares[line[0]] != '' \
                    and squares[line[0]] == squares[line[1]] \
                    and squares[line[0]] == squares[line[2]]:
                return squares[line[0]]
        return None


root = tk.Tk()
Application(master=root)
root.mainloop()
```
マス目の右にボタンと手番のメッセージを配置するために、一番外枠にframeを用意し、その中にさらに左と右でframeを用意しました。

**結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526982/fbb461d3-2f53-6f38-4962-0e331ee18adc.png)

#感想
Python独特のインデントで区別する感じはまだ違和感があります・・・。
レイアウト配置に少し手間取ったので、VisualStudioみたいにデザイナーでGUIを使って配置したいんですけど、Pythonにあるのでしょうか。

機械学習等もやっていきたいので、これからもPythonは触っていこうと思います。