From 2f1a3e8555441e5ab61cdde780cb6766f2f61abc Mon Sep 17 00:00:00 2001 From: Dave LeCompte Date: Wed, 3 Mar 2021 22:37:43 -0800 Subject: [PATCH 1/2] update port CHECKERS to python Moved a lot of methods into the board class, documented the logic for the board evaluation heuristics. Documented some known issues with the original BASIC source code. I've noticed that the computer has resigned to me when it has legal, if bad, moves. A bug in my code? A bug in the BASIC code? Still need to investigate further. --- 23 Checkers/README.md | 8 + 23 Checkers/python/checkers.py | 518 ++++++++++++++++++--------------- 2 files changed, 286 insertions(+), 240 deletions(-) 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..adec5a64 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,245 @@ 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 +349,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 +359,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(): From a62fbc49ad05a0f0ae784196cc80336b5de2e0f0 Mon Sep 17 00:00:00 2001 From: Dave LeCompte Date: Wed, 3 Mar 2021 22:49:52 -0800 Subject: [PATCH 2/2] update CHECKERS for python found an indentation error, played game to completion. --- 23 Checkers/python/checkers.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/23 Checkers/python/checkers.py b/23 Checkers/python/checkers.py index adec5a64..fc1008ae 100644 --- a/23 Checkers/python/checkers.py +++ b/23 Checkers/python/checkers.py @@ -128,11 +128,13 @@ class Board: 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 new_move_record is None: + continue - if (move_record is None) or (new_move_record.quality > move_record.quality): - move_record = new_move_record + if (move_record is None) or ( + new_move_record.quality > move_record.quality + ): + move_record = new_move_record return move_record