Source code

Othello AI

A Python Othello AI using Negamax search, alpha-beta pruning, and heuristic board evaluation.

Runs with: Python

Project files

Download the original files, or browse the code below.

Browse source

othello.py
import sys; args = sys.argv[1:]
import time

#EVAL_BOARD_CACHE = {}

def main():
    global board, token, opposite, verbosePrint, HL, RL, TL
    global corners, badCorners, horribleCorners, row2, edges, innerStuff, boardWeights
    board = '.'*27 + "ox......xo" + '.'*27
    token = ''
    opposite = ''
    move = ''
    allMoves = []
    corners = [0, 7, 56, 63]
    badCorners = [1, 6, 8, 15, 48, 55, 57, 62]
    horribleCorners = [9, 14, 49, 54]
    boardWeights = [4, -3, 2, 2, 2, 2, -3, 4, -3, -4, -1, -1, -1, -1, -4, -3, 2, -1, 1, 0, 0, 1, -1, 2, 2, -1, 0, 1, 1, 0, -1, 2, 2, -1, 0, 1, 1, 0, -1, 2, 2, -1, 1, 0, 0, 1, -1, 2, -3, -4, -1, -1, -1, -1, -4, -3, 4, -3, 2, 2, 2, 2, -3, 4]
    verbosePrint = False
    HL = 10
    RL = 5
    TL = 1

    for arg in args:
        if len(arg) == 64 and board == '.'*27 + "ox......xo" + '.'*27:
            board = arg.lower()
        elif arg in ['X', 'O', 'x', 'o']:
            token = arg.lower()
        elif arg in ['v', 'V']:
            verbosePrint = True
        elif arg.startswith('HL'):
            HL = int(arg[2:])
        elif arg and len(arg) < 3:
            move = A1_to_index(arg) or int(arg)
            if int(move) >= 0:
                allMoves.append(move)
        elif arg and len(arg) > 2:
            newArg = list(arg)
            while newArg:
                if '_' in newArg[:2]:
                    allMoves.append(int(newArg[1])); newArg.pop(0); newArg.pop(0)
                elif '-' in newArg[:2]:
                    newArg.pop(0); newArg.pop(0)
                else:
                    allMoves.append(int("".join(newArg[:2]))); newArg.pop(0); newArg.pop(0)
    if not token: token = 'x' if (64-board.count('.')) % 2 == 0 else 'o'

    opposite = 'o' if token == 'x' else 'x'
    moves, flips = getMoves(board, token, opposite)
    if not verbosePrint:
        verbosePrint = True
        hasPossibles = displayPossibles(board, moves, token)
        verbosePrint = False
    else:
        hasPossibles = displayPossibles(board, moves, token)
    if not hasPossibles:
        #print(f"Since there are no possible moves for {token}, it is {opposite}'s turn.")
        temp = opposite; opposite = token; token = temp
        moves, flips = getMoves(board, token, opposite)
        if not verbosePrint:
            verbosePrint = True
            hasPossibles = displayPossibles(board, moves, token)
            verbosePrint = False
        else:
            hasPossibles = displayPossibles(board, moves, token)
    lastToken = None
    for move in allMoves:
        if move not in moves:
            break
        if move or move ==0:
            if verbosePrint: print(f"{token} plays to {move}");
            board = playMove(board, int(move), token, flips[int(move)]); lastToken = token
            temp = opposite; opposite = token; token = temp
            moves, flips = getMoves(board, token, opposite)
            if not moves:
                temp = opposite; opposite = token; token = temp
                moves, flips = getMoves(board, token, opposite)
                if not moves:
                    if not verbosePrint: print(f"{token} plays to {move}")
                    tempBoard = board[:move] + token.upper() + board[move + 1:]
                    formatBoard(tempBoard)
                    print("\n"+board+f" {board.count('x')}/{board.count('o')}")
                    print(f"No possible moves for {token}.")
                    return True
            else:
                displayPossibles(board, moves, lastToken, move)
    if allMoves: print(f"{lastToken} plays to {allMoves[-1]}"); move = allMoves[-1]
    if not verbosePrint:
        verbosePrint = True
        displayPossibles(board, moves, lastToken, move) if move else displayPossibles(board, moves, lastToken)
        verbosePrint = False
    else:
        displayPossibles(board, moves, lastToken, move) if move else displayPossibles(board, moves, lastToken)
    return False

def formatBoard(board):
    for i in range(8):
        for j in range(8):
            print(board[i * 8 + j], end=' ')
        print()

def displayPossibles(board, moves, token, move=None):
    global verbosePrint
    board2 = board
    xCount = board.count('x')
    oCount = board.count('o')
    if moves:
        for i in moves:
            board2 = board2[:i] + '*' + board2[i + 1:]
        if verbosePrint:
            if move: tempBoard = board2[:move] + token.upper() + board2[move + 1:]; formatBoard(tempBoard)
            else: formatBoard(board2)
            print("\n"+board+f" {xCount}/{oCount}")
            moves = sorted([*moves], key=lambda index: boardWeights[index])
            print(f"Possible moves for {token}: {moves}"+'\n')
        return True
    else:
        print(f"No possible moves for {token}.")
        return False

def A1_to_index(move):
    col_map = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7}
    if len(move) == 2 and move[0].lower() in col_map and move[1].isdigit():
        col = col_map[move[0].lower()]
        row = int(move[1]) - 1
        return row * 8 + col
    return None

def getMoves(board, token, opposite):
    moves = []
    flips = {}
    directions = [-9, -8, -7, -1, 1, 7, 8, 9]

    for i in range(64):
        if board[i] != '.':
            continue

        potential_flips = []

        for direction in directions:
            if (i % 8 == 0 and direction in [-9, -1, 7]) or (i % 8 == 7 and direction in [-7, 1, 9]):
                continue

            next = i + direction
            if next < 0 or next >= 64:
                continue

            direction_flips = []

            while 0 <= next < 64:
                if board[next] == opposite:
                    direction_flips.append(next)
                    next += direction
                elif board[next] == token:
                    if direction_flips:
                        potential_flips.extend(direction_flips)
                    break
                else:
                    break

                if (next % 8 == 0 and direction in [1, -7, 9]) or (next % 8 == 7 and direction in [-1, -9, 7]):
                    break

        if potential_flips:
            moves.append(i)
            flips[i] = potential_flips

    return moves, flips

def playMove(board, move, token, flipped):
    newBrd = list(board)
    for thing in flipped:
        newBrd[thing] = token
    newBrd[move] = token
    return ''.join(newBrd)

def alphabeta(board, token, opposite, lower, upper, level=None, sTime=0):
    if (time.time() - sTime <= TL) or (sTime == 0):
        moves, flips = getMoves(board, token, opposite)
        if level is not None and level <= 0:
            if not moves:
                moves2 = getMoves(board, opposite, token)[0]
                if not moves2:
                    return [board.count(token)-board.count(opposite)]
                res = alphabeta(board, opposite, token, -upper, -lower, 1, sTime)
                if res:
                    return [-res[0]] + res[1:] + [-1]
                return
            else:
                return [evalBoard(board, token, opposite, moves)]

        if not moves:
            moves = getMoves(board, opposite, token)[0]
            if not moves:
                return [board.count(token)-board.count(opposite)]
            res = None
            if level is None:
                res = alphabeta(board, opposite, token, -upper, -lower, sTime=sTime)
            else:
                res = alphabeta(board, opposite, token, -upper, -lower, level-1, sTime=sTime)
            if res:
                return [-res[0]] + res[1:] + [-1]
            else: return

        best = [lower-1]
        moves = sorted([*moves], key=lambda index: boardWeights[index], reverse = True)
        for move in moves:
            newBrd = playMove(board, int(move), token, flips[int(move)])
            if level is None:
                res = alphabeta(newBrd, opposite, token, -upper, -lower, sTime=sTime)
            else:
                res = alphabeta(newBrd, opposite, token, -upper, -lower, level - 1, sTime=sTime)
            if not res: return
            score = -res[0]
            if score < lower: continue
            if score > upper: return [score]
            if score > best[0]: best = [score] + res[1:] + [move]
            lower = score+1
        return best
    return

def quickMove(board, token):
    global HL, corners
    if not board: HL = token; return
    opposite = 'o' if token == 'x' else 'x'
    final = None
    moves, flips = getMoves(board, token, opposite)
    veryGood = []
    good = []
    bad = []
    veryBad = []
    negamaxList = []
    if board.count('.') <= HL:
        negamaxList = alphabeta(board, token, opposite, -65, 65)
        final = negamaxList[-1]
    elif board.count('.') <= 40:
        start = time.time()
        for i in range(1, RL):
            negamaxList = alphabeta(board, token, opposite, float('-inf'), float('inf'), i, sTime=start)
            if negamaxList:
                final = negamaxList[-1]
                temp = final
            else:
                return temp
    #play to corner
    if final is None:
        for move in moves:
            if move in corners:
                final = move
                break

    #adjacent to empty corner --> very bad
    #adjacent to friendly corner --> very good
    if final is None:
        for move in moves: #only make the x square very bad
            if (board[0] != token and move in [1, 8, 9]) or (board[7] != token and move in [6, 14, 15]) or (board[56] != token and move in [48, 49, 57]) or (board[63] != '.' and move in [54, 55, 62]):
                veryBad.append(move)
            elif (board[9] == opposite and move in [16, 2]) or (board[14] == opposite and move in [5, 23]) or (board[49] == opposite and move in [40, 58]) or (board[54] == opposite and move in [61, 47]) :
                veryBad.append(move)
            elif (board[0] == token and move in [1, 8, 9]) or (board[7] == token and move in [6, 14, 15]) or (board[56] == token and move in [48, 49, 57]) or (board[63] == token and move in [54, 55, 62]):
                final = move

    #connect to corners
    if final is None:
        for move in moves:
            if (0 <= move <= 7 or 56 <= move <= 63 or move % 8 == 0 or move % 8 == 7) and move not in veryBad:
                veryGood.append(move)

                #right
                if move % 8 != 7:
                    temp = move + 1
                    while temp % 8 != 7 and board[temp] == opposite:
                        temp += 1
                    if board[temp] == token:
                        final = move
                        break

                #left
                if move % 8 != 0:
                    temp = move - 1
                    while temp % 8 != 0 and board[temp] == opposite:
                        temp -= 1
                    if board[temp] == token:
                        final = move
                        break

                #up
                if move // 8 != 0:
                    temp = move - 8
                    while temp // 8 != 0 and board[temp] == opposite:
                        temp -= 8
                    if board[temp] == token:
                        final = move
                        break
                #down
                if move // 8 != 7:
                    temp = move + 8
                    while temp // 8 != 7 and board[temp] == opposite:
                        temp += 8
                    if board[temp] == token:
                        final = move
                        break
    #wedge moves
    if final is None:
        for move in moves:
            if (0 <= move <= 7 or 56 <= move <= 63) and board[move + 1] == board[move - 1] == opposite:
                final = move
                break

            elif (move % 8 == 0 or move % 8 == 7) and board[move + 8] == board[move - 8] == opposite:
                final = move
                break

    #good moves
    if final is None:
        seen = veryGood + bad + veryBad
        for move in moves:
            if move not in seen:
                good.append(move)

    #mobility and opponent move reduction
    if final is None:
        best = None
        temp = float('-inf')

        #xMoveCount = len(getMoves(board, token, opposite)[0])

        categories = [veryGood, good, bad, veryBad]
        for category in categories:
            for move in category:
                #newBoard = playMove(board, move, token, flips[int(move)])
                #newXMoveCount = len(getMoves(newBoard, token, opposite)[0])
                #newOMoves, newOFlips = getMoves(newBoard, opposite, token)
                #cornerSpottedWeight = -3 if any(cornerSpot in newOMoves for cornerSpot in corners) else 0

                #weight = cornerSpottedWeight

                categoryWeight = 0
                if category is veryGood:
                    categoryWeight = 2.8
                elif category is good:
                    categoryWeight = 1
                elif category is bad:
                    categoryWeight = -1
                else:
                    categoryWeight = -2

                move_evaluation_score = categoryWeight

                if move_evaluation_score > temp:
                    temp = move_evaluation_score
                    best = move

    if final is None:
        final = best

    print(f"My preferred move is: {final}")
    if negamaxList:
        print(f"Min score: {negamaxList[0]} Move sequence: {[*negamaxList[1:]]}")

    return final

def evalBoard(board, token, opposite, moves):
    #if (board, token) in EVAL_BOARD_CACHE:
        #return EVAL_BOARD_CACHE[(board, token)]

    opponentTokens = board.count(opposite)
    if opponentTokens ==0:
        return 100000000000000

    numMoves = len(moves)
    numOppMoves = len(getMoves(board, opposite, token)[0])
    myTokens= board.count(token)

    if numMoves == 0 or numOppMoves == 0:
        return (myTokens - opponentTokens) * 100000

    score = (numMoves - numOppMoves) * 9
    for i in corners:
        if board[i] == token:
            score += 100
        elif board[i] == opposite:
            score -= 100


        elif board[i] == '.':
            for j in horribleCorners:
                if board[j] == token:
                    score -= 50
                elif board[j] == opposite:
                    score += 50
            for t in badCorners:
                if board[t] == token:
                    score -= 30
                elif board[t] == opposite:
                    score += 30

    if myTokens < opponentTokens:
        score += -3 * (myTokens-opponentTokens)

    return score

def func():
    global board
    global token
    gameOver = main()
    if not gameOver:
        mypref = quickMove(board, token)
if __name__ == '__main__': func()
else:
    main()

#Brian Ho