Checkers (Python): Check if field has valid moves

Introduces a new error message:
    ({start_x}, {start_y}) has no legal moves. Choose again.

Closes #677

Additionally, add type annotations
This commit is contained in:
Martin Thoma
2022-03-24 14:23:35 +01:00
parent e8ba8bdf24
commit c29c751d21

View File

@@ -6,7 +6,7 @@ How about a nice game of checkers?
Ported by Dave LeCompte Ported by Dave LeCompte
""" """
import collections from typing import Iterator, NamedTuple, Optional, Tuple
PAGE_WIDTH = 64 PAGE_WIDTH = 64
@@ -21,9 +21,13 @@ EMPTY_SPACE = 0
TOP_ROW = 7 TOP_ROW = 7
BOTTOM_ROW = 0 BOTTOM_ROW = 0
MoveRecord = collections.namedtuple(
"MoveRecord", ["quality", "start_x", "start_y", "dest_x", "dest_y"] class MoveRecord(NamedTuple):
) quality: int
start_x: int
start_y: int
dest_x: int
dest_y: int
def print_centered(msg: str) -> None: def print_centered(msg: str) -> None:
@@ -39,7 +43,7 @@ def print_header(title: str) -> None:
print() print()
def get_coordinates(prompt): def get_coordinates(prompt: str) -> Tuple[int, int]:
err_msg = "ENTER COORDINATES in X,Y FORMAT" err_msg = "ENTER COORDINATES in X,Y FORMAT"
while True: while True:
print(prompt) print(prompt)
@@ -57,7 +61,7 @@ def get_coordinates(prompt):
return x, y return x, y
def is_legal_board_coordinate(x, y): def is_legal_board_coordinate(x: int, y: int) -> bool:
return (0 <= x <= 7) and (0 <= y <= 7) return (0 <= x <= 7) and (0 <= y <= 7)
@@ -94,24 +98,24 @@ class Board:
return s return s
def get_spaces(self): def get_spaces(self) -> Iterator[Tuple[int, int]]:
for x in range(0, 8): for x in range(0, 8):
for y in range(0, 8): for y in range(0, 8):
yield x, y yield x, y
def get_spaces_with_computer_pieces(self): def get_spaces_with_computer_pieces(self) -> Iterator[Tuple[int, int]]:
for x, y in self.get_spaces(): for x, y in self.get_spaces():
contents = self.spaces[x][y] contents = self.spaces[x][y]
if contents < 0: if contents < 0:
yield x, y yield x, y
def get_spaces_with_human_pieces(self): def get_spaces_with_human_pieces(self) -> Iterator[Tuple[int, int]]:
for x, y in self.get_spaces(): for x, y in self.get_spaces():
contents = self.spaces[x][y] contents = self.spaces[x][y]
if contents > 0: if contents > 0:
yield x, y yield x, y
def get_legal_deltas_for_space(self, x, y): def get_legal_deltas_for_space(self, x: int, y: int) -> Iterator[Tuple[int, int]]:
contents = self.spaces[x][y] contents = self.spaces[x][y]
if contents == COMPUTER_PIECE: if contents == COMPUTER_PIECE:
for delta_x in (-1, 1): for delta_x in (-1, 1):
@@ -121,7 +125,14 @@ class Board:
for delta_y in (-1, 1): for delta_y in (-1, 1):
yield (delta_x, delta_y) yield (delta_x, delta_y)
def pick_computer_move(self): def get_legal_moves(self, x: int, y: int) -> Iterator[MoveRecord]:
for delta_x, delta_y in self.get_legal_deltas_for_space(x, y):
new_move_record = self.check_move(x, y, delta_x, delta_y)
if new_move_record is not None:
yield new_move_record
def pick_computer_move(self) -> Optional[MoveRecord]:
move_record = None move_record = None
for start_x, start_y in self.get_spaces_with_computer_pieces(): for start_x, start_y in self.get_spaces_with_computer_pieces():
@@ -138,7 +149,9 @@ class Board:
return move_record return move_record
def check_move(self, start_x, start_y, delta_x, delta_y): def check_move(
self, start_x: int, start_y: int, delta_x: int, delta_y: int
) -> Optional[MoveRecord]:
new_x = start_x + delta_x new_x = start_x + delta_x
new_y = start_y + delta_y new_y = start_y + delta_y
if not is_legal_board_coordinate(new_x, new_y): if not is_legal_board_coordinate(new_x, new_y):
@@ -158,8 +171,11 @@ class Board:
return None return None
if self.spaces[landing_x][landing_y] == EMPTY_SPACE: if self.spaces[landing_x][landing_y] == EMPTY_SPACE:
return self.evaluate_move(start_x, start_y, landing_x, landing_y) return self.evaluate_move(start_x, start_y, landing_x, landing_y)
return None
def evaluate_move(self, start_x, start_y, dest_x, dest_y): def evaluate_move(
self, start_x: int, start_y: int, dest_x: int, dest_y: int
) -> MoveRecord:
quality = 0 quality = 0
if dest_y == 0 and self.spaces[start_x][start_y] == COMPUTER_PIECE: if dest_y == 0 and self.spaces[start_x][start_y] == COMPUTER_PIECE:
# promoting is good # promoting is good
@@ -193,7 +209,7 @@ class Board:
quality -= 2 quality -= 2
return MoveRecord(quality, start_x, start_y, dest_x, dest_y) return MoveRecord(quality, start_x, start_y, dest_x, dest_y)
def remove_r_pieces(self, move_record): def remove_r_pieces(self, move_record: MoveRecord) -> None:
self.remove_pieces( self.remove_pieces(
move_record.start_x, move_record.start_x,
move_record.start_y, move_record.start_y,
@@ -201,7 +217,9 @@ class Board:
move_record.dest_y, move_record.dest_y,
) )
def remove_pieces(self, start_x, start_y, dest_x, dest_y): def remove_pieces(
self, start_x: int, start_y: int, dest_x: int, dest_y: int
) -> None:
self.spaces[dest_x][dest_y] = self.spaces[start_x][start_y] self.spaces[dest_x][dest_y] = self.spaces[start_x][start_y]
self.spaces[start_x][start_y] = EMPTY_SPACE self.spaces[start_x][start_y] = EMPTY_SPACE
@@ -210,7 +228,7 @@ class Board:
mid_y = (start_y + dest_y) // 2 mid_y = (start_y + dest_y) // 2
self.spaces[mid_x][mid_y] = EMPTY_SPACE self.spaces[mid_x][mid_y] = EMPTY_SPACE
def play_computer_move(self, move_record): def play_computer_move(self, move_record: MoveRecord) -> None:
print( print(
f"FROM {move_record.start_x} {move_record.start_y} TO {move_record.dest_x} {move_record.dest_y}" f"FROM {move_record.start_x} {move_record.start_y} TO {move_record.dest_x} {move_record.dest_y}"
) )
@@ -249,11 +267,11 @@ class Board:
test_record = self.try_extend( test_record = self.try_extend(
landing_x, landing_y, delta_x, delta_y landing_x, landing_y, delta_x, delta_y
) )
if not (move_record is None): if (move_record is not None) and (
if (best_move is None) or ( (best_move is None)
move_record.quality > best_move.quality or (move_record.quality > best_move.quality)
): ):
best_move = test_record best_move = test_record
if best_move is None: if best_move is None:
return return
@@ -261,7 +279,9 @@ class Board:
print(f"TO {best_move.dest_x} {best_move.dest_y}") print(f"TO {best_move.dest_x} {best_move.dest_y}")
move_record = best_move move_record = best_move
def try_extend(self, start_x, start_y, delta_x, delta_y): def try_extend(
self, start_x: int, start_y: int, delta_x: int, delta_y: int
) -> Optional[MoveRecord]:
new_x = start_x + delta_x new_x = start_x + delta_x
new_y = start_y + delta_y new_y = start_y + delta_y
@@ -275,13 +295,18 @@ class Board:
self.spaces[jumped_x][jumped_y] > 0 self.spaces[jumped_x][jumped_y] > 0
): ):
return self.evaluate_move(start_x, start_y, new_x, new_y) return self.evaluate_move(start_x, start_y, new_x, new_y)
return None
def get_human_move(self): def get_human_move(self) -> Tuple[int, int, int, int]:
is_king = False is_king = False
while True: while True:
start_x, start_y = get_coordinates("FROM?") start_x, start_y = get_coordinates("FROM?")
legal_moves = list(self.get_legal_moves(start_x, start_y))
if not legal_moves:
print(f"({start_x}, {start_y}) has no legal moves. Choose again.")
continue
if self.spaces[start_x][start_y] > 0: if self.spaces[start_x][start_y] > 0:
break break
@@ -293,15 +318,16 @@ class Board:
if (not is_king) and (dest_y < start_y): if (not is_king) and (dest_y < start_y):
# CHEATER! Trying to move non-king backwards # CHEATER! Trying to move non-king backwards
continue continue
if ( is_free = self.spaces[dest_x][dest_y] == 0
(self.spaces[dest_x][dest_y] == 0) within_reach = abs(dest_x - start_x) <= 2
and (abs(dest_x - start_x) <= 2) is_diagonal_move = abs(dest_x - start_x) == abs(dest_y - start_y)
and (abs(dest_x - start_x) == abs(dest_y - start_y)) if is_free and within_reach and is_diagonal_move:
):
break break
return start_x, start_y, dest_x, dest_y return start_x, start_y, dest_x, dest_y
def get_human_extension(self, start_x, start_y): def get_human_extension(
self, start_x: int, start_y: int
) -> Tuple[bool, Optional[Tuple[int, int, int, int]]]:
is_king = self.spaces[start_x][start_y] == HUMAN_KING is_king = self.spaces[start_x][start_y] == HUMAN_KING
while True: while True:
@@ -319,14 +345,16 @@ class Board:
): ):
return True, (start_x, start_y, dest_x, dest_y) return True, (start_x, start_y, dest_x, dest_y)
def play_human_move(self, start_x, start_y, dest_x, dest_y): def play_human_move(
self, start_x: int, start_y: int, dest_x: int, dest_y: int
) -> None:
self.remove_pieces(start_x, start_y, dest_x, dest_y) self.remove_pieces(start_x, start_y, dest_x, dest_y)
if dest_y == TOP_ROW: if dest_y == TOP_ROW:
# KING ME # KING ME
self.spaces[dest_x][dest_y] = HUMAN_KING self.spaces[dest_x][dest_y] = HUMAN_KING
def check_pieces(self): def check_pieces(self) -> bool:
if len(list(self.get_spaces_with_computer_pieces())) == 0: if len(list(self.get_spaces_with_computer_pieces())) == 0:
print_human_won() print_human_won()
return False return False
@@ -381,6 +409,7 @@ def play_game() -> None:
if abs(dest_x - start_x) == 2: if abs(dest_x - start_x) == 2:
while True: while True:
extend, move = board.get_human_extension(dest_x, dest_y) extend, move = board.get_human_extension(dest_x, dest_y)
assert move is not None
if not extend: if not extend:
break break
start_x, start_y, dest_x, dest_y = move start_x, start_y, dest_x, dest_y = move