-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgame.py
More file actions
488 lines (378 loc) · 19.7 KB
/
game.py
File metadata and controls
488 lines (378 loc) · 19.7 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
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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
from board import Board
from pieces import Piece
from squares import Square
class Game:
'''Responsible for managing game states, move legality, player turns and logging'''
def __init__(self):
self.board = Board()
self.running = True
self.players = ['W', 'B'] # aka ['white', 'black']
self.player_color = self.players[0]
self.message = lambda error, description: self.msg(error, description)
self.castling = False
def play(self, start, desti) -> bool:
'''Main game loop, passing turns'''
if self.running:
board = self.board
castling = self.castling
# get players move attempt
start_square = board.get_square(start)
desti_square = board.get_square(desti)
# get pieces from start and destination squares
moving_piece = start_square.piece
target_piece = desti_square.piece
if not self.move_is_legal(moving_piece, target_piece, start_square, desti_square):
return False
promotion = False
if self.is_pawn_promoted(start_square, desti_square):
promotion = True
print(f'self.castling -> {self.castling}')
if self.castling:
print('call castle_rook method')
rook_square, rook_desti = self.castling
self.castle_rook(rook_square, rook_desti)
return self.move(moving_piece, start_square, desti_square, pawn_promotion=promotion)
def get_board_state(self) -> list:
'''Data from board for frontend HTML template'''
result = []
colors = ['B', 'W']
i = 0
for row in self.board.board:
row_result = []
# alternate for row (board colors)
if i:
i -=1
else:
i +=1
for square in row:
# alterante B/W each square (board colors) - jank
if i:
i -=1
else:
i +=1
if square.is_ocupied():
row_result.append([square.piece.svg, colors[i], square.get_notation()])
else:
row_result.append(['', colors[i], square.get_notation()])
result.append(row_result)
return result[::-1] # for player 1 perspective or naur
def move_is_legal(self, moving_piece, target_piece, start_square, desti_square) -> bool:
'''Check if move is legal, if not -> give reason via msg()'''
msg = self.message
if not start_square.is_ocupied():
return msg(False, 'no piece on this square to move')
if start_square == desti_square:
return msg(False, 'you cannot move to start square')
if moving_piece.color != self.player_color:
return msg(False, 'you cant move your opponents pieces')
path = self.get_path(moving_piece, start_square, desti_square)
king = self.board.get_king_square(self.player_color)
if isinstance(path, type(None)):
if moving_piece.kind != 'knight':
return msg(False, 'cannot find path to destination square')
if moving_piece.kind == 'king':
desti_attacked_by = self.get_square_attackers(start_square, desti_square)
if desti_attacked_by:
return msg(False, 'king can not go to attacked square')
else: # could refactor this else block into methods
king_attackers = self.get_square_attackers(king, king)
if king_attackers:
amount = len(king_attackers)
if amount >= 2:
return msg(False, 'king in double check, forced king move')
if amount == 1:
attacker = king_attackers[0]
if attacker.piece.kind == 'knight':
if desti_square != attacker:
return msg(False, 'king in check by knight, forced attacker capture or king move')
else:
path = self.get_path(king.piece, king, attacker)
if isinstance(path, type(None)):
return msg(False, 'big time error 2 path == None')
in_line_of_sight = desti_square in path
if desti_square == attacker or in_line_of_sight:
pass
else:
return msg(False, 'king in check, forced attacker capture or king move')
pin_instigator = self.is_pinned(start_square, king)
if pin_instigator and pin_instigator != desti_square: # allows for capturing of pin instigator
return msg(False, f'{moving_piece.kind} is pinned to the king by {pin_instigator.piece.kind} on {pin_instigator.get_notation()}')
if moving_piece.kind == 'pawn':
if not self.is_pawn_moving_forward(start_square, desti_square):
return msg(False, 'pawns only go forward')
if self.is_pawn_moving_diagonally(start_square, desti_square):
if self.is_pawn_capture_valid(desti_square, path):
return msg(False, 'illegal pawn capture')
else:
if desti_square.is_ocupied():
return msg(False, 'pawns path forward is blocked')
if moving_piece.kind == 'knight':
if desti_square not in self.board.get_knight_moves(start_square):
return msg(False, 'destination not in knight square list')
if not self.is_move_in_range(moving_piece, path):
return msg(False, 'exceeded piece range')
if self.path_obstructed(path):
return msg(False, 'you cant move thru pieces')
if desti_square.is_ocupied():
if self.is_same_color(moving_piece, target_piece):
return msg(False, 'same color piece on target square')
else:
return msg(True, description=f'capturing {target_piece.kind} on {desti_square.get_notation()}')
if moving_piece.kind == 'king' and not moving_piece.has_moved:
attackers = self.get_square_attackers(start_square, start_square)
if len(path) == 1: # (2) because destination is not counted
if attackers != None:
return msg(False, 'cant castle while in check')
castle_squares = self.board.get_king_castle_squares(self.player_color)
rook = self.board.get_rook_for_castling(self.player_color, desti_square)
if not rook:
return msg(False, 'no rook available to castle with')
if desti_square in castle_squares:
for square in path:
if self.get_square_attackers(start_square, square):
return msg(False, 'cannot castle thru check or into a check')
else:
return msg(False, 'invalid castling destination')
print('upadte self.castling')
rook_castling_desti = self.board.get_rook_castling_destination(rook)
self.castling = [rook, rook_castling_desti]
return msg(True, description=f'{start_square.get_notation()} -> {desti_square.get_notation()}')
def get_square_attackers(self, start, desti) -> list | None: # exclude king in the path for checking attaced squares, so you cant move the king backwards into attacked square
'''Check if square is attacked by opponents piece (for king mainly) -> returns list Square obj of attackers or None'''
attacker_squares = []
my_color = self.player_color
board = self.board
possible_knight_moves = self.board.get_knight_moves(desti)
# check if knight is attacking the destination square
for square in possible_knight_moves:
if square.piece.kind == 'knight':
if square.piece.color != my_color:
attacker_squares.append(square)
# checking if square is covered by opponents pawn
# take destination square and find its diagonal 1 up left, and 1 up right squares (respectively for color)
x, y = board.get_array_idx(desti)
if my_color == 'W': # adjust row based on player color
x +=1
else:
x -=1
top_left = [x, y-1]
top_right = [x, y+1]
left_valid = board.in_bounds(array=top_left)
right_valid = board.in_bounds(array=top_right)
if left_valid:
row, col = top_left[0], top_left[1]
square_left = board.board[row][col]
if square_left.piece.kind == 'pawn' and square_left.piece.color != my_color:
attacker_squares.append(square_left)
if right_valid:
row, col = top_right[0], top_right[1]
square_right = board.board[row][col]
if square_right.piece.kind == 'pawn' and square_right.piece.color != my_color:
attacker_squares.append(square_right)
# pawns are not attacking the square - checking for other pieces now (rooks, bishops, queen and king)
# check files and diags for unobstructed paths to opponent piece
diagnonal_ne_sw = board.get_diagonal(desti, 'NE-SW')
diagnonal_nw_se = board.get_diagonal(desti, 'NW-SE')
vertical = board.get_vertical_file(desti)
horizontal = board.get_horizontal_file(desti)
x, y = board.get_array_idx(desti)
results = [
self.is_attacked_by('rook', vertical, desti, start.piece),
self.is_attacked_by('rook', horizontal, desti, start.piece),
self.is_attacked_by('bishop', diagnonal_ne_sw, desti, start.piece),
self.is_attacked_by('bishop', diagnonal_nw_se, desti, start.piece)
]
all_directions = [vertical, horizontal, diagnonal_nw_se, diagnonal_ne_sw]
for dir in all_directions:
results.append(self.is_attacked_by('queen', dir, desti, start.piece))
results.append(self.is_attacked_by('king', dir, desti, start.piece))
for result in results:
if result:
attacker_squares.append(result)
return attacker_squares if attacker_squares != [] else None
def is_attacked_by(self, aggressor, full_path: list, desti, moving_piece) -> object | None:
'''Helper method for get_square_attackers, parses path of squares'''
for square in full_path:
if square.piece.kind == aggressor and square.piece.color != self.player_color:
path = self.get_path(moving_piece, desti, square) # odd args because we are checking for desti square not start
if aggressor == 'king':
if len(path) <= 0:
print(f'hit king edge case 1 for {aggressor} - {len(path)}')
return square # edge case for the king vs king
return
if path == [] and desti == square: # edge case allow capturing of pieces with king
return
if not self.path_obstructed(path):
print(f'hit king edge case 2 for {aggressor} - {len(path)}')
return square # opponent "piece" can see us
print('big chilling')
def __get_data_for_pin(self, start, king) -> list | bool:
'''Helper - Returns full file or diagonal of your start square and king square, and returns board indices of king and start args: if fail -> False'''
if start.is_ocupied:
data = self.get_path(start.piece, king, start, ignore_type=True)
if isinstance(data, type(None)) or data == []:
print(f'* early return 1')
return False # if your king is not in moving pieces files/diags don't check if it's pinned
full_path, path_type = data # unpack
if full_path == None: # bug here yes
print(f'* early return 2')
return False # if your king is not in moving pieces files/diags don't check if it's pinned
king_idx = full_path.index(king)
start_idx = full_path.index(start)
if king_idx >= start_idx:
# for consistency and ease of use have king on the left of moving piece array index
full_path = full_path[::-1]
king_idx = full_path.index(king)
start_idx = full_path.index(start)
return [full_path, path_type, king_idx, start_idx]
def is_pinned(self, start, king) -> Square | bool:
'''Take a full diag/file of the start (your moving piece square) and king (square) and determine piece pin state via LoS check -> Square of pin instigator or False if not pinned'''
# traverse the array from king to your start arg piece and find los of attacker
pinned = False
full_path = None
path_type = ''
data = self.__get_data_for_pin(start, king)
if data:
full_path, path_type, king_idx, start_idx = data # unpack
for idx, square in enumerate(full_path):
if idx > king_idx:
if square.is_ocupied():
if idx == start_idx:
continue
if square.piece.color == self.player_color:
print(f'* los block by friendly {square.piece.kind} on {square.get_notation()}')
break # blocked LoS by friendly no further test needed for pin
else:
if square.piece.kind == 'queen':
print(f'* pinned by queen')
return square
elif square.piece.kind == 'rook' and path_type == 'file':
print(f'* pinned by rook')
return square
elif square.piece.kind == 'bishop' and path_type == 'diagonal':
print(f'* pinned by bishop')
return square
else:
return pinned
return pinned
def is_king_moving_horizontally(self, start, desti) -> bool:
'''Check if row1 and row2 are equal, and if the columns differ'''
row1, col1 = self.board.get_array_idx(start)
row2, col2 = self.board.get_array_idx(desti)
return row1 == row2 and col1 != col2
def is_pawn_promoted(self, start, desti) -> bool:
'''Determine if destination square is the last row possible'''
if self.player_color == 'W':
return desti.number == 8
if self.player_color == 'B':
return desti.number == 1
def is_pawn_moving_forward(self, start, desti) -> bool:
'''Determine move direction based on player color and start-end squares'''
row_1 = start.number
row_2 = desti.number
if self.player_color == 'W':
return row_1 < row_2
else:
return row_1 > row_2
def is_pawn_moving_diagonally(self, start, desti) -> bool:
'''Compare column idx of two squares'''
col1 = self.board.get_array_idx(start)[1]
col2 = self.board.get_array_idx(desti)[1]
return col1 != col2
def is_pawn_capture_valid(self, desti, path) -> bool:
'''Check if pawn capture attempt is legal - en pessant not implemented yet'''
return len(path) != 0 or desti.is_ocupied() == False # one diagonal square has lenght of path of 0
def path_obstructed(self, path) -> bool:
'''Check each square in between start square and end square'''
if path == None:
return True
for square in path:
if square.is_ocupied():
return True
return False
def msg(self, result, description='') -> None:
'''Print reason for error and return bool'''
if result == True:
msg_type = 'valid move ->'
if result == False:
msg_type = 'invalid move ->'
print(f'{msg_type} {description}')
return result
def is_move_in_range(self, piece, path) -> bool:
'''Check piece range against path to destination'''
return piece.range >= len(path) + 1 # path doesn't include end square
def get_path(self, moving_piece, start, destination, ignore_type=False) -> list | None:
'''Returns all squares in between start square and destination square, or the whole diag or file connecting start and dest'''
if moving_piece.kind == 'knight' and not ignore_type: # make an excpetion for determinging pins, then knights can have paths to king
return []
path = None
full_path = None
board = self.board
vertical = board.get_vertical_file(start)
horizontal = board.get_horizontal_file(start)
diagonal_nw_se = board.get_diagonal(start, direction='NW-SE')
diagonal_ne_sw = board.get_diagonal(start, direction='NE-SW')
is_not_bishop = moving_piece.kind != 'bishop' or ignore_type # discriminate by piece type unless arg specifies to ignore piece type
is_not_rook = moving_piece.kind != 'rook' or ignore_type
path_type, diag, file = '', 'diagonal', 'file'
# ... and false == false (by defualt), in other scenario it will be... and false == true
if destination in vertical and is_not_bishop:
full_path, path_type = vertical, file
elif destination in horizontal and is_not_bishop:
full_path, path_type = horizontal, file
elif destination in diagonal_nw_se and is_not_rook:
full_path, path_type = diagonal_nw_se, diag
elif destination in diagonal_ne_sw and is_not_rook:
full_path, path_type = diagonal_ne_sw, diag
if full_path == []: # failsafe check
return []
elif full_path == None:
return None
if ignore_type:
return [full_path, path_type]
path_start = full_path.index(start) + 1 # ommit start square: idx+1
path_end = full_path.index(destination)
# does not work if start-end indices are end-start
# therfore invert array and get indices again
if path_start > path_end:
full_path = full_path[::-1]
path_start = full_path.index(start) + 1 # ommit start square: idx+1
path_end = full_path.index(destination)
path = full_path[path_start : path_end]
return path
def show_path(self, path) -> None:
s = ''
for square in path:
s += square.get_notation()
s += ':'
if square.is_ocupied():
s += square.piece.show()
else:
s += ' '
s += ' | '
return s
def move(self, piece, start, destination, pawn_promotion=False) -> bool:
'''Move the piece and clear previous square -> log message'''
# move piece and clear previous square
if pawn_promotion:
row = destination.piece.row
col = destination.piece.col
piece = Piece(col, row, captured=False, promoted=True)
piece.update()
destination.piece = piece
start.clear()
if self.player_color == 'W':
self.player_color = 'B'
else:
self.player_color = 'W'
return True
def is_same_color(self, piece_1, piece_2) -> bool:
'''Are two pieces same color'''
return piece_1.color == piece_2.color
def castle_rook(self, rook_square, desti):
rook = rook_square.piece
rook.update()
desti.piece = rook
rook_square.clear()
self.castling = False
print('castled')