0
0

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

Last updated at Posted at 2023-09-21

目次と前回の記事

前回までのおさらい

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

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) のマスに 〇 のマークを配置する処理は、下記のプログラムのように記述できます。また、× のマークを配置する場合は、"〇" の代わりに "×" を代入します。

board[x][y] = ""

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

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

board[0][1] = ""
print(board)

実行結果

[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

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

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

board[0][1] = "×"
print(board)

実行結果

[[' ', '×', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

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

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

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

Python の if 文

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

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 左右のデータが等しくない
< 左のデータの方が右のデータより小さい(未満)
<= 左のデータが右のデータ以下である
> 左のデータの方が右のデータより大きい
>= 左のデータが右のデータ以上である

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

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

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

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

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

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

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

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

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

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

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

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

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

if board[0][1] == " ":
    board[0][1] = ""

プログラムの動作の確認

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

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

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

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

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

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

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)
行番号のないプログラム
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)

実行結果

[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]
[[' ', '〇', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

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

行数 行われる処理
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) のマスに × が 配置されていない ことがわかります。

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

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

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

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

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

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

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

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)
修正箇所
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)

実行結果

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

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

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

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

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

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

次回の記事

更新履歴

更新日時 更新内容
2023/09/29 for y in range(3)for x in range(3)
  1. 「選択処理」や「分岐処理」と呼ぶ場合もあります

  2. 他のプログラム言語では、else if など、別の表記を行う場合があります。論理演算については必要になった時点で説明します

  3. 「論理式」と呼ぶ場合もあります

  4. 他のプログラム言語では、trueTRUE など、別の表記を行う場合があります

  5. 「ブーリアン型」や「ブール型」と呼ぶ場合もあります。論理演算(ブール演算)の計算結果を表すデータであることから、そのような名前が付けられています

  6. == のように = を 2 つ並べて記述するのは、代入 を表す = 演算子と 区別する ためです。 Visual Basic のように、等しいことをチェックする比較演算子に = を使うようなプログラム言語もあるようです

  7. 多くのプログラム言語では、等しくないことをチェックする比較演算子に != を使いますが、Excel や Visual Basic では <> を使うようです

  8. 文字が 1 文字も存在しない 文字列型のデータのことを 空文字 と呼びます。空文字のリテラルは "" です

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0