diff --git a/23 Checkers/README.md b/23 Checkers/README.md index 178b7957..05681815 100644 --- a/23 Checkers/README.md +++ b/23 Checkers/README.md @@ -5,3 +5,11 @@ https://www.atariarchives.org/basicgames/showpage.php?page=40 Downloaded from Vintage Basic at http://www.vintage-basic.net/games.html + + +## Known Issues In the Original BASIC Code + - If the computer moves a checker to the bottom row, it promotes, but + leaves the original checker in place. (See line 1240) + - Human players may move non-kings as if they were kings. (See lines 1590 to 1810) + - Curious writing to "I" variable without ever reading it. (See lines 1700 and 1806) + \ No newline at end of file diff --git a/23 Checkers/python/checkers.py b/23 Checkers/python/checkers.py index 3a1d1236..fc1008ae 100644 --- a/23 Checkers/python/checkers.py +++ b/23 Checkers/python/checkers.py @@ -6,6 +6,7 @@ How about a nice game of checkers? Ported by Dave LeCompte """ +import collections PAGE_WIDTH = 64 @@ -17,7 +18,12 @@ COMPUTER_PIECE = -1 COMPUTER_KING = -2 EMPTY_SPACE = 0 -INVALID_MOVE = -99 +TOP_ROW = 7 +BOTTOM_ROW = 0 + +MoveRecord = collections.namedtuple( + "MoveRecord", ["quality", "start_x", "start_y", "dest_x", "dest_y"] +) def print_centered(msg): @@ -33,6 +39,28 @@ def print_header(title): print() +def get_coordinates(prompt): + err_msg = "ENTER COORDINATES in X,Y FORMAT" + while True: + print(prompt) + response = input() + if "," not in response: + print(err_msg) + continue + + try: + x, y = [int(c) for c in response.split(",")] + except ValueError as ve: + print(err_msg) + continue + + return x, y + + +def is_legal_board_coordinate(x, y): + return (0 <= x <= 7) and (0 <= y <= 7) + + class Board: def __init__(self): self.spaces = [[0 for y in range(8)] for x in range(8)] @@ -66,6 +94,247 @@ class Board: return s + def get_spaces(self): + for x in range(0, 8): + for y in range(0, 8): + yield x, y + + def get_spaces_with_computer_pieces(self): + for x, y in self.get_spaces(): + contents = self.spaces[x][y] + if contents < 0: + yield x, y + + def get_spaces_with_human_pieces(self): + for x, y in self.get_spaces(): + contents = self.spaces[x][y] + if contents > 0: + yield x, y + + def get_legal_deltas_for_space(self, x, y): + contents = self.spaces[x][y] + if contents == COMPUTER_PIECE: + for delta_x in (-1, 1): + yield (delta_x, -1) + else: + for delta_x in (-1, 1): + for delta_y in (-1, 1): + yield (delta_x, delta_y) + + def pick_computer_move(self): + move_record = None + + for start_x, start_y in self.get_spaces_with_computer_pieces(): + for delta_x, delta_y in self.get_legal_deltas_for_space(start_x, start_y): + new_move_record = self.check_move(start_x, start_y, delta_x, delta_y) + + if new_move_record is None: + continue + + if (move_record is None) or ( + new_move_record.quality > move_record.quality + ): + move_record = new_move_record + + return move_record + + def check_move(self, start_x, start_y, delta_x, delta_y): + new_x = start_x + delta_x + new_y = start_y + delta_y + if not is_legal_board_coordinate(new_x, new_y): + return None + + contents = self.spaces[new_x][new_y] + if contents == EMPTY_SPACE: + return self.evaluate_move(start_x, start_y, new_x, new_y) + if contents < 0: + return None + + # check jump landing space, which is an additional dx, dy from new_x, newy + landing_x = new_x + delta_x + landing_y = new_y + delta_y + + if not is_legal_board_coordinate(landing_x, landing_y): + return None + if self.spaces[landing_x][landing_y] == EMPTY_SPACE: + return self.evaluate_move(start_x, start_y, landing_x, landing_y) + + def evaluate_move(self, start_x, start_y, dest_x, dest_y): + quality = 0 + if dest_y == 0 and self.spaces[start_x][start_y] == COMPUTER_PIECE: + # promoting is good + quality += 2 + if abs(dest_y - start_y) == 2: + # jumps are good + quality += 5 + if start_y == 7: + # prefer to defend back row + quality -= 2 + if dest_x in (0, 7): + # moving to edge column + quality += 1 + for delta_x in (-1, 1): + if not is_legal_board_coordinate(dest_x + delta_x, dest_y - 1): + continue + + if self.spaces[dest_x + delta_x][dest_y - 1] < 0: + # moving into "shadow" of another computer piece + quality += 1 + + if not is_legal_board_coordinate(dest_x - delta_x, dest_y + 1): + continue + + if ( + (self.spaces[dest_x + delta_x][dest_y - 1] > 0) + and (self.spaces[dest_x - delta_x][dest_y + 1] == EMPTY_SPACE) + or ((dest_x - delta_x == start_x) and (dest_y + 1 == start_y)) + ): + # we are moving up to a human checker that could jump us + quality -= 2 + return MoveRecord(quality, start_x, start_y, dest_x, dest_y) + + def remove_r_pieces(self, move_record): + self.remove_pieces( + move_record.start_x, + move_record.start_y, + move_record.dest_x, + move_record.dest_y, + ) + + def remove_pieces(self, start_x, start_y, dest_x, dest_y): + self.spaces[dest_x][dest_y] = self.spaces[start_x][start_y] + self.spaces[start_x][start_y] = EMPTY_SPACE + + if abs(dest_x - start_x) == 2: + mid_x = (start_x + dest_x) // 2 + mid_y = (start_y + dest_y) // 2 + self.spaces[mid_x][mid_y] = EMPTY_SPACE + + def play_computer_move(self, move_record): + print( + f"FROM {move_record.start_x} {move_record.start_y} TO {move_record.dest_x} {move_record.dest_y}" + ) + + while True: + if move_record.dest_y == BOTTOM_ROW: + # KING ME + self.remove_r_pieces(move_record) + self.spaces[move_record.dest_x][move_record.dest_y] = COMPUTER_KING + return + else: + self.spaces[move_record.dest_x][move_record.dest_y] = self.spaces[ + move_record.start_x + ][move_record.start_y] + self.remove_r_pieces(move_record) + + if abs(move_record.dest_x - move_record.start_x) != 2: + return + + landing_x = move_record.dest_x + landing_y = move_record.dest_y + + best_move = None + if self.spaces[landing_x][landing_y] == COMPUTER_PIECE: + for delta_x in (-2, 2): + test_record = self.try_extend(landing_x, landing_y, delta_x, -2) + if not (move_record is None): + if (best_move is None) or ( + move_record.quality > best_move.quality + ): + best_move = test_record + else: + assert self.spaces[landing_x][landing_y] == COMPUTER_KING + for delta_x in (-2, 2): + for delta_y in (-2, 2): + test_record = self.try_extend( + landing_x, landing_y, delta_x, delta_y + ) + if not (move_record is None): + if (best_move is None) or ( + move_record.quality > best_move.quality + ): + best_move = test_record + + if best_move is None: + return + else: + print(f"TO {best_move.dest_x} {best_move.dest_y}") + move_record = best_move + + def try_extend(self, start_x, start_y, delta_x, delta_y): + new_x = start_x + delta_x + new_y = start_y + delta_y + + if not is_legal_board_coordinate(new_x, new_y): + return None + + jumped_x = start_x + delta_x // 2 + jumped_y = start_y + delta_y // 2 + + if (self.spaces[new_x][new_y] == EMPTY_SPACE) and ( + self.spaces[jumped_x][jumped_y] > 0 + ): + return self.evaluate_move(start_x, start_y, new_x, new_y) + + def get_human_move(self): + is_king = False + + while True: + start_x, start_y = get_coordinates("FROM?") + + if self.spaces[start_x][start_y] > 0: + break + + is_king = self.spaces[start_x][start_y] == HUMAN_KING + + while True: + dest_x, dest_y = get_coordinates("TO?") + + if (not is_king) and (dest_y < start_y): + # CHEATER! Trying to move non-king backwards + continue + if ( + (self.spaces[dest_x][dest_y] == 0) + and (abs(dest_x - start_x) <= 2) + and (abs(dest_x - start_x) == abs(dest_y - start_y)) + ): + break + return start_x, start_y, dest_x, dest_y + + def get_human_extension(self, start_x, start_y): + is_king = self.spaces[start_x][start_y] == HUMAN_KING + + while True: + dest_x, dest_y = get_coordinates("+TO?") + + if dest_x < 0: + return False, None + if (not is_king) and (dest_y < start_y): + # CHEATER! Trying to move non-king backwards + continue + if ( + (self.spaces[dest_x][dest_y] == EMPTY_SPACE) + and (abs(dest_x - start_x) == 2) + and (abs(dest_y - start_y) == 2) + ): + return True, (start_x, start_y, dest_x, dest_y) + + def play_human_move(self, start_x, start_y, dest_x, dest_y): + self.remove_pieces(start_x, start_y, dest_x, dest_y) + + if dest_y == TOP_ROW: + # KING ME + self.spaces[dest_x][dest_y] = HUMAN_KING + + def check_pieces(self): + if len(list(self.get_spaces_with_computer_pieces())) == 0: + print_human_won() + return False + if len(list(self.get_spaces_with_computer_pieces())) == 0: + print_computer_won() + return False + return True + def print_instructions(): print("THIS IS THE GAME OF CHECKERS. THE COMPUTER IS X,") @@ -82,225 +351,6 @@ def print_instructions(): print() -def get_spaces(): - for x in range(0, 8): - for y in range(0, 8): - yield x, y - - -def get_spaces_with_computer_pieces(board): - for x, y in get_spaces(): - contents = board.spaces[x][y] - if contents < 0: - yield x, y - - -def get_spaces_with_human_pieces(board): - for x, y in get_spaces(): - contents = board.spaces[x][y] - if contents > 0: - yield x, y - - -def pick_computer_move(board): - r = [INVALID_MOVE] * 5 - for x, y in get_spaces_with_computer_pieces(board): - contents = board.spaces[x][y] - if contents == COMPUTER_PIECE: - for dx in (-1, 1): - dy = -1 - sub_650(board, x, y, dx, dy, r) - else: - for dx in (-1, 1): - for dy in (-1, 1): - sub_650(board, x, y, dx, dy, r) - - if r[0] != INVALID_MOVE: - dx = r[3] - r[1] - dy = r[4] - r[2] - if abs(dx) != abs(dy): - print(r) - assert abs(dx) == abs(dy) - return r - - -def sub_650(board, x, y, dx, dy, r): - new_x = x + dx - new_y = y + dy - if not ((0 <= new_x <= 7) and (0 <= new_y <= 7)): - return - - contents = board.spaces[new_x][new_y] - if contents == 0: - sub_910(board, x, y, new_x, new_y, r) - return - if contents < 0: - return - - # check landing space - landing_x = new_x + dx - landing_y = new_y + dy - - # line 790 - if not ((0 <= landing_x <= 7) and (0 <= landing_y <= 7)): - return - if board.spaces[landing_x][landing_y] == 0: - sub_910(board, x, y, landing_x, landing_y, r) - - -def sub_910(board, start_x, start_y, dest_x, dest_y, r): - q = 0 - if dest_y == 0 and board.spaces[start_x][start_y] == COMPUTER_PIECE: - q += 2 - if abs(start_y - dest_y) == 2: - q += 5 - if start_y == 7: - q -= 2 - if dest_x in (0, 7): - q += 1 - for c in (-1, 1): - if (0 <= dest_x + c <= 7) and (1 <= dest_y): - # line 1035 - if board.spaces[dest_x + c][dest_y - 1] < 0: - q += 1 - # line 1040 - elif (0 <= dest_x - c <= 7) and (dest_y + 1 <= 7): - # line 1045 - if ( - (board.spaces[dest_x + c][dest_y - 1] > 0) - and (board.spaces[dest_x - c][dest_y + 1] == EMPTY_SPACE) - or ((dest_x - c == start_x) and (dest_y + 1 == start_y)) - ): - q -= 2 - # line 1080 - - if q > r[0]: - r[0] = q - r[1] = start_x - r[2] = start_y - r[3] = dest_x - r[4] = dest_y - - -def remove_r_pieces(board, r): - remove_pieces(board, r[1], r[2], r[3], r[4]) - - -def remove_pieces(board, start_x, start_y, dest_x, dest_y): - board.spaces[dest_x][dest_y] = board.spaces[start_x][start_y] - board.spaces[start_x][start_y] = EMPTY_SPACE - - if abs(dest_x - start_x) == 2: - mid_x = (start_x + dest_x) // 2 - mid_y = (start_y + dest_y) // 2 - board.spaces[mid_x][mid_y] = EMPTY_SPACE - - -def play_computer_move(board, r): - print(f"FROM {r[1]} {r[2]} TO {r[3]} {r[4]}") - - while True: - if r[4] == 0: - # KING ME - board.spaces[r[3]][r[4]] = COMPUTER_KING - remove_r_pieces(board, r) - return - else: - # line 1250 - board.spaces[r[3]][r[4]] = board.spaces[r[1]][r[2]] - remove_r_pieces(board, r) - - if abs(r[1] - r[3]) != 2: - return - - # line 1340 - x = r[3] - y = r[4] - r[0] = INVALID_MOVE - if board.spaces[x][y] == COMPUTER_PIECE: - for a in (-2, 2): - try_extend(board, r, x, y, a, -2) - else: - assert board.spaces[x][y] == COMPUTER_KING - for a in (-2, 2): - for b in (-2, 2): - try_extend(board, r, x, y, a, b) - if r[0] != INVALID_MOVE: - print(f"TO {r[3]} {r[4]}") - else: - return - - -def try_extend(board, r, x, y, a, b): - # line 1370 - nx = x + a - ny = y + b - if not ((0 <= nx <= 7) and (0 <= ny <= 7)): - return - if (board.spaces[nx][ny] == EMPTY_SPACE) and ( - board.spaces[nx + a // 2][ny + b // 2] > 0 - ): - sub_910(board, x, y, nx, ny, r) - - -def get_human_move(board): - is_king = False - - while True: - print("FROM?") - from_response = input() - x, y = [int(c) for c in from_response.split(",")] - - if board.spaces[x][y] > 0: - break - - is_king = board.spaces[x][y] == HUMAN_KING - - while True: - print("TO?") - to_response = input() - a, b = [int(c) for c in to_response.split(",")] - - if (not is_king) and (b < y): - # CHEATER! Trying to move non-king backwards - continue - if ( - (board.spaces[a][b] == 0) - and (abs(a - x) <= 2) - and (abs(a - x) == abs(b - y)) - ): - break - return x, y, a, b - - -def get_human_extension(board, sx, sy): - is_king = board.spaces[sx][sy] == HUMAN_KING - - while True: - print("+TO?") - to_response = input() - a1, b1 = [int(c) for c in to_response.split(",")] - if a1 < 0: - return False, None - if (not is_king) and (b1 < sy): - # CHEATER! Trying to move non-king backwards - continue - if ( - (board.spaces[a1][b1] == EMPTY_SPACE) - and (abs(a1 - sx) == 2) - and (abs(b1 - sy) == 2) - ): - return True, (sx, sy, a1, b1) - - -def play_human_move(board, start_x, start_y, dest_x, dest_y): - remove_pieces(board, start_x, start_y, dest_x, dest_y) - - if dest_y == 7: - # KING ME - board.spaces[dest_x][dest_y] = HUMAN_KING - - def print_human_won(): print() print("YOU WIN.") @@ -311,40 +361,30 @@ def print_computer_won(): print("I WIN.") -def check_pieces(board): - if len(list(get_spaces_with_computer_pieces(board))) == 0: - print_human_won() - return False - if len(list(get_spaces_with_computer_pieces(board))) == 0: - print_computer_won() - return False - return True - - def play_game(): board = Board() while True: - r = pick_computer_move(board) - if r[0] == INVALID_MOVE: + move_record = board.pick_computer_move() + if move_record is None: print_human_won() return - play_computer_move(board, r) + board.play_computer_move(move_record) print(board) - if not check_pieces(board): + if not board.check_pieces(): return - sx, sy, dx, dy = get_human_move(board) - play_human_move(board, sx, sy, dx, dy) - if abs(dx - sx) == 2: + start_x, start_y, dest_x, dest_y = board.get_human_move() + board.play_human_move(start_x, start_y, dest_x, dest_y) + if abs(dest_x - start_x) == 2: while True: - extend, move = get_human_extension(board, dx, dy) + extend, move = board.get_human_extension(dest_x, dest_y) if not extend: break - sx, sy, dx, dy = move - play_human_move(board, sx, sy, dx, dy) + start_x, start_y, dest_x, dest_y = move + board.play_human_move(start_x, start_y, dest_x, dest_y) def main():