Python
python3

「プログラミング初心者がpythonの勉強がてら、オセロAIを作ってみた」をリファクタリングさせていただいた

More than 1 year has passed since last update.

@keisuke1111さんの「プログラミング初心者がpythonの勉強がてら、オセロAIを作ってみた」を拝見しました。
初心者でありながら挑戦的な課題に取り組まれていて関心しました。
ただ、初心者にありがちなグローバル変数の多用や冗長な処理や深いネストなど、ずいぶん苦労されている様子でした。
そこで、プログラム作成の参考になればと思い、役割毎にクラス化してグローバル変数を使わないようにし、一つの関数やメソッドが端末一画面24行以内に収まるように処理分轄してみました。
今後のプログラム作成の参考になれば幸いです。

私自身も試行錯誤しながらリファクタリングしている状況なので、他にも良いアイデアなどありましたらコメントいただけたると有難いです。

othello.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

'''
@author: Keisuke Ueda
http://qiita.com/keisuke1111/items/53353896a957136b1d7e
refactoring by @shiracamus
'''

import time
import random
import copy


class Othello:

    def play(self):
        start_time = time.time()
        board = Board()
        player1 = computer = Computer(BLACK, "PC")
        player2 = user = User(WHITE, "あなた")
        #player1 = user = User(BLACK, "あなた")
        #player2 = computer = Computer(WHITE, "PC")

        turn = 0
        hand1 = hand2 = None
        while board.is_playable() and not hand1 == hand2 == "pass":
            turn += 1
            print("TURN = %s" % turn)

            print(board)
            hand1 = player1.play(board)
            print("%sの手: %s" % (player1.name, hand1))

            print(board)
            hand2 = player2.play(board)
            print("%sの手: %s" % (player2.name, hand2))

        self.show_result(board)

        end_time = time.time()
        print("試合時間:" + str(end_time - start_time))

    def show_result(self, board):
        print("------------------RESULT-------------------")  # 結果発表
        print(board)
        computer_stones = board.count(computer.stone)
        user_stones = board.count(user.stone)
        print("Computer: %s" % computer_stones)
        print("You: %s" % user_stones)
        print()
        if computer_stones > user_stones:
            print("YOU LOST!!!!!")
        elif computer_stones < user_stones:
            print("YOU WON!!!!!")
        else:
            print("DRAW")


class Stone(str):
    pass

BLACK = Stone("●")
WHITE = Stone("○")
BLANK = Stone("×")
OPPONENT = {BLACK: WHITE, WHITE: BLACK}


class Board:
    SIZE = 8
    DIRECTIONS_XY = ((-1, -1), (+0, -1), (+1, -1),
                     (-1, +0),           (+1, +0),
                     (-1, +1), (+0, +1), (+1, +1))

    def __init__(self):
        size = self.SIZE
        center = size // 2
        square = [[BLANK for y in range(size)] for x in range(size)]  # 最初の盤面を定義
        square[center - 1][center - 1:center + 1] = [WHITE, BLACK]
        square[center + 0][center - 1:center + 1] = [BLACK, WHITE]
        self.square = square

    def __str__(self):
        return '\n'.join(''.join(row) for row in self.square)

    def __getitem__(self, x):
        return self.square[x]

    def is_playable(self):
        return any(col != BLANK
                   for row in self.square
                   for col in row)

    def count(self, stone):         # 石が何個あるかを返す関数
        return sum(col == stone     # True is 1, False is 0
                   for row in self.square
                   for col in row)

    def put(self, x, y, stone):     # y,xは置く石の座標、stoneにはWHITEかBLACKが入る。
        self[x][y] = stone
        # reverse
        for dx, dy in Board.DIRECTIONS_XY:
            n = self.count_reversible(x, y, dx, dy, stone)
            for i in range(1, n + 1):
                self[x + i * dx][y + i * dy] = stone

    def count_reversible(self, x, y, dx, dy, stone):
        size = self.SIZE
        for n in range(size):
            x += dx
            y += dy
            if not (0 <= x < size and 0 <= y < size):
                return 0
            if self[x][y] == BLANK:
                return 0
            if self[x][y] == stone:
                return n
        return 0

    def is_available(self, x, y, stone):
        if self[x][y] != BLANK:
            return False
        return any(self.count_reversible(x, y, dx, dy, stone) > 0
                   for dx, dy in self.DIRECTIONS_XY)

    def availables(self, stone):  # 打てる場所の探索
        return [(x, y)
                for x in range(self.SIZE)
                for y in range(self.SIZE)
                if self.is_available(x, y, stone)]


class Player:   # abstract class

    def __init__(self, stone, name):
        self.stone = stone
        self.name = name

    def play(self, board):
        availables = board.availables(self.stone)
        if not availables:
            return "pass"
        return self.think(board, availables)


class Computer(Player):

    def think(self, board, availables):
        starttime = time.time()
        print(availables)
        print("thinking……")
        own = self.stone
        opponent = OPPONENT[own]
        evaluations, x, y = AlphaBeta(board, availables, own, opponent)
        print(evaluations)
        board.put(x, y, self.stone)
        endtime = time.time()
        interval = endtime - starttime
        print("%s秒" % interval)  # 計算時間を表示
        return x, y


class User(Player):

    def think(self, board, availables):
        while True:
            print("打てる場所(Y, X): " + str(availables))   # 内部のx,yと表示のX,Yが逆なので注意
            try:
                line = input("Y X or quit: ")
            except:
                print("強制終了")
                exit(1)
            if line == "quit" or line == "exit":
                print("放棄終了")
                exit(1)
            try:
                x, y = map(int, line.split())
                if (x, y) in availables:
                    board.put(x, y, self.stone)
                    return x, y
                else:
                    print("そこには置けません")
            except:
                print("意味不明")


def AlphaBeta(board, availables, own, opponent):  # AlphaBeta法で探索する
    evaluations = AlphaBeta_evaluate1(board, availables, own, opponent)
    maximum_evaluation_index = evaluations.index(max(evaluations))
    x, y = availables[maximum_evaluation_index]
    return evaluations, x, y

def AlphaBeta_evaluate1(board, availables, own, opponent):
    def pruning2(max_evaluations3):
        return len(evaluations1) > 0 and max(evaluations1) >= max_evaluations3
    evaluations1 = []
    for x, y in availables:
        board1 = copy.deepcopy(board)
        board1.put(x, y, own)
        evaluations2 = AlphaBeta_evaluate2(board1, own, opponent, pruning2)
        if len(evaluations2) > 0:
            evaluations1 += [min(evaluations2)]
    return evaluations1

def AlphaBeta_evaluate2(board, own, opponent, pruning):
    def pruning3(min_evaluations4):
        return len(evaluations2) > 0 and min(evaluations2) <= min_evaluations4
    evaluations2 = []
    for x, y in board.availables(opponent):
        board2 = copy.deepcopy(board)
        board2.put(x, y, opponent)
        evaluations3 = AlphaBeta_evaluate3(board2, own, opponent, pruning3)
        if len(evaluations3) > 0:
            max_evaluations3 = max(evaluations3)
            evaluations2 += [max_evaluations3]
            if pruning(max_evaluations3):
                break
    return evaluations2

def AlphaBeta_evaluate3(board, own, opponent, pruning):
    def pruning4(max_evaluations5):
        return len(evaluations3) > 0 and max(evaluations3) >= max_evaluations5
    evaluations3 = []
    for x, y in board.availables(own):
        board3 = copy.deepcopy(board)
        board3.put(x, y, own)
        evaluations4 = AlphaBeta_evaluate4(board3, own, opponent, pruning4)
        if len(evaluations4) > 0:
            min_evaluations4 = min(evaluations4)
            evaluations3 += [min_evaluations4]
            if pruning(min_evaluations4):
                break
    return evaluations3

def AlphaBeta_evaluate4(board, own, opponent, pruning):
    def pruning5(evaluation5):
        return len(evaluations4) > 0 and min(evaluations4) <= evaluation5
    evaluations4 = []
    for x, y in board.availables(opponent):
        board4 = copy.deepcopy(board)
        board4.put(x, y, opponent)
        evaluations5 = AlphaBeta_evaluate5(board4, own, opponent, pruning5)
        if len(evaluations5) > 0:
            max_evaluation5 = max(evaluations5)
            evaluations4 += [max_evaluation5]
            if pruning(max_evaluation5):
                break
    return evaluations4

def AlphaBeta_evaluate5(board, own, opponent, pruning):
    evaluations5 = []
    for x, y in board.availables(own):
        board5 = copy.deepcopy(board)
        board5.put(x, y, own)
        ev_own = evaluate(board5, own)
        ev_opponent = evaluate(board5, opponent)
        evaluation = ev_own - ev_opponent
        evaluations5 += [evaluation]
        if pruning(evaluation):
            break
    return evaluations5


# pp = [45,-11,-16,4,-1,2,-1,-3,-1,0]
EVALUATION_BOARD = (  # どのマスに石があったら何点かを表す評価ボード
    ( 45, -11,  4, -1, -1,  4, -11,  45),
    (-11, -16, -1, -3, -3, -1, -16, -11),
    (  4,  -1,  2, -1, -1,  2,  -1,   4),
    ( -1,  -3, -1,  0,  0, -1,  -3,  -1),
    ( -1,  -3, -1,  0,  0, -1,  -3,  -1),
    (  4,  -1,  2, -1, -1,  2,  -1,   4),
    (-11, -16, -1, -3, -3, -1, -16, -11),
    ( 45, -11,  4, -1, -1,  4, -11,  45))

def evaluate(board, stone):  # 任意の盤面のどちらかの石の評価値を計算する
    bp = 0
    for x in range(board.SIZE):
        for y in range(board.SIZE):
            if board[x][y] == BLANK:
                pass
            elif board[x][y] == stone:
                bp += EVALUATION_BOARD[x][y] * random.random() * 3
            else:
                bp -= EVALUATION_BOARD[x][y] * random.random() * 3

    p = confirm_stone(board, stone)
    q = confirm_stone(board, OPPONENT[stone])
    fs = ((p - q) + random.random() * 3) * 11

    b = board.availables(stone)
    cn = (len(b) + random.random() * 2) * 10

    evaluation = bp * 2 + fs * 5 + cn * 1
    return evaluation

def confirm_stone(board, stone):  # 確定石の数を数える
    forward = range(0, board.SIZE)
    backward = range(board.SIZE - 1, -1, -1)
    corners = ((+0, +0, forward, forward),
               (+0, -1, forward, backward),
               (-1, +0, backward, forward),
               (-1, -1, backward, backward))
    confirm = 0
    for x, y, rangex, rangey in corners:
        for ix in rangex:
            if board[ix][y] != stone:
                break
            confirm += 1
        for iy in rangey:
            if board[x][iy] != stone:
                break
            confirm += 1
    return confirm


if __name__ == '__main__':
    Othello().play()