0
0

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

Last updated at Posted at 2023-12-21

目次と前回の記事

前回までのおさらい

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

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

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

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

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

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

前回までのおさらい

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

play メソッドの改良

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

結果の表示

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

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

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

mb = Marubatsu()
mb.play()

実行結果

略
Turn o
oo.
xx.
...

Turn x
ooo
xx.
...

勝者の表示

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

 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
行番号のないプログラム
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
修正箇所
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

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

mb.play()

実行結果

略
Turn o
oo.
xx.
...

winner o
Turn x
ooo
xx.
...

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

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

mb.play()

実行結果

略
Turn x
xx.
oo.
o..

winner x
Turn o
xxx
oo.
o..

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

mb.play()

実行結果

略
Turn o
oox
xxo
ox.

winner draw
Turn x
oox
xxo
oxo

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

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

Marbatsu.CIRCLEMarubatsu.CROSS"o""x" 以外の文字列を代入 した場合も 同様 です。

ゲーム盤の表示の改良

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

ゲーム盤の 表示 に関する 処理__str__ メソッドで行っているので、この メソッドの中 で、ゲームの 決着がついている場合手番を表示しない ように 修正 することにします。また、そのついでに、勝利者の表示__str__ メソッドの中で まとめて行う ように 修正 することにします。そのように修正することで、決着がついた場合 に、print(self) だけを記述 すれば済むようになります。修正後の __str__ メソッドは以下のようになります。

  • 3、4 行目judge メソッドを使って、ゲームの 決着がついていないこと判定 し、決着がついていない 場合は、手番を表す文字列text に代入 する
  • 6、7 行目決着がついている 場合は、judge メソッドを使って 勝者を表す文字列text に代入 する1
 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__

行番号のないプログラム
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__
修正箇所
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__

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

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

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

mb.play()

実行結果

略
Turn o
oo.
xx.
...

winner o
ooo
xx.
...

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

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

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

無駄な処理の確認

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

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 回呼び出される ことがわかります。

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 度行う という、無駄な作業 を行っていることになります。

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

属性の追加

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

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

属性の名前

test_judge では、winner という名前の変数に期待される judge メソッドの返り値を代入していましたが、judge メソッドの返り値には、勝者(winner)とは 関係のないMarubatsu.PLAYING がある ので、winner という名前は ふさわしくありませんjudge メソッドの 返り値 は、ゲームの状態(status)を表すので、status という名前にします。

初期化処理

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

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
行番号のないプログラム
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    
Marubatsu.restart = restart
修正箇所
def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
+   self.status = Marubatsu.PLAYING
    
Marubatsu.restart = restart

更新処理

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

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

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

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        
行番号のないプログラム
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
修正箇所
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

利用

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

 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__
行番号のないプログラム
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__
修正箇所
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__

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

 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
行番号のないプログラム
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
修正箇所
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

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

mb.play()

間違った入力への対処

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

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

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)

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

  • 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 の 素の数一致しないエラーが発生 してしまいます。

print("11".split(","))

実行結果

['11']

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

このこと は、input のようなキーボードからの文字の入力だけでなく、定義 した 関数の入力 にも あてはまります。ただし、関数の場合は、すべての入力を考慮 したプログラムを 記述 することが 大変すぎる、または ほぼ不可能 な場合が多いので、実際 には 型アノテーションdocstring などを 記述 して、関数を 使う際 に、間違ったデータ入力しない ように 促す という 工夫が重要 になります。

バグの修正

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

  • 11 行目coord.split(",")計算結果xylist に代入 する
  • 13 ~ 17 行目xylist要素の数2 でない 場合、15 行目で エラーメッセージを表示 し、17 行目で continue 文実行 することで、残り の while 文の ブロック実行せず に、次の繰り返し処理行う
  • 18 行目xylist要素の数2 である ことが 確定 したので、xylist のそれぞれの 要素xy に展開 する
 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
行番号のないプログラム
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
修正箇所
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

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

mb.play()

実行結果

Turn o
...
...
...

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

ゲームの強制終了

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

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

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

  • 9 行目exit を入力すると終了するこをと 説明するメッセージを追加 する
  • 11 ~ 13 行目:テキストボックスに 入力した文字列"exit"等しいかどうか判定 し、等しければ 12 行目で ゲームの終了メッセージを表示 し、12 行目で return 文 を実行することで、play メソッドの 処理を終了 する
 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
行番号のないプログラム
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
修正箇所
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

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

mb.play()

実行結果

Turn o
...
...
...

ゲームを終了します

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

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

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

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

  • IndexError
    インデックス(index)に関するエラー
  • list index out of range
    list のインデックス(index)が範囲外(out of range)である

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

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

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 または y0 未満 または ゲーム盤のサイズである self.BOARD_SIZE 以上 の場合なので、その 4 つの条件 を表す 条件式or 演算子 で連結することで、ゲーム盤の 範囲外 であることを 判定 する。範囲外と判定 された場合は、3 行目で エラーメッセージを表示 し、4 行目で 配置できなかった ことを表す False を返す
  • 5 行目:2 ~ 4 行目に if 文を記述 したので、元の if を elif に修正 した
 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
行番号のないプログラム
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
修正箇所
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

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

mb.play()

実行結果

Turn o
...
...
...

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

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

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

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 を実行すると、下記のように、先程と同じエラー が発生します。

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判定 するようにしたことで、x5 が代入 された状態で この式の計算が行われる ためです。

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

別の記述方法

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

  • 2 行目(x, y) がゲーム盤の 範囲内の座標 である 条件 は、xy両方0 以上 ゲーム盤のサイズである self.BOARD_SIZE 未満 の場合なので、その 4 つの条件 を表す 条件式and 演算子 で連結することで、ゲーム盤の 範囲外 であることを 判定 する。
  • 3 ~ 8 行目範囲内と判定 された場合は、元の place_mark で行っていた 処理を行う
  • 9 ~ 11 行目範囲外と判定 された場合は、エラーメッセージを表示 し、配置できなかった ことを表す False を返す
 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
行番号のないプログラム
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

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

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

mb.play()

上記のプログラムの 2 行目の if 文は、以前の記事で紹介した、比較演算子の連鎖 を使って、下記のように簡潔に記述することができます。

    if 0 <= x < self.BOARD_SIZE and 0 <= y < self.BOARD_SIZE:
修正箇所
-   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:

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

mb.play()

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

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

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

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'

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

  • ValueError
    値(Value)に関するエラー
  • invalid syntax
    "a" は、10 を基数(base)とする(10 進数の事)数値型としては不正(invalid)なリテラル(literal)である

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

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

例外処理

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

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

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

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

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

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

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

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

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

 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
行番号のないプログラム
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
修正箇所
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

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

mb.play()

実行結果

Turn o
...
...
...

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

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

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

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

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

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

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

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

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

txt = "abc1+?"
print(txt.upper())
print(txt)

実行結果

ABC1+?
abc1+?

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

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

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

属性の名前直前(last)の 着手(move)を記憶するので、last_move という名前にする

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

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
行番号のないプログラム
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
修正箇所
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

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

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  
行番号のないプログラム
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  
修正箇所
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  

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

  • 10、11 行目:(x, y) のマスに 直前の着手が行われている条件 は、「self.last_moveNone が代入されていない」、「self.last_move[0]x が等しい」、「self.last_move[1]y が等しい」の 3 つの条件が すべて満たされた場合 である。10 行目で その条件を判定 し、満たされている場合 は、11 行目で upper メソッドを使って 大文字に変換 して 表示 する
  • 12、13 行目:上記の条件が 満たされていない 場合は、直前の着手が行われたマスではないので、これまでと同じ方法で表示 する
 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__
行番号のないプログラム
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__
修正箇所
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__

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

mb.play()

実行結果

Turn o
...
...
...

Turn x
...
.O.
...

Turn o
X..
.o.
...

Turn x
x..
Oo.
...

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

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

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

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

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

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

のプログラムの 条件式の中 にも 同じ x == self.last_move[0]記述されている ので、エラーが発生すると 思う人がいる かもしれませんが、以前の記事で説明した、and 演算子の短絡評価 によって、以下の手順で計算が行われるため エラーは発生しません

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

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 に代入することにします。

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
行番号のないプログラム
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
修正箇所
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

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

  • 10 行目self.last_move には、必ず 要素が 2 つ ある tuple が代入 されるので、tuple の展開 を使って lastxlasty最後の着手の座標代入 する
  • 11 行目(x, y)(lastx, lasty)等しいかどうか判定 する
 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__
行番号のないプログラム
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__
修正箇所
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__

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

mb.play()

今回の記事のまとめ

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

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

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

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

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

次回の記事

  1. 最後改行 を表す "\n" を結合 することを 忘れないように して下さい。筆者はこの記事を最初に記述する際に、それを記述し忘れて表示がおかしくなりました

  2. 同様のメソッドに、アルファベットを小文字に変換する lower とメソッドがあります

  3. return x, y の場合と同様に、tuple の外側の () を省略しています

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