-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathplayer.py
370 lines (282 loc) · 11.7 KB
/
player.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
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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
"""
This module deals with the logical representation of a Board, Player and Coins.
"""
from config import PLAYER_COLORS
from config import log
class Board(object):
"""Logical representation of a board."""
# There is no __init__ because we'll never create a board
# but only use methods & variables defined on it.
# Starting squares & Star squares
safe_squares = [1, 9, 14, 22, 27, 35, 40, 48]
home_column = list(range(52, 58))
@staticmethod
def is_safe(rel_pos):
"""Checks whether a given relative position is safe or not?"""
if rel_pos in Board.safe_squares + Board.home_column + [0]:
return True
return False
class Player(object):
"""Represent a player."""
def __init__(self, color):
# Player's Color: Red, Green, Blue, Yellow
self.color = color
self.number = PLAYER_COLORS.index(color)
# Each Player has 4 Coins
self.coins = {}
for idx in range(0, 4):
coin = Coin(color, idx)
self.coins[str(coin)] = coin
@property
def percent_complete(self):
"""How much game have I completed?"""
return sum([25 * (c.rel_pos / 57) for c in self.coins.values()])
@property
def in_jail(self):
"""Which coins are still in the Jail?"""
return [c for c in self.coins.values() if c.rel_pos == 0]
@property
def finished_coins(self):
"""Which coins have reached finishing square?"""
return [c for c in self.coins.values() if c.rel_pos == 57]
@property
def on_home_col(self):
"""Which coins are on home column?"""
return [c for c in self.coins.values() if 52 <= c.rel_pos <= 56]
def movable_coins(self, die):
"""Coins which can move on a die roll."""
return [
coin for coin in self.coins.values()
if (
coin not in self.finished_coins and # not finished
coin not in self.in_jail and # not in jail
coin.rel_pos <= 57 - die # move is allowed
)
]
def non_stacking_moves(self, die):
"""Returns a list of movable coins that wont cause stacking"""
# Remove coins which if moved will cause stacking
rel_pos_of_my_coins = [
coin.rel_pos
for coin in self.coins.values()
]
# log.info("Movable: %s", self.movable_coins(die))
movable_coins = [
coin
for coin in self.movable_coins(die)
# either this coin moves to a safe square
# (where stacking is allowed)
if Board.is_safe(coin.rel_pos + die) or
# or it does not cause stacking
(coin.rel_pos + die) not in rel_pos_of_my_coins
]
return movable_coins
def can_finish(self, die):
"""Coins which can finish on a die roll."""
return [coin for name, coin in self.coins.items() if coin.rel_pos + die == 57]
def in_danger(self, opponent):
"""Coins which can get_killed in the next die roll"""
in_danger = [coin for coin in self.coins.values() if self.threat(coin.rel_pos, opponent) > 0]
# sorted in increasing order of relative position
return sorted(in_danger, key = lambda coin: coin.rel_pos)
def threat(self, relpos, opponent):
"""Returns threat at a relpos"""
if relpos > 57:
relpos = 57
# if this position is safe then no threat
if Board.is_safe(relpos):
return 0
abs_pos = list(self.coins.values())[0].rel_to_abs(relpos)
threat = 0
for coin in opponent.coins.values():
if (coin.rel_pos >= 1 and # not in yard
coin.rel_pos <= 51 and # not in home column or finished
coin.rel_to_abs(coin.rel_pos + 6) >= abs_pos): # and can reach abs_pos in one die roll
threat += 1
return threat
def can_kill(self, die, opponent):
"""Who can i kill with this die roll
Returns a list of tuple : (killer_coin, target_coin)
"""
# My coins that can kill
killers = [
# Coin and the Position it will move to
(coin, coin.rel_to_abs(coin.rel_pos + die))
for coin in self.movable_coins(die)
if (
coin not in self.on_home_col and # not on home column
not Board.is_safe(coin.rel_pos + die) # does not land on a safe square
)
]
possible_kills = []
for killer, kill_spot in killers:
for target in opponent.coins.values():
if kill_spot == target.abs_pos:
possible_kills.append((killer, target))
# Sort the possible kills in ascending order of rel_pos of targets
return sorted(possible_kills, key=lambda target: target[1].rel_pos)
def make_moves(self, moves, opponent):
"""
Make a coin move.
Takes in a list of move strings of the form: "<Coin ID>_<Die Roll>"
eg: "R0_1" will move Coin 0 of Player Red 1 position ahead.
Since these moves will be read from the client,
they are assumed to be valid.
"""
# No move to play
if "NA" in moves:
return
for move in moves:
log.debug("Making Move: %s" % move)
move_coin_name, die = move.split('_')
coin_to_move = self.coins[move_coin_name]
# Even if you open with 6, you still move 1 step
if coin_to_move.rel_pos == 0 and die == '6':
die = '1'
# Move my coin
coin_to_move += int(die)
# If my coin killed someone then place them back in their yards
for coin in opponent.coins.values():
if (coin.abs_pos == coin_to_move.abs_pos and
not Board.is_safe(coin.rel_pos)):
coin.rel_pos = 0
def get_multiple_moves(self, die_rolls, opponent):
"""
Extends get_move to a list of die rolls.
"""
all_moves = []
# Play each roll consecutively
total_benefit = 0
for die in die_rolls:
# Apply strategies to find what next move should be
move, benefit = self.get_move(die, opponent)
# If moves are possible
if move:
# Convert them to a representation that others understand
move = "%s_%d" % (move[0], move[1])
# Perform them on the board
# So that next decision is based on updated board state
self.make_moves([move], opponent)
all_moves.append(move)
total_benefit += benefit
return all_moves, total_benefit
def get_move(self, die, opponent):
"""
Use positions of other players to make a move.
This only works for a single die roll and is extended by get_multiple_moves.
Returns a list of tuples: [(coin, die_roll), ...]
"""
# Find all possible kills I can make using this die
possible_kills = self.can_kill(die, opponent)
# Find all possible coins that can finish using this die
coin_to_finish = self.can_finish(die)
# Find all non stacking movable_coins
movable_coins = self.non_stacking_moves(die)
# Find all my coins that can get killed
can_get_killed = self.in_danger(opponent)
can_get_killed = [coin for coin in can_get_killed
if coin in movable_coins]
# Move coin that can finish
if coin_to_finish:
log.info("Finishing Move: %s", coin_to_finish[0])
# move any coin that can finish
return (coin_to_finish[0], die), 20
# Open
elif (die in [1, 6]) and self.in_jail:
log.info("Opening Move: %s", self.in_jail[0])
# Open the lowest coin from jail
return (self.in_jail[0], die), 15
# Kill
elif possible_kills:
# Kill opponent's farthest possible coin
log.info("Killing Move: %s -> %s", possible_kills[-1][0], possible_kills[-1][1])
return (possible_kills[-1][0], die), 14
# if in danger save that coin
elif can_get_killed:
# save the farthest coin in danger
log.info("Defensive move, Saving %s", can_get_killed[-1])
return (can_get_killed[-1], die), 10
# Modified Fast
else:
# log.info("Movable: %s", movable_coins)
# Choose the coin that has moved farthest
if movable_coins:
movable_coins.sort(key=lambda c: c.rel_pos)
# if at least two coins movable
if (len(movable_coins) > 1 and
# threat at future pos of second farthest coin is less than
# the threat at future pos of the farthest coin
self.threat(movable_coins[-2].rel_pos + die, opponent) <
self.threat(movable_coins[-1].rel_pos + die, opponent)):
log.info("Fast Move : %s" % movable_coins[-2])
return (movable_coins[-2], die), 9 # move second farthest coin
log.info("Fast Move : %s" % movable_coins[-1])
return (movable_coins[-1], die), 8
# No move possible
log.info("No Move Possible")
return None, 0
class Coin(object):
"""Represent a player's coin piece."""
def __init__(self, color, idx):
# The (color, num) pair can uniquely identify a coin
self.color = color
self.num = idx
# Position of the coin on the board
self._rel_pos = 0
self.abs_pos = 0
def __str__(self):
return self.color[0] + str(self.num)
def __repr__(self):
return "<Coin: %s>" % self.__str__()
def __iadd__(self, die):
self.rel_pos += die
return self
def rel_to_abs(self, rel_pos):
"""
Convert relative position to absolute position.
The formula is based on the color (player number) of this coin.
This is used in functions that need to check where two coins
(of different colors) are with respect to one another.
"""
for idx, color in enumerate(PLAYER_COLORS):
if color[0] == self.color[0]:
mycolor_index = idx
if rel_pos == 0: # Inside yard
abs_pos = 0
elif rel_pos >= 52: # Inside home column
abs_pos = -1
# -1 can be used directly to check whether a coin is
# inside home column or not
else:
abs_pos = (rel_pos - 1 + 13 * mycolor_index) % 52 + 1
# Subtract 1 to make it 0 based; add it back to make 1 based again
return abs_pos
@property
def rel_pos(self):
"""
Position of this coin relative to the a player.
Is in range(0, 58)
Some functions are simpler when using the relative position.
While some need an absolute position which is calculated later.
"""
return self._rel_pos
@rel_pos.setter
def rel_pos(self, square):
# if in finishing square, then stay in finishing square
self._rel_pos = square
# Update absolute position
self.abs_pos = self.rel_to_abs(self._rel_pos)
def main():
p1 = Player("RED")
p2 = Player("GREEN")
p1.coins["R1"].rel_pos = 15
p1.coins["R2"].rel_pos = 23
p1.coins["R3"].rel_pos = 38
# print(p1.get_move([3], [p2]))
p2.coins["G1"].rel_pos = 8
p2.coins["G2"].rel_pos = 1
p2.coins["G3"].rel_pos = 23
print(p1.in_danger(p2))
# print(p1.threat(0,p2))
if __name__ == '__main__':
main()