Pythonで〇×ゲームのAIを一から作成する　その12　マークの配置と条件分岐

# 目次と前回の記事

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

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

# 前回までのおさらい

オブジェクトに関する話がかなり長くなりましたが、前々回の記事で下記のように、ようやく初期化されたゲーム盤のプログラムを記述することができるようになりました。

```python
board = [[" "] * 3 for x in range(3)]
print(board)
```
実行結果
```
[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
```

# 次に行う実装の検討

次に何を実装するかを検討するために、〇×ゲームの仕様を再掲します。

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

今回の記事から、〇×ゲームの実装の進捗状況がわかるように、仕様を以下のように表記することにします。

* 実装が完了した部分を `背景が灰色の長方形` で記述する
* 実装の一部が完了した部分を、__太字__ で記述する（上記にはまだありません）

仕様 1、2 実装のが完了したので、次は 仕様 3、4 を実装するのがよさそうです。

仕様 3 の 「一人は 〇 を、もう一人は × のマークを受け持つ」と、仕様 4 の「マスに自分のマークを 1 つ置く」から、__ゲーム盤のマスに、〇 か × のマークを配置する__ 処理を行う必要がある事がわかります。

仕様 4 には、「交互に」という条件が記述されていますが、現時点では手番に関係する処理は実装していないので、「交互に」の部分は __後回し__ にして実装を行うことにします。

# マークの配置の実装

マークの配置の処理を実装する前に、「その 5」の記事で定義したゲーム盤の __データ構造__ のことを忘れてしまった方も多いのではないかと思いますので再掲します。

* 〇×ゲームのゲーム盤のマスの座標を、左右方向を x 座標、上下方向を y 座標とする __2 次元の座標__ で表現する
* x 座標はゲーム盤の左端の列を 0 とし、右に 1 つ列がずれるたびに x 座標が 1 増える
* y 座標はゲーム盤の上端の行を 0 とし、下に 1 つ行がずれるたびに y 座標が 1 増える
* ゲーム盤の各マスを表すデータを __2 次元配列__ を表す __list__ で表現する
* 2 次元配列の list の 1 つ目のインデックスを x 座標、2 つ目のインデックスを y 座標に対応させる
* 空白、〇、× が配置されたのセルを表すデータを、それぞれ `" "`、`"〇"`、`"×"` という __文字列型のデータ__ で表現する
* ゲーム盤の各セルを表す list の要素に、そのセルに配置されたマークに対応する文字列を代入する

上記から、(x, y) のマスを表す要素は __`board[x][y]`__ で __参照__ できることがわかります。従って、(x, y) のマスに 〇 のマークを配置する処理は、下記のプログラムのように記述できます。また、× のマークを配置する場合は、`"〇"` の代わりに `"×"` を代入します。

```python
board[x][y] = "〇"
```

:::note
以降は「〇 のマークを配置」を単に「__〇 を配置__」と表記します。
:::

上記のプログラムの実行結果を表記していないのは、`x` と `y` に __値を代入していない__ ので、実行すると __エラーが発生する__ からです。下記は、具体的な座標として (0, 1) のマスに 〇 のマークを配置するプログラムです。2 行目の実行結果から、`board` の (0, 1) のマスを表す要素のみに `"〇"` が代入されていることがわかります。

```python
board[0][1] = "〇"
print(board)
```
実行結果
```
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
```

## 仕様 4 の実装に関するバグ

上記の処理には __重大なバグ__ があります。それは、上記の処理では 〇 を配置した (0, 1) に × のマークを  __配置できてしまう__ というバグです。上記のプログラムに続けて、下記のプログラムを実行すると、実行結果から (0, 1) のマスに × のマークが配置されてしまうことがわかります。これは、仕様 4 の「__空いている好きなマスに__」という部分に反しています。

```python
board[0][1] = "×"
print(board)
```
実行結果
```
[[' ', '×', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
```

このバグを修正するためには、以下のような処理を行う必要があります。

1. マークを配置したいマスに、既にマークが __配置されているか__ どうかを __チェック__ する
2. マークが配置されていない __場合のみ__、そのマスにマークを配置する

このような、__特定の条件が満たされた場合のみ__ 行うような処理の事を、__条件分岐__[^1]と呼びます。Python では if 文を使って条件分岐を記述することができます。

## Python の if 文

Python では if 文は以下のように記述します。`elif`[^2] とそのブロックは __必要な数だけ__ 記述することができます。また、`elif`、`else` とそのブロックは必要がなければ __省略する__ ことができます。ブロックの記述方法は、「その 5」の記事で説明した for 文のブロックと同様です。

```python
if 条件式1:
    条件式1 が真の場合に実行する処理を記述するブロック
elif 条件式2:
    条件式2 が真の場合に実行する処理を記述するブロック
(elif とそのブロックは必要なだけ記述できる)
else:
    全ての条件式が偽の場合に実行する処理を記述するブロック
```

条件式[^3]には、一般的には計算結果が Python の __論理型のデータ__ である __True__ または __False__ になるような式を記述します。Python では、論理型のデータのリテラルは、頭文字を __大文字で記述__ するという __決まり__ になっている点に注意して下さい[^4]。

論理（boolean）型[^5]のデータは、__正しい__ という意味を表す True と、__間違っている__ という意味を表す False の __2 種類__ のデータ __しかありません__。また、日本語では True を __真__、False を __偽__ という用語で表記します。

if 文は以下のような手順で処理が行われます。

1. if や elif の後に記述された条件式を __上から順番に__ 計算する
2. __最初に__ 条件式の計算結果が __True__ になった行の __直後のブロック__ のプログラム __のみ__ を __実行する__。__それ以降__ に記述されている if 文の __残りのプログラムは実行しない__
3. __すべて__ の条件式の計算結果が __False__ の場合は、else の行の直後のブロックのプログラムを実行する。else が記述されていない場合は __何の処理も行わない__

if 文を実行した場合は、__最大でも__ その中に記述されているブロックのうちの __1 つのブロック__ のプログラム __しか実行されない__ 点に注意して下さい。if 文の複数の条件式の計算結果が True であっても、__最初に True になった__ 条件式の直後のブロックのプログラム __だけが実行__ されます。

### 比較演算子

条件式では、一般的に __2 つのデータを比較__ し、比較した結果が __正しい場合は True__ を、__正しくない場合は False__ を計算結果として返す __比較演算子__ が使われます。下記は Python の比較演算子の一覧です。

| 比較演算子 | 意味 |
|:-:|:--|
| `==`[^6]  | 左右のデータが等しい  |
| `!=`[^7]  | 左右のデータが等しくない  |
| `<`  | 左のデータの方が右のデータより小さい（未満）  |
| `<=` | 左のデータが右のデータ以下である  |
| `>`  | 左のデータの方が右のデータより大きい  |
| `>=`  | 左のデータが右のデータ以上である  |

:::note
上記以外に `is` と `is not` という比較演算子があります。これらについては必要になった時点で説明します。
:::

数値型のデータの場合は、比較演算子の意味は簡単に理解できると思いますが、__数値型以外__ のデータに関しては、それぞれの比較演算子が具体的にどのような意味を持つかを __個別に覚える__ 必要があります。また、データ型によっては、__`==` と `!=` 以外__ の演算子を __使うことができない__ 場合もあります。数値型以外のデータに対して比較演算子がどのような計算を行うかについては、必要になった時点で紹介します。

:::note
等しくないことをチェックする演算子に全角の `≠` を、以上や以下であることをチェックする演算子に全角の `≧` や `≦` を使うことは __できません__。その理由の一つは、プログラム言語が誕生した頃のコンピューターには、半角文字しか存在しなかったからです。そのため、等しくない、以上、以下を表す比較演算子を、半角の記号を組み合わせて `!=`、`<=`、`>=` のように表記することにしました。

また、全角文字は入力する際にキーを複数回押す必要があるため、__入力が面倒__ です。そのため、全角文字が使えるようになった現在でも、プログラム言語では、__予約語などの文字__ や、演算子などを表す __記号__ は __半角文字しか使わない__ のが一般的です。
:::

### False とみなされるデータの一覧

比較演算子 __以外__ の演算子を記述した場合などで、if 文の条件式の計算結果が __論理型以外__ のデータになった場合は __以下の表__ のデータは __False とみなされ__、__それ以外__ のデータは __True とみなされます__。False とみなされるデータは、__何もないことを表す__ ようなデータであるという特徴があります。下記の表には本記事で紹介していないデータもありますが、それらについては必要になった時点で紹介します。

| データ | リテラル |
|:--|:--|
| 数値型のゼロ  | `0` または `0.0`  |
| 空文字列[^8]  | `""`  |
| 空のリスト  | `[]`  |
| 空のタプル  | `()`  |
| 空の辞書  | `{}`  |
| 空の集合  | `set()`  |
| 値の非存在  | `None`  |

実際に、if 文の条件式に上記のような計算結果になるような式を __記述する必要がある__ 場合がありますが、慣れないうちは __わかりづらい__ ので、__特に理由がない場合__ は、条件式には計算結果が論理型になる __比較演算子を使う__ ことを __お勧めします__。

if 文の条件式に、計算結果が論理型のデータにならないような式を記述する例については、必要になった時点で紹介します。

:::note
Python の数値型には、整数を表す int と浮動小数点数を表す float があります。この 2 つを __区別してリテラルを表記__ したい場合は、int のゼロのリテラルは `0`、float のゼロのリテラルは `0.0` と表記します。ただし、ほとんどの場合はどちらを記述しても計算結果は変わりません。
:::

## バグを修正したプログラム

ゲーム盤のデータ構造から、空のマスを表す要素には、半角の空白文字の __`" "` が代入__ されています。従って、空のマスであるかどうかは、そのマスを表す要素と `" "` が等しいことを __`==` 演算子でチェック__ するという条件式を記述すればよいことがわかります。

下記のプログラムの 1 行目では、if 文の条件式に (0, 1) のマスが空であることをチェックする式を記述し、その直後のブロックに (0, 1) のマスに 〇 のマークを配置する処理を記述することで、(0, 1) の __マスが空だった場合のみマークを配置する__ 処理が行われます。

```python
if board[0][1] == " ":
    board[0][1] = "〇"
```

### プログラムの動作の確認

プログラムを記述した場合は、なるべくそのプログラムが __正しく動作__ するかどうかを __確認する作業__ を行うべきです。その理由は、プログラムの入力ミスや、間違った処理を記述するなどの理由で、記述したプログラムが __意図通りに正しく動作しない__ 場合が __良くある__ からです。

また、プログラムの動作の __確認をせずに__ 次の処理を実装してしまうと、その後でプログラムのバグが見つかった場合に、どこにバグがあるかを __発見することが困難__ になります。

プログラムの動作の確認は、__実装した部分の処理__ が正しく動作するかを __確認できる__ ようなプログラムを記述して実行することで行います。

先ほど記述したプログラムは、(0, 1) のマスが __空の場合のみ__、そのマスに __マークを配置する__ という処理を行うプログラムでした。そのため、このプログラムの動作の確認は、以下の処理を行うようなプログラムを記述して実行することで行うことができます。

1. (0, 1) のマスが空の状態で 〇 を配置しようとする
2. (0, 1) のマスに 〇 が配置されている状態で、× を配置しようとする

下記のプログラムは、上記の動作確認を行うプログラムです。

```python
1  board = [[" "] * 3 for x in range(3)]
2  if board[0][1] == " ":
3      board[0][1] = "〇"
4  print(board)
5  if board[0][1] == " ":
6      board[0][1] = "×"
7  print(board)
```

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

```diff_python
board = [[" "] * 3 for x in range(3)]
if board[0][1] == " ":
    board[0][1] = "〇"
print(board)
if board[0][1] == " ":
    board[0][1] = "×"
print(board)
```
</details>

実行結果
```
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
```

下記の表は、上記のプログラムの各行で行われる処理を説明したものです。

| 行数 | 行われる処理 |
|:--|:--|
| 1  | これまでの処理で、board の (0, 1) のマスにマークが配置されているので、board を空のゲーム盤で初期化しています  |
| 2、3  | (0, 1) のマスが空の場合のみ、〇 のマークを配置しています |
| 4 | `board` を表示して中身を確認できるようにしています |
| 5、6  | (0, 1) のマスが空の場合のみ、× のマークを配置しています |
| 7 | `board` を表示して中身を確認できるようにしています |

1 行目で board に初期化されたゲーム盤を代入しているので、__(0, 1) のマスは空になります__。そのため、上記のプログラムの 2、3 行目は、「(0, 1) のマスが空の状態で 〇 を配置しようとする」処理になります。4 行目の実行結果から、__意図通り__ に (0, 1) のマスに 〇 が __配置されている__ ことがわかります。

上記のプログラムの 5、6 行目は、「(0, 1) のマスが 〇 の状態で × を配置しようとする」処理になります。7 行目の実行結果から、__意図通り__ に (0, 1) のマスに × が __配置されていない__ ことがわかります。

上記の事から、記述したプログラムは正しく動作していることを確認することができました。

:::note
残念ながら、上記のような単純なプログラムの動作確認では、プログラムにバグがないことを __100 %__ 確認することは __できません__。バグがないことを確認するためには、より詳細な確認を行う必要がありますが、そのような確認作業は __かなりの手間と時間__ を必要とします。また、かなり手間をかけて確認作業を行っても __バグを見落としてしまう場合もあります__。

理想的にはプログラムを少し実装するたびに、詳細な確認を行ったほうが良いのですが、__現実的には不可能__ なので、プログラムの確認は上記のようなざっとした確認で済ましてしまうことが多いのではないかと思います。また、プログラムの全ての処理に対して確認を行うと __時間がいくらあっても足りなくなる__ ので、明らかにバグがなさそうな __単純な処理__ に対しては __確認作業を省略する__ のが一般的です。

実際には、実装したプログラムが正しく動作するかどうかについて __少しでも不安を感じた場合__ に、__ざっとした確認__ を行うと良いでしょう。もちろん、大きな不安を感じた場合は、詳細な確認を行うべきです。ざっとした確認でも、やるとやらないのでは、__バグの発見の確率が大きく変わってくる__ ので、少しでも不安を感じた場合は必ず確認作業を行うことを __強くお勧めします__。
:::

## 配置できなかった場合にメッセージを表示する

先程のプログラムでは、5、6 行目で (0, 1) のマスに × を __配置できなかったことが__ 実行結果から  __わかりづらい__ ので、マークを配置できなかった場合にメッセージを表示するようにプログラムを修正することにします。

そのような修正は、(0, 1) のマスが空 __でない__ 場合に表示を行えばよいので、下記のプログラムのように、if 文の最後に __else のブロック__ を記述して、そこにメッセージを表示する処理を記述することで行うことができます。

```python
board = [[" "] * 3 for x in range(3)]
if board[0][1] == " ":
    board[0][1] = "〇"
else:
    print("(0, 1) のマスにはマークが配置済です")
print(board)
if board[0][1] == " ":
    board[0][1] = "×"
else:
    print("(0, 1) のマスにはマークが配置済です")
print(board)
```

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

```diff_python
board = [[" "] * 3 for x in range(3)]
if board[0][1] == " ":
    board[0][1] = "〇"
+ else:
+     print("(0, 1) のマスにはマークが配置済です")
print(board)
if board[0][1] == " ":
    board[0][1] = "×"
+ else:
+     print("(0, 1) のマスにはマークが配置済です")
print(board)
```
</details>

実行結果
```
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
(0, 1) のマスにはマークが配置済です
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
```

実行結果から (0, 1) のマスに 〇 を配置した後で × を配置しようとした際に、メッセージが表示されていることがわかります。

## 他のマスへのマークの配置

(0, 1) のマスに 〇 や × のマークを配置する方法がわかりました。他のマスでも同様の処理を記述すれば良いのですが、その度に上記のプログラムの 2 ~ 5 行目のような 4 行分のプログラムを記述するのは大変です。そこで、次回の記事では関数を使って、マスにマークを配置する処理をまとめる方法について説明します。

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

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

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

# 次回の記事

https://qiita.com/ysgeso/items/95282cb1532602799052

# 更新履歴

| 更新日時 | 更新内容 |
|:--|:--|
| 2023/09/29 | `for y in range(3)` → `for x in range(3)` |

[^1]:「選択処理」や「分岐処理」と呼ぶ場合もあります
[^2]: 他のプログラム言語では、`else if` など、別の表記を行う場合があります。論理演算については必要になった時点で説明します
[^3]:「論理式」と呼ぶ場合もあります
[^4]: 他のプログラム言語では、`true` や `TRUE` など、別の表記を行う場合があります
[^5]:「ブーリアン型」や「ブール型」と呼ぶ場合もあります。論理演算（ブール演算）の計算結果を表すデータであることから、そのような名前が付けられています
[^6]:`==` のように `=` を 2 つ並べて記述するのは、__代入__ を表す `=` 演算子と __区別する__ ためです。 Visual Basic のように、等しいことをチェックする比較演算子に `=` を使うようなプログラム言語もあるようです
[^7]:多くのプログラム言語では、等しくないことをチェックする比較演算子に `!=` を使いますが、Excel や Visual Basic では `<>` を使うようです 
[^8]:文字が __1 文字も存在しない__ 文字列型のデータのことを __空文字__ と呼びます。空文字のリテラルは `""` です