-
Notifications
You must be signed in to change notification settings - Fork 0
/
OthelloAI.py
executable file
·181 lines (150 loc) · 6.82 KB
/
OthelloAI.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""Othello AI implementation"""
from Othello import *
INF = float("inf")
class OthelloAI(Othello):
def __init__(self, humanFirst=True):
super().__init__()
self.COMP = WHITE if humanFirst else BLANK
self.HUMAN = BLACK if humanFirst else WHITE
def evaluateBoard(self, board, player):
"""
Heuristic that weights corner tiles and edge tiles as positive
adjacent to corners (if the corner is not yours) as negative
Weights other tiles as one point
"""
score = 0
# easier to track value changes rather than going to specific parts of code
NORMAL = 1 # average square
EDGE = 5 # good square
CORNER_EDGES = -10 # depends on the situation
CORNER = 25 # most precious square
PASS_PENALTY = 50 # if no moves available (subtracted from score)
# Go through all the tiles
for x in range(self.spanX):
for y in range(self.spanY):
value = NORMAL
# check if tiles present adjacent to every corner, three each
tiles_near_corner = (
(x == 0 and y == 1) or (x == 1 and 0 <= y <= 1), # TL corner
(x == 0 and y == 6) or (x == 1 and 6 <= y <= 7), # TR corner
(x == 7 and y == 1) or (x == 6 and 0 <= y <= 1), # BL corner
(x == 7 and y == 6) or (x == 6 and 6 <= y <= 7)) # BR corner
corners = (board[0][0], board[7][0], board[0][7], board[7][7])
# if corner belongs to us then no use placing tiles there
for corner, ce_tiles in zip(corners, tiles_near_corner):
if ce_tiles is False: # no corner edge tile
continue
if corner == player:
value = CORNER_EDGES
else:
value = -CORNER_EDGES
# evaluate corners and edges (not including corner)
if any((x == 0 and 1 < y < 6,
x == 7 and 1 < y < 6,
y == 0 and 1 < x < 6,
y == 7 and 1 < x < 6)):
value = EDGE
elif any((x == 0 and y == 0,
x == 0 and y == 7,
x == 7 and y == 0,
x == 7 and y == 7)):
value = CORNER
# add if location belongs to player
if board[x][y] == player:
score += value
# subtract if location belongs to opponent
elif board[x][y] == -player:
score -= value
# reward the AI for keeping higher score
# otherwise punish it
Bpoints, Wpoints = self.scoreboard(board)
if player == BLACK:
if Bpoints > Wpoints:
score += min(10, Bpoints - Wpoints)
else:
score -= max(5, Bpoints - Wpoints)
if player == WHITE:
if Bpoints < Wpoints:
score += min(10, Wpoints - Bpoints)
else:
score -= max(5, Wpoints - Bpoints)
# penalize the AI if no moves are available
available_moves = len(self.getValidMoves(board, player))
if available_moves == 0:
score -= PASS_PENALTY
else:
score += min(5, available_moves)
return score
# MiniMax Algo with Alpha-Beta pruning
def minimax(self, state, depth, player, alpha=-INF, beta=INF):
"""
create a game tree of given depth
alpha is the losing factor in the best case
beta is the winning factor in the worst case
try to keep alpha as low as possible and beta as high
as soon as beta <= alpha, prune that branch
"""
if depth == 0 or self.isGameOver(state):
# as game is over, move can't be provided in base case
return (None, None, self.evaluateBoard(state, player))
# 'best' is a list of move and score (best ones returned)
if player == self.COMP:
# set worst score for Computer to improve on :: -inf
best = (None, None, -INF)
else:
# set the best score assuming player is pro :: +inf
best = (None, None, INF)
# TODO: the game tree is re-evaluated after every 'real' move
# we can memoize that info to speed up the process and increase the
# search depth, picking up from where we left off
# TODO: the branches of other potential moves must be pruned
for cell in self.getValidMoves(state):
# extract coordinates
x, y = cell
# temporarily set the (x,y) position as player's number (-1 / +1)
state[x][y] = player
stateVal = self.minimax(state, depth-1, -player, alpha, beta)[2]
# return board to original state
state[x][y] = BLANK
if player == self.COMP:
if stateVal > best[2]:
# computer is improving, update the best move and score
best = *cell, stateVal
# update losing-factor alpha
alpha = max(alpha, best[2])
else:
# update to the best move and score human may make
# prepares the computer for the worst
# and at best hope for draw
if stateVal < best[2]:
best = *cell, stateVal
# update winning-factor beta
beta = min(beta, best[2])
# if winning is not possible, then skip that branch
if beta <= alpha:
break
return best
def aiPlay(self, ai_power=8):
x, y, _ = self.minimax(self.board, depth=ai_power, player=self.COMP)
self.checkValidAndMove(x, y)
return x, y
def getStatString(self, board, player):
"""add the win_factor to the status"""
score_p1, score_p2 = self.scoreboard()
score_p1, score_p2 = str(score_p1), str(score_p2)
# as the players are switched as soon as move is made
# 1 is WHITE and -1 is BLACK instead... hence the -ve sign
# '*' indicates current player
p1_stat = " *W: " if player == -WHITE else " W: "
p2_stat = "*B: " if player == -BLACK else "B: "
B_win = self.evaluateBoard(board, BLACK)
W_win = -self.evaluateBoard(board, WHITE)
try:
win_factor = round(
(B_win + W_win) / (math.fabs(B_win) + math.fabs(W_win)),
ndigits=2)
except ZeroDivisionError:
win_factor = 0.
win_factor = str(win_factor). \
center(4*self.spanY - 4 - len(score_p1 + score_p2), ' ')
return p1_stat + score_p1 + win_factor + p2_stat + score_p2 + "\n\n"