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