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を一から作成する その195 座標を表すデータ構造に対するポリモーフィズム

Last updated at Posted at 2025-09-22

目次と前回の記事

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 の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

マスの座標を表すデータに関する問題点

これまでの記事では 2 次元の list1 次元の list でゲーム盤のデータ構造を表現する ListBoard と List1dBoard を定義しました。これらのクラスでは ゲーム盤のデータを board 属性に代入 しますが、ListBoard では (x, y) のマスを board[x][y]List1dBoard では board[y + x * self.BOARD_SIZE] で参照する点が異なります。

別の言葉で説明すると、ListBoard クラスではゲーム盤の マスの座標 を (x, y) の 2 次元の座標で表現 するのに対し、List1dBoard ではゲーム盤の座標を以前の記事で説明した下図の 1 次元の数値座標で表現 するという点が異なります。

現状の List1dBoard クラスでは、下記のプログラムのように ゲーム盤のマスの参照と代入 を行う getmarksetmark の仮引数に 2 次元の座標 を表す xy を代入するようにしているため、2 次元の座標y + x * self.BOARD_SIZE という式で 数値座標に変換する処理 を行う必要があります。

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

def setmark(self, x, y, mark):

    self.board[y + x * self.BOARD_SIZE] = mark

上記のプログラムを、下記のプログラムのように ListBoardマスを参照する際に利用 する 1 次元の数値座標 を代入する move という仮引数を持つように修正することで、座標の変換の処理を行う必要がなくなる ため、処理速度の改善が期待 できます。

def getmark(self, move):
    return self.board[move]

def setmark(self, move, mark):

    self.board[move] = mark

上記では ListBoard と List1dBoard のように 異なるデータ構造でゲーム盤を表現 する際に、座標を表すデータ構造異なるデータ構造表現 したほうが 処理の効率が良く なるという例を紹介しました。そこで、今回の記事では ゲーム盤を表すデータ構造ごと に、座標を表すデータ構造を変更できる ようにプログラムを修正することにします。どのように修正すればよいかについて少し考えてみて下さい。

ゲーム盤のデータを表すクラスの修正

まず、ゲーム盤のデータを表現するクラス である ListBoardList1dBoard クラスの修正からはじめることにします。

getmarksetmark の仮引数の変更

ゲーム盤を表すクラス に対して 必ず定義を行う必要がある getmarksetmark を Marubatsu クラスなどのプログラムで 利用するため には、getmarksetmark共通の仮引数を持つ必要 があります。上記のように List1dBoardgetmark の定義def getmark(self, move): のように変更 してしまうと、ListBoardgetmark の定義def getmark(self, x, y): であることから 仮引数が一致しなくなる という問題が発生します。この問題は setmark メソッドでも同様 です。

そこで、getmarksetmark座標を代入する仮引数move の 1 つに統一する ことにします。ListBoard クラスの場合は、move(x, y) という tuple を代入 することで 2 次元の座標を 1 つの仮引数に代入 します。

下記はそのように ListBoard クラス の getmarksetmark を修正 するプログラムです。なお、下記の修正によって 4、10 行目 の処理の分だけ 処理時間が増えると思う人がいるかもしれませんが、後で説明するように getmark を呼び出す前に行われていた x, y = move という処理を省略することができるので、全体としての処理時間は大きく変化しません

  • 3、9 行目:仮引数 xymove に修正する
  • 4、10 行目move には座標を表す (x, y) という tuple が代入されるので、tuple の展開を行うことで x 座標と y 座標を xy に代入する

なお、説明は省略しますが、setmark 内で間違って changedmark という名前の変数を countmark という名前にしていたことが判明したので changemdmark に修正しました。

 1  from marubatsu import ListBoard
 2  
 3  def getmark(self, move):
 4      x, y = move
 5      return self.board[x][y]    
 6  
 7  ListBoard.getmark = getmark
 8  
 9  def setmark(self, move, mark):
10      x, y = move
元と同じなので省略
11      self.board[x][y] = mark
12      
13  ListBoard.setmark = setmark
行番号のないプログラム
from marubatsu import Marubatsu, ListBoard

def getmark(self, move):
    x, y = move
    return self.board[x][y]    

ListBoard.getmark = getmark

def setmark(self, move, mark):
    x, y = move
    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
修正箇所
from marubatsu import ListBoard

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

ListBoard.getmark = getmark

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

下記はそのように List1dBoard クラス の getmarksetmark を修正 するプログラムです。

  • 3、8 行目:仮引数 xymove に修正する
  • 4、17、24 行目self.board の添字に move を記述するように修正する
  • 10、11 行目:直線上のマークの数を数える場合は xy 座標の値が必要であるため、残念ながら数値座標を表す move から x と y 座標を計算する必要があるので、以前の記事1で説明した方法でその計算を行う。直線上のマークの数を数えない場合はこの処理は不要なので、9 行目の if 文の条件式が True の場合のみこの処理を行うようにした

こちらは元から changedmark が使われているのでそれに関する修正はありません。

 1  from marubatsu import List1dBoard
 2  
 3  def getmark(self, move):
 4      return self.board[move]    
 5  
 6  List1dBoard.getmark = getmark
 7  
 8  def setmark(self, move, mark):
 9      if self.count_linemark:
10          x = move // self.BOARD_SIZE
11          y = move % self.BOARD_SIZE
12          if mark != Marubatsu.EMPTY:
13              diff = 1
14              changedmark = mark
15          else:
16              diff = -1
17              changedmark = self.board[move]
18          self.colcount[changedmark][x] += diff
19          self.rowcount[changedmark][y] += diff
20          if x == y:
21              self.diacount[changedmark][0] += diff
22          if x + y == self.BOARD_SIZE - 1:
23              self.diacount[changedmark][1] += diff
24      self.board[move] = mark
25      
26  List1dBoard.setmark = setmark
行番号のないプログラム
from marubatsu import List1dBoard

def getmark(self, move):
    return self.board[move]    

List1dBoard.getmark = getmark

def setmark(self, move, mark):
    if self.count_linemark:
        x = move // self.BOARD_SIZE
        y = move % self.BOARD_SIZE
        if mark != Marubatsu.EMPTY:
            diff = 1
            changedmark = mark
        else:
            diff = -1
            changedmark = self.board[move]
        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[move] = mark
    
List1dBoard.setmark = setmark
修正箇所
from marubatsu import List1dBoard

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

List1dBoard.getmark = getmark

-def setmark(self, x, y, mark):
+def setmark(self, move, mark):
    if self.count_linemark:
+       x = move // self.BOARD_SIZE
+       y = move % self.BOARD_SIZE
        if mark != Marubatsu.EMPTY:
            diff = 1
            changedmark = mark
        else:
            diff = -1
-           changedmark = self.board[y + x * self.BOARD_SIZE]
+           changedmark = self.board[move]
        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
+   self.board[move] = mark
    
List1dBoard.setmark = setmark

上記の修正後に 下記の処理 を行うプログラムを実行すると、実行結果から 正しい処理が行われることが確認 できます。

  • 直線状のマークの数を数える処理を行う ListBoard クラスのインスタンスを作成し、board 属性を表示する
  • setmark で (0, 0)、(1, 0) に 〇 と × を着手し、board 属性を表示する
  • getmark で (0, 0)、(1, 0)、(2, 0) を表示する
lb = ListBoard(count_linemark=True)
print(lb.board)
lb.setmark((0, 0), Marubatsu.CIRCLE)
lb.setmark((0, 1), Marubatsu.CROSS)
print(lb.board)
print(lb.getmark((0, 0)))
print(lb.getmark((0, 1)))
print(lb.getmark((0, 2)))

実行結果

[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
[['o', 'x', '.'], ['.', '.', '.'], ['.', '.', '.']]
o
x
.

次に、上記と 同様の処理List1dBoard クラスのインスタンスを作成した場合で行う下記のプログラムを実行すると、実行結果から 正しい処理が行われることが確認 できます。なお、getmarksetmark の実引数に記述する 座標は数値座標 なので、(0, 0) のマスは 0(0, 1) のマスは 1(0, 2) のマスは 2 を記述する必要があります。

lb1d = List1dBoard(count_linemark=True)
print(lb1d.board)
lb1d.setmark(0, Marubatsu.CIRCLE)
lb1d.setmark(1, Marubatsu.CROSS)
print(lb1d.board)
print(lb1d.getmark(0))
print(lb1d.getmark(1))
print(lb1d.getmark(2))

実行結果

['.', '.', '.', '.', '.', '.', '.', '.', '.']
['o', 'x', '.', '.', '.', '.', '.', '.', '.']
o
x
.

calc_legal_moves の定義と修正

上記のように getmark メソッドと setmark メソッドの 仮引数を変更 したことで、ゲーム盤を表すクラスが変わるgetmarksetmark 呼び出す際に 実引数に記述する座標のデータが変わる ことになります。そのため、Marubatsu クラスなどで getmarksetmark を呼び出す処理 を、ゲーム盤を表すクラスの種類ごと に if 文などで 区別して記述する必要があるのではないか と思った人がいるかもしれません。この問題は、AI どうしの対戦を行う場合うまく解決することができます。その方法について少し考えてみて下さい。

AI の関数Marubatsu クラスの calc_legal_moves メソッドで計算された 合法手の一覧の中から着手を選択 するという処理を行います。Marubatsu クラスの calc_legal_moves が計算する 合法手 は、下記のプログラムの 4 行目で 常に 2 次元の座標を表す tuple が計算されるので、これらの座標は ListBoardgetmarksetmark直接利用することができます が、List1dBoardgetmarksetmark では 利用できません

1  def calc_legal_moves(self):
2      if self.status != Marubatsu.PLAYING:
3          return []
4      legal_moves = [(x, y) for y in range(self.BOARD_SIZE) 
5                          for x in range(self.BOARD_SIZE)
6                          if self.board.getmark(x, y) == Marubatsu.EMPTY]
7      return legal_moves

この問題を解決する方法として、calc_legal_movesゲーム盤のデータを表現するクラスのメソッド として定義し、その ゲーム盤のデータ構造に適したデータ構造での座標の一覧を返す ようにするという方法があります。具体的には、ListBoard クラスの calc_legal_moves では 2 次元の座標を表す tuple を、List1dBoard クラスでは 数値座標を表す整数 を要素として持つ list を返すようにします。そのように修正することで、calc_legal_moves が計算した 合法手の 座標をそのまま getmarksetmark の実引数に記述できる ようになります。その結果、AI の関数calc_legal_moves が計算した座標の中から最善手を選択 するので、AI が計算した最善手そのまま setmark の実引数に記述 して着手することができます。

なお、「ゲーム盤のデータ構造に適したデータ構造での座標」という表現は長いので、以後は「ゲーム盤のクラスの座標」と表記することにします。

下記は ListBoard クラスの calc_legal_moves メソッドの定義です。下記の説明と修正箇所は Marubatsu クラスの calc_legal_moves メソッドとの違いです。

  • ListBoard クラスには status 属性は存在しないので status 属性が Marubatsu.PLAYING でない場合の処理は削除した。削除した処理status 属性が存在する Marubatsu クラスの calc_legal_moves で行う ことにする
  • 4 行目self.getmark(x, y) で (x, y) のマスのマークを参照する処理は、getmark 内で self.board[x][y] を返すという処理なので、直接 self.board[x][y] を参照するように修正した。メソッドの呼び出し処理には非常に短いながらも時間がかかるので、このように修正したほうが処理時間が短くなることが期待できる
1  def calc_legal_moves(self):
2      legal_moves = [(x, y) for y in range(self.BOARD_SIZE) 
3                          for x in range(self.BOARD_SIZE)
4                          if self.board[x][y] == Marubatsu.EMPTY]
5      return legal_moves
6  
7  ListBoard.calc_legal_moves = calc_legal_moves
行番号のないプログラム
def calc_legal_moves(self):
    legal_moves = [(x, y) for y in range(self.BOARD_SIZE) 
                        for x in range(self.BOARD_SIZE)
                        if self.board[x][y] == Marubatsu.EMPTY]
    return legal_moves

ListBoard.calc_legal_moves = calc_legal_moves
修正箇所
def calc_legal_moves(self):
-   if self.status != Marubatsu.PLAYING:
-        return []
    legal_moves = [(x, y) for y in range(self.BOARD_SIZE) 
                        for x in range(self.BOARD_SIZE)
-                       if self.board.getmark(x, y) == Marubatsu.EMPTY]
+                       if self.board[x][y] == Marubatsu.EMPTY]
    return legal_moves

ListBoard.calc_legal_moves = calc_legal_moves

下記は List1dBoard クラスの calc_legal_moves メソッドの定義で、下記の説明と修正箇所は、ListBoard クラスの calc_legal_moves との違いです。

  • 2、3 行目:数値座標は 0 ~ ゲーム盤のマスの数 - 1 までの整数なので、リスト内包表記の繰り返し処理ではその数だけの繰り返し処理を行い、3 行目では数値座標を self.board のインデックスに直接記述するように修正した
1  def calc_legal_moves(self):
2      legal_moves = [move for move in range(self.BOARD_SIZE ** 2) 
3                          if self.board[move] == Marubatsu.EMPTY]
4      return legal_moves
5  
6  List1dBoard.calc_legal_moves = calc_legal_moves
行番号のないプログラム
def calc_legal_moves(self):
    legal_moves = [move for move in range(self.BOARD_SIZE ** 2) 
                        if self.board[move] == Marubatsu.EMPTY]
    return legal_moves

List1dBoard.calc_legal_moves = calc_legal_moves
修正箇所
def calc_legal_moves(self):
-   legal_moves = [(x, y) for y in range(self.BOARD_SIZE) 
-                       for x in range(self.BOARD_SIZE)
+   legal_moves = [move for move in range(self.BOARD_SIZE ** 2) 
-                       if self.board[x][y] == Marubatsu.EMPTY]
+                       if self.board[move] == Marubatsu.EMPTY]
    return legal_moves

List1dBoard.calc_legal_moves = calc_legal_moves

上記の定義後に、下記のプログラムで ListBoardList1dBoard のインスタンスに対して calc_legal_moves を呼び出すと、実行結果から ListBoard では 2 次元の座標を表す tuple が、List1dBoard では 整数の数値座標 が計算されるようになったことが確認できます。なお、先程 (0, 0) と (0, 1) に着手を行ったので、それらの座標は表示されません。

print(lb.calc_legal_moves())
print(lb1d.calc_legal_moves())

実行結果

[(1, 0), (2, 0), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
[2, 3, 4, 5, 6, 7, 8]

ポリモーフィズムによる座標に対する展開処理の実装

座標を表すデータ構造 が、ゲーム盤を表すクラスによって異なる ようになったことで、List1dBoard の judge メソッドが 正しく動作しなくなる という問題が発生します。

下記は先ほどの ListBoard クラスのインスタンスである lb に対して さらに (0, 2)、(1, 0)、(1, 1) の着手 を行った、5 手目の局面に対して judge メソッドで判定を行う プログラムです。5 手目までの着手を行った理由は、4 手目以下 の場合は List1dBoard の場合に 問題が発生する処理が行われる前Marubatsu.PLAYING を返り値として返すため、正しい処理が行われてしまう ためです。この局面は 決着がついていない局面 であり、実行結果のように、ListBoard クラスの場合はエラーは発生せずに 正しい結果が表示 されます。

lb.setmark((0, 2), Marubatsu.CIRCLE)
lb.setmark((1, 0), Marubatsu.CROSS)
lb.setmark((1, 1), Marubatsu.CIRCLE)
last_turn = Marubatsu.CIRCLE
last_move = (1, 1)
move_count = 5
print(lb.judge(last_turn=last_turn, last_move=last_move, move_count=move_count))

実行結果

'playing'

Marubatsu クラスの movejudge メソッドで上記の処理を実行すれば、last_turnlast_movemove_count の計算を行う必要がないと思った人がいるかもしれませんが、setmarkgetmark の仮引数を変更したため、それに合わせた Marubatsu クラスの修正を行う必要があります。そのため、現状では Marubatsu クラスのインスタンスを作成して move メソッドを呼び出すとエラーが発生します。Marubatsu クラスの修正は後で行います。

一方、同様の処理List1dBoard クラスのインスタンスである lb1d に対して行う と、実行結果のように エラーが発生 します。エラーの原因について少し考えてみて下さい。

lb1d.setmark(2, Marubatsu.CIRCLE) # 2 は (0, 2) の数値座標
lb1d.setmark(3, Marubatsu.CROSS)  # 3 は (1, 0) の数値座標
lb1d.setmark(4, Marubatsu.CIRCLE) # 4 は (1, 1) の数値座標
last_move = 4                     # 4 は直前に着手した (1, 1) の数値座標
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)

実行結果

略
--> 194     x, y = last_move
    195     if self.count_linemark:
    196         if self.rowcount[player][y] == self.BOARD_SIZE or \
    197         self.colcount[player][x] == self.BOARD_SIZE:

TypeError: cannot unpack non-iterable int object

エラーの原因の検証

エラーメッセージから x, y = last_move という処理でエラーが発生したことがわかります。last_move には 4 という数値型 の数値座標を代入したため、x, y = 4 という処理が行われる ことになります。

x, y = last_move のような 反復可能オブジェクト(iterable)の展開(unpack)処理は、以前の記事で説明したように last_move反復可能オブジェクトである必要 があります。数値型(int)の 4反復可能オブジェクトではない(non-iterable)ので cannot unpack non-iterable int object というエラーが表示されます。

なお、「反復可能オブジェクトの展開処理」という表記は長いので、以後は単に「展開処理」と表記することにします。

反復可能オブジェクトの性質を持つ座標を表すクラスの定義

この問題を解決するためには、上記の x, y = lastmove の処理を x = lastmove // self.BOARD_SIZEy = lastmove % self.BOARD_SIZE に修正する必要がありますが、x, y = lastmove のような 展開処理 は Marubatsu クラスのメソッドや AI の関数などの 様々な場所で記述されている ため、それらをすべて修正する必要がある点がかなり面倒 です。

そこで、今回の記事では ポリモーフィズム を利用することで、上記のような修正を行わずに済む方法 を紹介します。そのためには、x, y = lastmove のような 展開処理 の際に 行われる処理を理解 する必要があります。

以前の記事で説明したように、反復可能オブジェクト__iter__ メソッドまたは、__getitem__ が適切に定義されている必要があります。__iter__ メソッドに関してはまだ説明していませんが、以前の記事で説明したように、__getitem__ メソッドを定義 することで 添字を利用した参照 を行うことができるようになります。

x, y = lastmove は、x = lastmove[0]y = lastmove[1] という 2 つの処理を 1 つにまとめた ものです。従って、lastmove に対して 0 と 1 の添字を記述 することで x 座標と y 座標を参照 することができれば x, y = lastmove の処理を行うことができます。

従って、座標を表す lastmove に対して下記の処理を行う __getitem__2メソッドが定義 されていれば x, y = lastmove という記述で x 座標と y 座標を計算できる ようになります。

  • 添字が 0 の場合に x 座標 を返り値として返す
  • 添字が 1 の場合に y 座標 を返り値として返す
  • それ以外の添字 の場合は IndexError を発生 させる。この処理は、展開処理では 要素を代入する変数の数要素の数が一致していないエラーが発生する ため必要である

座標を表すデータ構造共通の処理を行う __getitem__ メソッドを持つ ようにすることで、同じプログラムで共通して扱うことができるようにする のが ポリモーフィズム です。

下記は __getitem__ メソッドが定義された、数値座標 を表す Move クラスの定義 です。

  • 2 ~ 4 行目:数値座標を代入する仮引数 move と、数値座標から 2 次元の (x、y) の座標を計算する際に必要となるゲーム盤のサイズを代入する仮引数 board_size を持つ __init__ メソッドを定義し、それらを同名の属性に代入する
  • 6 ~ 12 行目:インデックスが 0 と 1 の場合にそれぞれ x, y 座標を計算して返し、他の値の場合は IndexError を発生させる処理を行う __getitem__ メソッドを定義する
 1  class Move:
 2      def __init__(self, move, board_size):
 3          self.move = move
 4          self.board_size = board_size
 5      
 6      def __getitem__(self, key):
 7          if key == 0:
 8              return self.move // self.board_size
 9          elif key == 1:
10              return self.move % self.board_size
11          else:
12              raise IndexError
行番号のないプログラム
class Move:
    def __init__(self, move, board_size):
        self.move = move
        self.board_size = board_size
    
    def __getitem__(self, key):
        if key == 0:
            return self.move // self.board_size
        elif key == 1:
            return self.move % self.board_size
        else:
            raise IndexError

下記は先ほどの lb1dcalc_legal_moves で計算した 合法手の一覧 から 数値座標を取り出して Move クラスのインスタンスを作成 し、展開処理で x, y 座標を計算して表示 するプログラムです。実行結果から 正しい処理が行われた ことが確認できます。

for move in lb1d.calc_legal_moves():
    x, y = Move(move=move, board_size=3)
    print(f"数値座標 {move} = ({x}, {y})")

実行結果

数値座標 5 = (1, 2)
数値座標 6 = (2, 0)
数値座標 7 = (2, 1)
数値座標 8 = (2, 2)

ローカルクラスとしての Move クラスの定義

上記の Move クラスList1dBoard クラスの メソッドでのみ利用 されるクラスなので、下記のプログラムのように List1dBoard クラスの中で定義 したほうが良いでしょう。クラスの定義内定義されたクラスローカルクラス と呼ばれ、クラス属性 と同様に self.クラス名List1dBoard.クラス名 を記述して利用することができます。

class List1dBoard(ListBoard):
    class Move:
        def __init__(self, move, board_size):
            self.move = move
            self.board_size = board_size
    
        def __getitem__(self, key):
            if key == 0:
                return self.move // self.board_size
            elif key == 1:
                return self.move % self.board_size
            else:
                raise IndexError

    他のメソッドの定義を記述する

なお、上記のように List1dBoard の クラスの定義を記述し直す 場合は、List1dBoard クラスの すべてのメソッドの定義を記述する必要がある ので、今回の記事ではクラスのメソッドを追加、修正する場合と同様の方法で、下記のプログラムで Move クラスを List1dBoard のローカルクラスとして定義 することにします。

List1dBoard.Move = Move

なお、(x, y) という tuple は 0 と 1 の添字で x 座標と y 座標を参照できる ので ポリモーフィズムの要件を満たしています。従って、ListBoard に対して座標を表すクラスを定義したり、calc_legal_moves メソッドを 修正する必要はありません

calc_legal_moves メソッドの修正

合法手の一覧を計算 する calc_legal_moves を下記のプログラムのように修正します。

  • 2 行目:リスト内包表記の要素を self.Move のインスタンスを作成する処理に修正する
1  def calc_legal_moves(self):
2      legal_moves = [self.Move(move, self.BOARD_SIZE) for move in range(self.BOARD_SIZE ** 2) 
3                          if self.board[move] == Marubatsu.EMPTY]
4      return legal_moves
5  
6  List1dBoard.calc_legal_moves = calc_legal_moves
行番号のないプログラム
def calc_legal_moves(self):
    legal_moves = [self.Move(move, self.BOARD_SIZE) for move in range(self.BOARD_SIZE ** 2) 
                        if self.board[move] == Marubatsu.EMPTY]
    return legal_moves

List1dBoard.calc_legal_moves = calc_legal_moves
修正箇所
def calc_legal_moves(self):
-   legal_moves = [move for move in range(self.BOARD_SIZE ** 2) 
+   legal_moves = [self.Move(move, self.BOARD_SIZE) for move in range(self.BOARD_SIZE ** 2) 
                        if self.board[move] == Marubatsu.EMPTY]
    return legal_moves

List1dBoard.calc_legal_moves = calc_legal_moves

上記の修正を行うことで、下記のプログラムの実行結果のように calc_legal_moves が計算する list の要素が List1dBoard.Move クラスのインスタンス になり、展開処理を行うことができる ようになります。先ほどのプログラムとの違いは以下の通りです。3 行目の修正は忘れやすいので注意して下さい。

  • 2 行目moveList1dBoard.Move クラスのインスタンスになったので、展開処理で x 座標と y 座標を計算するように修正した
  • 3 行目数値座標movemove 属性に代入 されているので、それを数値座標として表示するように修正した
for move in lb1d.calc_legal_moves():
    x, y = move
    print(f"数値座標 {move.move} = ({x}, {y})")
修正箇所
for move in lb1d.calc_legal_moves():
-   x, y = Move(move=move, board_size=3)
+   x, y = move
-   print(f"数値座標 {move} = ({x}, {y})")
+   print(f"数値座標 {move.move} = ({x}, {y})")

実行結果

数値座標 5 = (1, 2)
数値座標 6 = (2, 0)
数値座標 7 = (2, 1)
数値座標 8 = (2, 2)

また、上記の修正を行うことで List1dBoard クラスの 座標を表すデータ反復可能オブジェクトになった ので、下記のプログラムのように judge メソッドを実行してもエラーが発生しなくなります。なお、先程と異なり、last_move には List1dBoard.Move クラスのインスタンスを代入 する必要がある点に注意して下さい。

last_move = List1dBoard.Move(4, 3)  # 4 は直前に着手した (1, 1) の数値座標, 3 はゲーム盤のサイズ
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)
修正箇所
-last_move = 4
+last_move = List1dBoard.Move(4, 3)
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)

実行結果

'playing'

getmarksetmark の修正

List1dBoard座標のデータ構造List1dBoard.Move クラスのインスタンスに 変化した ので、getmarksetmark を下記のプログラムのように修正する必要があります。

  • 2、9、10 行目:数値座標は ListBoard.Move クラスの move 属性に代入されるので、movemove.move に修正する
  • 8 行目:x と y 座標を反復オブジェクトの展開を利用して代入するように修正する
 1  def getmark(self, move):
 2      return self.board[move.move]    
 3  
 4  List1dBoard.getmark = getmark
 5  
 6  def setmark(self, move, mark):
 7      if self.count_linemark:
 8          x, y = move
元と同じなので省略
 9              changedmark = self.board[move.move]
元と同じなので省略
10      self.board[move.move] = mark
11      
12  List1dBoard.setmark = setmark
行番号のないプログラム
def getmark(self, move):
    return self.board[move.move]    

List1dBoard.getmark = getmark

def setmark(self, move, mark):
    if self.count_linemark:
        x, y = move
        if mark != Marubatsu.EMPTY:
            diff = 1
            changedmark = mark
        else:
            diff = -1
            changedmark = self.board[move.move]
        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[move.move] = mark
    
List1dBoard.setmark = setmark
修正箇所
def getmark(self, move):
-   return self.board[move]    
+   return self.board[move.move]    

List1dBoard.getmark = getmark

def setmark(self, move, mark):
    if self.count_linemark:
-       x = move // self.BOARD_SIZE
-       y = move % self.BOARD_SIZE
+       x, y = move
元と同じなので省略
-           changedmark = self.board[move]
+           changedmark = self.board[move.move]
元と同じなので省略
-   self.board[move.move] = mark
+   self.board[move.move] = mark
    
List1dBoard.setmark = setmark

上記の修正後に下記のプログラムで、先程と同様の処理を行うと、先程と同じ表示が行われる ことが確認できます。なお、setmark の実引数に記述する 数値座標List1dBoard.Move クラスのインスタンスに変更 する必要がある点に注意して下さい。

lb1d = List1dBoard(count_linemark=True)
print(lb1d.board)
lb1d.setmark(List1dBoard.Move(0, 3), Marubatsu.CIRCLE)
lb1d.setmark(List1dBoard.Move(1, 3), Marubatsu.CROSS)
print(lb1d.board)
print(lb1d.getmark(List1dBoard.Move(0, 3)))
print(lb1d.getmark(List1dBoard.Move(1, 3)))
print(lb1d.getmark(List1dBoard.Move(2, 3)))

実行結果

[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
[['o', 'x', '.'], ['.', '.', '.'], ['.', '.', '.']]
o
x
.

また下記のプログラムで続けて (0, 2)、(1, 0)、(1, 1) の順で着手を行った後で judge メソッドを呼び出すと、実行結果のように 正しい判定を行うことができる ことが確認できます。

lb1d.setmark(List1dBoard.Move(2, 3), Marubatsu.CIRCLE)
lb1d.setmark(List1dBoard.Move(3, 3), Marubatsu.CROSS)
lb1d.setmark(List1dBoard.Move(4, 3), Marubatsu.CIRCLE)
last_move = List1dBoard.Move(4, 3)
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)

実行結果

'playing'

人間が着手を行う場合などへの対応

getmarksetmark の仮引数の修正 と、calc_legal_moves が計算する座標のデータ構造を修正 することで、同じプログラムで AI が着手を行う場合の処理を記述できる ようになりましたが、人間が着手を行う場合 は下記のような 問題が発生 します。

  • 人間が着手を行う場合明らかに x と y の 2 次元で座標を設定したほうがわかりやすい。例えば List1dBoard でゲーム盤のデータ構造が表現されていた場合に、(1, 2) のマスに着手を行う際に、2 + 1 * 3 = 5 という式で数値座標を計算して指定するのは大変
  • Marubatsu クラスの play メソッドや、座標のチェックを行う cmove メソッドで 人間が着手を行う場合calc_legal_moves で計算された 合法手の一覧からではなく、キーボードに座標を記述したり、GUI でマスをクリックすることで着手を行うので、ゲーム盤のクラスごと異なるデータ構造の座標データを計算 する処理を記述する必要が生じる

また、人間が着手を行う以外 でも、例えば AI の関数 の中には下記の ai3s の定義の 3 行目のように、特定のマスのマークgetmark メソッドで参照 して評価値を計算するようなものがあります。他にも ai14s などでも同様の処理が記事術されています。

1  @ai_by_candidate
2  def ai3(mb:Marubatsu, debug:bool=False) -> list[tuple[int, int]]:
3      if mb.board.getmark(1, 1) == Marubatsu.EMPTY:
4          candidate = [(1, 1)]
5      else:
6          candidate = mb.calc_legal_moves()
7     return candidate

getmark の仮引数を変更 したため、上記のプログラムは下記のプログラムの 3 ~ 6 行目のように、ゲーム盤を表すデータ構造ごと異なる座標を計算する処理を記述 する必要があります。これらの問題をうまく解決する方法について少し考えてみて下さい。

 1  @ai_by_candidate
 2  def ai3(mb, debug=False) -> list[tuple[int, int]]:
 3      if mb.boardclass == ListBoard:
 4          move = (1, 1)
 5      elif mb.boardclass == List1dBoard:
 6          move = List1dBoard.Move(4, 3)
 7      if mb.board.getmark(move) == Marubatsu.EMPTY:
 8          candidate = [move]
 9      else:
10          candidate = mb.calc_legal_moves()
11      return candidate

2 次元の座標とゲーム盤のクラスの座標を扱うメソッドの分離

上記の問題を解決する方法として、x、y による 2 次元の座標 と、ゲーム盤のクラスの座標 を扱うメソッドを 別々に定義して分離する という方法が考えられます。具体的には下記のようなメソッドを定義することにします。

メソッド 処理
getmark(x, y) 今回の記事で修正を行う前の、x, y の 2 次元の座標で
座標を指定する getmark と同じ処理を行う
setmark(x, y, mark) 今回の記事で修正を行う前の、x, y の 2 次元の座標で
座標を指定する setmark と同じ処理を行う
xy_to_move(x, y) x, y の 2 次元の座標を、ゲーム盤のクラスの座標に
変換した値を返す
getmark_by_move(move) 今回の記事で修正した getmark と同じ処理を行う
setmark_by_move(move, mark) 今回の記事で修正した setmark と同じ処理を行う

getmarksetmark今回の記事の修正前の処理を行うよう戻した のは、Marubatsu クラスや AI の関数などで記述されている getmarksetmark を呼び出す処理を変更しなくても済む ようにするためです。また、xy_to_move を定義したのは、ゲーム盤のクラスに関わらず、同じプログラム で x, y の 2 次元の座標からゲーム盤のクラスの座標を計算 できるようするためです。その具体例は後述します。

なお、getmarksetmark では 2 次元の座標をゲーム盤のクラスの座標に 座標の変換を行う必要が生じる ため、その分だけ 処理速度が若干遅くなる 可能性がありますが、この 2 つのメソッドを人間が着手を行う場合や、ai3ai14s などの AI の関数などで特定のマスのマークの情報が必要になる場合などの、限られた場面でしか呼び出さない ようにすることで 処理速度の低下を最小限に抑える ことにします。

下記は ListBoard クラスの上記のメソッドの定義です。getmark_by_movesetmark_by_move の定義は 先ほどの getmarksetmark の定義と同じ なので 説明は省略 します。

  • 11、12 行目:ListBoard クラスの座標を表す (x, y) を返すように xy_to_move メソッドを定義する
  • 17、22 行目xy_to_move メソッドで xy に代入された座標を変換した値を実引数に記述して get_mark_by_moveset_mark_by_move を呼び出すように修正する
 1  def getmark_by_move(self, move):
 2      x, y = move
 3      return self.board[x][y]    
 4  
 5  ListBoard.getmark_by_move = getmark_by_move
 6  
 7  def setmark_by_move(self, move, mark):
元と同じなので省略
 8      
 9  ListBoard.setmark_by_move = setmark_by_move
10  
11  def xy_to_move(self, x, y):
12      return (x, y)
13
14  ListBoard.xy_to_move = xy_to_move
15
16  def getmark(self, x, y):
17      return self.getmark_by_move(self.xy_to_move(x, y))
18
19  ListBoard.getmark = getmark
20
21  def setmark(self, x, y, mark):
22      return self.setmark_by_move(self.xy_to_move(x, y), mark)
23
24  ListBoard.setmark = setmark
行番号のないプログラム
def getmark_by_move(self, move):
    x, y = move
    return self.board[x][y]    

ListBoard.getmark_by_move = getmark_by_move

def setmark_by_move(self, move, mark):
    x, y = move
    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_by_move = setmark_by_move

def xy_to_move(self, x, y):
    return (x, y)

ListBoard.xy_to_move = xy_to_move

def getmark(self, x, y):
    return self.getmark_by_move(self.xy_to_move(x, y))

ListBoard.getmark = getmark

def setmark(self, x, y, mark):
    return self.setmark_by_move(self.xy_to_move(x, y), mark)

ListBoard.setmark = setmark
修正箇所
-def getmark(self, move):
+def getmark_by_move(self, move):
    x, y = move
    return self.board[x][y]    

ListBoard.getmark_by_move = getmark_by_move

-def setmark(self, move, mark):
+def setmark_by_move(self, move, mark):
元と同じなので省略
    
ListBoard.setmark_by_move = setmark_by_move

+def xy_to_move(self, x, y):
+   return (x, y)

ListBoard.xy_to_move = xy_to_move

def getmark(self, x, y):
+   return self.getmark_by_move(self.xy_to_move(x, y))

ListBoard.getmark = getmark

def setmark(self, x, y, mark):
+   return self.setmark_by_move(self.xy_to_move(x, y), mark)

ListBoard.setmark = setmark

下記は List1dBoard クラスの上記のメソッドの定義です。getmark_by_movesetmark_by_move の定義の説明は ListBoard と同じ理由で省略します。

  • 10、11 行目:ListBoard クラスの座標を表すデータを計算して返すように xy_to_move メソッドを定義する
  • 16、21 行目xy_to_move メソッドで xy に代入された座標を変換した値を実引数に記述して get_mark_by_moveset_mark_by_move を呼び出すように修正する。ListBoard クラスの getmarksetmark と完全に同じ 定義である
 1  def getmark_by_move(self, move):
 2      return self.board[move.move]    
 3  
 4  List1dBoard.getmark_by_move = getmark_by_move
 5  
 6  def setmark_by_move(self, move, mark):
元と同じなので省略
 7      
 8  List1dBoard.setmark_by_move = setmark_by_move
 9  
10  def xy_to_move(self, x, y):
11      return self.Move(y + x * self.BOARD_SIZE, self.BOARD_SIZE)
12
13  List1dBoard.xy_to_move = xy_to_move
14
15  def getmark(self, x, y):
16      return self.getmark_by_move(self.xy_to_move(x, y))
17
18  List1dBoard.getmark = getmark
19
20  def setmark(self, x, y, mark):
21    return self.setmark_by_move(self.xy_to_move(x, y), mark)
22
23  List1dBoard.setmark = setmark
行番号のないプログラム
def getmark_by_move(self, move):
    return self.board[move.move]    

List1dBoard.getmark_by_move = getmark_by_move

def setmark_by_move(self, move, mark):
    if self.count_linemark:
        x, y = move
        if mark != Marubatsu.EMPTY:
            diff = 1
            changedmark = mark
        else:
            diff = -1
            changedmark = self.board[move.move]
        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[move.move] = mark
    
List1dBoard.setmark_by_move = setmark_by_move

def xy_to_move(self, x, y):
    return self.Move(y + x * self.BOARD_SIZE, self.BOARD_SIZE)

List1dBoard.xy_to_move = xy_to_move

def getmark(self, x, y):
    return self.getmark_by_move(self.xy_to_move(x, y))

List1dBoard.getmark = getmark

def setmark(self, x, y, mark):
    return self.setmark_by_move(self.xy_to_move(x, y), mark)

List1dBoard.setmark = setmark
修正箇所
-def getmark(self, move):
+def getmark_by_move(self, move):
    return self.board[move.move]    

List1dBoard.getmark_by_move = getmark_by_move

-def setmark(self, move, mark):
+def setmark_by_move(self, move, mark):
元と同じなので省略
    
List1dBoard.setmark_by_move = setmark_by_move

+def xy_to_move(self, x, y):
+   return self.Move(y + x * self.BOARD_SIZE, self.BOARD_SIZE)

List1dBoard.xy_to_move = xy_to_move

def getmark(self, x, y):
+   return self.getmark_by_move(self.xy_to_move(x, y))

List1dBoard.getmark = getmark

def setmark(self, x, y, mark):
+   return self.setmark_by_move(self.xy_to_move(x, y), mark)

List1dBoard.setmark = setmark

上記の修正後によって getmarksetmark今回の記事の修正前の処理に戻った ので、下記のように x 座標と y 座標getmarksetmark実引数に記述して実行したプログラム先程と同じ実行結果 になることが確認できます。なお、last_move には ゲーム盤のクラスの座標 のデータを代入する必要がありますが、xy_to_move メソッドを利用することで、ListBoard と List1dBoard クラスの両方の場合で、同じプログラム で x, y の 2 次元の座標を記述 して ゲーム盤のクラスの座標データを代入 することができます。下記のプログラムの ListBoardList1dBoardlblb1d 以外が同じ であることを確認して下さい。

lb = ListBoard(count_linemark=True)
print(lb.board)
lb.setmark(0, 0, Marubatsu.CIRCLE)
lb.setmark(0, 1, Marubatsu.CROSS)
print(lb.board)
print(lb.getmark(0, 0))
print(lb.getmark(0, 1))
print(lb.getmark(0, 2))

実行結果

[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
[['o', 'x', '.'], ['.', '.', '.'], ['.', '.', '.']]
o
x
.
lb.setmark(0, 2, Marubatsu.CIRCLE)
lb.setmark(1, 0, Marubatsu.CROSS)
lb.setmark(1, 1, Marubatsu.CIRCLE)
last_turn = Marubatsu.CIRCLE
last_move = lb.xy_to_move(1, 1)
move_count = 5
lb.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)

実行結果

'playing'
lb1d = List1dBoard(count_linemark=True)
print(lb1d.board)
lb1d.setmark(0, 0, Marubatsu.CIRCLE)
lb1d.setmark(0, 1, Marubatsu.CROSS)
print(lb1d.board)
print(lb1d.getmark(0, 0))
print(lb1d.getmark(0, 1))
print(lb1d.getmark(0, 2))

実行結果

['.', '.', '.', '.', '.', '.', '.', '.', '.']
['o', 'x', '.', '.', '.', '.', '.', '.', '.']
o
x
.
lb1d.setmark(0, 2, Marubatsu.CIRCLE)
lb1d.setmark(1, 0, Marubatsu.CROSS)
lb1d.setmark(1, 1, Marubatsu.CIRCLE)
last_turn = Marubatsu.CIRCLE
last_move = lb1d.xy_to_move(1, 1)
move_count = 5
lb1d.judge(last_turn=last_turn, last_move=last_move, move_count=move_count)

実行結果

'playing'

Marubatsu クラスと AI の関数などの修正

ListBoard と List1dBoard の修正が完了したので、行った 下記の修正に合わせて Marubatsu クラスや AI の関数などの 修正を行う必要 があります。

  • x, y の 2 次元の座標を ゲーム盤のクラスの座標で表現 するようにし、その座標でマスの参照と代入 を行う getmark_by_movesetmark_by_move メソッドを定義した
  • x, y の 2 次元の座標を ゲーム盤のクラスの座標に変換 する xy_to_move を定義した
  • calc_legal_moves の処理を ゲーム盤のクラスで行う ようにした

なお、下記の処理は ポリモーフィズムの仕組みを利用 して プログラムを変更しなくても済む ようにしたので、変更する必要はありません

  • x, y の 2 次元の座標でマスの参照と代入の処理を行う getmarksetmark メソッドを利用する処理
  • x, y = move のような 展開処理 を記述することで、ゲーム盤のクラスの座標を x 座標と y 座標の 2 次元の座標に変換 する処理

具体的に修正する必要がある箇所は以下の通りです。

  • Marubatsu クラスの cmove メソッドに対してこの後で説明する修正を行う
  • Marubatsu クラスの move メソッドに対して下記の修正を行う
    • 座標を代入する仮引数 xymove に修正する
    • setmark の呼び出しを setmark_by_move に修正する
  • Marubatsu クラスの unmove メソッドに対して下記の修正を行う
    • setmark の呼び出しを setmark_by_move に修正する
  • Marubatsu クラスの calc_legal_moves メソッドに対して下記の修正を行う
    • ゲーム盤のクラスの calc_legal_moves メソッドを呼び出し、その返り値を返すように修正する
  • move メソッドを呼び出す際の 座標 の実引数 を、ゲーム盤のクラスの座標 に修正する
  • x 座標と y 座標に関する処理を行う必要がない場合x, y = move を削除 する
  • (x, y) という tuple で記述していた座標を、xy_to_move(x, y) を呼び出すことで ゲーム盤のクラスの座標に修正 する

cmove メソッドの修正

座標のチェックを伴った着手を行う cmove メソッドは、人間が着手を選択した際に利用する ことを目的として定義したので、x 座標と y 座標で座標を指定したほうが使いやすい でしょう。そこで、仮引数 xy は変更しない ことにします。

cmove メソッドで着手を行う際の 座標のチェック は 現状では move メソッド内で行っています が、下記の理由からその処理を cmove メソッドで行うように修正 することにします。

  • move メソッドの 仮引数 xymove に変更 されたので、cmove から move を呼び出す際cmove の 仮引数 x, y の 2 次元の座標を xy_to_move を呼び出して ゲーム盤のクラスの座標に変換する必要 がある
  • cmove仮引数 xy には、play_loop メソッド内の処理で キーボードから座標を入力した場合文字列型のデータが代入 されるので xy_to_move を呼び出すと エラーが発生する可能性 が生じる
  • この問題は、cmoveplace_mark メソッドを呼び出して 座標のチェックを行ってから xy_to_move で座標の変換を行う ことで発生しなくなる

cmove 内で place_mark を呼び出す ように修正すると、(x, y) の座標のチェックと着手が cmove メソッド内で行われる ようになるので、move メソッドの仮引数 check_coord を廃止 し、代わりに マークを配置(place)済み であることを表す仮引数 placed を追加 して、placed=True を実引数に記述して move メソッドを呼び出す ことにします。

下記はそのように cmove メソッドを修正したプログラムです。

  • 2 ~ 4 行目place_mark メソッドで (x, y) のマスに self.turn のマークを配置し、配置できた場合は 3 行目で xy の座標をゲーム盤のクラス座標に変換し、placed=True を実引数に記述して move メソッドを呼び出すように修正する。xy には文字列が代入されている場合がある ので、3 行目では組み込み関数 int を利用して 整数型のデータに型変換を行う必要がある 点に注意する事。なお、xy が整数型に変換できない場合はplace_mark の返り値が False になるので、3 行目でエラーが発生することはない
1  def cmove(self, x, y):
2      if self.place_mark(x, y, self.turn):
3          move = self.board.xy_to_move(int(x), int(y))
4          self.move(move, placed=True) 
5  
6  Marubatsu.cmove = cmove 
行番号のないプログラム
def cmove(self, x, y):
    if self.place_mark(x, y, self.turn):
        move = self.board.xy_to_move(int(x), int(y))
        self.move(move, placed=True) 

Marubatsu.cmove = cmove 
修正箇所
def cmove(self, x, y):
-   self.move(x, y, check_coord=True)     
+   if self.place_mark(x, y, self.turn):
+       move = self.board.xy_to_move(int(x), int(y))
+       self.move(move, placed=True) 
        
Marubatsu.cmove = cmove 

move メソッドの修正

下記は move メソッドを修正したプログラムです。

  • 1 行目:仮引数 xymove に修正し、仮引数 check_coord を廃止し、デフォルト値を False とする仮引数 placed を追加する
  • 2、3 行目check_coordplaced に修正し、placedFalse の場合の処理を setmark(x, y, self.turn) から setmark_by_move(move, self.turn) に修正する
  • placedTrue の場合は cmove メソッドでマークを配置済なので、3 行目の下にあったマークを配置する処理を行っていた else のブロックの処理を削除する
  • 8 行目x, y という tuple で記述していた座標を move に修正する。この修正によって、11、13 行目の処理で records 属性に記録される 座標のデータ(x, y) という tuple から ゲーム盤のクラスの座標のデータに変更 されたことになる
 1  def move(self, move, placed=False):
 2      if not placed:
 3          self.board.setmark_by_move(move, self.turn)
 4  
 5      self.last_turn = self.turn
 6      self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE  
 7      self.move_count += 1
 8      self.last_move = move
 9      self.status = self.board.judge(self.last_turn, self.last_move, self.move_count)
10      if len(self.records) <= self.move_count:            
11          self.records.append(self.last_move)
12      else:
13          self.records[self.move_count] = self.last_move
14          self.records = self.records[0:self.move_count + 1]
15          
16  Marubatsu.move = move
行番号のないプログラム
def move(self, move, placed=False):
    if not placed:
        self.board.setmark_by_move(move, 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 = move
    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, check_coord=False):
+def move(self, move, placed=False):
-   if not placed:
+   if not check_coord:
-       self.board.setmark(x, y, self.turn)
+       self.board.setmark_by_move(move, self.turn)
-   else:
-       if not self.place_mark(x, y, self.turn):
-           return 

    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.last_move = move
    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

上記の修正後に下記のプログラムを実行することで、実行結果から 下記の条件で move で着手を正しく行うことができることが確認 できます。

  • ゲーム盤のデータ構造として ListBoard と List1dBoard を利用した場合の処理を行う
  • (0, 0)、(1, 0) の順で着手を行う
 1  for boardclass in [ListBoard, List1dBoard]:
 2      print(f"boardclass: {boardclass.__name__}")
 3      mb = Marubatsu(boardclass=boardclass)
 4      move = mb.board.xy_to_move(0, 0)
 5      mb.move(move)
 6      print(mb)
 7      move = mb.board.xy_to_move(1, 0)
 8      mb.move(move)
 9      print(mb)
10      print()
行番号のないプログラム
for boardclass in [ListBoard, List1dBoard]:
    print(f"boardclass: {boardclass.__name__}")
    mb = Marubatsu(boardclass=boardclass)
    move = mb.board.xy_to_move(0, 0)
    mb.move(move)
    print(mb)
    move = mb.board.xy_to_move(1, 0)
    mb.move(move)
    print(mb)
    print()

実行結果

boardclass: ListBoard
Turn x
O..
...
...

Turn o
oX.
...
...


boardclass: List1dBoard
Turn x
O..
...
...

Turn o
oX.
...
...

下記のプログラムを実行することで、実行結果から下記の条件で cmove で着手を正しく行うことができることが確認できます。cmove は座標のチェックを行う ので、既に着手を行ったマスの座標、ゲーム盤外の座標、文字列の座標を行っています。

  • ゲーム盤のデータ構造として ListBoard と List1dBoard を利用した場合の処理を行う
  • キーボードから 0,0、1,0、0,0、3,5、a,b の順で入力された場合の着手を行う
for boardclass in [ListBoard, List1dBoard]:
    print(f"boardclass: {boardclass.__name__}")
    mb = Marubatsu(boardclass=boardclass)
    mb.cmove(0, 0)
    print(mb)
    mb.cmove(1, 0)
    print(mb)
    mb.cmove(0, 0)
    print(mb)
    mb.cmove(3, 5)
    print(mb)
    mb.cmove("a", "b")
    print(mb)
    print()

実行結果

boardclass: ListBoard
Turn x
O..
...
...

Turn o
oX.
...
...

( 0 , 0 ) のマスにはマークが配置済です
Turn o
oX.
...
...

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

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


boardclass: List1dBoard
Turn x
O..
...
...

Turn o
oX.
...
...

( 0 , 0 ) のマスにはマークが配置済です
Turn o
oX.
...
...

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

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

unmove メソッドの修正

下記は unmove メソッドを修正したプログラムです。

  • 2 行目の下にあった x, y = self.last_move は、unmove の処理で x 座標と y 座標は必要がない ので 削除した
  • その下にあった if self.move_count == 0 という if 文は、2 行目の if self.move_count > 0: の if 文から絶対に実行されないことに気づいたので削除した
  • 6、7 行目:records 属性 に記録する着手の一覧の 座標のデータゲーム盤のクラスの座標に変更 されたので、7 行目で その最後のデータを last_move に代入 し、8 行目で set_mark_by_move を呼び出して 直前に着手したマークを削除 するように修正した
 1  def unmove(self):
 2      if self.move_count > 0:
 3          self.move_count -= 1
 4          self.turn, self.last_turn = self.last_turn, self.turn
 5          self.status = Marubatsu.PLAYING
 6          last_move = self.records.pop()
 7          self.board.setmark_by_move(last_move, Marubatsu.EMPTY)       
 8          self.last_move = self.records[-1]      
 9          
10  Marubatsu.unmove = unmove
行番号のないプログラム
def unmove(self):
    if self.move_count > 0:
        self.move_count -= 1
        self.turn, self.last_turn = self.last_turn, self.turn
        self.status = Marubatsu.PLAYING
        last_move = self.records.pop()
        self.board.setmark_by_move(last_move, Marubatsu.EMPTY)       
        self.last_move = self.records[-1]      
        
Marubatsu.unmove = unmove
修正箇所
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()
+       last_move = self.records.pop()
-       self.board.setmark(x, y, Marubatsu.EMPTY)       
+       self.board.setmark_by_move(last_move, Marubatsu.EMPTY)       
        self.last_move = self.records[-1]      
        
Marubatsu.unmove = unmove

上記の修正後に下記のプログラムで (0, 0)、(1, 0)、(2, 0) の順で着手 を行い、unmove メソッドを 3 回実行 すると、実行結果から 着手が正しく取り消される ことが確認できます。

for boardclass in [ListBoard, List1dBoard]:
    print(f"boardclass: {boardclass.__name__}")
    mb = Marubatsu(boardclass=boardclass)
    mb.cmove(0, 0)
    print(mb)
    mb.cmove(1, 0)
    print(mb)
    mb.cmove(2, 0)
    print(mb)
    mb.unmove()
    print(mb)
    mb.unmove()
    print(mb)
    mb.unmove()
    print(mb)
    print()

実行結果

boardclass: ListBoard
Turn x
O..
...
...

Turn o
oX.
...
...

Turn x
oxO
...
...

Turn o
oX.
...
...

Turn x
O..
...
...

Turn o
...
...
...


boardclass: List1dBoard
Turn x
O..
...
...

Turn o
oX.
...
...

Turn x
oxO
...
...

Turn o
oX.
...
...

Turn x
O..
...
...

Turn o
...
...
...

calc_legal_moves の修正

下記は calc_legal_moves を修正したプログラムで、ゲームの決着がついている場合 はこれまでどおり 空の list を、そうででない場合は ゲーム盤のクラスの calc_legal_moves メソッドの返り値を返す ように修正します。修正箇所は省略します。

def calc_legal_moves(self):
    if self.status != Marubatsu.PLAYING:
        return []
    return self.board.calc_legal_moves()

Marubatsu.calc_legal_moves = calc_legal_moves

play_loop の修正

Marubatsu クラスの play_loop メソッドでは AI が計算した着手 から x 座標と y 座標を計算 して move(x, y) で着手を行う 処理を行っていました。

AI の関数calc_legal_moves で計算した合法手の中から 1 つを選択 して 返り値として返す 処理を行うので、上記の calc_legal_moves の修正 によって ゲーム盤のクラスの座標が返る ようになっています。従って、下記のプログラムの 4、5 行目のように AI の関数の返り値を直接 move メソッドの実引数に記述 して呼び出すように修正する必要があります。

なお、人間がキーボードから入力した着手 の場合は x 座標と y 座標で座標を入力 するので、8 行目の cmove の呼び出し処理を 修正する必要はありません

 1  def play_loop(self, mb_gui, params=None):
元と同じなので省略
 2          # ai が着手を行うかどうかを判定する
 3          if ai[index] is not None:
 4              move = ai[index](self, **params[index])
 5              self.move(move)
 6          else:
元と同じなので省略
 7              x, y = xylist
 8              self.cmove(x, y)
元と同じなので省略
 9  
10  Marubatsu.play_loop = play_loop
行番号のないプログラム
def play_loop(self, mb_gui, params=None):
    if params is None:
        params = [{}, {}]
    
    ai = self.ai
    verbose = self.verbose
    gui = self.gui
    
    # ゲームの決着がついていない間繰り返す
    while self.status == Marubatsu.PLAYING:
        # 現在の手番を表す ai のインデックスを計算する
        index = 0 if self.turn == Marubatsu.CIRCLE else 1
        # ゲーム盤の表示
        if verbose:
            if gui:
                # AI どうしの対戦の場合は画面を描画しない
                if ai[0] is None or ai[1] is None:
                    mb_gui.update_gui()
                # 手番を人間が担当する場合は、play メソッドを終了する
                if ai[index] is None:
                    return
            else:
                print(self)
                
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
            move = ai[index](self, **params[index])
            self.move(move)
        else:
            # キーボードからの座標の入力
            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
            self.cmove(x, y)

    # 決着がついたので、ゲーム盤を表示する
    if verbose:
        if gui:
            mb_gui.update_gui()
        else:
            print(self)
            
    return self.status

Marubatsu.play_loop = play_loop
修正箇所
def play_loop(self, mb_gui, params=None):
元と同じなので省略
        # ai が着手を行うかどうかを判定する
        if ai[index] is not None:
-           x, y = ai[index](self, **params[index])
+           move = ai[index](self, **params[index])
-           self.move(x, y)
+           self.move(move)
        else:
元と同じなので省略
            x, y = xylist
            self.cmove(x, y)
元と同じなので省略

Marubatsu.play_loop = play_loop

その他の修正

先程説明した、下記のまだ行っていない修正を行います。

  • move メソッドを呼び出す際の 座標 の実引数 を、ゲーム盤のクラスの座標 に修正する
  • x 座標と y 座標に関する処理を行う必要がない場合x, y = move を削除 する
  • (x, y) という tuple で記述していた座標を、xy_to_move(x, y) を呼び出すことで ゲーム盤のクラスの座標に修正 する

1 つ目と 2 つ目の修正は下記のように行います。

合法手の一覧から着手を行う処理 では、下記のようなプログラムが記述されています。

for move in self.mb.calc_legal_moves():
    x, y = move
    mb.move(x, y)

また、上記と同じ処理が下記のように記述されている場合もあるようです。

for x, y in self.mb.calc_legal_moves():
    mb.move(x, y)

move の仮引数が変化 したので上記のプログラムを下記のように修正する必要があります。ただし、x,y の値を 後で利用する場合x, y = move削除することはできません。具体例としては Marubatsu_GUI クラスの update_gui があります。

for move in self.mb.calc_legal_moves():
    mb.move(move)

他にも move メソッドを呼び出す処理 はすべて ゲームの盤クラスの座標を実引に記述 するように修正する必要があります。

修正の必要があるメソッドは以下の通りです。以前の記事でも言及しましたが、ポリモーフィズム を利用する場合は、共通するメソッドの仮引数などの仕様を変更 すると、そのメソッドを利用するプログラムをすべて変更する必要 が生じるので、今回のようにどうしても必要でない場合は、なるべく共通するメソッドを後から修正しないようしたほうが良い でしょう。とはいっても、後から変更したくなることが良くあるのが悩みの種になります。

  • Marubatsu クラスの change_step メソッド
  • Marubatsu_GUI クラスの update_gui メソッド。なお、update_guix, y = move は、その後で xy の値が必要になるので削除できない点に注意すること
  • Node クラスの calc_children メソッド
  • Mbtree クラスの create_tree_by_dfcreate_subtree メソッド
  • Mbtree_GUI クラスの update_gui メソッド
  • mbtest.py の test_judge
  • ai.py の ai_by_scoreai_by_mmscoreshow_progressai1ai4ai5ai6ai_gt7ai_mmdfsai_mmdfs_ttai_absai_abs2ai_abs3ai_abs_ttai_abs_tt2ai_abs_tt3ai_nws_3scoreai_nws_3score2ai_nws_3score_ttai_mmdfs_allai_abs_allai_scoutai_mtdfai_abs_dlsai_ab_iddfsai_pvs_dls

下記は上記の一部を修正したプログラムで、プログラムが長いので折りたたみ、修正箇所の説明は省略します。上記の中で 前回の記事ベンチマークの処理に関連しない test_judge は省略 しました。同様の理由から ai.py の関数に関しては ai_by_scoreai_by_mmscoreai_abs_dls のみを記述しました。全てを修正したプログラムは marubatsu_new.py、tree_new.py、mbtest_new.py、ai_new.py を見て下さい。

なお、修正箇所ほとんどが x, ymove に修正 するというものなので、Ctrl + H のショートカットキーで呼び出せる VSCode の置換機能 を利用して x, y を検索 し、その中で 修正する必要があるものを move に置換 すると良いでしょう。その際に 「すべて置換」ボタンをクリック すると、置換してはいけない x, y までもが置換されてしまう ので押さないように注意して下さい。

なお、修正箇所が多いため、間違いや修正漏れがあるかもしれません。それらについては、間違いが見つかり次第今後の記事で修正したいと思います。

修正したプログラム
from marubatsu import Marubatsu_GUI
from tree import Node, Mbtree, Mbtree_GUI
from functools import wraps
from ai import dprint
from random import choice
from time import perf_counter
from copy import deepcopy

def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.records) - 1, step))
    records = self.records
    self.restart()
    for move in records[1:step+1]:
        self.move(move)
    self.records = records
    
Marubatsu.change_step = change_step

def update_gui(self):
    def calc_status_txt(score):
        if score > 0:
            return ""
        elif score == 0:
            return ""
        else:
            return "×"
    
    ax = self.ax

    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()

    # y 軸を反転させる
    ax.invert_yaxis()

    # 枠と目盛りを表示しないようにする
    ax.axis("off")   

    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}", 
            fontsize=7*self.size, ha="center")   

    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
        score = self.score_table[self.mb.board_to_str()]["score"]
        if self.show_status:
            text += " 状況 " + calc_status_txt(score)
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(1.5, -0.2, text, fontsize=7*self.size, ha="center")

    self.draw_board(ax, self.mb, lw=0.7*self.size)
    
    if self.show_status:
        bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
        ai, params = self.status_dropdown.value
        if ai == "Auto":
            index = 0 if self.mb.turn == Marubatsu.CIRCLE else 1
            ai = self.mb.ai[index]
            params = self.params[index]
        if ai is not None:
            analyze = ai(self.mb, analyze=True, **params)
            score_by_move = analyze["score_by_move"]
            candidate = analyze["candidate"]
        for move in self.mb.calc_legal_moves():
            x, y = move
            mb = deepcopy(move)
            mb.move(move)
            score = self.score_table[mb.board_to_str()]["score"]
            color = "red" if move in bestmoves else "black"
            text = calc_status_txt(score)
            ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
            if ai is not None:
                if score_by_move is not None:
                    color = "red" if move in candidate else "black"
                    ax.text(x + 0.1, y + 0.65, score_by_move[move], fontsize=5*self.size, c=color)
                elif move in candidate:
                    ax.text(x + 0.1, y + 0.65, "候補手", fontsize=4.8*self.size)
                
    self.update_widgets_status()

    if hasattr(self, "mbtree_gui"):
        from tree import Node

        self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
        self.mbtree_gui.update_gui()
        
Marubatsu_GUI.update_gui = update_gui

def calc_children(self, bestmoves_and_score_by_board=None):
    self.children = []
    for move in self.mb.calc_legal_moves():
        childmb = deepcopy(self.mb)
        childmb.move(move)
        self.insert(Node(childmb, parent=self, depth=self.depth + 1,
                        bestmoves_and_score_by_board=bestmoves_and_score_by_board))
        
Node.calc_children = calc_children

def create_tree_by_df(self, N):
    for move in N.mb.calc_legal_moves():
        mb = deepcopy(N.mb)
        mb.move(move)
        node = Node(mb, parent=N, depth=N.depth + 1)
        N.insert(node)
        self.nodelist.append(node)
        self.nodelist_by_depth[node.depth].append(node)
        self.nodenum += 1
        self.create_tree_by_df(node)
        
Mbtree.create_tree_by_bf = create_tree_by_df

def create_subtree(self):  
    bestmoves_and_score_by_board = self.subtree["bestmoves_and_score_by_board"]
    self.root = Node(Marubatsu(), bestmoves_and_score_by_board=bestmoves_and_score_by_board)
    
    depth = 0
    nodelist = [self.root]
    centermb = self.subtree["centermb"]
    centerdepth = centermb.move_count
    if centerdepth == 0:
        self.centernode = self.root
    records = centermb.records
    maxdepth = self.subtree["maxdepth"]
    while len(nodelist) > 0:
        childnodelist = []
        for node in nodelist:
            if depth < centerdepth - 1:
                childmb = deepcopy(node.mb)
                move = records[depth + 1]
                childmb.move(move)
                childnode = Node(childmb, parent=node, depth=depth+1, 
                                bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
                node.insert(childnode)
                childnodelist.append(childnode)
            elif depth < maxdepth:
                node.calc_children(bestmoves_and_score_by_board=bestmoves_and_score_by_board)                   
                if depth == centerdepth - 1:
                    for move, childnode in node.children_by_move.items():
                        if move == records[depth + 1]:
                            self.centernode = childnode
                            childnodelist.append(self.centernode)
                        else:
                            if childnode.mb.status == Marubatsu.PLAYING:
                                childnode.children.append(None)
                else:
                    childnodelist += node.children
            else:
                if node.mb.status == Marubatsu.PLAYING:
                    childmb = deepcopy(node.mb)
                    board_str = node.mb.board_to_str()               
                    move = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
                    childmb.move(move)
                    childnode = Node(childmb, parent=node, depth=depth+1, 
                                    bestmoves_and_score_by_board=bestmoves_and_score_by_board)   
                    node.insert(childnode)
                    childnodelist.append(childnode)
        nodelist = childnodelist
        depth += 1

    selectedmb = self.subtree["selectedmb"]
    self.selectednode = self.root
    for move in selectedmb.records[1:selectedmb.move_count+1]:
        self.selectednode = self.selectednode.children_by_move[move]
        
Mbtree.create_subtree = create_subtree

def update_gui(self):
    self.ax.clear()
    self.ax.set_xlim(-1, self.width - 1)
    self.ax.set_ylim(-1, self.height - 1)   
    self.ax.invert_yaxis()
    self.ax.axis("off")   
    
    if self.selectednode.depth <= 4:
        maxdepth = self.selectednode.depth + 1
    elif self.selectednode.depth == 5:
        maxdepth = 7
    else:
        maxdepth = 9
    if self.selectednode.depth <= 6:
        centermb = self.selectednode.mb
    else:
        centermb = Marubatsu()
        for move in self.selectednode.mb.records[1:7]:
            centermb.move(move)
    self.mbtree = Mbtree(subtree={"centermb": centermb, "selectedmb": self.selectednode.mb, "maxdepth": maxdepth, 
                        "bestmoves_and_score_by_board": self.bestmoves_and_score_by_board})
    self.selectednode = self.mbtree.selectednode
    self.mbtree.draw_subtree(centernode=self.mbtree.centernode, selectednode=self.selectednode,
                            show_bestmove=True, show_score=self.show_score,
                            ax=self.ax, maxdepth=maxdepth, size=self.size)
    
    disabled = self.selectednode.parent is None
    self.set_button_status(self.left_button, disabled=disabled)
    disabled = self.selectednode.depth >= 6 or len(self.selectednode.children) == 0
    self.set_button_status(self.right_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children.index(self.selectednode) == 0
    self.set_button_status(self.up_button, disabled=disabled)
    disabled = self.selectednode.parent is None or self.selectednode.parent.children[-1] is self.selectednode
    self.set_button_status(self.down_button, disabled=disabled)
    self.set_button_color(self.score_button, value=self.show_score)

Mbtree_GUI.update_gui = update_gui

def ai_by_score(eval_func):
    @wraps(eval_func)
    def wrapper(mb_orig, debug=False, *args, rand=True, 
                analyze=False, calc_score=False, minimax=False, **kwargs):
        if calc_score:
            score = eval_func(mb_orig, debug, *args, **kwargs)
            if minimax and mb_orig.turn == Marubatsu.CIRCLE:
                score *= -1
            return score

        dprint(debug, "Start ai_by_score")
        dprint(debug, mb_orig)
        legal_moves = mb_orig.calc_legal_moves()
        dprint(debug, "legal_moves", legal_moves)
        best_score = float("-inf")
        best_moves = []
        if analyze:
            score_by_move = {}
        for move in legal_moves:
            dprint(debug, "=" * 20)
            dprint(debug, "move", move)
            mb_orig.move(move)
            dprint(debug, mb_orig)
            score = eval_func(mb_orig, debug, *args, **kwargs)
            mb_orig.unmove()
            dprint(debug, "score", score, "best score", best_score)
            if analyze:
                score_by_move[move] = score
            
            if best_score < score:
                best_score = score
                best_moves = [move]
                dprint(debug, "UPDATE")
                dprint(debug, "  best score", best_score)
                dprint(debug, "  best moves", best_moves)
            elif best_score == score:
                best_moves.append(move)
                dprint(debug, "APPEND")
                dprint(debug, "  best moves", best_moves)

        dprint(debug, "=" * 20)
        dprint(debug, "Finished")
        dprint(debug, "best score", best_score)
        dprint(debug, "best moves", best_moves)
        if analyze:
            return {
                "candidate": best_moves,
                "score_by_move": score_by_move,
            }
        elif rand:   
            return choice(best_moves)
        else:
            return best_moves[0]
        
    return wrapper

def ai_by_mmscore(eval_func):
    @wraps(eval_func)
    def wrapper(mb_orig, debug=False, *args, rand=True, share_tt=True,
                analyze=False, calc_score=False, **kwargs):
        if calc_score:
            score, count = eval_func(mb_orig, debug, *args, **kwargs)
            return score
        
        starttime = perf_counter()
        dprint(debug, "Start ai_by_mmscore")
        dprint(debug, mb_orig)
        legal_moves = mb_orig.calc_legal_moves()
        dprint(debug, "legal_moves", legal_moves)
        maxnode = mb_orig.turn == Marubatsu.CIRCLE
        best_score = float("-inf") if maxnode else float("inf")
        best_moves = []
        tt = {} if share_tt else None
        totalcount = 0
        if analyze:
            score_by_move = {}
        for move in legal_moves:
            dprint(debug, "=" * 20)
            dprint(debug, "move", move)
            mb_orig.move(move)
            dprint(debug, mb_orig)
            score, count = eval_func(mb_orig, debug, tt=tt, *args, **kwargs)
            mb_orig.unmove()
            totalcount += count
            dprint(debug, "score", score, "best score", best_score)
            if analyze:
                score_by_move[move] = score
            
            if (maxnode and best_score < score) or (not maxnode and best_score > score):
                best_score = score
                best_moves = [move]
                dprint(debug, "UPDATE")
                dprint(debug, "  best score", best_score)
                dprint(debug, "  best moves", best_moves)
            elif best_score == score:
                best_moves.append(move)
                dprint(debug, "APPEND")
                dprint(debug, "  best moves", best_moves)

        dprint(debug, "=" * 20)
        dprint(debug, "Finished")
        dprint(debug, "best score", best_score)
        dprint(debug, "best moves", best_moves)
        bestmove = choice(best_moves) if rand else best_moves[0]
        if analyze:
            if share_tt:
                PV = []
                mb = deepcopy(mb_orig)
                while mb.status == Marubatsu.PLAYING:
                    PV.append(bestmove)
                    if mb.board.getmark_by_move(bestmove) != Marubatsu.EMPTY:
                        print("そのマスには着手済みです")
                        break
                    mb.move(bestmove)
                    boardtxt = mb.board_to_str()
                    if boardtxt in tt:
                        _, _, bestmove = tt[boardtxt]
                    else:
                        break                
            else:
                PV = bestmove
            return {
                "candidate": best_moves,
                "score_by_move": score_by_move,
                "tt": tt,
                "time": perf_counter() - starttime,
                "bestmove": PV[0],
                "score": best_score,
                "count": totalcount,
                "PV": PV,
            }
        else:
            return bestmove
        
    return wrapper

@ai_by_mmscore
def ai_abs_dls(mb, debug=False, timelimit_pc=None, maxdepth=1,
               eval_func=None, eval_params={}, use_tt=False,
               tt=None, tt_for_mo=None):
    count = 0
    def ab_search(mborig, depth, tt, alpha=float("-inf"), beta=float("inf")):
        nonlocal count
        if timelimit_pc is not None and perf_counter() >= timelimit_pc:
            raise RuntimeError("time out")
        
        count += 1
        if mborig.status != Marubatsu.PLAYING or depth == maxdepth:
            return eval_func(mborig, calc_score=True, **eval_params)
        
        if use_tt:
            boardtxt = mborig.board_to_str()
            if boardtxt in tt:
                lower_bound, upper_bound, _ = tt[boardtxt]
                if lower_bound == upper_bound:
                    return lower_bound
                elif upper_bound <= alpha:
                    return upper_bound
                elif beta <= lower_bound:
                    return lower_bound
                else:
                    alpha = max(alpha, lower_bound)
                    beta = min(beta, upper_bound)
            else:
                lower_bound = min_score
                upper_bound = max_score
        
        alphaorig = alpha
        betaorig = beta

        legal_moves = mborig.calc_legal_moves()
        if tt_for_mo is not None:
            if not use_tt:            
                boardtxt = mborig.board_to_str()
            if boardtxt in tt_for_mo:
                _, _, bestmove = tt_for_mo[boardtxt]
                index = legal_moves.index(bestmove)
                legal_moves[0], legal_moves[index] = legal_moves[index], legal_moves[0]        
        if mborig.turn == Marubatsu.CIRCLE:
            score = float("-inf")
            for move in legal_moves:
                mborig.move(move)
                abscore = ab_search(mborig, depth + 1, tt, alpha, beta)
                mborig.unmove()
                if abscore > score:
                    bestmove = move
                score = max(score, abscore)
                if score >= beta:
                    break
                alpha = max(alpha, score)
        else:
            score = float("inf")
            for move in legal_moves:
                mborig.move(move)
                abscore = ab_search(mborig, depth + 1, tt, alpha, beta)
                mborig.unmove()
                if abscore < score:
                    bestmove = move
                score = min(score, abscore)
                if score <= alpha:
                    break
                beta = min(beta, score)   
            
        from util import calc_same_boardtexts

        if use_tt:
            boardtxtlist = calc_same_boardtexts(mborig, bestmove)
            if score <= alphaorig:
                upper_bound = score
            elif score < betaorig:
                lower_bound = score
                upper_bound = score
            else:
                lower_bound = score
            for boardtxt, move in boardtxtlist.items():
                tt[boardtxt] = (lower_bound, upper_bound, move)
        return score
                
    min_score = float("-inf")
    max_score = float("inf")
    
    if tt is None:
        tt = {}
    score = ab_search(mb, depth=0, tt=tt, alpha=min_score, beta=max_score)
    dprint(debug, "count =", count)
    return score, count
修正箇所
from marubatsu import Marubatsu_GUI
from tree import Node, Mbtree, Mbtree_GUI
from functools import wraps
from ai import dprint
from random import choice
from time import perf_counter

def change_step(self, step):
    # step の範囲を正しい範囲に修正する
    step = max(0, min(len(self.records) - 1, step))
    records = self.records
    self.restart()
-   for x, y in records[1:step+1]:
+   for move in records[1:step+1]:
-       self.move(x, y)
+       self.move(move)
    self.records = records
    
Marubatsu.change_step = change_step

def update_gui(self):
元と同じなので省略
        for move in self.mb.calc_legal_moves():
            x, y = move
            mb = deepcopy(move)
-           mb.move(x, y)
+           mb.move(move)
元と同じなので省略
        
Marubatsu_GUI.update_gui = update_gui

def calc_children(self, bestmoves_and_score_by_board=None):
    self.children = []
-   for x, y in self.mb.calc_legal_moves():
+   for move in self.mb.calc_legal_moves():
        childmb = deepcopy(self.mb)
-       childmb.move(x, y)
+       childmb.move(move)
        self.insert(Node(childmb, parent=self, depth=self.depth + 1,
                        bestmoves_and_score_by_board=bestmoves_and_score_by_board))
        
Node.calc_children = calc_children

def create_tree_by_df(self, N):
-   legal_moves = N.mb.calc_legal_moves()
-   for x, y in legal_moves:
+   for move in N.mb.calc_legal_moves():
        mb = deepcopy(N.mb)
-       mb.move(x, y)
+       mb.move(move)
元と同じなので省略
        
Mbtree.create_tree_by_bf = create_tree_by_df

def create_subtree(self):  
元と同じなので省略
        for node in nodelist:
            if depth < centerdepth - 1:
                childmb = deepcopy(node.mb)
-               x, y = records[depth + 1]
+               move = records[depth + 1]
-               childmb.move(x, y)
+               childmb.move(move)
元と同じなので省略
-                   x, y = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
+                   move = bestmoves_and_score_by_board[board_str]["bestmoves"][0]
-                   childmb.move(x, y)
+                   childmb.move(move)
元と同じなので省略
        
Mbtree.create_subtree = create_subtree

def update_gui(self):
元と同じなので省略
    if self.selectednode.depth <= 6:
        centermb = self.selectednode.mb
    else:
        centermb = Marubatsu()
-       for x, y in self.selectednode.mb.records[1:7]:
+       for move in self.selectednode.mb.records[1:7]:
-           centermb.move(x, y)
+           centermb.move(move)
元と同じなので省略

Mbtree_GUI.update_gui = update_gui

def ai_by_score(eval_func):
    @wraps(eval_func)
    def wrapper(mb_orig, debug=False, *args, rand=True, 
                analyze=False, calc_score=False, minimax=False, **kwargs):
元と同じなので省略
        for move in legal_moves:
            dprint(debug, "=" * 20)
            dprint(debug, "move", move)
-           x, y = move
-           mb_orig.move(x, y)
+           mb_orig.move(move)
元と同じなので省略

def ai_by_mmscore(eval_func):
    @wraps(eval_func)
    def wrapper(mb_orig, debug=False, *args, rand=True, share_tt=True,
                analyze=False, calc_score=False, **kwargs):
元と同じなので省略
        for move in legal_moves:
            dprint(debug, "=" * 20)
            dprint(debug, "move", move)
-           x, y = move
-           mb_orig.move(x, y)
+           mb_orig.move(move)
元と同じなので省略
-                   x, y = bestmove
-                   if mb.board.getmark(x, y) != Marubatsu.EMPTY:
+                   if mb.board.getmark_by_move(bestmove) != Marubatsu.EMPTY:
                        print("そのマスには着手済みです")
                        break
-                   mb.move(x, y)
+                   mb.move(bestmove)
元と同じなので省略

@ai_by_mmscore
def ai_abs_dls(mb, debug=False, timelimit_pc=None, maxdepth=1,
               eval_func=None, eval_params={}, use_tt=False,
               tt=None, tt_for_mo=None):
元と同じなので省略               
        if mborig.turn == Marubatsu.CIRCLE:
            score = float("-inf")
-           for x, y in legal_moves:
+           for move in legal_moves:
-               mborig.move(x, y)
+               mborig.move(move)
元と同じなので省略  
        else:
            score = float("inf")
-           for x, y in legal_moves:
+           for move in legal_moves:
-               mborig.move(x, y)
+               mborig.move(move)
元と同じなので省略

ベンチマークの実行

上記の修正後に前回の記事のベンチマークを実行することにします。ただし、util.py の benchmark では上記で修正した ai_abs_dls上記の修正が行われていない ai.py からローカルにインポート しているので、上記の修正を行った ai_new.py からインポート するように benchmark を再定義 する必要があります。なお、ローカルにインポートする関数を修正しない場合はこのような再定義を行う必要はありません。また、ai_new.py の内容は次回以降の記事では ai.py に反映するので、util.py の benchmark を修正する必要はありません。

  • 9 行目:ai_new.py からローカルにインポートするように修正する
1  import random
2  import timeit
3  from statistics import mean, stdev
4  
5  def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
6      if seed is not None:
7          random.seed(seed)       
8          
9      from ai_new import ai2, ai14s, ai_match, ai_abs_dls
元と同じなので省略
行番号のないプログラム
import random
import timeit
from statistics import mean, stdev

def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
    if seed is not None:
        random.seed(seed)       
        
    from ai_new import ai2, ai14s, ai_match, ai_abs_dls

    ai_match(ai=[ai2, ai2], match_num=match_num, mbparams=mbparams)   
    ai_match(ai=[ai14s, ai2], match_num=match_num, mbparams=mbparams)

    mb = Marubatsu(**mbparams)
    eval_params = {"minimax": True}
    stmt = "ai_abs_dls(mb, eval_func=ai14s, eval_params=eval_params, use_tt=True, maxdepth=8)"
    print("ai_abs_dls")
    result = timeit.repeat(stmt=stmt, number=number, repeat=repeat, globals=locals())
    result = [time / number for time in result]
    print(f"{mean(result) * 1000:5.1f} ms ± {stdev(result) * 1000:5.1f} ms per loop (mean ± std. dev. of {repeat} runs, {number} loops each)")
修正箇所
import random
import timeit
from statistics import mean, stdev

def benchmark(mbparams={}, match_num=50000, seed=0, number=10, repeat=7):
    if seed is not None:
        random.seed(seed)       
        
-   from ai import ai2, ai14s, ai_match, ai_abs_dls
+   from ai_new import ai2, ai14s, ai_match, ai_abs_dls
元と同じなので省略

上記の再定義後に下記のプログラムでベンチマークを実行します。

from util import benchmark
from marubatsu import ListBoard, List1dBoard

for boardclass in [ListBoard, List1dBoard]:
    for count_linemark in [False, True]:
        print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
        benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
        print()

実行結果

boardclass: ListBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 11848.56it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [01:00<00:00, 829.88it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 22.7 ms ±   1.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: ListBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 15077.96it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:25<00:00, 1951.29it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 20.6 ms ±   2.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: List1dBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:05<00:00, 9057.91it/s] 
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [01:13<00:00, 681.72it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 31.7 ms ±   2.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

boardclass: List1dBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:04<00:00, 10490.45it/s]
count     win    lose    draw
o       29454   14352    6194
x       14208   29592    6200
total   43662   43944   12394

ratio     win    lose    draw
o       58.9%   28.7%   12.4%
x       28.4%   59.2%   12.4%
total   43.7%   43.9%   12.4%

ai14s VS ai2
100%|██████████| 50000/50000 [00:32<00:00, 1528.86it/s]
count     win    lose    draw
o       49446       0     554
x       44043       0    5957
total   93489       0    6511

ratio     win    lose    draw
o       98.9%    0.0%    1.1%
x       88.1%    0.0%   11.9%
total   93.5%    0.0%    6.5%

ai_abs_dls
 30.2 ms ±   1.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

下記は 前回の記事のベンチマークの結果と上記の結果をまとめた表です。上段の数値が前回の記事の結果 で、下段の数値が上記の結果 を表します。

boardclass count_linemark ai2 VS ai2 ai14s VS ai2 ai_abs_dls
ListBoard False 12391.16 回/秒
11848.56 回/秒
969.45 回/秒
829.88 回/秒
17.4 ms
22.7 ms
ListBoard True 12340.74 回/秒
15077.96 回/秒
1882.32 回/秒
1951.29 回/秒
17.6 ms
20.6 ms
List1dBoard False 11801.20 回/秒
9059.91 回/秒
956.38 回/秒
681.72 回/秒
17.6 ms
31.7 ms
List1dBoard True 12300.84 回/秒
10490.45 回/秒
1875.35 回/秒
1528.86 回/秒
18.6 ms
30.2 ms

上記の表から下記のことがわかります。

  • ai2 VS ai2ai14s VS ai2
    • ListBoard を利用し、count_linemarkTrue の場合は 処理速度が速く なる
    • それ以外の場合 ListBoard を利用する場合は処理速度が 少しだけList1dBoard を利用する場合は 目に見えて遅く なる
  • ai_abs_dls ではいずれの場合も 処理速度が遅くなる が、List1dBoard のほうが より処理速度が遅くなる

このようなことが起きる理由と、修正方法については次回の記事で説明します。

Board クラスの修正

今回の記事の修正によって、Board クラスで定義する 抽象メソッドなどを下記の表のように修正 することにします。なお、下記の表の moveゲーム盤のクラスの座標 を表します。

抽象メソッド 処理
getmark_by_move(move) move のマスのマークを返す
setmark_by_move(move, mark) move のマスに mark を代入する
board_to_str() ゲーム盤を表す文字列を返す
judge(last_turn, last_move, move_count) 勝敗判定を計算して返す
count_markpats(turn, last_turn) 局面のマークのパターンを返す
xy_to_move(x, y) (x, y) のマスのゲーム盤のクラスの座標を返す
calc_legal_moves() 合法手の一覧を表す、ゲーム盤のクラスの座標を要素とする list を返す

また、下記のメソッドは ゲーム盤のクラスによって処理が大きく変わることはないと思われる ので、Board クラスのメソッドでその処理を定義 し、ListBoard や List1dBoard クラスなどの ゲーム盤のクラスでは定義しない ことにしました。ゲーム盤のクラスによっては より効率的な方法で getmarksetmark の定義を行える場合 があるかもしれません。そのような場合はそのクラスで これらのメソッドをオーバーライドして定義 すると良いでしょう。

メソッド 処理
getmark(move) (x, y) のマスのマークを返す
setmark(x, y, mark) (x, y) のマスに mark を代入する

今回の記事のまとめ

今回の記事では 座標を表すデータ構造 を、ゲーム盤のデータ構造に適したデータ構造で表現できる ように修正しました。ただし、その修正によって処 理速度が低下 するという問題があることが判明したので、次回の記事でその問題の修正を行うことにします。

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

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

次回の記事

  1. List1dBoard の数値座標は縦方向に数えるので、x 座標は move // self.BOARD_SIZE で計算する必要があります

  2. 実は __iter__ メソッドを定義したほうが簡潔にプログラムを記述できますが、__iter__ メソッドの説明が長くなるので今回の記事では __getitem__ メソッドを定義することにしました

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?