I saw @ keisuke1111's "A programming beginner studied python and tried to make Othello AI". Even though I was a beginner, I was interested in tackling challenging issues. However, it seemed that he was having a lot of trouble with the heavy use of global variables, redundant processing, and deep nesting, which are common to beginners. Therefore, I thought that it would be a reference for program creation, so I tried to classify each role so that global variables are not used, and divide the processing so that one function or method fits within 24 lines on one screen of the terminal. I hope it will be useful for future program creation.
I myself am refactoring through trial and error, so if you have any other good ideas, I would appreciate it if you could comment.
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, "you")
        #player1 = user = User(BLACK, "you")
        #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 hand: %s" % (player1.name, hand1))
 
            print(board)
            hand2 = player2.play(board)
            print("%s hand: %s" % (player2.name, hand2))
 
        self.show_result(board)
 
        end_time = time.time()
        print("Match time:" + str(end_time - start_time))
 
    def show_result(self, board):
        print("------------------RESULT-------------------")  #Result announcement
        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)]  #Define the first board
        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):         #A function that returns how many stones there are
        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 is the coordinate of the stone to be placed, and stone is WHITE or 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):  #Searching for a place to hit
        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 seconds" % interval)  #Show calculation time
        return x, y
 
 
class User(Player):
 
    def think(self, board, availables):
        while True:
            print("Where you can hit(Y, X): " + str(availables))   #Internal x,X displayed as y,Note that Y is the opposite
            try:
                line = input("Y X or quit: ")
            except:
                print("forced termination")
                exit(1)
            if line == "quit" or line == "exit":
                print("End of abandonment")
                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("Can't put there")
            except:
                print("Unknown")
 
 
def AlphaBeta(board, availables, own, opponent):  #Search by Alpha Beta method
    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 = (  #Evaluation board showing how many points there are stones in which square
    ( 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):  #Calculate the evaluation value of either stone on any board
    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):  #Count the number of fixed stones
    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()
Recommended Posts