Pythonで〇×ゲームのAIを一から作成する　その38　play メソッドの改良

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/d2cce68c7a32bf3790ef
# 前回までのおさらい

## 〇×ゲームの仕様と進捗状況

1. `正方形で区切られた 3 x 3 の 2 次元のゲーム盤上でゲームを行う`
2. `ゲーム開始時には、ゲーム盤のすべてのマスは空になっている`
3. `2 人のプレイヤーが遊ぶゲームであり、一人は 〇 を、もう一人は × のマークを受け持つ`
4. `2 人のプレイヤーは、交互に空いている好きなマスに自分のマークを 1 つ置く`
5. `先手は 〇 のプレイヤーである`
6. `プレイヤーがマークを置いた結果、縦、横、斜めのいずれかの一直線の 3 マスに同じマークが並んだ場合、そのマークのプレイヤーの勝利とし、ゲームが終了する`
7. `すべてのマスが埋まった時にゲームの決着がついていない場合は引き分けとする`

仕様の進捗状況は、以下のように表記します。

* 実装が完了した部分を `背景が灰色の長方形` で記述する
* 実装の一部が完了した部分を、__太字__ で記述する

## これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/038/marubatsu.py

## 前回までのおさらい

前回の記事では、〇×ゲームを遊ぶための `play` メソッドを実装しましたが、`play` メソッドにはいくつかの問題点があります。今回の記事では、それらの問題点を修正します。

# `play` メソッドの改良

下記を見る前に、`play` メソッドにどのような問題があり、どのような方法で改良できるかについて少し考えてみて下さい。

## 結果の表示

実際に __決着がつくまで__ 〇×ゲームを遊ぶとわかると思いますが、__`play`__ メソッドでは、__決着がついた場合__ のゲーム盤が __表示されません__。この問題は、下記のプログラムのように、__ゲームの決着がついた__、while 文の __ブロックの後__ で __ゲーム盤を表示__ することで解決できます。

```python
from marubatsu import Marubatsu

def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.judge() == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
from marubatsu import Marubatsu

def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.judge() == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
+   print(self)

Marubatsu.play = play
```
</details>


下記のプログラムを実行し、`0,0`、`0,1`、`1,0`、`1,1`、`2,0` の順でテキストボックスに入力することで、決着がついたゲーム盤が表示されることを確認できます。

```python
mb = Marubatsu()
mb.play()
```
実行結果
```
略
Turn o
oo.
xx.
...

Turn x
ooo
xx.
...

```

## 勝者の表示

上記の改良により、決着がついた際のゲーム盤が表示されるようになりましたが、__誰が勝利したか__ の __表示__ が __行われない__ という問題があります。__誰が勝利したか__ は、__`judge`__ メソッドで __判定__ でき、下記のプログラムの 16 行目のように、ゲーム盤を表示する前に `judge` メソッドの返り値である __判定結果を表示__ することで、勝者を表示できます。

```python
 1  def play(self):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.judge() == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # キーボードからの座標の入力
 9          coord = input("x,y の形式で座標を入力して下さい")
10          # x 座標と y 座標の計算
11          x, y = coord.split(",")
12          # (x, y) に着手を行う
13          self.move(int(x), int(y))
14
15      # 決着がついたので、勝者とゲーム盤を表示する
16      print("winner", self.judge())
17      print(self)
18
19  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.judge() == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、勝者とゲーム盤を表示する
    print("winner", self.judge())
    print(self)

Marubatsu.play = play
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.judge() == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、勝者とゲーム盤を表示する
+   print("winner", self.judge())
    print(self)

Marubatsu.play = play
```
</details>


下記のプログラムを実行し、`0,0`、`0,1`、`1,0`、`1,1`、`2,0` の順でテキストボックスに入力することで、勝者と、決着がついたゲーム盤が表示されることを確認できます。

```python
mb.play()
```
実行結果
```
略
Turn o
oo.
xx.
...

winner o
Turn x
ooo
xx.
...
```

上記は 〇 の勝利の場合ですが、念のため、__× の勝利__ と __引き分け__ の場合も __確認__ します。

下記のプログラムを実行し、`0,1`、`0,0`、`1,1`、`1,0`、`0,2`、`2,0` の順でテキストボックスに入力することで、× の勝利が表示されることを確認できます。

```python
mb.play()
```
実行結果
```
略
Turn x
xx.
oo.
o..

winner x
Turn o
xxx
oo.
o..
```

下記のプログラムを実行し、`0,0`、`0,1`、`1,0`、`1,1`、`2,1`、`2,0`、`0,2`、`1,2`、`2,2` の順でテキストボックスに入力することで、引き分けが表示されることを確認できます。

```python
mb.play()
```
実行結果
```
略
Turn o
oox
xxo
ox.

winner draw
Turn x
oox
xxo
oxo
```

:::note
実は、`Marubatsu.DRAW` に `"draw"` という文字列を代入したのは、 上記のように __`Marubatsu.DRAW`__ を __そのまま表示__ することで、__引き分け__ であることを __表示__ できるようにするためでした。__`Marubatsu.DRAW`__ に __別の文字列__ を __代入__ した場合は、下記のようなプログラムを記述する必要があります。

```python
if self.judge() == Marubatsu.DRAW:
    print("winner draw")
else:
    print("winner", self.judge())
```

__`Marbatsu.CIRCLE`__ や __`Marubatsu.CROSS`__ に `"o"` や `"x"` __以外の文字列を代入__ した場合も __同様__ です。
:::

## ゲーム盤の表示の改良

上記の実行結果では、__決着が付いた状態__ で、`winner draw` の下に __`Turn x`__ のように、__次の手番__ が × のプレイヤーであることが __表示__ されていますが、__決着がついているのに__ 次の手番が __表示__ されるのは __おかしい__ と思いませんか？そこで、次は、__決着が付いた場合__ は、__手番を表示しない__ ように改良することにします。

ゲーム盤の __表示__ に関する __処理__ は __[`__str__`](https://qiita.com/ysgeso/items/ed1eb1cacf9a3e7dfa29#__str__-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)__ メソッドで行っているので、この __メソッドの中__ で、ゲームの __決着がついている場合__ は __手番を表示しない__ ように __修正__ することにします。また、そのついでに、__勝利者の表示__ も __`__str__`__ メソッドの中で __まとめて行う__ ように __修正__ することにします。そのように修正することで、__決着がついた場合__ に、__`print(self)` だけを記述__ すれば済むようになります。修正後の `__str__` メソッドは以下のようになります。

* __3、4 行目__：`judge` メソッドを使って、ゲームの __決着がついていないこと__ を __判定__ し、__決着がついていない__ 場合は、__手番を表す文字列__ を __`text` に代入__ する
* __6、7 行目__：__決着がついている__ 場合は、`judge` メソッドを使って __勝者を表す文字列__ を __`text` に代入__ する[^1]

```python
 1  def __str__(self):
 2      # ゲームの決着がついていない場合は、手番を表示する
 3      if self.judge() == Marubatsu.PLAYING:
 4          text = "Turn " + self.turn + "\n"
 5      # 決着がついていれば勝者を表示する
 6      else:
 7          text = "winner " + self.judge() + "\n"
 8      for y in range(self.BOARD_SIZE):
 9          for x in range(self.BOARD_SIZE):
10              text += self.board[x][y]
11          text += "\n"
12      return text
13
14  Marubatsu.__str__ = __str__

```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def __str__(self):
    # ゲームの決着がついていない場合は、手番を表示する
    if self.judge() == Marubatsu.PLAYING:
        text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.judge() + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def __str__(self):
-   text = "Turn " + self.turn + "\n"
    # ゲームの決着がついていない場合は、手番を表示する
+   if self.judge() == Marubatsu.PLAYING:
+       text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
+   else:
+       text = "winner " + self.judge() + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__
```
</details>


決着がついた際の表示を `__str__` メソッドで記述するように修正したので、__`play`__ メソッドは、下記のように、__決着がついた際の表示__ を行う処理を __削除__ します。

```python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.judge() == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.judge() == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
-   print("winner", self.judge())
    print(self)

Marubatsu.play = play
```
</details>


下記のプログラムを実行し、`0,0`、`0,1`、`1,0`、`1,1`、`2,0` の順でテキストボックスに入力することで、決着がついた場面で手番が表示されなくなることが確認できます。

```python
mb.play()
```
実行結果
```
略
Turn o
oo.
xx.
...

winner o
ooo
xx.
...
```

## `judge` メソッドの呼び出しの抑制

修正したプログラムは __正しく動作__ しますが、__`judge`__ メソッドを __必要がない__ のに __何度も呼び出す__ という、__無駄な処理__ が行われています。

具体的にどのような点が無駄であるかについて少し考えてみて下さい。

### 無駄な処理の確認

どのような点が無駄であるかについては、__`play`__ メソッドが行う処理を順に __辿っていく__ ことで __確認__ することができます。`play` メソッドは、下記のプログラムの 5 行目のように、while 文の __条件式の中__ で __`judge`__ メソッドを __呼び出し__ ています。

```python
1  def play(self):
2      # 〇×ゲームを再起動する
3      self.restart()
4      # ゲームの決着がついていない間繰り返す
5      while self.judge() == Marubatsu.PLAYING:
6          # ゲーム盤の表示
7          print(self)
8          以下略
```

また、その直後の 7 行目で __`print(self)`__ が実行されると、__`__str__`__ メソッドが __呼び出され__ ますが、下記のプログラムのように、`__str__` メソッドの 3 行目の __if 文__ の __条件式__ でも __`judge`__  メソッドが __呼び出され__ ています。このことから、__`play`__ メソッドの __5 行目__ と __7 行目__ が __実行__ されると、必ず __`judge`__ メソッドが __2 回呼び出される__ ことがわかります。

```python
1  def __str__(self):
2      # ゲームの決着がついていない場合は、手番を表示する
3      if self.judge() == Marubatsu.PLAYING:
4          text = "Turn " + self.turn + "\n"
5      # 決着がついていれば勝者を表示する
6      else:
7          text = "winner " + self.judge() + "\n"
8      以下略
```

長くなるので再掲はしませんが、__`judge`__ メソッドの __ブロックの中__ では、__勝敗判定__ を行うための、ある程度 __複雑な処理__ が行われますが、`judge` メソッドの __返り値__ は、__ゲーム盤の状況__ が __同じ__ であれば、__常に同じ結果__ が計算されます。__`play`__ メソッドの __5 行目__ と __7 行目__ を実行する際に、__着手__ は __一度も行われない__ ので、どちらも __ゲーム盤の状況__ は __同じ__ です。従って、__5 行目__ と、__7 行目__ で __`judge`__ メソッドを __呼び出し__ て判定を行うのは、__結果が変わらない__ 複雑な __作業__ を __2 度行う__ という、__無駄な作業__ を行っていることになります。

:::note
__`__str__`__ メソッドの中にも、__3 行目__ の __条件式__ の計算結果が __`False`__ になる場合に、__7 行目__ で __もう一度__ `judge` メソッドが __実行__ されるという、__同様の無駄__ があります。
:::

### 属性の追加

このような __無駄な作業__ を行う必要が __生じる理由__ は、__`judge`__ メソッドの __返り値__ がどこにも __記録されていない__ からです。__`judge`__ メソッドの __返り値__ を __記録__ するための __属性__ を用意し、そこに __`judge`__ メソッドの __返り値を代入__ することで、__勝敗判定の結果__ が __必要になった時__ に、何度も __`judge`__ メソッドを __呼び出す必要__ が __なくなります__。

プログラムに __新しい変数__ や __属性__ を __追加__ する場合は、その変数や属性の __名前__、__初期化処理__、__更新処理__、__利用方法__ について __考える__ 必要があります。

#### 属性の名前

[`test_judge`](https://qiita.com/ysgeso/items/273ed49a00b90c753031#%E8%A4%87%E6%95%B0%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E3%82%92%E3%81%BE%E3%81%A8%E3%82%81%E3%81%9F%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0) では、__`winner`__ という名前の変数に期待される `judge` メソッドの返り値を代入していましたが、`judge` メソッドの返り値には、__勝者__（winner）とは __関係のない__、__`Marubatsu.PLAYING` がある__ ので、__`winner`__ という名前は __ふさわしくありません__。__`judge`__ メソッドの __返り値__ は、__ゲームの状態__（status）を表すので、__`status`__ という名前にします。

#### 初期化処理

ゲームの __開始時__ は、__ゲームの状態__ は __`Marubatsu.PLAYING`__ なので、下記のプログラムの 5 行目のように、__再起動__ を行う __`restart`__ メソッドの中 で __初期化処理__ を行います。

```python
1  def restart(self):
2      self.initialize_board()
3      self.turn = Marubatsu.CIRCLE     
4      self.move_count = 0
5      self.status = Marubatsu.PLAYING
6   
7  Marubatsu.restart = restart
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    
Marubatsu.restart = restart
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
+   self.status = Marubatsu.PLAYING
    
Marubatsu.restart = restart
```
</details>

#### 更新処理

__`status`__ 属性には、__`judge`__ メソッドの __返り値を代入__ する必要があるので、__`judge`__ メソッドの __呼び出し__ を行う部分を __`self.status = self.judge()`__ のように修正します。

ただし、プログラムに __記述__ されている __すべて__ の __`judge`__ メソッドの __呼び出し__ をそのように __修正__ しても、__`judge`__ メソッドが __呼び出される__ ことに __変わりはない__ ので、__`judge`__ メソッドの __無駄な呼び出し__ は __解消されません__。__`judge`__ メソッドの __無駄な呼び出し__ を __解消__ するためには、__どの `judge`__ メソッドの __呼び出し__ が __必要であるか__ について考える必要があります。

〇×ゲームの、__勝敗判定__ の __結果が変わる__ のは、ゲーム盤の __状況が変わった時だけ__ で、ゲーム盤の __状況が変わる__ のは __`move`__ メソッドによって __着手を行った場合だけ__ です。従って、__`judge`__ メソッドを __実行__ する __必要がある__ のは、__`move`__ メソッドで __着手__ が行われた __場合だけ__ なので、下記のプログラムの 5 行目のように、__`move`__ メソッドで __着手__ が行われた場合に __`judge`__ メソッドを呼び出して __勝敗判定__ を行い、その __返り値__ を __`status` 属性__ に __代入__ するように修正します。

```python
1  def move(self, x, y):
2      if self.place_mark(x, y, self.turn):
3          self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
4          self.move_count += 1
5          self.status = self.judge()
6
7  Marubatsu.move = move        
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()

Marubatsu.move = move
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
+       self.status = self.judge()

Marubatsu.move = move
```
</details>

#### 利用

__`status` 属性__ の __利用場面__ は、__これまで `judge`__ メソッドの __呼び出しを記述__ していた部分なので、それらの __`self.judge()`__ を __`self.status`__ に __置きかえます__。具体的には、下記のプログラムのように __`__str__`__ メソッドの 3、7 行目を修正します。

```python
 1  def __str__(self):
 2      # ゲームの決着がついていない場合は、手番を表示する
 3      if self.status == Marubatsu.PLAYING:
 4          text = "Turn " + self.turn + "\n"
 5      # 決着がついていれば勝者を表示する
 6      else:
 7          text = "winner " + self.status + "\n"
 8      for y in range(self.BOARD_SIZE):
 9          for x in range(self.BOARD_SIZE):
10              text += self.board[x][y]
11          text += "\n"
12      return text
13
14  Marubatsu.__str__ = __str__
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def __str__(self):
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.status + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def __str__(self):
    # ゲームの決着がついていない場合は、手番を表示する
-   if self.judge() == Marubatsu.PLAYING:
+   if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
    else:
-       text = "winner " + self.judge() + "\n"
+       text = "winner " + self.status + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__

```
</details>

次に `play` メソッドの 5 行目を下記のプログラムのように修正します。

```python
 1  def play(self):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # キーボードからの座標の入力
 9          coord = input("x,y の形式で座標を入力して下さい")
10          # x 座標と y 座標の計算
11          x, y = coord.split(",")
12          # (x, y) に着手を行う
13          self.move(int(x), int(y))
14
15      # 決着がついたので、ゲーム盤を表示する
16      print(self)
17
18  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
-   while self.judge() == Marubatsu.PLAYING:
+   while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標の計算
        x, y = coord.split(",")
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>


下記のプログラムを実行し、`0,0`、`0,1`、`1,0`、`1,1`、`2,0` の順でテキストボックスに入力することで、修正したプログラムが正しく動作することが確認できます。実行結果は先ほどと同様なので省略します。

```python
mb.play()
```

## 間違った入力への対処

何度か `play` メソッドを実行して〇×ゲームを遊べばわかると思いますが、__座標の入力__ を __間違う__ と、このプログラムは __エラーが発生__ してプログラムの __処理が止まって__ しまいます。

たとえば、下記のプログラムを実行し、テキストボックスに `1,1` を入力したつもりが、__`,` を書き忘れ__ て __`11`__ を入力すると、下記のような __エラーが発生__ します。

```python
mb.play()
```
実行結果
```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 22 line 1
----> 1 mb.play()

c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 22 line 1
      9 coord = input("x,y の形式で座標を入力して下さい")
     10 # x 座標と y 座標の計算
---> 11 x, y = coord.split(",")
     12 # (x, y) に着手を行う
     13 self.move(int(x), int(y))

ValueError: not enough values to unpack (expected 2, got 1)
```

:::note
上記のエラーメッセージは、以下のような意味を持ちます。

* __ValueError__
値（value）に関するエラー
* __not enough values to unpack (expected 2, got 0)__
展開する（to unpack）ために必要な値（value）が 2 つ期待されている（expected）が、0 個（got 0）しかないため足りない（not enough）
:::

### エラーの原因

エラーの __原因__ は、`"11"` という、__中に `","` が存在しない__ 文字列に対して、__`split(",")`__ を実行すると、下記のプログラムのように、__`["11"]`__ という、__要素が 1 つしかない list__ が返されてしまうからです。__`x, y = coord.split(",")`__ のような、__list の展開__ を記述した場合、__代入__ する __変数の数__ と __list の 素の数__ が __一致しない__ と __エラーが発生__ してしまいます。

```python
print("11".split(","))
```
実行結果
```
['11']
```

__`input`__ のように、__ユーザの入力__ に __対応した処理__ を行うようなプログラムを記述する際に、上記のような、__意図しない__ ような __入力__ が行われることを __考慮する必要__ があります。このことを __考慮せず__ にプログラムを __記述__ してしまうと、上記のような __エラーが発生__ したり、エラーは発生しないが、__プログラムの挙動__ が __おかしくなってしまう__ という __バグが発生__ するからです。プログラムの初心者のうちは、そのことに気が回らないかもしれませんが、__意図しない入力__ による __バグ__ は __良く発生する__ バグなので、__意識する__ ように __心がけて__ 下さい。

:::note
__このこと__ は、`input` のようなキーボードからの文字の入力だけでなく、__定義__ した __関数の入力__ にも __あてはまります__。ただし、関数の場合は、__すべての入力を考慮__ したプログラムを __記述__ することが __大変すぎる__、または __ほぼ不可能__ な場合が多いので、__実際__ には __[型アノテーション](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#%E9%96%A2%E6%95%B0%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%A8%E8%BF%94%E3%82%8A%E5%80%A4%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B%E3%81%AE%E3%83%92%E3%83%B3%E3%83%88%E5%9E%8B%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%A1%A8%E8%A8%98)__ や __[docstring](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#docstring)__ などを __記述__ して、関数を __使う際__ に、__間違ったデータ__ を __入力しない__ ように __促す__ という __工夫が重要__ になります。
:::

### バグの修正

上記のエラーは、__`coord.split(",")`__ によって返された __list の要素__ が __2 つ以外__ の場合に発生するので、下記のプログラムのように、返された __list の要素__ が __2 つ以外__ の場合は __エラーメッセージを表示__ して、__着手の処理__ を __行わない__ ようにすることで修正できます。

* __11 行目__：__`coord.split(",")`__ の __計算結果__ を __`xylist` に代入__ する
* __13 ~ 17 行目__：__`xylist`__  の __要素の数__ が __2 でない__ 場合、15 行目で __エラーメッセージを表示__ し、17 行目で __[continue 文](https://qiita.com/ysgeso/items/1a8a4319ef3c896ef271#break-%E6%96%87%E3%81%A8-continue-%E6%96%87)__ を __実行__ することで、__残り__ の while 文の __ブロック__ を __実行せず__ に、__次の繰り返し処理__ を __行う__
* __18 行目__：__`xylist`__ の __要素の数__ が __2 である__ ことが __確定__ したので、__`xylist`__ のそれぞれの __要素__ を __`x` と `y` に展開__ する

```python
 1  def play(self):
 2      # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # キーボードからの座標の入力
 9          coord = input("x,y の形式で座標を入力して下さい")
10          # x 座標と y 座標を要素として持つ list を計算する
11          xylist = coord.split(",")
12          # xylist の要素の数が 2 ではない場合
13          if len(xylist) != 2:
14              # エラーメッセージを表示する
15              print("x, y の形式ではありません")
16              # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
17              continue
18          x, y = xylist
19          # (x, y) に着手を行う
20          self.move(int(x), int(y))
21
22      # 決着がついたので、ゲーム盤を表示する
23      print(self)
24
25  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標を要素として持つ list を計算する
        xylist = coord.split(",")
        # xylist の要素の数が 2 ではない場合
        if len(xylist) != 2:
            # エラーメッセージを表示する
            print("x, y の形式ではありません")
            # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
            continue
        x, y = xylist
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい")
        # x 座標と y 座標を要素として持つ list を計算する
-       x, y = coord.split(",")
+       xylist = coord.split(",")
        # xylist の要素の数が 2 ではない場合
+       if len(xylist) != 2:
            # エラーメッセージを表示する
+           print("x, y の形式ではありません")
            # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
+           continue
+       x, y = xylist
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

下記のプログラムを実行し、テキストボックスに `11` を入力することで、実行結果のように __エラーメッセージが表示__ され、__着手を行わず__ に __ゲームが続行__ されることが確認できます。

```python
mb.play()
```

実行結果
```
Turn o
...
...
...

x, y の形式ではありません
Turn o
...
...
...
```

## ゲームの強制終了

上記は __`11`__ という入力に対して __意図した処理__ が行われるかどうかを __確認__ するために行った作業ですが、__`11`__ を __入力した後__ も __ゲームは続行__ されます。そのため、__ゲームを終了__ させるためには、`0,0`、`0,1`、`1,0`、`1,1`、`2,0` などの順で __テキストボックスに入力__ して実際にゲームを終了させる必要があります。これは __面倒__ だと思いませんか？

VSCode の JupyterLab では、__タイトルバーの下__ にある、下図の __割り込みボタン__ を __クリック__ することで、プログラムの __実行途中__ でプログラムを __強制的に終了__ させることができます。ただし、このボタンをクリックしても __なかなか__ プログラムが __終了しなかったり__、__再起動を促すパネルが表示__ されるようなことがあるので、〇×ゲームの場合は、__別の方法__ でゲームを __簡単に終了__ させる __方法を用意__ しておいたほうが便利です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/bbe8abea-e8fe-57ba-d03d-4158526ee2d5.png" width="100">

そのような方法の一つに、__テキストボックス__ に __特定の文字列を入力__ することで、〇×ゲームを __終了させる__ という方法があります。例えば、下記は、__`exit`__ という文字列を __入力__ すると __ゲームが終了する__ ように __`play`__ メソッドを __修正__ したプログラムです。もちろん、他の文字列を入力することでゲームが終了するように変更してもかまいません。

* __9 行目__：`exit` を入力すると終了するこをと __説明するメッセージを追加__ する
* __11 ~ 13 行目__：テキストボックスに __入力した文字列__ が __`"exit"`__ と __等しいかどうか__ を __判定__ し、__等しければ__  12 行目で __ゲームの終了メッセージを表示__ し、12 行目で __return 文__ を実行することで、__`play`__ メソッドの __処理を終了__ する

```python
 1  def play(self):
 2   # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # キーボードからの座標の入力
 9          coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
10          # "exit" が入力されていればメッセージを表示して関数を終了する
11          if coord == "exit":
12              print("ゲームを終了します")
13              return       
14          # x 座標と y 座標を要素として持つ list を計算する
15          xylist = coord.split(",")
16          # xylist の要素の数が 2 ではない場合
17          if len(xylist) != 2:
18              # エラーメッセージを表示する
19              print("x, y の形式ではありません")
20              # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
21              continue
22          x, y = xylist
23          # (x, y) に着手を行う
24          self.move(int(x), int(y))
25
26      # 決着がついたので、ゲーム盤を表示する
27      print(self)
28
29  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
        # "exit" が入力されていればメッセージを表示して関数を終了する
        if coord == "exit":
            print("ゲームを終了します")
            return       
        # x 座標と y 座標を要素として持つ list を計算する
        xylist = coord.split(",")
        # xylist の要素の数が 2 ではない場合
        if len(xylist) != 2:
            # エラーメッセージを表示する
            print("x, y の形式ではありません")
            # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
            continue
        x, y = xylist
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
-       coord = input("x,y の形式で座標を入力して下さい")
+       coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
        # "exit" が入力されていればメッセージを表示して関数を終了する
+       if coord == "exit":
+           print("ゲームを終了します")
+           return       
        # x 座標と y 座標を要素として持つ list を計算する
        xylist = coord.split(",")
        # xylist の要素の数が 2 ではない場合
        if len(xylist) != 2:
            # エラーメッセージを表示する
            print("x, y の形式ではありません")
            # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
            continue
        x, y = xylist
        # (x, y) に着手を行う
        self.move(int(x), int(y))

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

下記のプログラムを実行し、`exit` をテキストボックスに入力することで、メッセージを表示してゲームが終了することが確認できます。

```python
mb.play()
```

実行結果

```
Turn o
...
...
...

ゲームを終了します
```

## ゲーム盤の外の座標の入力への対処

先程の修正で、`11` のような入力に対応する処理を記述できましたが、__`5,1`__ のような、〇×ゲームの __ゲーム盤__ に __存在しない座標__ を入力すると __エラーが発生__ します。

```python
mb.play()
```
実行結果
```
略
File c:\Users\ys\ai\marubatsu\038\marubatsu.py:74, in Marubatsu.place_mark(self, x, y, mark)
     55 def place_mark(self, x: int, y: int, mark: str):
     56     """ ゲーム盤の指定したマスに指定したマークを配置する.
     57 
     58     (x, y) のマスに mark で指定したマークを配置する.
   (...)
     71         マークを配置できた場合は True、配置できなかった場合は False
     72     """
---> 74     if self.board[x][y] == Marubatsu.EMPTY:
     75         self.board[x][y] = mark
     76         return True

IndexError: list index out of range
```

:::note
上記のエラーメッセージは、以下のような意味を持ちます。

* __IndexError__
インデックス（index）に関するエラー
* __list index out of range__
list のインデックス（index）が範囲外（out of range）である
:::

このエラーは、エラーメッセージの `--->` の部分の __`if self.board[x][y]`__ を計算する際に、__`x`__ に __`5` が代入__ されているため、__対応するインデックス__ が __存在しない__ `self.board` の __要素__ を __参照__ しようとしたことが原因です。〇×ゲームの __ゲーム盤のサイズ__ は __3 x 3__ なので、__x 座標__ と __y 座標__ はいずれも __0 以上 3 未満__ の __整数__ である必要があります。

現状の `move` メソッドでは、2 行目で __`place_mark` を呼び出す__ ことで __着手__ を行っています。

```python
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()
```
        
また、__`place_mark`__ メソッドでは、__すでにマークが配置されている座標__ に対して __マークを配置__ しようとした場合は、__エラーメッセージを表示__ して __配置を行わない__ ようにしていますが、__範囲外の座標__ に __対する処理__ は __記述していません__。そこで、下記のプログラムのように、ゲーム盤の __範囲外の座標__ に対して __マークを配置__ しようとした場合に、__エラーメッセージを表示__ して __配置を行わない__ ように修正します。

* __2 ~ 4 行目__：__(x, y)__ がゲーム盤の __範囲外の座標__ である __条件__ は、__`x`__ または __`y`__ が __0 未満__ または ゲーム盤のサイズである __`self.BOARD_SIZE` 以上__ の場合なので、その __4 つの条件__ を表す __条件式__ を __or 演算子__ で連結することで、ゲーム盤の __範囲外__ であることを __判定__ する。__範囲外と判定__ された場合は、3 行目で __エラーメッセージを表示__ し、4 行目で __配置できなかった__ ことを表す __`False` を返す__
* __5 行目__：2 ~ 4 行目に __if 文を記述__ したので、元の __if を elif に修正__ した

```python
 1  def place_mark(self, x, y, mark):
 2      if x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
 3          print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
 4          return False         
 5      elif self.board[x][y] == Marubatsu.EMPTY:
 6          self.board[x][y] = mark
 7          return True
 8      else:
 9          print("(", x, ",", y, ") のマスにはマークが配置済です")
10          return False
11  
12  Marubatsu.place_mark = place_mark
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def place_mark(self, x, y, mark):
    if x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
        print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
        return False         
    elif self.board[x][y] == Marubatsu.EMPTY:
        self.board[x][y] = mark
        return True
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")
        return False

Marubatsu.place_mark = place_mark
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def place_mark(self, x, y, mark):
+   if x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
+       print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
+       return False         
-   if self.board[x][y] == Marubatsu.EMPTY:
+   elif self.board[x][y] == Marubatsu.EMPTY:
        self.board[x][y] = mark
        return True
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")
        return False

Marubatsu.place_mark = place_mark
```
</details>

下記のプログラムを実行し、`5,1` をテキストボックスに入力することで、エラーメッセージが表示されて、マークが配置されずにゲームが続行することが確認できます。なお、実行結果には表示していませんが、確認後に `exit` を入力してゲームを終了させました。

```python
mb.play()
```

実行結果

```
Turn o
...
...
...

( 5 , 1 ) はゲーム盤の範囲外の座標です
Turn o
...
...
...
```

### if 文の判定の順番に関する注意点

__`place_mark`__ で行う __if 文の判定__ を、下記のプログラムのように、__先に__ (x, y) のマスに __マークが配置されていない__ ことを __判定__ し、__その後__ で (x, y) が __ゲーム盤の範囲外__ であることを __判定__ するように記述しても良いのではないかと __思う人がいるかも__ しれません。

```python
def place_mark(self, x, y, mark):
    if self.board[x][y] == Marubatsu.EMPTY:
        self.board[x][y] = mark
        return True
    elif x < 0 or x >= self.BOARD_SIZE or y < 0 or y > self.BOARD_SIZE:
        print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
        return False         
    else:
        print("(", x, ",", y, ") のマスにはマークが配置済です")
        return False

Marubatsu.place_mark = place_mark
```

実際に上記のように `place_mark` を修正し、下記のプログラムを実行して `5,1` を実行すると、下記のように、__先程と同じエラー__ が発生します。

```python
mb.play()
```

実行結果

```
略
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 37 line 2
      1 def place_mark(self, x, y, mark):
----> 2     if self.board[x][y] == Marubatsu.EMPTY:
      3         self.board[x][y] = mark
      4         return True

IndexError: list index out of range
```

このエラーが発生する理由は、__先に `if self.board[x][y] == Marubatsu.EMPTY`__ を __判定__ するようにしたことで、__`x` に `5` が代入__ された状態で __この式の計算が行われる__ ためです。

このように、if 文などの __条件式__ の __記述の順番__ が、__処理の結果__ に __大きな影響を及ぼす__ 場合があるので、if 文などを記述する際には、その点に __注意しながら記述する必要__ があります。

### 別の記述方法

先程は、(x, y) が __ゲーム盤の外__ にあることを __or 演算子__ を使って __判定__ しましたが、下記のプログラムのように、__and 演算子__ を使って (x, y) が __ゲーム盤の中__ にあることを __判定__ するように `place_mark` を記述することもできます。ただし、この場合は修正前のように __1 つ__ の if 文 __ではなく__、(x, y) がゲーム盤の中にあることが判定された場合の __ブロックの中__ に、__別の if 文__ を __入れ子__ で記述する必要がある点に注意して下さい。

* __2 行目__：__(x, y)__ がゲーム盤の __範囲内の座標__ である __条件__ は、__`x`__ と __`y`__ の __両方__ が __0 以上__ ゲーム盤のサイズである __`self.BOARD_SIZE` 未満__ の場合なので、その __4 つの条件__ を表す __条件式__ を __and 演算子__ で連結することで、ゲーム盤の __範囲外__ であることを __判定__ する。
* __3 ~ 8 行目__：__範囲内と判定__ された場合は、__元の__ `place_mark` で行っていた __処理を行う__
* __9 ~ 11 行目__：__範囲外と判定__ された場合は、__エラーメッセージを表示__ し、__配置できなかった__ ことを表す __`False` を返す__

```python
 1  def place_mark(self, x, y, mark):
 2      if x >= 0 and x < self.BOARD_SIZE and y >= 0 and y < self.BOARD_SIZE:
 3          if self.board[x][y] == Marubatsu.EMPTY:
 4              self.board[x][y] = mark
 5              return True
 6          else:
 7              print("(", x, ",", y, ") のマスにはマークが配置済です")
 8              return False
 9      else:
10          print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
11          return False         
12
13  Marubatsu.place_mark = place_mark
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def place_mark(self, x, y, mark):
    if x >= 0 and x < self.BOARD_SIZE and y >= 0 and y < self.BOARD_SIZE:
        if self.board[x][y] == Marubatsu.EMPTY:
            self.board[x][y] = mark
            return True
        else:
            print("(", x, ",", y, ") のマスにはマークが配置済です")
            return False
    else:
        print("(", x, ",", y, ") はゲーム盤の範囲外の座標です")
        return False         

Marubatsu.place_mark = place_mark
```
</details>

なお、修正箇所は、かなりわかりづらかったので省略します。

下記のプログラムを実行し、`5,1` をテキストボックスに入力することで、エラーメッセージが表示されて、マークが配置されずにゲームが続行することが確認できます。実行結果は先ほどと同じなので省略します。

```python
mb.play()
```

:::note
上記のプログラムの 2 行目の if 文は、[以前の記事](https://qiita.com/ysgeso/items/1fdd19037bbf64082a42#python-%E3%81%AE%E6%AF%94%E8%BC%83%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%AE%E9%80%A3%E9%8E%96%E3%81%AB%E3%82%88%E3%82%8B-and-%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%AE%E7%9C%81%E7%95%A5)で紹介した、__比較演算子の連鎖__ を使って、下記のように簡潔に記述することができます。

```python
    if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
-   if x >= 0 and x < self.BOARD_SIZE and y >= 0 and y < self.BOARD_SIZE:
+   if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
```
</details>

上記のように `place_mark` を修正し、下記のプログラムを実行し、`5,1` をテキストボックスに入力することで、エラーメッセージが表示されて、マークが配置されずにゲームが続行することが確認できます。実行結果は先ほどと同じなので省略します。

```python
mb.play()
```

こちらの方がわかりやすいので、本記事でもこちらを採用することにします。
:::

## 座標に整数以外の文字を入力した場合の処理

上記の修正で、座標の入力に関するバグがすべて修正されたと思った人がいるかもしれませんが、実際には __もう一つバグ__ が残っています。それは、`a,b` のような、__整数以外__ の座標を入力した場合で、下記のプログラムを実行して __`a,b`__ を入力すると __エラーが発生__ します。

```python
mb.play()
```
実行結果
```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 44 line 1
----> 1 mb.play()

c:\Users\ys\ai\marubatsu\038\marubatsu.ipynb セル 44 line 2
     22     x, y = xylist
     23     # (x, y) に着手を行う
---> 24     self.move(int(x), int(y))
     26 # 決着がついたので、ゲーム盤を表示する
     27 print(self)

ValueError: invalid literal for int() with base 10: 'a'
```

:::note
上記のエラーメッセージは、以下のような意味を持ちます。

* __ValueError__
値（Value）に関するエラー
* __invalid syntax__
"a" は、10 を基数（base）とする（10 進数の事）数値型としては不正（invalid）なリテラル（literal）である
:::

このエラーは、`x` に `"a"` という、__整数に変換__ することが __不可能な文字列__ が代入された状態で、`x` を `int` を使って __整数型__ に __変換しようとしたこと__ が原因です。

このエラーが発生しないようにする方法の一つに、`x` と `y` に __整数に変換できない__ ような文字列が代入されていた場合に、`int(x)` と `int(y)` を __実行しないようにする__ という方法がありますが、__文字列__ を __整数に変換できるかどうか__ を __判定__ することは __簡単ではありません__。

### 例外処理

別の方法として、__エラーが発生した場合__ に、プログラムの __処理__ を __停止せず__ に、__別の処理を行う__、__例外処理__ という方法があります。これまで説明していませんでしたが、プログラムの __処理が停止__ するような __エラー__ のことを __例外__（exception）と呼び、__例外処理__ では __例外が発生した際__ に __行う処理__ を下記のように記述します。

```python
try:
    プログラム
except:
    try のブロックで例外が発生した時に行う処理
```

例外処理では、以下のような処理を行います。

* __try のブロック__ のプログラムを __実行した際__ に __例外（エラー）が発生__ すると、その時点で __try のブロック__ の __処理__ を __即座に中断__ し、__except のブロック__ を __実行__ する。その際に、本来表示されるはずの __エラーメッセージ__ は __表示されず__、__プログラム__ は __停止しない__
* __try のブロック__ のプログラムを実行した際に __エラーが発生しなかった場合__ は、__except のブロック__ は __実行されない__

例外処理についての詳細については下記のリンク先を参照して下さい

https://docs.python.org/ja/3/tutorial/errors.html


__`self.move(int(x), int(y))`__ を、下記のプログラムのように __例外処理で記述__ することで、__`int(x)`__ と __`int(y)`__ を __実行__ した際に __エラーが発生__ した場合に、プログラムが __停止せず__ に、__except のブロック__ に記述した __エラーメッセージを表示__ するプログラムが __実行__ されます。従って、__エラーが発生__ した際に __`move`__ メソッドは __実行されない__ ので、着手が行われたり、手番が変わったり、`status` 属性が変化したりすることはありません。

```python
try:
    self.move(int(x), int(y))
except:
    print("整数の座標を入力して下さい")
```

上記のプログラムを実行する際に __`x`__ に __`"a"` が代入__ されていた場合は、__`int(x)`__ を __実行した時点__ で __例外が発生__ し、その時点で __try のブロック__ の __処理が中断__ されて即座に `except` のブロックが実行されるので、__`move`__ メソッドが __実行される__ ことは __ありません__。

下記は `play` メソッドの 24 ~ 27 行目を上記のように修正したプログラムです。

```python
 1  def play(self):
 2   # 〇×ゲームを再起動する
 3      self.restart()
 4      # ゲームの決着がついていない間繰り返す
 5      while self.status == Marubatsu.PLAYING:
 6          # ゲーム盤の表示
 7          print(self)
 8          # キーボードからの座標の入力
 9          coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
10          # "exit" が入力されていればメッセージを表示して関数を終了する
11          if coord == "exit":
12              print("ゲームを終了します")
13              return       
14          # x 座標と y 座標を要素として持つ list を計算する
15          xylist = coord.split(",")
16          # xylist の要素の数が 2 ではない場合
17          if len(xylist) != 2:
18              # エラーメッセージを表示する
19              print("x, y の形式ではありません")
20              # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
21              continue
22          x, y = xylist
23          # (x, y) に着手を行う
24          try:
25              self.move(int(x), int(y))
26          except:
27              print("整数の座標を入力して下さい")
28              
29      # 決着がついたので、ゲーム盤を表示する
30      print(self)
31
32  Marubatsu.play = play
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
        # "exit" が入力されていればメッセージを表示して関数を終了する
        if coord == "exit":
            print("ゲームを終了します")
            return       
        # x 座標と y 座標を要素として持つ list を計算する
        xylist = coord.split(",")
        # xylist の要素の数が 2 ではない場合
        if len(xylist) != 2:
            # エラーメッセージを表示する
            print("x, y の形式ではありません")
            # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
            continue
        x, y = xylist
        # (x, y) に着手を行う
        try:
            self.move(int(x), int(y))
        except:
            print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def play(self):
    # 〇×ゲームを再起動する
    self.restart()
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # ゲーム盤の表示
        print(self)
        # キーボードからの座標の入力
        coord = input("x,y の形式で座標を入力して下さい。exit を入力すると終了します")
        # "exit" が入力されていればメッセージを表示して関数を終了する
        if coord == "exit":
            print("ゲームを終了します")
            return       
        # x 座標と y 座標を要素として持つ list を計算する
        xylist = coord.split(",")
        # xylist の要素の数が 2 ではない場合
        if len(xylist) != 2:
            # エラーメッセージを表示する
            print("x, y の形式ではありません")
            # 残りの while 文のブロックを実行せずに、次の繰り返し処理を行う
            continue
        x, y = xylist
        # (x, y) に着手を行う
-       self.move(int(x), int(y))
+       try:
+           self.move(int(x), int(y))
+       except:
+           print("整数の座標を入力して下さい")

    # 決着がついたので、ゲーム盤を表示する
    print(self)

Marubatsu.play = play
```
</details>

下記のプログラムを実行して `a,b` を実行すると、下記のようなエラーメッセージが表示され、プログラムが停止せずに〇×ゲームが続行することが確認できます。

```python
mb.play()
```

実行結果
```
Turn o
...
...
...

整数の座標を入力して下さい
Turn o
...
...
...
```

:::note warn
__`input`__ でキーボードから __数値を入力__ させて __処理を行いたい__ 場合は、__数値以外__ のデータが __入力__ されたときに __エラーが発生__ して __プログラムが停止しない__ ようにする必要がある。その __方法__ の一つに、__例外処理__ がある。
:::

例外処理は __便利__ ですが、__気をつけなければならない点__ がいくつか __あります__。また、今回の記事で説明していない __重要な機能__ がいくつかありますが、それらを説明するとかなり長くなるので、それらは必要になった時点で説明することにします。

## 直前に着手を行ったマークの表示

`play` メソッドでは、__着手を行うたび__ に __ゲーム盤が表示__ されますが、その際に、__どのマスに着手が行われたか__ が __わかるように表示__ を行ったほうが __わかりやすい__ と思いませんか？そこで、__直前に行われた着手__ を __区別できる__ ような __表示方法__ について考えてみて下さい。

現状のプログラムでは、__〇 のマーク__ を __半角の小文字の `o`__（オー）、__× のマーク__ を __半角の小文字の "x"__（エックス）のように、__半角の小文字__ の __アルファベット__ で __表示__ しています。この __性質を利用__ して、__直前__ に行われた __着手__ を表す __マーク__ を、半角の __大文字で表示__ するという方法が考えらるので、本記事ではその方法を紹介します。他にもさまざまな方法が考えられるので、良い方法を思いついた方は、実際に実装してみて下さい。

### `upper` メソッドによる大文字への変換

Python の __文字列型__ のデータには、__アルファベット__ を __大文字に変換__ した __新しい文字列__ を __返す `upper`__[^2] というメソッドがあるので、__大文字への変換__ は、それを利用します。

下記は、`upper` メソッドを使って大文字に変換するプログラムです。`upper` は __アルファベット以外__ の文字は __変換しません__。また、`upper` は __新しい文字列を作成__ して返すメソッドなので、3 行目の実行結果のように __元の文字列__ は __変化しない__ 点に注して下さい。

```python
txt = "abc1+?"
print(txt.upper())
print(txt)
```
実行結果
```
ABC1+?
abc1+?
```

`upper` メソッドの詳細については、下記のリンクを参照して下さい。

https://docs.python.org/ja/3/library/stdtypes.html?highlight=upper#str.upper

### 直前の着手を記録する属性

__直前__ に行われた __着手__ を __大文字で表示__ するためには、__直前__ に行われた __着手__ を __属性__ に __記憶__ しておく __必要__ があります。プログラムに __新しい属性__ を __追加__ するので、その属性の __名前__、__初期化処理__、__更新処理__、__利用方法__ について __考える__ ことにします。

__属性の名前__：__直前__（last）の __着手__（move）を記憶するので、__`last_move`__ という名前にする

__初期化処理__：__ゲームの開始時__ は、__直前__ の __着手__ が __存在しない__ ので、下記のプログラムのように、__`restart`__ メソッドの中の 6 行目で __`None` を代入__ して初期化する

```python
1  def restart(self):
2      self.initialize_board()
3      self.turn = Marubatsu.CIRCLE     
4      self.move_count = 0
5      self.status = Marubatsu.PLAYING
6      self.last_move = None
7  
8  Marubatsu.restart = restart
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = None
    
Marubatsu.restart = restart
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
+   self.last_move = None
    
Marubatsu.restart = restart
```
</details>

__更新処理__：`last_move` は着手を行う際に更新されるので、下記のプログラムのように、__`move`__ メソッドの中の 6 行目で着手した座標を表す __`x, y`__ という __tuple を代入__[^3] して更新する

```python
1  def move(self, x, y):
2      if self.place_mark(x, y, self.turn):
3          self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
4          self.move_count += 1
5          self.status = self.judge()
6          self.last_move = x, y
7
8  Marubatsu.move = move  
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()
        self.last_move = x, y

Marubatsu.move = move  
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.status = self.judge()
+       self.last_move = x, y

Marubatsu.move = move  
```
</details>

__利用方法__：`last_move` はゲーム盤の表示で利用するので、`__str__` メソッドを下記のプログラムのように修正する

* __10、11 行目__：(x, y) のマスに __直前の着手が行われている条件__ は、「`self.last_move` に `None` が代入されていない」、「`self.last_move[0]` と `x` が等しい」、「`self.last_move[1]` と `y` が等しい」の 3 つの条件が __すべて満たされた場合__ である。10 行目で __その条件を判定__ し、__満たされている場合__ は、11 行目で __`upper`__ メソッドを使って __大文字に変換__ して __表示__ する
* __12、13 行目__：上記の条件が __満たされていない__ 場合は、直前の着手が行われたマスではないので、__これまでと同じ方法で表示__ する

```python
 1  def __str__(self):
 2   # ゲームの決着がついていない場合は、手番を表示する
 3      if self.status == Marubatsu.PLAYING:
 4          text = "Turn " + self.turn + "\n"
 5      # 決着がついていれば勝者を表示する
 6      else:
 7          text = "winner " + self.status + "\n"
 8      for y in range(self.BOARD_SIZE):
 9          for x in range(self.BOARD_SIZE):
10              if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
11                  text += self.board[x][y].upper()
12              else:
13                  text += self.board[x][y]
14          text += "\n"
15      return text
16
17  Marubatsu.__str__ = __str__
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def __str__(self):
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.status + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
                text += self.board[x][y].upper()
            else:
                text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def __str__(self):
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.status + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
-           text += self.board[x][y]
+           if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
+               text += self.board[x][y].upper()
+           else:
+               text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__
```
</details>


下記のプログラムを実行し、`1,1`、`0,0`、`0,1` をテキストボックスに入力することで、直前の着手が大文字で表示されることが確認できます。

```python
mb.play()
```

実行結果

```
Turn o
...
...
...

Turn x
...
.O.
...

Turn o
X..
.o.
...

Turn x
x..
Oo.
...
```

### and 演算子と短絡評価に関する注意点

`__str__` メソッドでは、__複数の式__ を __`and` 演算子__ で __連結__ した条件式を記述しました。

```python
if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
```            

この条件式の __順番__ を、下記のように __変更__ すると __エラーが発生__ するようになります。

```python
if x == self.last_move[0] and self.last_move is not None and y == self.last_move[1]:
```            

エラーが発生する __理由__ は、__`self.last_move`__ に __`None` が代入__ されている場合に上記の条件式を実行すると、__最初__ の __`x == self.last_move[0]`__ の計算を行う際に、__`x == None[0]`__ が計算されることになり、__`None` は list ではない__ ので、__`None[0]` が計算できない__ からです。

__元__ のプログラムの __条件式の中__ にも __同じ `x == self.last_move[0]`__ が __記述されている__ ので、エラーが発生すると __思う人がいる__ かもしれませんが、[以前の記事](https://qiita.com/ysgeso/items/1fdd19037bbf64082a42#and-%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%A8-or-%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%AE%E7%9F%AD%E7%B5%A1%E8%A9%95%E4%BE%A1)で説明した、__and 演算子の短絡評価__ によって、以下の手順で計算が行われるため __エラーは発生しません__。

1. __`self.last_move`__ に __`None` が代入されている場合__ は、最初の __`self.last_move is not None`__ が __`False`__ になる
2. __and 演算子のみ__ で __連結__ された __条件式__ は、連結された __式の計算結果__ が __1 つでも `False` になった時点__ で、__全体の計算結果__ が __`False`__ であることが __確定__ する
3. そのため、__`self.last_move is not None`__ が __`False`__ になった時点で、__残りの式__ の __計算__ が __行われない__ ので、__エラーは発生しない__

:::note warn
__and 演算子のみ__ で __連結__ された __条件式__ の中で、__計算結果が `False` になった場合__ に、__残りの式__ が __計算されないようにする__ ことを目的とした __式__ がある場合は、__その式__ は __先頭に記述__ する __必要__ がある。なお、これは __or 演算子の場合も同様__ である。
:::

### `last_move` の初期化処理の工夫

先程のプログラムでは、__`last_move`__ 属性の __初期化処理__ で、__`None` を代入__ していましたが、__`move`__ メソッドで __着手__ を行った場合に __`last_move`__ に __代入される値__ は、`1, 2` のような __tuple__ です。このように、__`last_move`__ に代入される __データ型__ が、__状況によって異なる__ と、__`last_move`__ を __利用__ した処理を行う __`__str__`__ メソッドの中で、__`self.last_move is not None`__ のような __条件式を記述__ して、__データ型ごとの処理__ を __記述__ する必要が生じます。

そこで、__`last_move`__ 属性に __代入__ する __データ型を統一する__ という工夫が考えられます。`None` のほうに統一することはできないので、__tuple に統一__ する必要がありますが、その場合に初期化処理で `last_move` にどのような値を代入すればよいかについて考えてみて下さい。

__`last_move`__ は、__最後の着手の座標__ を表すデータで、__`__str__`__ メソッドの中で、__`last_move`__ と __表示する__ (x, y) の __座標が等しい場合__ にマークを大文字で表示する際に __利用__ されます。__ゲーム開始時点__ では、__着手は行われていない__ ので、__`__str__`__ メソッドの中で __マークを大文字で表示__ する処理を __行ってはいけません__。そこで、__初期化処理__ で __`last_move`__ に〇×ゲームのゲーム盤に __存在しない座標__ を __代入__ しておくことで、__ゲーム開始時__ でマークを __大文字で表示する処理__ が __実行__ されることが __なくなります__。

__初期化処理__ で __`last_move`__ に __代入する座標__ は、ゲーム盤に __存在しない座標__ であれば __なんでもかまいません__。そこで、本記事では、下記のプログラムの `restart` メソッドの 6 行目のように __`-1, -1`__ という座標のデータを 初期化処理で `last_move` に代入することにします。

```python
1  def restart(self):
2      self.initialize_board()
3      self.turn = Marubatsu.CIRCLE     
4      self.move_count = 0
5      self.status = Marubatsu.PLAYING
6      self.last_move = -1, -1
7    
8  Marubatsu.restart = restart
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1
    
Marubatsu.restart = restart
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
-   self.last_move = None
+   self.last_move = -1, -1
    
Marubatsu.restart = restart
```
</details>

__`last_move`__ に常に __要素が 2 つ__ ある __tuple が代入__ されるようになったので、__str__ メソッドを下記のプログラムのようにより __簡潔に記述__ することができるようになります。

* __10 行目__：`self.last_move` には、必ず __要素が 2 つ__ ある __tuple が代入__ されるので、__tuple の展開__ を使って __`lastx` と `lasty`__ に __最後の着手の座標__ を __代入__ する
* __11 行目__：__(x, y)__ と __(lastx, lasty)__ が __等しいかどうか__ を __判定__ する

```python
 1  def __str__(self):
 2      # ゲームの決着がついていない場合は、手番を表示する
 3      if self.status == Marubatsu.PLAYING:
 4          text = "Turn " + self.turn + "\n"
 5      # 決着がついていれば勝者を表示する
 6      else:
 7          text = "winner " + self.status + "\n"
 8      for y in range(self.BOARD_SIZE):
 9          for x in range(self.BOARD_SIZE):
10              lastx, lasty = self.last_move
11              if x == lastx and y == lasty:
12                  text += self.board[x][y].upper()
13              else:
14                  text += self.board[x][y]
15          text += "\n"
16      return text
17
18  Marubatsu.__str__ = __str__
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def __str__(self):
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.status + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
            lastx, lasty = self.last_move
            if x == lastx and y == lasty:
                text += self.board[x][y].upper()
            else:
                text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def __str__(self):
    # ゲームの決着がついていない場合は、手番を表示する
    if self.status == Marubatsu.PLAYING:
        text = "Turn " + self.turn + "\n"
    # 決着がついていれば勝者を表示する
    else:
        text = "winner " + self.status + "\n"
    for y in range(self.BOARD_SIZE):
        for x in range(self.BOARD_SIZE):
+           lastx, lasty = self.last_move
-           if self.last_move is not None and x == self.last_move[0] and y == self.last_move[1]:
+           if x == lastx and y == lasty:
                text += self.board[x][y].upper()
            else:
                text += self.board[x][y]
        text += "\n"
    return text

Marubatsu.__str__ = __str__
```
</details>

下記のプログラムを実行し、`1,1`、`0,0`、`0,1` をテキストボックスに入力することで、直前の着手が大文字で表示されることが確認できます。実行結果は先程と同じなので省略します。

```python
mb.play()
```

# 今回の記事のまとめ

今回の記事では、`play` メソッドのさまざまな問題点を紹介し、それらの問題点を修正する方法について説明しました。今回の記事で紹介した以外の `play` メソッドの改良案を思いついた方は、独自に改良することにチャレンジしてみて下さい。

〇×ゲームを遊べるようになったので、次回の記事からは、AI の実装を開始します。

# 本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/038/marubatsu.ipynb

以下のリンクは、今回の記事で更新した marubatsu.py です。

https://github.com/ysgeso/marubatsu/blob/master/038/marubatsu_new.py

# 次回の記事

https://qiita.com/ysgeso/items/bdbcd4c1ab1d8731df58

[^1]:__最後__ に __改行__ を表す __"\n" を結合__ することを __忘れないように__ して下さい。筆者はこの記事を最初に記述する際に、それを記述し忘れて表示がおかしくなりました
[^2]:同様のメソッドに、アルファベットを小文字に変換する [`lower`](https://docs.python.org/ja/3/library/stdtypes.html?highlight=upper#str.lower) とメソッドがあります
[^3]:`return x, y` の場合と同様に、tuple の外側の `(` と `)` を省略しています