-
Notifications
You must be signed in to change notification settings - Fork 0
/
board.py
413 lines (369 loc) · 15.6 KB
/
board.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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
"""
By Zufeng Wang
"""
BOARD_SIZE = 8
# board constants
BLACK = "@"
WHITE = "O"
TEAMS = (WHITE, BLACK)
EMPTY = "-"
CORNER = 'X'
# phases: 0: placing phase, 1: moving phase begin
# 2: after shrink 1, 3: after shrink 2
PLACING_PHASE = 0
MOVING_PHASE = 1
SHRINK1_PHASE = 2
SHRINK2_PHASE = 3
class Board(object):
def __init__(self):
# initialise empty board
self.board = []
for i in range(BOARD_SIZE):
self.board.append([None] * BOARD_SIZE)
self.pieces = [None] * 24
# start in placing phase
self.phase = PLACING_PHASE
# number of turns taken place since start of current game phase
self.turn_count = 0
def do_action(self, action, team):
"""
Updates the board with a team's action
:param action: action to make (same format as for referee)
:param team: team doing the action
:return:
"""
# TODO consider validation
if action is None: # forfeit turn. Turn count still increases
pass
# Placing phase: (x, y)
elif self.phase == PLACING_PHASE:
self.place_piece(action, team)
else:
# Move phase: ((a, b), (c, d))
start_pos, end_pos = action
self.move_piece(start_pos, end_pos)
self.turn_count += 1
# advance to next phase if enough moves have happened
if self.phase == PLACING_PHASE and self.turn_count == 24:
self.advance_phase()
elif self.phase == MOVING_PHASE and self.turn_count == 128:
self.advance_phase()
elif self.phase == SHRINK1_PHASE and self.turn_count == 192:
self.advance_phase()
def place_piece(self, position, team):
"""
Suitable for placing phase.
This is the only way to add a new piece to the board.
Doesn't check if move is valid
:param position: (c, r)
:param team: WHITE or BLACK
:return:
"""
c, r = position
# TODO: consider adding/removing validation of this move e.g. in bounds
assert self.in_bounds(position)
assert self.get_piece_at_pos(position) is None
new_piece = {
"pos": position,
"team": team
}
# find an empty position in the list to place the piece
new_piece_index = self.pieces.index(None)
self.pieces[new_piece_index] = new_piece
self.board[c][r] = new_piece_index # store the index on the board
# check if any pieces were taken
self.update_pieces_removed(position)
def move_piece(self, start_pos, end_pos):
"""
Moves the piece from start to end. For moving phase.
:param start_pos: starting position, which must have piece
:param end_pos: destination of this move
:return:
"""
# TODO: consider adding/removing validation of the positions and phase
# Some validation of move
assert self.phase != PLACING_PHASE # cannot move in placing phase
assert self.in_bounds(start_pos) and self.in_bounds(end_pos)
assert self.get_piece_at_pos(end_pos) is None # destination is empty
assert end_pos not in self.get_corners()
piece = self.get_piece_at_pos(start_pos)
assert piece is not None # make sure there is a piece to move
# TODO: consider adding validation of the move itself
piece['pos'] = end_pos # update position in pieces list
# update positions on board
self.board[end_pos[0]][end_pos[1]] = self.board[start_pos[0]][start_pos[1]]
self.board[start_pos[0]][start_pos[1]] = None
# update pieces removed due to this piece moving
self.update_pieces_removed(piece['pos'])
def get_all_actions(self, team):
"""
Returns a list of all possible moves for a team.
Form of actions returned depends on board's phase.
:param team: WHITE or BLACK
:return: list of placing positions (c, r), or moves ((a, b), (c, d))
"""
possible_moves = []
if self.phase == PLACING_PHASE:
# the valid places are all empty squares within team zone
# if team == WHITE
rows_start = 0
rows_end = 6
if team == BLACK:
rows_start = 2
rows_end = BOARD_SIZE
corners = self.get_corners()
for c in range(BOARD_SIZE):
for r in range(rows_start, rows_end):
if self.get_piece_at_pos((c, r)) is None \
and (c, r) not in corners:
possible_moves.append((c, r))
return possible_moves
# moving phase. Return all valid moves of the team's pieces
for piece in self.pieces:
if piece is not None and piece['team'] == team:
piece_moves = self.get_all_moves_for_piece_at_pos(piece['pos'])
# add this piece's moves to the list
possible_moves += piece_moves
return possible_moves
def get_all_moves_for_piece_at_pos(self, position):
"""
Returns a list of all moves that can be made by the piece at position
:param position: position of this piece
:return: list of possible moves: ((a, b), (c, d))
"""
possible_moves = []
# represents four cardinal directions that each piece can move in
offsets = ((1, 0), (0, 1), (-1, 0), (0, -1))
corners = self.get_corners()
for c_off, r_off in offsets:
adj_pos = (position[0] + c_off, position[1] + r_off)
if self.in_bounds(adj_pos):
piece_index = self.board[adj_pos[0]][adj_pos[1]]
if piece_index is None:
if adj_pos not in corners:
# position has no pieces or corners. Can move here
possible_moves.append((position, adj_pos))
else:
# there is a piece here. Can we jump over?
opp_pos = (adj_pos[0] + c_off, adj_pos[1] + r_off)
if self.pos_empty(opp_pos):
possible_moves.append((position, opp_pos))
return possible_moves
def advance_phase(self):
"""advance the phase of this board"""
if self.phase == PLACING_PHASE:
# turn count doesn't reset if board shrinks
self.turn_count = 0
self.phase += 1
corners = self.get_corners() # new corners
# Remove pieces outside the new bounds or overlapping with new corners
for index, piece in enumerate(self.pieces):
if piece is not None:
if not self.in_bounds(piece['pos']) or piece['pos'] in corners:
self.remove_piece_at_pos(piece['pos'])
# apply corners and remove more pieces if needed
# top-left corner
col, row = corners[0]
self.update_pieces_removed((col + 1, row), attack=False)
self.update_pieces_removed((col, row + 1), attack=False)
# bottom-left corner L
col, row = corners[1]
self.update_pieces_removed((col + 1, row), attack=False)
self.update_pieces_removed((col, row - 1), attack=False)
# bottom-right corner _|
col, row = corners[2]
self.update_pieces_removed((col - 1, row), attack=False)
self.update_pieces_removed((col, row - 1), attack=False)
# top-right corner -.
col, row = corners[3]
self.update_pieces_removed((col - 1, row), attack=False)
self.update_pieces_removed((col, row + 1), attack=False)
def get_piece_at_pos(self, position):
"""
Returns the piece at a position (c, r),
or None if out of bounds or no piece.
First checks if position is within bounds
:param position: tuple (column, row)
:return: piece, or None if there isn't one or out of bounds
"""
# also check if position is in bounds
if self.in_bounds(position):
index = self.board[position[0]][position[1]]
if index is not None:
return self.pieces[index]
return None
def has_piece_of_team(self, position, team):
"""
:return: whether or not the square contains a piece
belonging to team
"""
piece = self.get_piece_at_pos(position)
if piece:
return piece['team'] == team
return False
def pos_empty(self, pos):
"""
Returns whether or not position is in bounds and free of pieces
and corners. (i.e. available for other pieces to move there)
:param pos: position on board
:return: whether or not position is in bounds
and free of pieces and corners
"""
return self.in_bounds(pos) and self.board[pos[0]][pos[1]] is None \
and pos not in self.get_corners()
def remove_piece_at_pos(self, position):
""" removes a piece from the game """
index = self.board[position[0]][position[1]]
# delete the piece from the board and the piece list
self.board[position[0]][position[1]] = None
# results in an empty spot in the piece list
self.pieces[index] = None
def in_bounds(self, position):
""":return: whether or not a position is on the board. Depends on phase
"""
# but in-bounds also depends on the game state
if self.phase == PLACING_PHASE or self.phase == MOVING_PHASE:
return 0 <= position[0] < BOARD_SIZE \
and 0 <= position[1] < BOARD_SIZE
elif self.phase == SHRINK1_PHASE:
return 1 <= position[0] < BOARD_SIZE - 1 \
and 1 <= position[1] < BOARD_SIZE - 1
else:
return 2 <= position[0] < BOARD_SIZE - 2 \
and 2 <= position[1] < BOARD_SIZE - 2
def update_pieces_removed(self, mid_pos, attack=True):
"""
Removes pieces from the board due to a new piece entering this position
If attack=False, then the piece here (if there is one)
is not the aggressor.
If there is no piece in mid_pos, nothing happens.
:param mid_pos: position of this piece (c, r)
:param attack: whether or not this piece moved here itself
(if not, it's just defending against corners shrinking)
:return:
"""
# TODO fix this
mid_piece = self.get_piece_at_pos(mid_pos)
if mid_piece is None:
return # no piece in the middle, so nothing to remove
mid_team = mid_piece['team']
enemy_team = Board.get_opponent_team(mid_team)
c_mid, r_mid = mid_pos
# check if any 4 surrounding squares have an opponent to take
if attack:
offsets = (-1, 0), (0, 1), (1, 0), (0, -1)
for c_off, r_off in offsets:
adj_pos = (c_mid + c_off, r_mid + r_off)
if self.has_piece_of_team(adj_pos, enemy_team):
# enemy piece here, check other side
opp_pos = (adj_pos[0] + c_off, adj_pos[1] + r_off)
if self.has_piece_of_team(opp_pos, mid_team):
# adj piece is surrounded. Remove it
self.remove_piece_at_pos(adj_pos)
elif opp_pos in self.get_corners():
# also gets removed if corner...
self.remove_piece_at_pos(adj_pos)
# Check if the mid piece is itself removed by opponents (and corners)
# either by up/down or L/R
u_pos = (c_mid, r_mid - 1)
d_pos = (c_mid, r_mid + 1)
l_pos = (c_mid - 1, r_mid)
r_pos = (c_mid + 1, r_mid)
corners = self.get_corners()
if ((self.has_piece_of_team(u_pos, enemy_team) or u_pos in corners) and
(self.has_piece_of_team(d_pos, enemy_team) or d_pos in corners)) or\
((self.has_piece_of_team(l_pos, enemy_team) or l_pos in corners) and
(self.has_piece_of_team(r_pos, enemy_team) or r_pos in corners)):
self.remove_piece_at_pos(mid_pos)
def distance_to_nearest_of_team(self, pos, team):
""" returns distance to nearest piece belonging to team
:param pos: (c, r)
:param team: team of piece search for
:return: Manhattan distance between pos and piece
"""
best_dist = 999
for piece in self.pieces:
if piece is not None and piece['team'] == team:
dist = abs(pos[0] - piece['pos'][0]) \
+ abs(pos[1] - piece['pos'][1])
best_dist = min(dist, best_dist)
if best_dist == 999:
return -1 # did not find any pieces
return best_dist
def get_piece_nearest_to_pos_of_team(self, pos, team):
""" returns piece nearest to pos and belonging to team
:param pos: (c, r)
:param team: team of piece search for
:return: piece object, or None if couldn't find
"""
best_dist = 999
nearest_piece = None
for piece in self.pieces:
if piece is not None and piece['team'] == team:
dist = abs(pos[0] - piece['pos'][0]) \
+ abs(pos[1] - piece['pos'][1])
if dist < best_dist:
best_dist = dist
nearest_piece = piece
return nearest_piece
def get_corners(self):
"""
returns the positions of the current corners of the board
in CCW order starting from top-left
:return: list of positions of corners
"""
if self.phase == PLACING_PHASE or self.phase == MOVING_PHASE:
return (0, 0), (0, 7), (7, 7), (7, 0)
elif self.phase == SHRINK1_PHASE:
return (1, 1), (1, 6), (6, 6), (6, 1)
else: # self.phase == SHRINK2_PHASE:
return (2, 2), (2, 5), (5, 5), (5, 2)
def check_winner(self):
"""
Checks if game is over and returns winner
T for tie. None for game not yet finished
:return: BLACK or WHITE or 'T' or None
"""
black_count = 0
white_count = 0
for piece in self.pieces:
if piece is not None:
if piece['team'] == WHITE:
white_count += 1
elif piece['team'] == BLACK:
black_count += 1
else:
raise ValueError("team should be BLACK or WHITE")
if black_count < 2 and white_count < 2:
# tie because both teams have fewer than 2 pieces on same turn
return 'T'
elif black_count < 2:
return WHITE
elif white_count < 2:
return BLACK
return None
@staticmethod
def get_opponent_team(team):
""" given a team (WHITE or BLACK), returns the opposing team"""
if team == WHITE:
return BLACK
elif team == BLACK:
return WHITE
raise ValueError("team should either be BLACK or WHITE")
def print_board(self):
""" prints the current layout of the board """
corners = self.get_corners()
print('=== BOARD: phase {}, turn {} ==='
.format(self.phase, self.turn_count))
print('0 1 2 3 4 5 6 7')
for row in range(BOARD_SIZE):
for col in range(BOARD_SIZE):
char_to_print = ' '
piece_index = self.board[col][row]
if piece_index is not None:
char_to_print = self.pieces[piece_index]['team']
elif (col, row) in corners:
char_to_print = CORNER
print(char_to_print, end=' ')
print("")
# TODO def tostring