0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その193 Board クラスの抽象メソッドの追加

Last updated at Posted at 2025-09-13

目次と前回の記事

Python のバージョンとこれまでに作成したモジュール

本記事のプログラムは Python の バージョン 3.13 で実行しています。

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

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
mbtest.py テストに関する関数
util.py ユーティリティ関数の定義
tree.py ゲーム木に関する Node、Mbtree クラスなどの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

Board クラスの抽象メソッドの追加

前回までの記事では、ListBoardList1dBoard という 2 種類の 異なるデータ構造 でゲーム盤を表現する クラスを定義 し、Marubatsu クラスでその 2 つのクラスのインスタンスを 切り替えて利用できる ようにしました。これは、以前の記事で説明した 異なるデータ型を扱うクラス共通するメソッドを持つ ようにすることで、それらのクラスのインスタンスを 共通して扱う ことができるようにするという ポリモーフィズム よるものです。

Python のポリモーフィズム では、共通するメソッド抽象メソッドとして定義 した 抽象クラスを定義 し、そのクラスを 継承することで実現 するのが一般的で、ゲーム盤のデータ構造を表すクラスの場合は、下記の表の抽象メソッドが定義された Board という抽象クラスを定義しました。

抽象メソッド 処理
getmark(x, y) (x, y) のマスのマークを返す
setmark(x, y, mark) (x, y) のマスに mark を代入する
board_to_str() ゲーム盤を表す文字列を返す

上記以外でも、ゲーム盤を表すデータ構造 によって 効率の良いアルゴリズムが変化するような処理抽象メソッドとして定義 する必要があります。例えば、勝敗判定を行う judge メソッドや、局面のマークのパターンを数える count_markpats メソッドなどは、ゲーム盤を表すデータ構造が変わると効率の良いアルゴリズムが変化します。

そこで、ゲーム盤のデータ構造が変わると効率の良いアルゴリズムが変わる処理を行うメソッドを Board クラスの抽象メソッド として定義し、それに従って ListBoardList1dBoardMarubatsu クラスの 定義を修正 することにします。

なお、以下の説明では先に ListBoard と Marubatsu クラスの修正を行い、List1dBoard クラスの修正はその後で行うことにします。

勝敗判定と直線上のマークの数を数える処理の修正

今後の記事で紹介する予定のゲーム盤を表すデータ構造では、ListBoard とは 異なるアルゴリズム勝敗判定を行う ので、勝敗判定を行う judge メソッドBoard クラスの 抽象メソッド とし、ListBoard クラスの メソッドとして定義 することにします。

judge メソッドでは count_linemark 属性が True の場合に 直線上のマークの数を数える ことによって勝敗判定を行いますが、直線上のマークの数を数える 効率の良いアルゴリズム はゲーム盤を表すデータ構造によって 異なる可能性 があります。そこで、直線上のマークの数を数える処理ListBoard クラスの メソッドで行う ように修正することにします。

具体的には下記のような修正を行います。

  • count_linemark 属性を ListBoard クラスの属性に変更する
  • 直線上のマークの数を数えるための属性を ListBoard クラスの属性に変更する
  • 直線上のマークの数を数える処理を ListBoard クラスのメソッドで行うようする

ListBoard クラスの修正

最初に ListBoard クラスの修正を行うことにします。

__init__ メソッドの修正

下記は __init__ メソッドを修正したプログラムです。行った修正は Marubatsu クラスの __init__ メソッドの 対応する処理をこちらに移動 したというものです。

  • 3、5 行目:デフォルト値を False とした仮引数 count_linemark を追加し、同名の属性に代入する
  • 7 ~ 19 行目:Marubatsu クラスの __init__ メソッドで行われていた count_linemarkTrue の場合に直線上のマークの数を記録する属性を初期化する処理を追加する
 1  from marubatsu import Marubatsu, ListBoard
 2  
 3  def __init__(self, board_size=3, count_linemark=False):
 4      self.BOARD_SIZE = board_size
 5      self.count_linemark = count_linemark
 6      self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
 7      if self.count_linemark:
 8          self.rowcount = {
 9              Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
10              Marubatsu.CROSS: [0] * self.BOARD_SIZE,
11          }
12          self.colcount = {
13              Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
14              Marubatsu.CROSS: [0] * self.BOARD_SIZE,
15          }
16          self.diacount = {
17              Marubatsu.CIRCLE: [0] * 2,
18              Marubatsu.CROSS: [0] * 2,
19          }
20          
21  ListBoard.__init__  = __init__
行番号のないプログラム
from marubatsu import Marubatsu, ListBoard

def __init__(self, board_size=3, count_linemark=False):
    self.BOARD_SIZE = board_size
    self.count_linemark = count_linemark
    self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
    if self.count_linemark:
        self.rowcount = {
            Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
            Marubatsu.CROSS: [0] * self.BOARD_SIZE,
        }
        self.colcount = {
            Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
            Marubatsu.CROSS: [0] * self.BOARD_SIZE,
        }
        self.diacount = {
            Marubatsu.CIRCLE: [0] * 2,
            Marubatsu.CROSS: [0] * 2,
        }
        
ListBoard.__init__  = __init__
修正箇所
from marubatsu import Marubatsu, ListBoard

-def __init__(self, board_size=3):
+def __init__(self, board_size=3, count_linemark=False):
    self.BOARD_SIZE = board_size
+   self.count_linemark = count_linemark
    self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
+   if self.count_linemark:
+       self.rowcount = {
+           Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+           Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+       }
+       self.colcount = {
+           Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
+           Marubatsu.CROSS: [0] * self.BOARD_SIZE,
+       }
+       self.diacount = {
+           Marubatsu.CIRCLE: [0] * 2,
+           Marubatsu.CROSS: [0] * 2,
+       }
        
ListBoard.__init__  = __init__

setmark メソッドの修正

setmark メソッドでは (x, y) のマスに マークを配置 する処理と、マークを削除 する処理の 両方を行います。そのため、下記のプログラムのように マークを配置 する場合は 対応する直線上のマークの数を増やしマークを削除 する場合は 対応する直線上のマークの数を減らす 処理を行うように修正する必要がある点に注意が必要です。

  • 1、15 行目:元のプログラムでは仮引数の名前が value となっていたが、mark のほうがふさわしいと思ったのでそのように修正した
  • 2 ~ 14 行目count_linemark 属性が True の場合に直線上のマークの数を変更する
  • 3 ~ 8 行目:直線上のマークの数の変化(difference)を表す diff と、どのマークが配置または削除されたかを表す changedmark の計算を行う。なお、15 行目(x, y) のマスに mark を代入する必要がある ため mark の値を変更してはいけない ので、mark とは別の changedmark という変数を用意した
  • 3 ~ 5 行目markMarubatsu.EMPTY でない場合は mark に代入されたマークを配置するので diff1 を、changedmarkmark を代入する
  • 6 ~ 8 行目markMarubatsu.EMPTY の場合は (x, y) に配置されたマークを削除するので diff-1 を、changedmarkself.board[x][y] を代入する
  • 9 ~ 14 行目diffchangedmark の値を利用して Marubatsu クラスの moveunmove メソッドと同様の方法で対応する直線上のマークの数を変更する
 1  def setmark(self, x, y, mark):
 2      if self.count_linemark:
 3          if mark != Marubatsu.EMPTY:
 4              diff = 1
 5              changedmark = mark
 6          else:
 7              diff = -1
 8              changedmark = self.board[x][y]
 9          self.colcount[changedmark][x] += diff
10          self.rowcount[changedmark][y] += diff
11          if x == y:
12              self.diacount[changedmark][0] += diff
13          if x + y == self.BOARD_SIZE - 1:
14              self.diacount[changedmark][1] += diff
15      self.board[x][y] = mark
16  
17  ListBoard.setmark = setmark
行番号のないプログラム
def setmark(self, x, y, mark):
    if self.count_linemark:
        if mark != Marubatsu.EMPTY:
            diff = 1
            changedmark = mark
        else:
            diff = -1
            changedmark = self.board[x][y]
        self.colcount[changedmark][x] += diff
        self.rowcount[changedmark][y] += diff
        if x == y:
            self.diacount[changedmark][0] += diff
        if x + y == self.BOARD_SIZE - 1:
            self.diacount[changedmark][1] += diff
    self.board[x][y] = mark

ListBoard.setmark = setmark
修正箇所
-def setmark(self, x, y, value):
+def setmark(self, x, y, mark):
+   if self.count_linemark:
+       if mark != Marubatsu.EMPTY:
+           diff = 1
+           changedmark = mark
+       else:
+           diff = -1
+           changedmark = self.board[x][y]
+       self.colcount[changedmark][x] += diff
+       self.rowcount[changedmark][y] += diff
+       if x == y:
+           self.diacount[changedmark][0] += diff
+       if x + y == self.BOARD_SIZE - 1:
+           self.diacount[changedmark][1] += diff
-   self.board[x][y] = value
+   self.board[x][y] = mark

ListBoard.setmark = setmark

なお、getmark メソッドが行う処理では 直線上のマークの数は変化しない ので、getmark メソッドを 修正する必要はありません

judge メソッドの定義

次に、ListBoard クラスに勝敗判定を行う judge メソッドを定義 します。行う処理は Marubatsu クラスの judge メソッドと同じ ですが、Marubatsu クラスの judge メソッドでは勝敗判定を行う際に Marubatsu クラスの last_turnlast_movemove_count 属性の値が必要 となるので、それらの値を ListBoard クラスの judge メソッドで参照する方法 を考える必要があります。

その方法としては下記の 2 種類の方法が考えられます。

  • Marubatsu クラスの last_turnlast_movemove_count 属性を ListBoard クラスの属性に変更 する
  • Marubatsu クラスから ListBoard クラスの judge メソッドを呼び出す際に、last_turnlast_movemove_count 属性の値を 実引数に記述 する

本記事では下記の理由から 後者の方法を採用 することにします。

  • 直前の手番 を表す last_turn直前の着手 を表す last_move着手した回数 を表す move_count の値はゲーム盤のデータ構造が変わっても 変化しない可能性が高い データなので、前者の方法を採用した場合は ゲーム盤のデータを表すクラスごとそれらの属性に対する同じ処理を何度も記述する必要 が生じる
  • Marubatsu クラスの中で それらの属性を扱うプログラムをすべて修正する必要 が生じる

下記はそのように judge メソッドを定義したプログラムで、説明と修正箇所は Marubatsu クラスの judge メソッドとの違いです。

  • 1 行目:仮引数 last_turnlast_movemove_count を追加する
  • 2、5、6 行目self.move_countself.last_turnmove_countlast_turn に修正する。また、この後で定義する is_winner メソッドでは last_move の情報が必要となるので last_move を実引数に加えた
  • 8 行目:元のプログラムでは引き分けの判定を is_full というメソッドで行っていたが、その判定処理は 1 行の条件文で記述できるのでその条件文を直接記述するように修正し、is_full メソッドの利用は廃止することにした
 1  def judge(self, last_turn, last_move, move_count):
 2      if move_count < self.BOARD_SIZE * 2 - 1:
 3          return Marubatsu.PLAYING
 4      # 直前に着手を行ったプレイヤーの勝利の判定
 5      if self.is_winner(last_turn, last_move):
 6          return last_turn
 7      # 引き分けの判定
 8      elif move_count == self.BOARD_SIZE ** 2:
 9          return Marubatsu.DRAW
10      # 上記のどれでもなければ決着がついていない
11      else:
12          return Marubatsu.PLAYING   
13  
14  ListBoard.judge = judge
行番号のないプログラム
def judge(self, last_turn, last_move, move_count):
    if move_count < self.BOARD_SIZE * 2 - 1:
        return Marubatsu.PLAYING
    # 直前に着手を行ったプレイヤーの勝利の判定
    if self.is_winner(last_turn, last_move):
        return last_turn
    # 引き分けの判定
    elif move_count == self.BOARD_SIZE ** 2:
        return Marubatsu.DRAW
    # 上記のどれでもなければ決着がついていない
    else:
        return Marubatsu.PLAYING   

ListBoard.judge = judge
修正箇所
-def judge(self):
+def judge(self, last_turn, last_move, move_count):
-   if self.move_count < self.BOARD_SIZE * 2 - 1:
+   if move_count < self.BOARD_SIZE * 2 - 1:
        return Marubatsu.PLAYING
    # 直前に着手を行ったプレイヤーの勝利の判定
-   if self.is_winner(self.last_turn):
+   if self.is_winner(last_turn, last_move):
-       return self.last_turn
+       return last_turn
    # 引き分けの判定
-   elif self.is_full():
+   elif move_count == self.BOARD_SIZE ** 2:
        return Marubatsu.DRAW
    # 上記のどれでもなければ決着がついていない
    else:
        return Marubatsu.PLAYING   

ListBoard.judge = judge

is_winner メソッドの定義

上記の judge メソッドから呼び出される is_winner メソッドを定義する必要があるので、下記のプログラムのように定義します。Marubatsu クラスの is_winner メソッドでは self.last_move を利用 するので、その値を代入する 仮引数 last_move を追加 しました

  • 1 行目:仮引数 last_move を追加する
  • 2 行目self.last_movelast_move に修正する
1  def is_winner(self, player, last_move):
2      x, y = last_move
元と同じなので省略
3  
4  ListBoard.is_winner = is_winner
行番号のないプログラム
def is_winner(self, player, last_move):
    x, y = last_move
    if self.count_linemark:
        if self.rowcount[player][y] == self.BOARD_SIZE or \
        self.colcount[player][x] == self.BOARD_SIZE:
            return True
        # 左上から右下方向の判定
        if x == y and self.diacount[player][0] == self.BOARD_SIZE:
            return True
        # 右上から左下方向の判定
        if x + y == self.BOARD_SIZE - 1 and \
            self.diacount[player][1] == self.BOARD_SIZE:
            return True
    else:
        if self.is_same(player, coord=[0, y], dx=1, dy=0) or \
        self.is_same(player, coord=[x, 0], dx=0, dy=1):
            return True
        # 左上から右下方向の判定
        if x == y and self.is_same(player, coord=[0, 0], dx=1, dy=1):
            return True
        # 右上から左下方向の判定
        if x + y == self.BOARD_SIZE - 1 and \
            self.is_same(player, coord=[self.BOARD_SIZE - 1, 0], dx=-1, dy=1):
            return True
    
    # どの一直線上にも配置されていない場合は、player は勝利していないので False を返す
    return False

ListBoard.is_winner = is_winner
修正箇所
-def is_winner(self, player):
+def is_winner(self, player, last_move):
+   x, y = last_move
元と同じなので省略

ListBoard.is_winner = is_winner

is_same メソッドの定義

上記の is_winner メソッドから呼び出される is_same メソッドを定義する必要があるので、下記のプログラムのように定義します。

  • 3 行目Marubatsu クラスの is_same メソッドでは self.board.getmark と記述していたが、ListBoard クラスの場 is_same メソッドでは self.getmark と記述する必要がある
1  def is_same(self, mark, coord, dx, dy):   
2      x, y = coord
3      text_list = [self.getmark(x + i * dx, y + i * dy)  
4                      for i in range(self.BOARD_SIZE)]
5      line_text = "".join(text_list)
6      return line_text == mark * self.BOARD_SIZE
7  
8  ListBoard.is_same = is_same
行番号のないプログラム
def is_same(self, mark, coord, dx, dy):   
    x, y = coord
    text_list = [self.getmark(x + i * dx, y + i * dy)  
                    for i in range(self.BOARD_SIZE)]
    line_text = "".join(text_list)
    return line_text == mark * self.BOARD_SIZE

ListBoard.is_same = is_same
修正箇所
def is_same(self, mark, coord, dx, dy):   
    x, y = coord
-   text_list = [self.board.getmark(x + i * dx, y + i * dy)  
+   text_list = [self.getmark(x + i * dx, y + i * dy)  
                    for i in range(self.BOARD_SIZE)]
    line_text = "".join(text_list)
    return line_text == mark * self.BOARD_SIZE

ListBoard.is_same = is_same

動作の確認

上記の 修正が正しいことを確認 することにします。

下記は、実引数に count_linemark=False を記述して 直線上のマークの数を数えない ListBoard クラスのインスタンスを作成し、5 手目の着手で 〇 が勝利 する (0, 0)、(1, 1)、(1, 0)、(2, 2)、(2, 0) の順で着手を行った場合board 属性と judge メソッドによる勝敗判定を 表示 するプログラムです。judge メソッドを呼び出す際に 必要な last_turnlast_movemove_count の値の 計算を行うためMarubatsu クラスの move メソッドと同様の処理 を行っています。

なお、ListBoard クラスには __str__ メソッドを定義していないので print(lb) によってゲーム盤を表示することはできません。

lb = ListBoard(count_linemark=False)
movelist = [(0, 0), (1, 1), (1, 0), (2, 2), (2, 0)]
turn = Marubatsu.CIRCLE
move_count = 0
for x, y in movelist:
    print(f"({x}, {y}) に {turn} を着手")
    lb.setmark(x, y, turn)
    move_count += 1
    last_move = (x, y)
    last_turn = turn
    turn = Marubatsu.CROSS if turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
    print(lb.board)
    print(lb.judge(last_turn, last_move, move_count))
    print()

実行結果

(0, 0) に o を着手
[['o', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
playing

(1, 1) に x を着手
[['o', '.', '.'], ['.', 'x', '.'], ['.', '.', '.']]
playing

(1, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', '.']]
playing

(2, 2) に x を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', 'x']]
playing

(2, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['o', '.', 'x']]
o

実行結果 から 5 手目で 〇 が勝利 することを 正しく判定できる ことが確認できました。興味がある方は × が勝利する場合や引き分けになる場合についても確認してみて下さい。

下記は 実引数に count_linemark=True を記述して 直線上のマークの数を数える 場合のプログラムで、先程の表示に加えて 各行、列、斜め方向の順直線上のマークを数も表示 するようにしました。実行結果から 正しい処理が行われている ことが確認できます。

lb = ListBoard(count_linemark=True)
movelist = [(0, 0), (1, 1), (1, 0), (2, 2), (2, 0)]
turn = Marubatsu.CIRCLE
move_count = 0
for x, y in movelist:
    print(f"({x}, {y}) に {turn} を着手")
    lb.setmark(x, y, turn)
    move_count += 1
    last_move = (x, y)
    last_turn = turn
    turn = Marubatsu.CROSS if turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
    print(lb.board)
    print("", lb.rowcount)
    print("", lb.colcount)   
    print("斜め", lb.diacount)
    print(lb.judge(last_turn, last_move, move_count))
    print()

実行結果

(0, 0) に o を着手
[['o', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
行 {'o': [1, 0, 0], 'x': [0, 0, 0]}
列 {'o': [1, 0, 0], 'x': [0, 0, 0]}
斜め {'o': [1, 0], 'x': [0, 0]}
playing

(1, 1) に x を着手
[['o', '.', '.'], ['.', 'x', '.'], ['.', '.', '.']]
行 {'o': [1, 0, 0], 'x': [0, 1, 0]}
列 {'o': [1, 0, 0], 'x': [0, 1, 0]}
斜め {'o': [1, 0], 'x': [1, 1]}
playing

(1, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', '.']]
行 {'o': [2, 0, 0], 'x': [0, 1, 0]}
列 {'o': [1, 1, 0], 'x': [0, 1, 0]}
斜め {'o': [1, 0], 'x': [1, 1]}
playing

(2, 2) に x を着手
[['o', '.', '.'], ['o', 'x', '.'], ['.', '.', 'x']]
行 {'o': [2, 0, 0], 'x': [0, 1, 1]}
列 {'o': [1, 1, 0], 'x': [0, 1, 1]}
斜め {'o': [1, 0], 'x': [2, 1]}
playing

(2, 0) に o を着手
[['o', '.', '.'], ['o', 'x', '.'], ['o', '.', 'x']]
行 {'o': [3, 0, 0], 'x': [0, 1, 1]}
列 {'o': [1, 1, 1], 'x': [0, 1, 1]}
斜め {'o': [1, 1], 'x': [2, 1]}
o

下記は 上記の局面 に対して 同じ順番でマークを削除 する処理を行うプログラムです。なお、勝敗判定を行う judge メソッドは マークが配置されたことを前提 としており、マークを削除した場合には利用できない1ので、judge メソッドに関する処理は削除 しました。実行結果から 正しい処理が行われている ことが確認できます。

for x, y in movelist:
    print(f"({x}, {y}) から {lb.getmark(x, y)} を削除")    
    lb.setmark(x, y, Marubatsu.EMPTY)
    print(lb.board)
    print("", lb.rowcount)
    print("", lb.colcount)   
    print("斜め", lb.diacount)
    print()

実行結果

(0, 0) から o を削除
[['.', '.', '.'], ['o', 'x', '.'], ['o', '.', 'x']]
行 {'o': [2, 0, 0], 'x': [0, 1, 1]}
列 {'o': [0, 1, 1], 'x': [0, 1, 1]}
斜め {'o': [0, 1], 'x': [2, 1]}

(1, 1) から x を削除
[['.', '.', '.'], ['o', '.', '.'], ['o', '.', 'x']]
行 {'o': [2, 0, 0], 'x': [0, 0, 1]}
列 {'o': [0, 1, 1], 'x': [0, 0, 1]}
斜め {'o': [0, 1], 'x': [1, 0]}

(1, 0) から o を削除
[['.', '.', '.'], ['.', '.', '.'], ['o', '.', 'x']]
行 {'o': [1, 0, 0], 'x': [0, 0, 1]}
列 {'o': [0, 0, 1], 'x': [0, 0, 1]}
斜め {'o': [0, 1], 'x': [1, 0]}

(2, 2) から x を削除
[['.', '.', '.'], ['.', '.', '.'], ['o', '.', '.']]
行 {'o': [1, 0, 0], 'x': [0, 0, 0]}
列 {'o': [0, 0, 1], 'x': [0, 0, 0]}
斜め {'o': [0, 1], 'x': [0, 0]}

(2, 0) から o を削除
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
行 {'o': [0, 0, 0], 'x': [0, 0, 0]}
列 {'o': [0, 0, 0], 'x': [0, 0, 0]}
斜め {'o': [0, 0], 'x': [0, 0]}

なお、test_judge を利用した 勝敗判定の確認 を行うためには Marubatsu クラスの修正が必要 なので、その後で行うことにします。

Marubatsu クラスの修正

ListBoard クラスの修正にあわせて Marubatsu クラスを修正 する必要があります。主な修正内容は count_linemark に関する処理の削除 と、勝敗判定の処理ListBoard クラス の judge メソッドを呼び出す ようにする点です。なお、Marubatsu クラスの is_winneris_sameis_full メソッドは 必要がなくなったので削除 することができます。

__init__ メソッドの修正

下記は __init__ メソッドを修正したプログラムです。仮引数 count_linemark は ListBoard クラスのインスタンスを作成する際に必要となるので 残したほうが良いと思う人がいるかもしれません が、今後作成する 別のゲーム盤のデータ構造を表すクラス__init__ メソッドには 仮引数 count_linemark が存在しない場合 や、別の仮引数が存在する 場合が考えられます。このような場合は 可変長引数 *args**kwargs にゲーム盤のデータを作成する際に 実引数に記述するデータを代入 する必要があります。

  • 1 行目:仮引数 count_linemark を削除し、11 行目の後にあった count_linemark に関する処理を削除する
  • 1、7、8 行目:可変長引数 *args**kwargs を追加し、同名の属性に代入する2argskwargs の値は initialize_board でゲーム盤のデータを表すインスタンスを作成する際に実引数に記述する
 1  def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, *args, **kwargs):
 2      # ゲーム盤のデータ構造を定義するクラス
 3      self.boardclass = boardclass
 4      # ゲーム盤の縦横のサイズ
 5      self.BOARD_SIZE = board_size
 6      # boardclass のパラメータ
 7      self.args = args
 8      self.kwargs = kwargs
 9      # move と unmove メソッドで座標などのチェックを行うかどうか
10      self.check_coord = check_coord
11      # 〇×ゲーム盤を再起動するメソッドを呼び出す
12      self.restart()
13      
14  Marubatsu.__init__ = __init__
行番号のないプログラム
def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, *args, **kwargs):
    # ゲーム盤のデータ構造を定義するクラス
    self.boardclass = boardclass
    # ゲーム盤の縦横のサイズ
    self.BOARD_SIZE = board_size
    # boardclass のパラメータ
    self.args = args
    self.kwargs = kwargs
    # move と unmove メソッドで座標などのチェックを行うかどうか
    self.check_coord = check_coord
    # 〇×ゲーム盤を再起動するメソッドを呼び出す
    self.restart()
    
Marubatsu.__init__ = __init__
修正箇所
-def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, count_linemark=False):
+def __init__(self, boardclass=ListBoard, board_size=3, check_coord=True, *args, **kwargs):
    # ゲーム盤のデータ構造を定義するクラス
    self.boardclass = boardclass
    # ゲーム盤の縦横のサイズ
    self.BOARD_SIZE = board_size
-   # 直線上のマークの数を数えるかどうか
-   self.count_linemark = count_linemark
+   # boardclass のパラメータ
+   self.args = args
+   self.kwargs = kwargs
    # move と unmove メソッドで座標などのチェックを行うかどうか
    self.check_coord = check_coord
    # 〇×ゲーム盤を再起動するメソッドを呼び出す
    self.restart()
    
Marubatsu.__init__ = __init__

initialize_board メソッドの修正

下記は initialize_board メソッドを修正したプログラムで、2 行目で ゲーム盤を表すインスタンスを作成 する際に *self.args**self.kwargs を実引数に記述 して 実引数の展開 を行うことで、Marubatsu クラスのインスタンスを作成 した際に 記述した実引数の一部3boardclass のインスタンスを作成 する際の 実引数に記述される ようになります。

def initialize_board(self):
    self.board = self.boardclass(self.BOARD_SIZE, *self.args, **self.kwargs)
    
Marubatsu.initialize_board = initialize_board
修正箇所
def initialize_board(self):
-   self.board = self.boardclass(self.BOARD_SIZE)
+   self.board = self.boardclass(self.BOARD_SIZE, *self.args, **self.kwargs)
    
Marubatsu.initialize_board = initialize_board

restart メソッドの修正

下記は restart メソッドを修正したプログラムで、count_linemark に関する処理を削除 するという修正を行いました。

def restart(self):
    self.initialize_board()
    self.turn = Marubatsu.CIRCLE     
    self.move_count = 0
    self.status = Marubatsu.PLAYING
    self.last_move = -1, -1          
    self.last_turn = None
    self.records = [self.last_move]

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          
    self.last_turn = None
    self.records = [self.last_move]
-   if self.count_linemark:
-       self.rowcount = {
-           Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
-           Marubatsu.CROSS: [0] * self.BOARD_SIZE,
-       }
-       self.colcount = {
-           Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
-           Marubatsu.CROSS: [0] * self.BOARD_SIZE,
-       }
-       self.diacount = {
-           Marubatsu.CIRCLE: [0] * 2,
-           Marubatsu.CROSS: [0] * 2,
-       }

Marubatsu.restart = restart

move メソッドの修正

下記は move メソッドを修正したプログラムで、count_linemark に関する処理の削除 と、judge メソッドの呼び出しの修正 を行いました。

  • 6 行目の下にあった count_linemark に関する処理を削除した
  • 7 行目:勝敗判定を、必要な実引数を記述した self.board.judge の呼び出しによって行うように修正した
 1  def move(self, x, y):
 2      if self.place_mark(x, y, self.turn):
 3          self.last_turn = self.turn
 4          self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
 5          self.move_count += 1
 6          self.last_move = x, y
 7          self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
 8          if len(self.records) <= self.move_count:            
 9              self.records.append(self.last_move)
10          else:
11              self.records[self.move_count] = self.last_move
12              self.records = self.records[0:self.move_count + 1]
13              
14  Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.last_turn = self.turn
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.last_move = x, y
        self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
        if len(self.records) <= self.move_count:            
            self.records.append(self.last_move)
        else:
            self.records[self.move_count] = self.last_move
            self.records = self.records[0:self.move_count + 1]
            
Marubatsu.move = move
修正箇所
def move(self, x, y):
    if self.place_mark(x, y, self.turn):
        self.last_turn = self.turn
        self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
        self.move_count += 1
        self.last_move = x, y
-       if self.count_linemark:
-           self.colcount[self.last_turn][x] += 1
-           self.rowcount[self.last_turn][y] += 1
-           if x == y:
-               self.diacount[self.last_turn][0] += 1        
-           if x + y == self.BOARD_SIZE - 1:
-               self.diacount[self.last_turn][1] += 1   
-       self.status = self.judge()
+       self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
        if len(self.records) <= self.move_count:            
            self.records.append(self.last_move)
        else:
            self.records[self.move_count] = self.last_move
            self.records = self.records[0:self.move_count + 1]
            
Marubatsu.move = move

unmove メソッドの修正

下記は unmove メソッドを修正したプログラムで、count_linemark に関する処理の削除 を行いました。

def unmove(self):
    if self.move_count > 0:
        x, y = self.last_move
        if self.move_count == 0:
            self.last_move = (-1, -1)
        self.move_count -= 1
        self.turn, self.last_turn = self.last_turn, self.turn
        self.status = Marubatsu.PLAYING
        x, y = self.records.pop()
        self.remove_mark(x, y)       
        self.last_move = self.records[-1]
        
Marubatsu.unmove = unmove  
修正箇所
def unmove(self):
    if self.move_count > 0:
        x, y = self.last_move
-       if self.count_linemark:
-           self.colcount[self.last_turn][x] -= 1
-           self.rowcount[self.last_turn][y] -= 1
-           if x == y:
-               self.diacount[self.last_turn][0] -= 1        
-           if x + y == self.BOARD_SIZE - 1:
-               self.diacount[self.last_turn][1] -= 1           
        if self.move_count == 0:
            self.last_move = (-1, -1)
        self.move_count -= 1
        self.turn, self.last_turn = self.last_turn, self.turn
        self.status = Marubatsu.PLAYING
        x, y = self.records.pop()
        self.remove_mark(x, y)       
        self.last_move = self.records[-1]
        
Marubatsu.unmove = unmove  

judge メソッドの修正

勝敗判定ゲーム盤のデータを表すクラスの judge メソッドで行う ことにしたので、Marubatsu クラスの judge メソッドを下記のプログラムのように修正する必要があります。なお、修正方法は先ほどの move メソッドでの judge メソッドの呼び出しと同じ なので省略します。

def judge(self):
    return self.board.judge(self.last_turn, self.last_move, self.move_count)

Marubatsu.judge = judge

動作と処理時間の確認

上記の 修正が正しいことを確認 することにします。

下記は ai_match のキーワード引数 mbparamsMarubatsu クラスの インスタンスを作成 する際に 実引数に記述できる count_linemarkscheck_coord4 種類の組み合わせ をそれぞれ記述して ai2s VS ai2s の対戦を 10000 回行う プログラムです。

実行結果から、いずれの場合でも 対戦成績はほぼ同じ になることが確認できます。また、1 秒あたりの対戦回数の平均前回の記事 の 2438.01 回と ほぼ同じ なので、今回の記事の修正によって 処理時間がほとんど変わらない ことが確認できます。

また、以前の記事 と同様に count_linemarks の有無 では 処理時間はほとんど変わりません が、check_coordFalse にして 座標のチェックを行わない場合は処理速度が若干速くなる ことが確認できました。

from ai import ai_match, ai2s

ai_match(ai=[ai2s, ai2s], match_num=5000, 
         mbparams={"count_linemark": False, "check_coord": True})
ai_match(ai=[ai2s, ai2s], match_num=5000, 
         mbparams={"count_linemark": True, "check_coord": True})
ai_match(ai=[ai2s, ai2s], match_num=5000, 
         mbparams={"count_linemark": False, "check_coord": False})
ai_match(ai=[ai2s, ai2s], match_num=5000, 
         mbparams={"count_linemark": True, "check_coord": False})

実行結果

ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2540.72it/s]
count     win    lose    draw
o        2927    1440     633
x        1435    2915     650
total    4362    4355    1283

ratio     win    lose    draw
o       58.5%   28.8%   12.7%
x       28.7%   58.3%   13.0%
total   43.6%   43.5%   12.8%

ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2590.08it/s]
count     win    lose    draw
o        2962    1416     622
x        1473    2891     636
total    4435    4307    1258

ratio     win    lose    draw
o       59.2%   28.3%   12.4%
x       29.5%   57.8%   12.7%
total   44.4%   43.1%   12.6%

ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2717.71it/s]
count     win    lose    draw
o        2907    1427     666
x        1412    2962     626
total    4319    4389    1292

ratio     win    lose    draw
o       58.1%   28.5%   13.3%
x       28.2%   59.2%   12.5%
total   43.2%   43.9%   12.9%

ai2s VS ai2s
100%|██████████| 5000/5000 [00:01<00:00, 2780.95it/s]
count     win    lose    draw
o        2934    1452     614
x        1394    2990     616
total    4328    4442    1230

ratio     win    lose    draw
o       58.7%   29.0%   12.3%
x       27.9%   59.8%   12.3%
total   43.3%   44.4%   12.3%

test_judge によるテスト

judge メソッドが正しく動作するかを test_judge で確認 することにします。ただし、現状の test_judgeMarubatsu クラスの インスタンスを作成 する際の 実引数を指定できない ので、以前の記事ai_match に対して行った修正と同様の方法で下記のプログラムように 仮引数 mbparams を追加 することにします。

  • 3 行目:デフォルト値を空の dict とする仮引数 mbparams を追加する
  • 8 行目:Marubatsu クラスのインスタンスを作成する際に、実引数に **mbparams を記述してマッピング型の展開が行われるように修正する
1  from mbtest import excel_to_xy
2  
3  def test_judge(testcases=None, debug=False, mbparams={}):
元と同じなので省略
4      print("Start")
5      for winner, testdata_list in testcases.items():
6          print("test winner =", winner) 
7          for testdata in testdata_list:
8              mb = Marubatsu(**mbparams)
元と同じなので省略
行番号のないプログラム
from mbtest import excel_to_xy

def test_judge(testcases=None, debug=False, mbparams={}):
    if testcases is None:
        testcases = {
            # 決着がついていない場合のテストケース
            Marubatsu.PLAYING: [
                # ゲーム盤に一つもマークが配置されていない場合のテストケース
                "",
                # 一つだけマークが配置されていない場合のテストケース
                "C3,A2,B1,B2,C2,C1,A3,B3",
                "A1,A2,C3,B2,C2,C1,A3,B3",
                "A1,A2,B1,B2,C2,C3,A3,B3",
                "A1,C3,B1,B2,C2,C1,A3,B3",
                "A1,A2,B1,C3,C2,C1,A3,B3",
                "A1,A2,B1,B2,C3,C1,A3,B3",
                "A1,A2,B1,B2,C2,C1,C3,B3",
                "A1,A2,B1,B2,A3,C1,C2,C3",
                "A1,A2,B1,B2,C2,C1,A3,B3",
            ],   
            # 〇の勝利のテストケース
            Marubatsu.CIRCLE: [
                "A1,A2,B1,B2,C1",
                "A2,A1,B2,B1,C2",
                "A3,A1,B3,B1,C3",
                "A1,B1,A2,B2,A3",
                "B1,A1,B2,A2,B3",
                "C1,A1,C2,A2,C3",
                "A1,A2,B2,A3,C3",
                "A3,A1,B2,A2,C1",
                # 簡易的な組み合わせ網羅の 6 のテストケース
                "A1,B1,A2,B2,B3,C1,C3,C2,A3", 
            ],
            # × の勝利のテストケース
            Marubatsu.CROSS: [
                "A2,A1,B2,B1,A3,C1",
                "A1,A2,B1,B2,A3,C2",
                "A1,A3,B1,B3,A2,C3",
                "B1,A1,B2,A2,C1,A3",
                "A1,B1,A2,B2,C1,B3",
                "A1,C1,A2,C2,B1,C3",
                "A2,A1,A3,B2,B1,C3",
                "A1,C1,B1,B2,A2,A3",
            ],
            # 引き分けの場合のテストケース
            Marubatsu.DRAW: [
                "A1,A2,B1,B2,C2,C1,A3,B3,C3",
            ], 
        }
            
    print("Start")
    for winner, testdata_list in testcases.items():
        print("test winner =", winner) 
        for testdata in testdata_list:
            mb = Marubatsu(**mbparams)
            for coord in [] if testdata == "" else testdata.split(","):
                x, y = excel_to_xy(coord)            
                mb.move(x, y)
            if debug:
                print(mb)

            if mb.judge() == winner:
                if debug:
                    print("ok")
                else:
                    print("o", end="")
            else:
                print()
                print("====================")
                print("test_judge error!")
                print(mb)
                print("mb.judge():", mb.judge())
                print("winner:    ", winner)
                print("====================")
        print()
    print("Finished")
修正箇所
from mbtest import excel_to_xy

-def test_judge(testcases=None, debug=False):
+def test_judge(testcases=None, debug=False, mbparams={}):
元と同じなので省略
    print("Start")
    for winner, testdata_list in testcases.items():
        print("test winner =", winner) 
        for testdata in testdata_list:
-           mb = Marubatsu()
+           mb = Marubatsu(**mbparams)
元と同じなので省略

上記の修正後に下記のプログラムを実行すると、count_linemarkFalseTrue の両方 の場合で judge メソッドの 勝敗判定が正しく行われたことが確認 できます。

test_judge(mbparams={"count_linemark": False})
test_judge(mbparams={"count_linemark": True})

実行結果

Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished
Start
test winner = playing
oooooooooo
test winner = o
ooooooooo
test winner = x
oooooooo
test winner = draw
o
Finished

局面のマークのパターンの数を数える処理の修正

局面の マークのパターンの数を数える count_markpats もゲーム盤のデータ構造によって 効率の良いアルゴリズムが変化する ので Board クラスの 抽象メソッド とし、ListBoard クラスの メソッドとして定義 することにします。

なお、マークのパターンを 数えるのではなく 列挙する という処理を行う enum_markpatsai8s から呼び出されていますが、enum_markpats が行う処理は count_markpats とほぼ同じ なので 両方を抽象メソッドとするのは冗長 です。調べたところ enum_markpatsai8s のみから呼び出されている ことが判明したので ai8scount_markpats を利用するように修正 し、enum_markpats は削除 することにします。

ListBoard クラスの修正

まず、ListBoard クラスの修正を行います。

count_markpats メソッドの定義

下記は count_markpats メソッドの定義です。この中で呼び出している count_marks メソッドでは Marubatsu クラスの turnlast_turn 属性の値が必要 になるため、先程の judge メソッドの定義方法と同様に それらを代入する仮引数を追加 することにしました。

下記の説明と修正箇所は Marubatsu クラスの count_markpats からの修正です。

  • 4 行目:仮引数 turnlast_turn を追加した
  • 11 行目self.last_turnlast_turn に修正した
  • 18、20、23、26 行目count_marks メソッドを呼び出す際に記述する実引数に turnlast_turn を追加した
 1  from marubatsu import Markpat
 2  from collections import defaultdict
 3  
 4  def count_markpats(self, turn, last_turn):
 5      markpats = defaultdict(int)
 6      
 7      if self.count_linemark:
 8          for countdict in [self.rowcount, self.colcount, self.diacount]:
 9              for circlecount, crosscount in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
10                  emptycount = self.BOARD_SIZE - circlecount - crosscount
11                  if last_turn == Marubatsu.CIRCLE:
12                      markpats[(circlecount, crosscount, emptycount)] += 1
13                  else:
14                      markpats[(crosscount, circlecount, emptycount)] += 1
15      else:
16          # 横方向と縦方向の判定
17          for i in range(self.BOARD_SIZE):
18              count = self.count_marks(turn, last_turn, coord=[0, i], dx=1, dy=0, datatype="tuple")
19              markpats[count] += 1
20              count = self.count_marks(turn, last_turn, coord=[i, 0], dx=0, dy=1, datatype="tuple")
21              markpats[count] += 1
22          # 左上から右下方向の判定
23          count = self.count_marks(turn, last_turn, coord=[0, 0], dx=1, dy=1, datatype="tuple")
24          markpats[count] += 1
25          # 右上から左下方向の判定
26          count = self.count_marks(turn, last_turn, coord=[2, 0], dx=-1, dy=1, datatype="tuple")
27          markpats[count] += 1
28  
29      return markpats   
30  
31  ListBoard.count_markpats = count_markpats
行番号のないプログラム
from marubatsu import Markpat
from collections import defaultdict

def count_markpats(self, turn, last_turn):
    markpats = defaultdict(int)
    
    if self.count_linemark:
        for countdict in [self.rowcount, self.colcount, self.diacount]:
            for circlecount, crosscount in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
                emptycount = self.BOARD_SIZE - circlecount - crosscount
                if last_turn == Marubatsu.CIRCLE:
                    markpats[(circlecount, crosscount, emptycount)] += 1
                else:
                    markpats[(crosscount, circlecount, emptycount)] += 1
    else:
        # 横方向と縦方向の判定
        for i in range(self.BOARD_SIZE):
            count = self.count_marks(turn, last_turn, coord=[0, i], dx=1, dy=0, datatype="tuple")
            markpats[count] += 1
            count = self.count_marks(turn, last_turn, coord=[i, 0], dx=0, dy=1, datatype="tuple")
            markpats[count] += 1
        # 左上から右下方向の判定
        count = self.count_marks(turn, last_turn, coord=[0, 0], dx=1, dy=1, datatype="tuple")
        markpats[count] += 1
        # 右上から左下方向の判定
        count = self.count_marks(turn, last_turn, coord=[2, 0], dx=-1, dy=1, datatype="tuple")
        markpats[count] += 1

    return markpats   

ListBoard.count_markpats = count_markpats
修正箇所
from marubatsu import Markpat
from collections import defaultdict

-def count_markpats(self):
+def count_markpats(self, turn, last_turn):
    markpats = defaultdict(int)
    
    if self.count_linemark:
        for countdict in [self.rowcount, self.colcount, self.diacount]:
            for circlecount, crosscount in zip(countdict[Marubatsu.CIRCLE], countdict[Marubatsu.CROSS]):
                emptycount = self.BOARD_SIZE - circlecount - crosscount
-               if self.last_turn == Marubatsu.CIRCLE:
+               if last_turn == Marubatsu.CIRCLE:
                    markpats[(circlecount, crosscount, emptycount)] += 1
                else:
                    markpats[(crosscount, circlecount, emptycount)] += 1
    else:
        # 横方向と縦方向の判定
        for i in range(self.BOARD_SIZE):
-           count = self.count_marks(coord=[0, i], dx=1, dy=0, datatype="tuple")
+           count = self.count_marks(turn, last_turn, coord=[0, i], dx=1, dy=0, datatype="tuple")
            markpats[count] += 1
-           count = self.count_marks(coord=[i, 0], dx=0, dy=1, datatype="tuple")
+           count = self.count_marks(turn, last_turn, coord=[i, 0], dx=0, dy=1, datatype="tuple")
            markpats[count] += 1
        # 左上から右下方向の判定
-       count = self.count_marks(coord=[0, 0], dx=1, dy=1, datatype="tuple")
+       count = self.count_marks(turn, last_turn, coord=[0, 0], dx=1, dy=1, datatype="tuple")
        markpats[count] += 1
        # 右上から左下方向の判定
-       count = self.count_marks(coord=[2, 0], dx=-1, dy=1, datatype="tuple")
+       count = self.count_marks(turn, last_turn, coord=[2, 0], dx=-1, dy=1, datatype="tuple")
        markpats[count] += 1

    return markpats   

ListBoard.count_markpats = count_markpats

count_marks メソッドの定義

count_markpats から呼び出されている count_marks を下記のプログラムのように定義します。下記の説明と修正箇所は Marubatsu クラスの count_marks からの修正です。

  • 1 行目:仮引数 turnlast_turn を追加した
  • 12 行目self.turnself.last_turnturnlast_turn に修正した
 1  def count_marks(self, turn, last_turn, coord, dx, dy, datatype="dict"):   
 2      x, y = coord   
 3      count = defaultdict(int)
 4      for _ in range(self.BOARD_SIZE):
 5          count[self.getmark(x, y)] += 1
 6          x += dx
 7          y += dy
 8  
 9      if datatype == "dict":
10          return count
11      else:
12          return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])  
13      
14  ListBoard.count_marks = count_marks
行番号のないプログラム
def count_marks(self, turn, last_turn, coord, dx, dy, datatype="dict"):   
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.getmark(x, y)] += 1
        x += dx
        y += dy

    if datatype == "dict":
        return count
    else:
        return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])  
    
ListBoard.count_marks = count_marks
修正箇所
-def count_marks(self, coord, dx, dy, datatype="dict"):   
+def count_marks(self, turn, last_turn, coord, dx, dy, datatype="dict"):   
    x, y = coord   
    count = defaultdict(int)
    for _ in range(self.BOARD_SIZE):
        count[self.getmark(x, y)] += 1
        x += dx
        y += dy

    if datatype == "dict":
        return count
    else:
-       return Markpat(count[self.last_turn], count[self.turn], count[Marubatsu.EMPTY])  
+       return Markpat(count[last_turn], count[turn], count[Marubatsu.EMPTY])  
    
ListBoard.count_marks = count_marks

Marubatsu クラスの修正

Marubatsu クラスの修正は、下記のプログラムのように count_markpats の処理で 上記で定義した count_markpats を呼び出す ように修正します。修正方法は Marubatsu クラスの judge メソッドの修正と同じなので説明と修正箇所は省略します。

def count_markpats(self):
    return self.board.count_markpats(self.turn, self.last_turn)

Marubatsu.count_markpats = count_markpats

ai8s の修正

enum_markpats メソッドを廃止 したので、そのメソッドを呼び出す ai8s を修正 する必要があります。ai8s では 特定のマークのパターンが存在する かどうかで 評価値の計算 を行いますが、その判定count_markpats で計算した マークのパターンの数が 1 以上であるか で判定することができます。従って、ai8s は下記のプログラムのように修正できます。

  • 5 行目enum_markpatscount_markpats に修正する
  • 7、10 行目:指定したマークのパターンが 0 より大きいことを判定することで、そのマークのパターンが存在することを判定するように修正する
 1  from ai import ai_by_score
 2  
 3  @ai_by_score
 4  def ai8s(mb, debug=False):
元と同じなので省略
 5      markpats = mb.count_markpats()
 6      # 相手が勝利できる場合は評価値として -1 を返す
 7      if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
 8          return -1
 9      # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
10      elif markpats[Markpat(last_turn=2, turn=0, empty=1)] > 0:
11          return 1
12      # それ以外の場合は評価値として 0 を返す
13      else:
14          return 0
行番号のないプログラム
from ai import ai_by_score

@ai_by_score
def ai8s(mb, debug=False):
    # 真ん中のマスに着手している場合は、評価値として 3 を返す
    if mb.last_move == (1, 1):
        return 3

    # 自分が勝利している場合は、評価値として 2 を返す
    if mb.status == mb.last_turn:
        return 2

    markpats = mb.count_markpats()
    # 相手が勝利できる場合は評価値として -1 を返す
    if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        return -1
    # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
    elif markpats[Markpat(last_turn=2, turn=0, empty=1)] > 0:
        return 1
    # それ以外の場合は評価値として 0 を返す
    else:
        return 0
修正箇所
from ai import ai_by_score

@ai_by_score
def ai8s(mb, debug=False):
元と同じなので省略
-   markpats = mb.enum_markpats()
+   markpats = mb.count_markpats()
    # 相手が勝利できる場合は評価値として -1 を返す
-   if Markpat(last_turn=0, turn=2, empty=1) in markpats:
+   if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
        return -1
    # 次の自分の手番で自分が勝利できる場合は評価値として 1 を返す
-   elif Markpat(last_turn=2, turn=0, empty=1) in markpats:    
+   elif markpats[Markpat(last_turn=2, turn=0, empty=1)] > 0:
        return 1
    # それ以外の場合は評価値として 0 を返す
    else:
        return 0

動作の確認

上記の修正後に下記のプログラムで上記で修正した ai8s と、count_markpats を利用する ai14s のそれぞれと ai2s の対戦 を行うことにします。また、その際に count_linemarkTrueFalse のそれぞれの場合で対戦を行います。

from ai import ai14s

ai_match(ai=[ai8s, ai2s], mbparams={"count_linemark": False})
ai_match(ai=[ai8s, ai2s], mbparams={"count_linemark": True})
ai_match(ai=[ai14s, ai2s], mbparams={"count_linemark": False})
ai_match(ai=[ai14s, ai2s], mbparams={"count_linemark": True})

実行結果

ai8s VS ai2s
100%|██████████| 10000/10000 [00:10<00:00, 938.44it/s]
count     win    lose    draw
o        9852      14     134
x        8953     226     821
total   18805     240     955

ratio     win    lose    draw
o       98.5%    0.1%    1.3%
x       89.5%    2.3%    8.2%
total   94.0%    1.2%    4.8%

ai8s VS ai2s
100%|██████████| 10000/10000 [00:05<00:00, 1784.56it/s]
count     win    lose    draw
o        9858      19     123
x        8856     225     919
total   18714     244    1042

ratio     win    lose    draw
o       98.6%    0.2%    1.2%
x       88.6%    2.2%    9.2%
total   93.6%    1.2%    5.2%

ai14s VS ai2s
100%|██████████| 10000/10000 [00:11<00:00, 858.05it/s]
count     win    lose    draw
o        9896       0     104
x        8849       0    1151
total   18745       0    1255

ratio     win    lose    draw
o       99.0%    0.0%    1.0%
x       88.5%    0.0%   11.5%
total   93.7%    0.0%    6.3%

ai14s VS ai2s
100%|██████████| 10000/10000 [00:06<00:00, 1449.95it/s]
count     win    lose    draw
o        9915       0      85
x        8774       0    1226
total   18689       0    1311

ratio     win    lose    draw
o       99.2%    0.0%    0.9%
x       87.7%    0.0%   12.3%
total   93.4%    0.0%    6.6%

以前の記事と上記でで行った ai8s VS ai2sai14s VS ai2s の対戦成績をまとめた表です。同じ AI どうし の対戦成績はいずれも ほぼ同じ なので、先程の 修正が正しく行われたことが確認 できます。

関数名 o 勝 o 負 o 分 x 勝 x 負 x 分
以前の ai8s 98.2 0.1 1.6 89.4 2.5 8.1 93.8 1.3 4.9
count_linemark=False
ai8s
98.5 0.1 1.3 89.5 2.3 8.2 94.0 1.2 4.8
count_linemark=True
ai8s
98.6 0.2 1.2 88.6 2.2 9.2 93.6 1.2 5.2
以前の ai14s 99.0 0.0 1.0 88.8 0.0 11.2 93.9 0.0 6.1
count_linemark=False
ai14s
99.0 0.0 1.0 88.5 0.0 11.5 93.7 0.0 6.3
count_linemark=False
ai14s
99.2 0.0 0.9 87.7 0.0 12.3 93.4 6.6 4.8

また、count_linemarkTrue の場合はいずれも 1 秒間の対戦回数の平均2 倍弱ほど増えている ので、以前の記事と同様に 直線上のマークの数を数える という手法によって ai8sai14scount_markpats 処理の高速化が行われる ことが確認できました。

Board クラスの修正

今回の記事で、Board クラスに 抽象メソッドとして定義するメソッドが増えた ので、下記のプログラムのように judgecount_markpats メソッドを Board クラスの抽象メソッドとして定義することにします。

from marubatsu import Board
from abc import abstractmethod

@abstractmethod
def judge(self, last_turn, last_move, move_count):
    pass

Board.judge = judge

@abstractmethod
def count_markpats(self, turn, last_turn):
    pass

Board.count_markpat = count_markpats

なお、judge メソッドから呼び出される is_winner メソッドや、count_markpats から呼び出される count_marks メソッドなどは、ゲーム盤のデータ構造が変化すると 必要がなくなる可能性があるメソッド です。また、それらのメソッドは Marubatsu クラスなどの ListBoard クラスのメソッド以外 から呼び出して 利用することはない ので 抽象メソッドとして定義する必要はありません

抽象メソッドの一覧

下記は現時点での Board クラスに定義する 抽象メソッドの一覧 です。

抽象メソッド 処理
getmark(x, y) (x, y) のマスのマークを返す
setmark(x, y, mark) (x, y) のマスに mark を代入する
board_to_str() ゲーム盤を表す文字列を返す
judge(last_turn, last_move, move_count) 勝敗判定を計算して返す
count_markpats(turn, last_turn) 局面のマークのパターンを返す

List1dBoard の修正

List1dBoard に対しても上記の 抽象メソッドを定義するように修正 を行う必要がありますが、List1dBoard が行う処理は __init__getmarksetmarkboard_to_str 以外 のメソッドは ListBoard のメソッドと全く同じ です。

以前の記事で説明したように、別のクラスを継承 した 派生クラス は、基底クラスのメソッドをそのまま利用 することができます。また、以前の記事で説明したように 基底クラス のメソッドと 同じ名前のメソッド派生クラスに定義 して オーバーライド することで、そのメソッドを呼び出した際に 派生クラスで定義したメソッドが呼び出される ようになります。

従って List1dBoard の定義の修正は ListBoard クラスを継承 し、処理が異なるメソッドだけを定義してオーバーライド することで簡単に修正することができます。

下記のプログラムはそのように List1dBoard クラスを定義するプログラムです。説明と修正箇所は ListBoard クラスのメソッドとの違いです。

  • 1 行目:ListBoard クラスを継承するように修正する
  • 5 行目board 属性に 1 次元の list でゲーム盤のデータを初期する
  • 14、15、18、21 行目:1 次元の list に対する処理を行うように修正する
 1  class List1dBoard(ListBoard):
 2      def __init__(self, board_size=3, count_linemark=False):
 3          self.BOARD_SIZE = board_size
 4          self.count_linemark = count_linemark
 5          self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
元と同じなので省略
 6              
 7      def setmark(self, x, y, mark):
 8          if self.count_linemark:
 9              if mark != Marubatsu.EMPTY:
10                  diff = 1
11                  changedmark = mark
12              else:
13                  diff = -1
14                  changedmark = self.board[y + x * self.BOARD_SIZE]
元と同じなので省略
15          self.board[y + x * self.BOARD_SIZE] = mark
16  
17      def getmark(self, x, y):
18          return self.board[y + x * self.BOARD_SIZE]
19  
20      def board_to_str(self):
21          return "".join(self.board)
行番号のないプログラム
-class List1dBoard(Board):
+class List1dBoard(ListBoard):
    def __init__(self, board_size=3, count_linemark=False):
        self.BOARD_SIZE = board_size
        self.count_linemark = count_linemark
        self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
        if self.count_linemark:
            self.rowcount = {
                Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
                Marubatsu.CROSS: [0] * self.BOARD_SIZE,
            }
            self.colcount = {
                Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
                Marubatsu.CROSS: [0] * self.BOARD_SIZE,
            }
            self.diacount = {
                Marubatsu.CIRCLE: [0] * 2,
                Marubatsu.CROSS: [0] * 2,
            }
            
    def setmark(self, x, y, mark):
        if self.count_linemark:
            if mark != Marubatsu.EMPTY:
                diff = 1
                changedmark = mark
            else:
                diff = -1
                changedmark = self.board[x][y]
            self.colcount[changedmark][x] += diff
            self.rowcount[changedmark][y] += diff
            if x == y:
                self.diacount[changedmark][0] += diff
            if x + y == self.BOARD_SIZE - 1:
                self.diacount[changedmark][1] += diff
        self.board[y + x * self.BOARD_SIZE] = mark

    def getmark(self, x, y):
        return self.board[y + x * self.BOARD_SIZE]

    def board_to_str(self):
        return "".join(self.board)
修正箇所
class List1dBoard(ListBoard):
    def __init__(self, board_size=3, count_linemark=False):
        self.BOARD_SIZE = board_size
        self.count_linemark = count_linemark
+       self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
-       self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
元と同じなので省略
            
    def setmark(self, x, y, mark):
        if self.count_linemark:
            if mark != Marubatsu.EMPTY:
                diff = 1
                changedmark = mark
            else:
                diff = -1
-               changedmark = self.board[x][y]
+               changedmark = self.board[y + x * self.BOARD_SIZE]
元と同じなので省略
-       self.board[x][y] = mark
+       self.board[y + x * self.BOARD_SIZE] = mark

    def getmark(self, x, y):
-       return self.board[x][y]
+       return self.board[y + x * self.BOARD_SIZE]

    def board_to_str(self):
-       txt = ""
-       for col in self.board:
-           txt += "".join(col)
-       return txt
+       return "".join(self.board)

動作の確認

上記の修正後に下記のプログラムで 先程と同様の対戦List1dBoard を利用した Marubatsu クラス で行うと、実行結果のように 先程とほぼ同じ結果が表示 されることから、List1dBoard の定義が正しく行われたことが確認 できます。

ai_match(ai=[ai8s, ai2s], 
         mbparams={"boardclass":List1dBoard, "count_linemark": False})
ai_match(ai=[ai8s, ai2s], 
         mbparams={"boardclass":List1dBoard, "count_linemark": True})
ai_match(ai=[ai14s, ai2s], 
         mbparams={"boardclass":List1dBoard, "count_linemark": False})
ai_match(ai=[ai14s, ai2s], 
         mbparams={"boardclass":List1dBoard, "count_linemark": True})

実行結果

ai8s VS ai2s
100%|██████████| 10000/10000 [00:10<00:00, 972.90it/s]
count     win    lose    draw
o        9851       8     141
x        8930     249     821
total   18781     257     962

ratio     win    lose    draw
o       98.5%    0.1%    1.4%
x       89.3%    2.5%    8.2%
total   93.9%    1.3%    4.8%

ai8s VS ai2s
100%|██████████| 10000/10000 [00:05<00:00, 1711.13it/s]
count     win    lose    draw
o        9849      10     141
x        8929     249     822
total   18778     259     963

ratio     win    lose    draw
o       98.5%    0.1%    1.4%
x       89.3%    2.5%    8.2%
total   93.9%    1.3%    4.8%

ai14s VS ai2s
100%|██████████| 10000/10000 [00:12<00:00, 810.29it/s]
count     win    lose    draw
o        9910       0      90
x        8808       0    1192
total   18718       0    1282

ratio     win    lose    draw
o       99.1%    0.0%    0.9%
x       88.1%    0.0%   11.9%
total   93.6%    0.0%    6.4%

ai14s VS ai2s
100%|██████████| 10000/10000 [00:07<00:00, 1380.18it/s]
count     win    lose    draw
o        9902       0      98
x        8831       0    1169
total   18733       0    1267

ratio     win    lose    draw
o       99.0%    0.0%    1.0%
x       88.3%    0.0%   11.7%
total   93.7%    0.0%    6.3%

今回の記事のまとめ

今回の記事では Board クラスの抽象メソッドの追加 を行い、それに従って ListBoardList1dBoardMarubatsu クラスを修正 しました。これで準備が整いましたので、次回の記事では今までとは異なるデータ構造でゲーム盤を表現するクラスを定義することにします。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
marubatsu.py 本記事で更新した marubatsu_new.py
ai.py 本記事で更新した ai_new.py
mbtest.py 本記事で更新した mbtest_new.py

次回の記事

  1. マークの削除は直前の手番を取り消す unmove メソッドで行われますが、その場合はゲームの状態が必ず Marubatsu.PLAYING になるので judge メソッドを呼び出す必要はありません

  2. *args**kwargs*** は、この仮引数が可変長引数であることを表す記号なので、この仮引数の名前は argskwargs です。従って、これらの可変長引数を同名の属性に代入する場合は *** を記述しない argskwargs と記述する必要がある点に注意が必要です

  3. Marubatsu クラスの __init__ メソッドの argskwargs に代入された実引数です

0
0
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?