-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgame.py
243 lines (213 loc) · 8.57 KB
/
game.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
from __future__ import annotations
from enum import Enum, Flag, auto
from itertools import product
def get_moves_from(color, start_pos, state_obj):
"""Return the possible moves from a given starting piece and board state"""
moves = []
for piece, positions, skips in state_obj.find_valid_moves(color):
curr_start, curr_end, *_ = positions
captures = len(skips)
if captures:
skips = (skips[0],)
if curr_start == start_pos:
moves.append((piece, (curr_start, curr_end), skips, captures))
return moves
def make_move(move, state):
"""Return the new board state from the given move and board state"""
piece, positions, skips = move
start_pos, *_, end_pos = positions
remove = set([start_pos, end_pos])
removed = tuple((pos, val) for pos, val in state if pos not in remove)
removed_opp = tuple(
(pos, Piece.EMPTY) if pos in skips else (pos, val) for pos, val in removed
)
if Piece.KING not in piece:
piece = handle_promotion(end_pos.imag, piece)
new_state = *removed_opp, (end_pos, piece), (start_pos, Piece.EMPTY)
return new_state
def handle_promotion(row, piece):
"""
Promote a piece to king if it lands on the row opposite from the pieces starting side
"""
promotion_row = 7 if piece == Piece.WHITE else 0
if promotion_row == row:
return Piece.BLACK_KING if piece == Piece.BLACK else Piece.WHITE_KING
return piece
def init_state():
"""Return the initial board state for a game of checkers"""
state = []
for x_coord, y_coord in product(range(8), range(8)):
pos = complex(x_coord, y_coord)
if (x_coord + y_coord) % 2 == 0: # Invalid squares
continue
elif y_coord in (0, 1, 2):
state.append((pos, Piece.WHITE))
elif y_coord in (3, 4):
state.append((pos, Piece.EMPTY))
else:
state.append((pos, Piece.BLACK))
return tuple(state)
def init_borders():
"""Return a set of border coordinates for a checkers board"""
borders = set()
borders_nums = [-1, 8]
for num1 in borders_nums:
for num2 in range(0, 7):
borders.add(complex(num1, num2))
borders.add(complex(num2, num1))
return borders
class Board:
"""
Represents a checker board with a board state, borders
contains methods for finding all valid moves for the
current board state
"""
def __init__(self, board_state, borders):
self._board_state = board_state
self._borders = borders
@property
def board_state(self):
return self._board_state
@board_state.setter
def board_state(self, new_state):
self._board_state = new_state
def find_valid_moves(self, color):
"""
Returns a list of valid moves for the given piece color and
the current board state
"""
moves = set()
for pos, piece in self._board_state:
if color in piece:
captures = 0
skips = ()
positions = (pos,)
move = (piece, positions, skips, captures)
self.next_move(move, moves)
valid_moves = self.prune_moves(moves)
return valid_moves
def next_move(self, move, moves, prev_pos=None):
"""
Finds the next possible squares for a given move and calls
the build path method for that next square
"""
piece, positions, skips, captures = move
*_, curr_pos = positions
forward_left = Direction.FORWARD_LEFT.move(curr_pos, piece)
forward_right = Direction.FORWARD_RIGHT.move(curr_pos, piece)
next_positions = [forward_left, forward_right]
if Piece.KING in piece:
back_left = Direction.BACK_LEFT.move(curr_pos, piece)
back_right = Direction.BACK_RIGHT.move(curr_pos, piece)
next_positions.extend([back_left, back_right])
for next_pos in next_positions:
if next_pos == prev_pos:
continue
next_move = (piece, (*positions, next_pos), skips, captures)
self.build_path(next_move, moves)
def build_path(self, move, moves):
"""Either discard the move if it's invalid, add the move to the moves list
if it's valid, or call the next move method if current path includes a
valid jump.
cases for valid paths:
It's the first move (not capture) and the next space is empty.
Or anytime after a capture move where the next space is not an
opponent piece. If the next space is an opponents piece trigger
a recursive call if the space the piece would jump to is empty.
"""
piece, positions, skips, captures = move
*rest, curr_pos, next_pos = positions
next_val = self.search_state(next_pos)
next_val_is_empty = next_val == Piece.EMPTY
opp_color = Piece.BLACK if Piece.WHITE in piece else Piece.WHITE
color = Piece.BLACK if Piece.BLACK in piece else Piece.WHITE
if not captures and next_val_is_empty:
moves.add((piece, (*rest, curr_pos, next_pos), skips, captures))
elif captures and next_val is None:
moves.add((piece, (*rest, curr_pos), skips, captures))
elif captures and (color in next_val or next_val_is_empty):
moves.add((piece, (*rest, curr_pos), skips, captures))
elif isinstance(next_val, Piece) and opp_color in next_val:
move = next_pos - curr_pos
jump = next_pos + move
jump_val = self.search_state(jump)
if jump_val == Piece.EMPTY:
positions = (*rest, curr_pos, jump)
skips = (*skips, next_pos)
jmp_move = (piece, positions, skips, captures + 1)
prev = next_pos
self.next_move(jmp_move, moves, prev)
elif captures:
moves.add((piece, (*rest, curr_pos), skips, captures))
def search_state(self, next_pos):
"""Return the val in the given position in the current board state"""
for pos, val in self._board_state:
if pos == next_pos:
next_val = val
break
else:
next_val = None
return next_val
def prune_moves(self, moves):
"""
Removes all invalid moves from the given moves list. A path is invalid if
there is another path in the list with more captures
"""
if moves:
max_captures = max(captures for _, _, _, captures in moves)
moves = set(
(piece, positions, skips)
for piece, positions, skips, captures in moves
if captures == max_captures
)
return moves
def __str__(self):
"""Returns the string version of the checker board for debugging"""
board_array = [[None] * 8 for _ in range(8)]
for pos, piece in self._board_state:
if piece != Piece.EMPTY:
x_coord, y_coord = int(pos.real), int(pos.imag)
board_array[y_coord][x_coord] = piece
board_str = []
for num in range(8):
board_str.append(f" {num}")
board_str.append("\n")
for idx, row in enumerate(board_array):
board_str.append(f"{idx} ")
for val in row:
if val == Piece.BLACK:
board_str.append("| b")
elif val == Piece.WHITE:
board_str.append("| w")
elif val == Piece.BLACK_KING:
board_str.append("|bk")
elif val == Piece.WHITE_KING:
board_str.append("|wk")
else:
board_str.append("| ")
board_str.append("|\n")
return "".join(board_str)
class Piece(Flag):
"""Represents a checker piece"""
EMPTY = auto()
WHITE = auto()
BLACK = auto()
KING = auto()
WHITE_KING = WHITE | KING
BLACK_KING = BLACK | KING
class Direction(Enum):
"""
Represents all the directions a checker piece can move
The 0 index of the direction tuples represents black piece
moves and the 1 index represents white piece moves
"""
FORWARD_LEFT = (-1 - 1j, 1 + 1j)
FORWARD_RIGHT = (1 - 1j, -1 + 1j)
BACK_LEFT = (-1 + 1j, 1 - 1j)
BACK_RIGHT = (1 + 1j, -1 - 1j)
def move(self, pos, piece):
"""Move a checker piece of a given position and color"""
# Possibly better way to interconnect piece and direction?
idx = 0 if Piece.BLACK in piece else 1
move = self.value[idx]
return pos + move