LoginSignup
3
3

More than 5 years have passed since last update.

コンピュータ囲碁の作成 原始モンテカルロ囲碁の実装

Posted at

コンピュータ囲碁

・コンピュータ囲碁の作成メモ。
・原始モンテカルロ囲碁の実装

今回もかなり苦戦しています。
原始モンテカルロ囲碁が作成できたと思いますが,かなり遅いです。

参考記事

原始モンテカルロ囲碁

関連記事

コンピュータ囲碁の作成
コンピュータ囲碁の作成 ランダム打ちの実装
コンピュータ囲碁の作成 ルールの実装

ソースコード

# -*- coding:utf-8 -*-
import random
import time
import copy
# 碁盤
BOARD_SIZE = 9              # 碁盤の大きさ

KOMI = 6.5

# 盤上の種類
SPACE,BLACK,WHITE,WALL = 0,1,2,3
VISUAL = ("・","🔴 ","⚪️ ", " ")
DIR4 = (-1,0),(1,0),(0,-1),(0,1)
# 石を打ったときの処理
SUCCESS = 0         # 打てる
KILL    = 1         # 自殺手
KO      = 2         # 劫
ME      = 3         # 眼
MISS    = 4         # すでに石がある
PASS    = 5         # パス

# 戦略
RANDOM = 1
MONTE_CARLO = 2

ERROR_MESSAGE = {KILL:  "自殺手",
                 KO:    "劫",
                 ME:    "眼",
                 MISS:  "すでに石がある",
                 PASS:  "パス",
                 }

'''
時間計測テンプレート
start = time.time()
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time)) + "[sec]"
'''

# 碁盤 
class Board(object):
    # 碁盤作成
    def __init__(self,size):
        self.size = size + 2    # 上下左右に外枠を含めた碁盤
        self.data = data = [[SPACE]*self.size for i in range(self.size)]
        # 外枠の作成
        for i in range(self.size):
            data[0][i] = data[-1][i] = data[i][0] = data[i][-1] = WALL
        # 直前に取った劫の位置
        self.ko = None

    def get(self,position):
        y,x = position
        return self.data[y][x]

    def set(self,position,stone):
        y,x = position
        self.data[y][x] = stone
    # 石を取り除く
    def remove(self,position):
        self.set(position,SPACE)

    # 碁盤描画
    def draw(self):
        print "  ", " ".join("%2d"%x for x in range(1,self.size-1))
        for y in range(1,self.size-1):
            print "%2d"%y, " ".join(VISUAL[data] for data in self.data[y][1:-1])

    # 盤上の空の場所を配列で取得
    def getNonePositions(self):
        return [(y, x)
                for y in range(1, self.size-1)
                for x in range(1, self.size-1)
                if self.data[y][x] == SPACE]


class JoinedLibertyCounter(object):
    def __init__(self,board):
        self.board = board

    def count(self,position,stone):
        return self.count_sub(position,stone,[])

    def  count_sub(self,position,stone,checked,joined=0,liberty=0):
        checked.append(position)
        joined += 1
        y,x = position
        for dy,dx in DIR4:
            adjacent = (y+dy,x+dx)
            if adjacent in checked:
                continue
            if self.board.get(adjacent) == SPACE:
                checked.append(adjacent)
                liberty+=1
            elif self.board.get(adjacent) == stone:
                joined, liberty = self.count_sub(adjacent,stone,checked,joined,liberty)
        return joined, liberty

class Player(object):

    def __init__(self,color,tact):
        self.color = color
        self.un_color = WHITE if color == BLACK else BLACK
        self.tact = tact

    # 戦術を選択
    def tactics(self,board,positions):
        if self.tact == RANDOM:
            return random_choice(self,board,positions)
        if self.tact == MONTE_CARLO:
            return monte_carlo(self,board,positions)

    # 相手の石を取る
    def capture(self,board, position):
        board.remove(position)
        y,x = position
        for dy,dx in DIR4:
            around = (y+dy,x+dx)
            if board.get(around) == self.un_color:
                self.capture(board,around)
    # 石を打つ
    def move(self,board,position):
        if position == (0,0):
            return PASS
        # すでに石がある場合
        if board.get(position) != SPACE:
            return MISS

        # positionに対して4方向の[連石の数,呼吸点の数,色]
        joineds  = [0]*4
        libertys = [0]*4
        colors   = [0]*4

        space = 0       # 4方向の空白の数
        wall  = 0       # 4方向の壁の数
        mikata_safe = 0 # 呼吸点が2以上の味方の数
        take_sum = 0    # 取れる石の合計
        ko = None       # 劫の候補

        # 打つ前に4方向を調べる
        joined_liberty = JoinedLibertyCounter(board)
        y,x = position
        for i, (dy,dx) in enumerate(DIR4):  # enumerate:インデックスとともにループ
            around = (y+dy,x+dx)
            colors[i] = c = board.get(around)
            if c == SPACE:
                space += 1
                continue
            if c == WALL:
                wall += 1
                continue

            # 連石と呼吸点の数を数える
            joineds[i],libertys[i] = joined_liberty.count(around,c)
            # 相手の石が取れるなら,劫の可能性があるので保持
            if c == self.un_color and libertys[i] == 1:
                take_sum += joineds[i]
                ko = around
            # 味方の石があって呼吸点が2つ以上あるなら眼の可能性
            if c == self.color and libertys[i] >= 2:
                mikata_safe += 1

        # ルール違反の処理
        # 敵の石に4方を囲まれている
        if take_sum == 0 and space == 0 and mikata_safe == 0:
            return KILL
        # 劫
        if position == board.ko:
            return KO
        # 眼には置かない (ルール違反ではない)
        if wall + mikata_safe == 4:
            return ME

        # 石を取る
        for i, (dy,dx) in enumerate(DIR4):
            if colors[i] == self.un_color and libertys[i] == 1:
                self.capture(board,(y+dy,x+dx))

        # 石を打つ
        board.set(position,self.color)

        # 劫を取った直後は相手が取り返せないようにする
        joined,liberty = joined_liberty.count(position,self.color)
        if take_sum == 1 and joined == 1 and liberty == 1:
            board.ko = ko       # 碁盤に劫の目印をつけておく
        else:
            board.ko = None     # 劫の目印を消す

        return SUCCESS

    # 置ける場所を配列で取得
    def getSuccessPositions(self,board):
        # 石を置ける場所を探すために,deepcopyした碁盤に石を置いて確認する
        return [(y,x)
                for y in xrange(1,board.size-1)
                for x in xrange(1,board.size-1)
                #if self.move(copy.deepcopy(board),(y,x)) == SUCCESS
                if board.get((y,x)) == SPACE
                and self.move(copy.deepcopy(board),(y,x)) == SUCCESS
                ]
# turn_colorが打つところから終局まで打ち,勝敗を返す
def playout(color,board,positions):
    playout_start = time.time()

    turn_player = Player(color,RANDOM)                  # ターンプレイヤー
    wait_player = Player(turn_player.un_color,RANDOM)   # 待機プレイヤー
    turn  = {turn_player:wait_player, wait_player:turn_player}
    board_copy = copy.deepcopy(board)                   # 試し打ち用ボード
    start1 = time.time()

    # 空点取得 試し打ち候補
    positions_copy = copy.deepcopy(positions)           
    # 先手
    player = turn_player
    # 対局開始
    passed = 0

    elapsed_time1 = time.time() - start1

    # print ("elapsed_time1:{0}".format(elapsed_time1)) + "[sec]"
    start2 = time.time()

    while passed < 2:
        while True:
            # 手がないならパス
            if len(positions_copy) == 0:
                result = PASS
            else:
                # 手があるなら取得する 
                position = random.choice(positions_copy)
                result = player.move(board_copy,position)       # 石を打つ
            if result == SUCCESS:
                #print VISUAL[player.color],position
                passed = 0
                break
            elif result == PASS:
                #print VISUAL[player.color],ERROR_MESSAGE[result]
                passed += 1
                break
            else:
                positions_copy.remove(position)             # 候補から外す
                #print VISUAL[player.color],ERROR_MESSAGE[result],position
        elapsed_time2 = time.time() - start2
        #board_copy.draw()
        # プレイヤー交代
        player = turn[player]
    # 終局
    #time.sleep(3)
    # 勝敗を返す
    score = score_counter(turn_player.color,board_copy)
    playout_elapsed_time = time.time() - playout_start
    #print ("playout_elapsed_time:{0}".format(playout_elapsed_time)) + "[sec]"
    #print ("elapsed_time2:{0}".format(elapsed_time2)) + "[sec]"
    # time.sleep(0)
    return score
def random_choice(player,board,positions):
    board_copy = copy.deepcopy(board)
    positions_copy = copy.deepcopy(positions)
    random.shuffle(positions_copy)
    position = (0,0)
    for p in positions_copy:
        result = player.move(board_copy,p)  # 石を打つ
        if result == SUCCESS:                       # 違反したら次
            position = p
            break
    return position

def monte_carlo(player,board,positions):
    monte_start = time.time()
    playout_count = 0

    TRY_GAMES = 30
    best_value = -999
    board_copy = copy.deepcopy(board)
    positions_copy = copy.deepcopy(positions)

    best_position = (0,0)
    count = 0

    random.shuffle(positions_copy)
    # すべての手に対して1手打ってみる
    for position in positions_copy:
        win_sum = 0
        result = player.move(board_copy,position)   # 石を打つ
        positions_copy.remove(position)             # 候補から外す
        if result != SUCCESS:                       # 違反したら次
            continue

        # (相手の手から)playoutをTRY_GAMES回繰り返す
        for i in range(TRY_GAMES):
            win_sum -= playout(player.un_color,board_copy,positions_copy)
            count += 1

        # 勝率が最も高いものを選ぶ
        win_rate = win_sum/TRY_GAMES
        if win_rate > best_value:
            best_value = win_rate
            best_position = position

    monte_elapsed_time = time.time() - monte_start
    print count
    print ("monte_elapsed_time:{0}".format(monte_elapsed_time)) + "[sec]"
    #time.sleep(2)
    return best_position


# 終局のスコアを計算 1:黒の勝ち -1:白勝ち  AIは空点があれば打つので空点の4方向を調べればよい
def score_counter(turn_color,board):
    score = 0
    # 盤上の[空点,黒石,白石]の数を取得
    kind = [0,0,0]
    for y in xrange(1,board.size-1):
        for x in xrange(1,board.size-1):
            col = board.data[y][x]
            kind[col] += 1
            # 空点は4方向の石の種類を調べる
            if col != SPACE:
                continue
            # mk[0] 空,[1] 黒,[2] 白,[3] 盤外
            mk = [0]*4
            for (dy,dx) in DIR4:
                mk[board.data[y+dy][x+dx]]+=1
            # 黒だけに囲まれていれば黒地
            if mk[1] > 0 and mk[2] == 0:
                score += 1
            # 白だけに囲まれていれば白地
            if mk[2] > 0 and mk[1] == 0:
                score -= 1
    # 地+盤上の石数
    score += kind[1] - kind[2]

    # コミを考慮した結果
    final_score = score - KOMI
    win = 0
    # turn_colorが黒で黒が勝っていれば1 負けていれば0
    if final_score > 0 :
        win = 1
    # turn_colorが白で白が勝っていれば0 負けていれば-1
    if turn_color == WHITE:
        win = -1
    return win

def judge(score):
    if score == 1:
        print VISUAL[BLACK],"勝ち"
    else:
        print VISUAL[WHITE],"勝ち"


def main():
    main_start = time.time()

    # 碁盤
    board = Board(BOARD_SIZE)

    # プレイヤー
    black = Player(BLACK,MONTE_CARLO)
    white = Player(WHITE,RANDOM)
    turn  = {black:white, white:black}

    # 先手
    player = black

    # 対局開始
    passed = 0
    while passed < 2:
        # 盤上の空点を取得  
        positions = board.getNonePositions()
        if len(positions) == 0:     # 打てないならパス
            result = PASS
        elif player == black:
            # 原始モンテカルロで打つ場所を取得
            position = player.tactics(board,positions)
            result = player.move(board,position)    # 打ってみる
        elif player == white:
            # ランダムに打つ場所を取得
            position = player.tactics(board,positions)
            result = player.move(board,position)    # 打ってみる

        if result == SUCCESS:           # 打てたら描画 選択終了
            passed = 0
            print VISUAL[player.color],position
            board.draw()
            print
        elif result == PASS:
            passed += 1
            print VISUAL[player.color],ERROR_MESSAGE[result]
        else:                           # 違反ならもう一度探す
            print VISUAL[player.color],ERROR_MESSAGE[result],position
            player = turn[player]
        # プレイヤー交代
        player = turn[player]
        #time.sleep(0)

    board.draw()
    print "対局終了"    
    judge(score_counter(BLACK,board))

    main_elapsed_time = time.time() - main_start
    print ("main_elapsed_time:{0}".format(main_elapsed_time)) + "[sec]"


if __name__ == '__main__':
    main()

github

https://github.com/Tsunehiko511/python_Go_origin
master:1次元配列
branch:2次元配列

参考図書

コンピュータ囲碁 ―モンテカルロ法の理論と実践―

3
3
20

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
3
3