じゅういっこめ

『囲碁ディープラーニングプログラミング』を読んでみた

上記タイトルの本が本屋さんにあったので、面白そうなので図書館で借りてみた。

『囲碁ディープラーニングプログラミング』

Max Pumperia、Kevin Ferguson 著
山岡忠夫 訳
マイナビ出版 2019年4月22日 初版第1刷

機械学習とは何か、とか、難しい話が最初にある。
第3章からは、実際に囲碁プログラムを作成する。

当然のことながら、Python3 についての説明や、掲載されているコードについての 詳しい説明はない。

なぜそういうコードが必要なのか、そのコードの狙いは何か、そういうのは説明されている。

僕は Pythonは、2年ほど前にちょっとかじった程度なので、ほとんど忘れている。
文法書も手元にはない。ネット情報だけがたよりである。

そこで、本文に載っているコードに、ネットで調べたことや、そのコードを読み解くために 必要なメモを記入した。

更に、各クラスのプロパティやメソッドについての説明、特に、メソッドについては、 引数と返り値をメモに書いた。

それらをまとめて、自分のブログに置いといたら、そのうち役に立つかもしれない。

ディープラーニングにいくまでの入り口のところでウロウロしてしまった。
でも、これはこれで、楽しい。

dlgo/gotypes.py (p55)
import enum

# print(Player.black)  ==> Player.black
# print(Player.black.name) ==> black
# print(Player.black.value) ==> 1
class Player( enum.Enum ):
    black = 1
    white = 2

    # a が Player.black だとすると、a.other は Player.white となる 
    @property
    def other( self ):
        return Player.black if self == Player.white else Player.white

プレーヤーを Player.black と Player.white で表現できる。

dlgo/gotypes.py(つづき)
from collections import namedtuple

# クラス Point型の定義
# namedtupleを引数にもってインスタンスを作成
# namedtuple(typename, field_names, ...) -- 名前付きフィールドをもつタプルのファクトリ関数
# field_namesには、'x y'や'x, y'などの文字列をわたすことができる
# (例)
#   >>> a = Point(row=5, col=5)        # <== インスタンスの作成のしかた
#   >>> print(a)               # Point(row=5, col=5)
#   >>> print(a.row)           # 5
#   >>> b = a.neighbors()
#   >>> print(b)
#   [Point(row=4, col=5), Point(row=6, col=5), Point(row=5, col=4), Point(row=5, col=6)]
class Point( namedtuple( 'Point', 'row col')):
    def neighbors( self ):
        return [
            Point( self.row - 1, self.col ),
            Point( self.row + 1, self.col ),
            Point( self.row, self.col - 1 ),
            Point( self.row, self.col + 1 ),
        ]

碁盤の点を Point(row, col) の形式で表現できる。
また、Point(row, col).neighbors() とすると、上下左右の点を取得できる。

dlgo/goboard_slow.py (p56)
# Move(着手)
#   Move.play(point) -- そのポイントをポイントとする
#   Move.pass_turn -- パスする (self.is_pass が true になる)
#   Move.resign -- 投了する (self.is_resign が true になる)
class Move():
    def __init__( self, point=None, is_pass=False, is_resign=False ):
        assert (point is not None) ^ is_pass ^ is_resign    # <1>
        self.point = point                                  # <2>
        self.is_play = (self.point is not None)             # <3>
        self.is_pass = is_pass
        self.is_resign = is_resign
        # <1> 排他的論理和 -- すべてが1だと0になる
        # <2> self -- インスタンス自身のこと。
        # <3> is_play -- その点は空ではない
        #     is_play, is_pass, is_resign は、True / False の値をとる

    # クラスメソッド -- インスタンス化せずにクラスから直接呼び出すことができる
    # cls -- クラスのこと。呼び出すときの引数は point から。
    # play(打つ) -- プロパティself.point に 引数point をセット。
    @classmethod                    
    def play( cls, point ):         
        return Move( point=point )

    # pass(パス)
    @classmethod
    def pass_turn( cls ):
        return Move( is_pass=True )

    # resign(投了)
    @classmethod
    def resign( cls ):
        return Move( is_resign=True )

Moveは、ここでは、「着手」の役割を持たせている。

クラス「Move」
 コンストラクタ引数: point, is_pass, is_resign
 プロパティ: point -- Point(row, col)
             is_play -- True / False
             is_pass -- True / False
             is_resign -- True / False
 クラスメソッド:
   Move.play(point)
       引数: point
       返り値: Move.point に point をセットしたインスタンス
           そのポイントに石をおく
           すると、そのポイントはNoneではないので、is_play は True となる。
   Move.pass_turn
       引数: なし
       返り値: Move.is_pass に Trueをセットしたインスタンス
           パスをする
   Move.resign
       引数: なし
       返り値: Move.is_resign に Trueをセットしたインスタンス
           投了する
dlgo/goboard_slow.py(つづき)
# 石の連 GoString
#   color -- 石の色
#   stones -- 石の集合 setを使っている。集合をあらわすデータ型だそうだ。
#   liberties -- 呼吸点の集合
# (例)
#   ren1 = GoString(white, 3, 8) -- ren1は白で、3つの石があり、呼吸点は8個である。
#   ren1.remove_liberty(point) -- 呼吸点を1つ削除する。
class GoString():
    def __init__( self, color, stones, liberties ):
        self.color = color
        self.stones = set(stones)
        self.liberties = set(liberties)

    # 呼吸点を削除
    # removeは集合型のメソッド。要素を削除する。
    def remove_liberty( self, point ):
        self.liberties.remove( point )

    # 呼吸点を追加
    # add -- 集合に要素を追加する
    def add_liberty( self, point ):
        self.liberties.add( point )

    # 2つの連を合体
    def merged_with( self, go_string ):
        assert go_string.color == self.color               # <1>
        combined_stones = self.stones | go_string.stones   # <2>
        return GoString(                                   # <3>
            self.color,
            combined_stones,
            ( self.liberties | go_string.liberties ) - combined_stones )
    # <1> 石の色が同じかをチェック
    # <2> | -- 論理和  combined_stones は、石の集合の論理和である。
    # <3> 新しくクラスをインタンス化して、それを返している
    #     第3引数 -- 両方の呼吸点の集合の論理和をとって、石の集合との差集合を求めている。
    #     論理和をとる場合、重複したものは1つとみなされる。
    #     差集合をとる場合、a - b なら、a集合のなかに b集合と重複するものがある場合、
    #     それは削除される。
    #     (参考) -- https://uxmilk.jp/14834

    # インスタンス.num_liberties -- 集合のlen(要素数)を返す
    @property
    def num_liberties( self ):
        return len( self.liberties )

    # 2つのインスタンスを比較できる -- a.__eq__(b)
    # isinstance( object, class) -- 第1引数のオブジェクトが、
    #                               第2引数の型のインスタンスであれば true を返す
    def __eq__( self, other ):
        return isinstance( other, GoString ) and \
            self.color == other.color and \
            self.stones == other.stones and \
            self.liberties == other.liberties
GoString
 コンストラクタ: color, stones, liberties
 プロパティ: ocolor -- Player.black / Player.white
             stones -- 石の集合 set              
             liberties -- 呼吸点の集合 set
 メソッド:
   remove_liberty( point ) -- 呼吸点からそのポイントを削除
       引数: point
       返り値: なし (liberties.remove(point))
   add_liberty( point ) -- そのポイントを呼吸点に追加
       引数: point
       返り値: なし (liberties.add(point))
   merged_with( go_string ) -- その連を吸収合体
       引数: go_string
       返り値: go_string
   __eq__( other ) -- 2つの連が同じかどうか( othier -- 他の連)
       引数: go_string
       返り値: boolean
 プロパティメソッド:
   num_liberties -- 呼吸点の数を返す
       引数: なし
       返り値: int -- len(self.liberties)
dlgo/goboard_slow.py(つづき)
# 盤面
# num_rows, num_cols -- 格子線の数
# _grid -- 盤面の情報を辞書リストでもっている。
#          keyは、Point(row=2, col=3)などのPoint。
#          値は、オブジェクトで、GoString情報をもっている
class Board():
    def __init__( self, num_rows, num_cols ):
        self.num_rows = num_rows
        self.num_cols = num_cols
        self._grid = {}                    # 辞書 -- {キー: 値, ...}
    # _grid -- Pointをkeyとした辞書である。
    #          value には、GoStringが入っている。

    # 呼吸点のために隣接する点をチェック
    # player -- (ex) Player.black
    # point -- (ex) Point(row=1, col=1)
    def place_stone( self, player, point ):
        assert self.is_on_grid(point)         # pointが盤上にあるかどうか
        assert self._grid.get(point) is None  # pointがまだ打たれていないかどうか
        adjacent_same_color = []              # 同じ色のリスト
        adjacent_opposite_color = []          # 相手の色のリスト
        liberties = []                        # 呼吸点のリスト
        # print( Point(3, 5).neighbors() )
        # [ Point(row=15, col=6), Point(row=17, col=6), Point(row=16, col=5), Point(row=16, col=7) ]
        for neighbor in point.neighbors():
            if not self.is_on_grid( neighbor ):                # <1>
                continue
            neighbor_string = self._grid.get( neighbor )       # <2>
            if neighbor_string is None:                        # <3>
                liberties.append( neighbor )
            elif neighbor_string.color == player:               # <4>
                if neighbor_string not in adjacent_same_color:
                    adjacent_same_color.append( neighbor_string )
                else:
                    if neighbor_string not in adjacent_opposite_color:
                        adjacent_opposite_color.append( neighbor_string )
        new_string = GoString( player, [point], liberties )      # <5>
        # <1> neighborが盤上の点ではなかったら、パス
        # <2> _grid.get( key ) -- 指定されたkeyがあれば、その連情報を返す。
        #                         なければ、Noneを返す。
        #       値は、GoStringである。
        # <3> もしneighbor_stringがNoneであれば、呼吸点のリストに追加
        # <4> もしneighbor_stringの色がプレイヤーと同じであれば
        #     adjacent_sama_color, adjacent_oppsite_colorにいれる
        # <5> 新しい連をつくる
        #
        for same_color_string in adjacent_same_color:                  # <6>
            new_string = new_string.merged_with( same_color_string )
        for new_string_point in new_string.stones:                     # <7>
            self._grid[ new_string_point ] = new_string
        for other_color_string in adjacent_opposite_color:             # <8>
            other_color_string.remove_liberty( point )
        for other_color_string in adjacent_opposite_color:             # <9>
            if other_color_string.num_liberties == 0:
                self._remove_string( other_color_string )
        # <6> 同じ色の隣接する連をマージする
        # <7> 新しくできた連のそれぞれのポイントに、連の情報をそれぞれセットする。
        # <8> 敵の色の隣接する連の呼吸点を減らす
        # <9> 敵の色の連の呼吸点が 0 になっている場合は、それを取り除く

    def is_on_grid( self, point ):
        return 1 <= point.row <= self.num_rows and \
            1 <= point.col <= self.num_cols

    # 盤上の点の連情報をかえす
    # その点に石がある場合はPlayerの色、それ以外はNoneを返す
    def get( self, point ):
        string = self._grid.get( point )
        if string is None:
            return None
        return string.color

    # ある点における石の連全体を返す。
    # その点に石がある場合はGoString、なければNoneを返す
    def get_go_string( self, point ):
        string = self._grid.get( point )
        if string is None:
            return None
        return string

    # 連を取り除く
    # 連を取り除くと、相手の石が呼吸点を得ることができる
    # string -- 取られる連
    def _remove_string( self, string ):
        for point in string.stones:                             # <1>
            for neighbor in point.neighbors():                  # <2>
                neighbor_string = self._grid.get( neighbor )    # <3>
                if neighbor_string is None:
                    continue
                if neighbor_string is not string:               # <4>
                    neighbor_string.add_liberty( point )
            self._grid[ point ] = None                          # <5>
    # <1> string.stones --- stonesはpointの集合
    # <2> point.neighbors() --- 隣の点のリスト
    # <3> neighbor_string -- 隣の連の情報
    # <4> neighbor_string が 取られる連でなかったら、相手の連の呼吸点の集合に追加する。
    # <5> 石を取り除いたので、そのポイントは None になる
Board
 コンストラクタ: num_rows, num_cols
 プロパティ: num_rows -- 行の数
              num_cols -- 列の数
             _grid -- 点の情報。辞書。
                      key : Point(row, col)
                      value : go_string -- (color, stones, liberties)
                      _grid.get(point)で、そのポイントの連の色を得ることができる。
                      _grid.get_go_string(point)で、そのポイントの連情報を得ることができる。
 メソッド:
   place_stone( player, point ) -- 盤上に石を置く
       引数: player -- Player.black あるいは Player.white
             point -- Point(row, col)
       返り値: なし。(Board._gridの情報を書き換えるから)
           メソッド内変数:adjacent_same_color -- 同じ色のリスト
                          adjacent_oppsite_color -- 相手の色のリスト
                          liberties -- 呼吸点のリスト
                          neighbor_string -- 隣接点の値 _grid.get(neighbor)
           上下左右の隣接点(neighbor_string=連)を調べる
               空なら、呼吸点に加える。
               同じ色の連であれば、adjacent_same_colorリストに加える。
               相手の色の連であれば、adjacent_oppsite_colorリストに加える。
           new_string(連)をつくる。GoString( player, [point], liberties )
           adjacent_same_colorリストの中に同じ色の連があれば、new_string(連)とその連を合体させる。
           新しくできた連のそれぞれのポイントに、新しい連の情報をセットする。
           相手の色の連の呼吸点リストから、今のポイントを削除する。
           もし、相手の色の連の呼吸点の数がゼロになれば、相手の色の連を削除する。
   is_on_grid( point ) -- そのポイントが盤上にあれば True、なければ False
       引数: point -- Point(row, col)
       返り値: boolean
   get( point ) -- そのポイントの連の色を返す。
       引数: point
       返り値: string.color
   get_go_string( point ) -- そのポイントの連情報を返す。(color, stones, liberties)
       引数: point
       返り値: string
   _remove_string( string ) -- 連を取り除く。
       引数: string -- GoStringのこと。
       返り値: なし。(Board._gridの情報を書き換える)
           取り除く対象の連のそれぞれのポイントをNoneにする。
           その際、そのポイントの隣の点を調べ、もし、相手の連であれば、その連に呼吸点を追加する。
dlgo/goboard_slow.py(つづき)
# ゲーム情報
class GameState():
    def __init__( self, board, next_player, previous, move ):
        self.board = board                      # 盤面の情報
        self.next_player = next_player          # 次のプレーヤー
        self.previous_state = previous          # 前のゲーム状態        
        self.last_move = move                   # 最後に行われた着手

    # 着手を適用したあと、新しい GameState を返す
    def apply_move( self, move ):
        if move.is_play:                                       # <1>
            next_board = copy.deepcopy( self.board )
            next_board.place_stone( self.next_player, move.point )
        else:
            next_board = self.board
        return GameState( next_board, self.next_player.other, self, move )
    # <1> 着手が is_play、つまり Move.play(point)で指し手をした場合、
    #     pointはNoneではなくなるので、is_play は True となる。

    # board_size -- 9 / 19 などの数値
    @classmethod
    def new_game( cls, board_size ):
        if isinstance( board_size, int ):                      # <1>
            board_size = ( board_size, board_size )
        board = Board( *board_size )                           # <2>
        return GameState( board, Player.black, None, None )
    # <1> board_sizeオブジェクトが int型のインスタンスであれば True
    # <2> *board_size -- 9 9 どうもタプルやリストの中の値だけをとりだしてくれるみたい

    # 終局しているか判定
    def is_over( self ):
        if self.last_move is None:                             # <1>
            return False
        if self.last_move.is_resign:                           # <2>
            return True
        second_last_move = self.previous_state.last_move       # <3>
        if second_last_move is None:
            return False
        return self.last_move.is_pass and second_last_move.is_pass  # <4>
    # <1> 最後の着手がまだの場合は False
    # <2> 最後の着手が「投了」の場合は True
    # <3> 前回の盤面の最後の着手を second_last_move とする
    # <4> last_move と second_last_move がともに パス なら True を
    #     そうでなければ False を返す

    # 自殺手のルールを強制する
    def is_move_self_capture( self, player, move ):
        if not move.is_play:                                   # <1>
            return False
        next_board = copy.deepcopy( self.board )
        next_board.place_stone( player, move.point )
        new_string = next_board.get_go_string( move.point )
        return new_string.num_liberties == 0
    # <1> Move.play(point)であれば is_play は True になっている。

    # 現在のゲーム状態はコウのルールに違反しているか?
    @property
    def situation( self ):
        return ( self.next_player, self.board )

    # player -- Player.black / Player.white
    # move -- Move.play(row, col)
    def does_move_violate_ko( self, player, move ):
        if not move.is_play:
            return False
        next_board = copy.deepcopy( self.board )           # <1>
        next_board.place_stone( player, move.point )       # <2>
        next_situation = ( player.other, next_board )      # <3>
        past_state = self.previous_state                   # <4>
        while past_state is not None:                      # <5>
            if past_state.situation == next_situation:     # <6>
                return True
            past_state = past_state.previous_state         # <7>
        return False                                       # <8>
    # <1> 現在の盤をdeepcopyして、next_boardとする。
    # <2> next_boardに次の指し手を実行(石を置く)。
    # <3> next_board と player.other をタプルにして next_situation とする。
    # <4> 前回の盤を past_state とする。
    # <5> 前回の盤が None でない限り実行。
    # <6> past_sitate.situation と next_situation が同じであれば、
    #     つまり、「コウ」であれば、True を返す。
    # <7> past_stateを past_stateの更に前の盤(previous_state)とする。
    #     それで、もう一度、past_state.situation と next_situation を比べる。
    # <8> 同じゲーム状況が無ければ、「コウ」ではないので、False を返す。

    # この着手は指定されたゲーム状態に対して有効か?
    def is_valid_move( self, move ):
        if self.is_over():                                     # <1>
            return False
        if move.is_pass or move.is_resign:                     # <2>
            return True
        return (                                               
            self.board.get( move.point ) is None and                        # <3>
            not self.is_move_self_capture( self.next_player, move ) and     # <4>
            not self.does_move_violate_ko( self.next_player, move ))        # <5>
    # <1> 終局の場合 False(無効)
    # <2> 着手がパスあるいは投了の場合 True(有効)
    # <3> 指し手の点に石(連)が無い場合
    # <4> 指し手が自殺手である場合
    # <5> 指し手がコウである場合
    #     <3><4><5>がともに成立することって、あるのかな?
GameState
 コンストラクタ: board, next_player, previous, move
 プロパティ: board -- 盤面の情報
             next_player -- プレーヤー
             previous_state -- 前の盤面の情報
             last_move -- 前回の指し手
 メソッド:
   apply_move( move )
       引数: move -- Move.play(row, col)、あるいは Move.pass_turn、Move.resign
       返り値: GameState( next_board, next_player.other, self, move)
             指し手を行った場合、盤面をディープコピーしてnext_boardとする。
             盤上に石を置く。
             それ以外(パス・投了の場合)は、盤面をそのまま引き継ぐ。
             そして、新しく GameState のインスタンスを作成する。
   new_game( board_size ) -- クラスメソッド。
       引数: board_size -- 9、19 などの数値
       返り値: GameState( board, Player.black, None, None )
             board -- (19, 19) あるいは (9, 9)のタプル。
   is_over -- 終局しているか、判定。
       引数: なし
       返り値: boolean -- 投了もしくは双方がパスの場合 True、すなわち終局。
   is_move_self_capture -- 自殺手のルールを強制する
       引数: player -- Player.black / Player.white
             move   -- Move.play(row, col), あるいは Move.pass_turn、Move.resign
       返り値: boolean (呼吸点がゼロになった = True)
   situation -- プロパティ・メソッド
       引数: なし
       返り値: タプル ( self.next_player, self.board )
             next_playerと盤面情報を返す
   does_move_violate_ko
       引数: player, move
       返り値: boolean -- コウであれば True、コウでなければ False を返す。
   is_valid_move
       引数: move -- Move.play(row, col)
       返り値: boolean -- 終局なら False。パスあるいは投了なら True
             石を置いたポイントが None であり、なおかつ、
             プレーヤーの指し手が自殺手であり、なおかつ、
             プレーヤーの指し手がコウである場合、True -- こんな場合って、ある?
dlgo/agent/helper.py (p69)
from dlgo.gotypes import Point

# 盤上の指定された点は、眼か?
def is_point_an_eye( board, point, color ):
    if board.get( point ) is not None:               # <1>
        return False
    for neighbor in point.neighbors():
        if board.is_on_grid( neighbor ):
            neighbor_color = board.get( neighbor )
            if neighbor_color != color:              # <2>
                return False

    friendly_corners = 0
    off_board_corners = 0
    corners = [                                      # <3>
        Point( point.row - 1, point.col - 1),
        Point( point.row - 1, point.col + 1),
        Point( point.row + 1, point.col - 1),
        Point( point.row + 1, point.col + 1),
    ]
    for corner in corners:
        if board.is_on_grid( corner ):
            corner_color = board.get( corner )
            if corner_color == color:                 # <4>
                friendly_corners += 1
        else:
            off_board_corners += 1                    # <5>
    if off_board_corners > 0:                         # <6>
        return off_board_corners + friendly_corners == 4
    return friendly_corners >= 3                      # <7>
# <1> 眼は空の点
# <2> 隣接するすべての点の色は、味方の色であること
# <3> corners -- その点の四隅の点
# <4> そのコーナーの色が味方の色であれば、friendly_corners をプラス1
# <5> そのコーナーが盤上の点ではなかったら、off_board_corners をプラス1
# <6> off_board_cornersが1つでもあれば、off_board_corners と
#     friendly_corners の合計が 4 ならば True,そうじゃなければ False を返す
# <7> friendly_corners が 3 以上なら True を返す
is_point_an_eye( board, point, color ) -- 「眼」であるかどうかの判定
     引数:  board, point, color
     返り値: boolean -- 「眼」であれば True。でなければ False。
         pointの盤面情報が None でなかったら、すなわち、石あるいは連であったら、False
         つまり、空の点であること。
         そのポイントの隣の点が自身の色ではなかったら、false。つまり、上下左右が
         自分の色の石であること。
         四隅のうち、三つ以上が自分の色の石であれば、True。
         辺に自身があれば、すなわち四隅のうち、いくつから盤面から外れている場合、
         外れている隅と自分の色の隅が合計で四でれば、True。
agent/base.py
# 対局ボットのためのインターフェース
class Agent():
    def __init__( self ):
        pass

    # この基底クラスをもとに派生クラスを作る場合
    # この抽象メソッドが派生クラスでオーバーライドされることを
    # 要求している。
    # その場合、この例外を記述しておくことが求められる。
    def select_move( self, game_state ):
        raise NotImplementedError()
Agent
   コンストラクタ: なし
   select_move -- インターフェース
       引数: game_state -- GameStateのインスタンスを引数にとる。
dlgo/agent/nave.py
import random
from dlgo.agent.base import Agent
from dlgo.agent.helpers import is_point_an_eye
from dlgo.goboard_slow import Move
from dlgo.gotypes import Point

# candidates -- 有効な着手ができる候補リスト
# もし、candidatesが無ければ、パスする
# ボットは、candidatesリストの中からランダムに手を選ぶ
class RandomBot( Agent ):
    def select_move( self, game_state ):
        """Choose a random valid move that preserves our own eyes.
        自分の眼を維持するランダムな有効な着手を選択する"""
        candidates = []              # 候補
        for r in range( 1, game_state.board.num_rows + 1 ):
            for c in range( 1, game_state.board.num_cols + 1 ):
                candidate = Point( row=r, col=c )
                if game_state.is_valid_move( Move.play( candidate ) ) and \
                   not is_point_an_eye( game_state.board,            # <1>
                                        candidate,
                                        game_state.next_player ):
                    candidates.append( candidate )                   # <2>
        if not candidates:                                           # <3>
            return Move.pass_turn()
        return Move.play( random.choice( candidates ))               # <4>
    # <1> 要するに、候補点が着手可能な点であるか
    #     not is_point_an_eye つまり、眼には打てないということ。
    # <2> 盤上のすべての着手可能な点を「候補点」として candidates リストに入れている。
    # <3> もしも、候補点が無ければ、パスせざるを得ん。
    # <4> 候補点のリストの中から、ランダムに選んで、「指し手」としている。
RandomBot( Agent )
     親クラスAgentを継承。
   select_move( game_state )
       引数: game_state -- GameStateクラスのインスタンス
       返り値: Move.play( random.choice( candidates )) -- 選択した指し手
           有効な指し手で、眼ではない点ものをリストにして、その中からランダムに
           指し手を選ぶ。
           もし、指し手が無ければ、パス。
dlgo/utils.py
from dlgo import gotypes

COLS = 'ABCDEFGHJKLMNOPQRST'

STONE_TO_CHAR = {
    None: '.',
    gotypes.Player.black: 'x',
    gotypes.Player.white: 'o',
}

def print_move( player, move ):
    if move.is_pass:
        move_str = 'passes'
    elif move.is_resign:
        move_str = 'resigns'
    else:
        move_str = '%s%d' % ( COLS[move.point.col - 1], move.point.row )
    print( '%s %s' % ( player, move_str ))

def print_board( board ):
    for row in range( board.num_rows, 0, -1 ):
        bump = " " if row <= 9 else ""
        line = []
        for col in range( 1, board.num_cols + 1 ):
            stone = board.get( gotypes.Point( row=row, col=col ))    # <1>
            line.append( STONE_TO_CHAR[stone] )
        print( '%s%d %s' % (bump, row, ''.join(line)) )
    print( '   ' + ''.join(COLS[ :board.num_cols ]))
    # <1> board.get -- 盤上の格子に石があれば、その色を返す。なければ None
print_move( player, move )
     引数:  player, move
     返り値: なし
     副作用: 画面にプレーヤーと着手の点を文字情報として表示
 print_board( board )
     引数: board -- クラスBoardのインスタンス
     返り値: なし
     副作用: 画面に盤面情報を表示。
bot_v_bot.py
#from dlgo.agent import base, helpers, naive
from dlgo import agent
from dlgo import goboard_slow
from dlgo import gotypes
from dlgo.utils import print_board, print_move
from explain import explain
import time
import sys

def main():
    board_size = 9
    game = goboard_slow.GameState.new_game( board_size )
    bots = {
        gotypes.Player.black: agent.RandomBot(),
        gotypes.Player.white: agent.RandomBot(),
    }
    count = 0
    while not game.is_over():
        count += 1
        time.sleep(0.3)
        print( chr(27) + "[2J" )
        print_board( game.board )                       # <1>
        bot_move = bots[ game.next_player ].select_move( game )
        print_move( game.next_player, bot_move )
        game = game.apply_move( bot_move )
	if count is 50:                  # 50回で止めてみた
	    sys.exit(0)
    # <1> botが着手する前の盤面を描く。
        
if __name__ == '__main__':                             # <2>
    main()
# <2> このファイルがimportされた場合、「import bot_v_bot」
#     __name__には、「bot_v_bot」がはいる。
#     もし、「python3 bot_v_bot.py」として起動した場合、
#     __name__ には、「__main__」がはいる。
main()
     引数: なし
     返り値: なし
         board_sizeを決定
         game -- ゲーム状態を新規作成
         bots -- 黒と白のボットを作成
         geme.is_over() になるまで繰り返す。--- (A)
         画面の消去。
         盤面( game.board )を描く。
         黒あるいは白の指し手を決定。
         指し手を文字情報として表示。
         指し手を適用して、gameに反映。
         (A)にもどる。

以上で、スクリプトは終わり。

実行は、以下。

$ python3 bot_v_bot.py

 9 oo..x.o.o
 8 oxx...o.x
 7 .....o.o.
 6 xx.oo.ox.
 5 xoo.x..x.
 4 xoo.xo.xo
 3 xxx.x.xoo
 2 x.oxxxooo
 1 x..o.x.x.
   ABCDEFGHJ
Player.white F6

これは、開始からカウントを数え、50回で止めてみたところである。

また、実行途中のオブジェクトのようすを見るために、以下の関数を使った。

explain.py
# explain.py
# オブジェクトの中身を表示する
# 出典
# python でよくわからないオブジェクトの中身を表示する
# https://qiita.com/halhorn/items/7b8351c5eafbfa28d768

import types

def explain(item, shows_private=False, shows_method=False):
    '''
    与えた python オブジェクトの詳細を表示します。
    '''
    print('EXPLAIN ------------------')
    print(item)
    print(type(item))
    print('ATTRIBUTES:')
    for d in dir(item):
        if d == 'type':
            continue
        if not shows_private and d.startswith('_'):
            continue
        attr = getattr(item, d)
        if not shows_method and (
                isinstance(attr, types.MethodType) or
                isinstance(attr, types.BuiltinMethodType) or
                isinstance(attr, types.CoroutineType) or
                isinstance(attr, types.FunctionType) or
                isinstance(attr, types.BuiltinFunctionType) or
                isinstance(attr, types.GeneratorType)
        ):
            continue
        print('{}:\t{}'.format(d, attr))

たとえば、

from explain import explain

としておいて、

explain(neighbor_string)
sys.exit(0)

などで、みることができる。

このような表示になる。

EXPLAIN ------------------
<dlgo.goboard_slow.GoString object at 0x7f071f8190b8>
<class 'dlgo.goboard_slow.GoString'>
ATTRIBUTES:
color:	Player.black
liberties:	{Point(row=3, col=7), Point(row=5, col=7), Point(row=4, col=6), Point(row=4, col=8)}
num_liberties:	4
stones:	{Point(row=4, col=7)}

(出典) python でよくわからないオブジェクトの中身を表示する
https://qiita.com/halhorn/items/7b8351c5eafbfa28d768

『囲碁ディープラーニングプログラミング』

Max Pumperia、Kevin Ferguson 著
山岡忠夫 訳
マイナビ出版 2019年4月22日 初版第1刷