-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplayer.py
More file actions
287 lines (246 loc) · 9.62 KB
/
player.py
File metadata and controls
287 lines (246 loc) · 9.62 KB
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import random
import threading
import sys
import time
import bao
import baoAgent
MAX_INT = sys.maxsize
MIN_INT = -(MAX_INT - 1)
# Player types:
PL_HUMAN = 0
PL_MACHINE = 1
class Player(baoAgent.BaoAgent):
def __init__(self):
self.id = 'Player'
self.type = PL_HUMAN
def get_id(self):
return self.id
def set_id(self, id_):
self.id = id_
def get_type(self):
return self.type
def set_type(self, type_):
self.type = type_
def set_arbiter(self, arbiter):
'''Set arbiter which must be a BaoAgent.'''
pass
class Malume(Player):
def __init__(self):
Player.__init__(self)
self.game_history = None
self.ponder_thread = None
self._stop_ponder = False
self.vars = {
'ponder_depth': 6,
'stop_ponder': 0,
'verbose': 1
}
self.type = PL_MACHINE
self.id = 'Malume'
self.arbiter = None
def set_arbiter(self, arbiter):
self.arbiter = arbiter
def new_game(self, field='ab', **kwargs):
if self.ponder_thread:
self._stop_ponder = True
self.ponder_thread.join()
self.ponder_thread = None
self.game_history = bao.new_game(**kwargs)
self.field = field
def move(self, mv):
if self.game_history.get_player() == self.field:
self.arbiter.ack(baoAgent.RQ_MOVE,
baoAgent.STS_REJECT,
'Not your turn')
return None
try:
self.game_history.branch(mv)
self.arbiter.ack(baoAgent.RQ_MOVE, baoAgent.STS_ACCEPT, None)
except bao.InvalidMoveError:
self.arbiter.ack(baoAgent.RQ_MOVE, baoAgent.STS_REJECT,
'Invalid move')
except bao.LongMoveError:
self.arbiter.ack(baoAgent.RQ_MOVE, baoAgent.STS_REJECT,
'Invalid move: Long move')
if self.game_history.get_player() == self.field:
self.make_move()
def stop_ponder(self):
if self.ponder_thread:
self._stop_ponder = True
self.ponder_thread.join()
self.ponder_thread = None
return True
else:
return False
def message(self, m, from_=None):
vcmd = m.split(None, 1)
if len(vcmd) > 1:
cmd = vcmd[0]
argv = vcmd[1].strip().split()
else:
cmd = vcmd[0]
argv = []
if cmd == 'set':
if len(argv) != 2:
self.arbiter.message(
'Error: Invalid command ({})\n'.format(m)
+ 'do \'set variable flag/value\'')
return None
if argv[0] in self.vars:
if argv[0] == 'ponder_depth' and self.ponder_thread:
self.arbiter.message('Can\'t change that while pondering.')
return None
self.vars[argv[0]] = int(argv[1])
self.arbiter.message('set ok')
else:
self.arbiter.message('Error: var ({}) not known'.format(argv[0]))
elif cmd == 'stop' or cmd == 'hault':
self.stop_ponder()
self.arbiter.message('Pondering haulted, will restart on go')
elif cmd == 'go':
if self.game_history.get_player() == self.field:
self.arbiter.message('Wait for it...')
self.make_move()
elif self.ponder_thread:
self.arbiter.message('Already pondering...')
else:
self.arbiter.message('Not my turn yet')
elif cmd == 'vars':
response = ''
for key, value in list(self.vars.items()):
response += key + ': ' + str(value) + '\n'
self.arbiter.message(response)
else:
self.arbiter.message('Invalid command.')
pass
def forfeit(self):
self._stop_ponder = True
def undo(self):
print("Malume undo!")
self.game_history.pop_node()
self.arbiter.ack(baoAgent.RQ_UNDO, baoAgent.STS_ACCEPT, None)
self.arbiter.message('Undo OK, say \'go\' for me to make my move')
# private:
def make_move(self):
def move_listener(move):
if move:
self.arbiter.message('Moving ' + move)
self.game_history.branch(move, exec_move=False)
self.arbiter.move(move)
if self.game_history.get_player() == self.field:
self.make_move()
else:
self.arbiter.message('I give up!')
self.arbiter.forfeit()
self.arbiter.message('Pondering... Please Wait.')
self.ponder(move_listener)
def ponder(self, callback, threaded=True):
"""Start pondering on next move if possible."""
if threaded:
self.ponder_thread = threading.Thread(target=self.ponder,
args=(callback, False))
self.ponder_thread.start()
else:
self.nodes_pondered = 0
self._stop_ponder = False
state = self.game_history.get_current_node()[1]
start_time = time.time()
move, score = self.get_best_move(state,
state.get_player(),
self.vars['ponder_depth'])
ponder_time = time.time() - start_time
if self.vars['verbose']:
self.arbiter.message(
'Ponder done: searched {} nodes in {} seconds'.format(
self.nodes_pondered, ponder_time)
)
callback(move)
self.ponder_thread = None
def get_best_move(self, state, max_player, depth, alpha=MIN_INT, beta=MAX_INT):
# Using negamax ponder
moves = [m for m in state.get_moves() if m is not None]
if len(moves) == 1:
return moves[0], self.eval_state(state, max_player, depth)
elif depth == 0 or not moves:
return None, self.eval_state(state, max_player, depth)
best_moves = []
best_score = MIN_INT
for move in moves:
child = state.get_child(move)
if child == None:
continue # long move...
if self._stop_ponder:
raise SystemExit()
hole, direction, mod = move.split_move(move)
if (state.in_namua_phase()
and state.is_mtaji()
and (hole[1] in '12' and direction == 'L')
or (hole[1] in '78' and direction == 'R')):
# redundant moves (similar to those going in opposite direction)
continue
if state.get_player() == child.get_player():
# score does not change sign if player does not change
# (min-max requires sign change), hence search 1 more ply
# down to get moves with similar signs only
_, score = self.get_best_move(child.copy(),
max_player,
depth,
-beta,
-max(alpha, best_score))
else:
_, score = self.get_best_move(child.copy(),
max_player,
depth - 1,
-beta,
-max(alpha, best_score))
score = -score
self.nodes_pondered += 1
if score > best_score:
best_score = score
best_moves = [move]
if best_score >= beta:
break
elif score == best_score:
best_moves.append(move)
# if depth == self.vars['ponder_depth']:
# if self.vars['verbose']:
# self.arbiter.message(
# '{} score {} and current {} scores {}'.format(
# best_moves, best_score, move, score))
if depth == self.vars['ponder_depth']:
print(score, move)
if depth == self.vars['ponder_depth']:
print(best_score, best_moves)
print("---------------------------------------------\n")
if best_moves:
return random.choice(best_moves), best_score
else:
return None, 0
def eval_state(self, state, max_player, depth):
if state.is_game_over():
return MAX_INT if state.get_player() == max_player else 0
score = 0.0
board = state.get_board()
for hole in board.holes():
v = board[hole]
if board.hole_in_field(hole, state.get_player()): # Hole belongs to current player
if hole[0] in 'Aa':
score += 0.5 * v # Avoid holding large values in front holes
else:
score += v
elif state.in_namua_phase() and v == 0 and hole[0] in 'Aa':
score += 1.5
elif state.in_mtaji_phase():
if hole[0] in 'Aa':
if v == 0:
score += 2
else:
score += 1
elif v < 2:
score += 1
if state.is_mtaji():
score *= 1.5
if state.get_player() == max_player:
return int(score)
else:
return -int(score)