""" HEXAPAWN A machine learning game, an interpretation of HEXAPAWN game as presented in Martin Gardner's "The Unexpected Hanging and Other Mathematical Diversions", Chapter Eight: A Matchbox Game-Learning Machine. Original version for H-P timeshare system by R.A. Kaapke 5/5/76 Instructions by Jeff Dalton Conversion to MITS BASIC by Steve North Port to Python by Dave LeCompte """ """ PORTING NOTES: I printed out the BASIC code and hand-annotated what each little block of code did, which feels amazingly retro. I encourage other porters that have a complex knot of GOTOs and semi-nested subroutines to do hard-copy hacking, it might be a different perspective that helps. A spoiler - the objective of the game is not documented, ostensibly to give the human player a challenge. If a player (human or computer) advances a pawn across the board to the far row, that player wins. If a player has no legal moves (either by being blocked, or all their pieces having been captured), that player loses. The original BASIC had 2 2-dimensional tables stored in DATA at the end of the program. This encoded all 19 different board configurations (Hexapawn is a small game), with reflections in one table, and then in a parallel table, for each of the 19 rows, a list of legal moves was encoded by turning them into 2-digit decimal numbers. As gameplay continued, the AI would overwrite losing moves with 0 in the second array. My port takes this "parallel array" structure and turns that information into a small Python class, BoardLayout. BoardLayout stores the board description and legal moves, but stores the moves as (row, column) 2-tuples, which is easier to read. The logic for checking if a BoardLayout matches the current board, as well as removing losing move have been moved into methods of this class. """ import collections import random PAGE_WIDTH = 64 HUMAN_PIECE = 1 EMPTY_SPACE = 0 COMPUTER_PIECE = -1 ComputerMove = collections.namedtuple( "ComputerMove", ["board_index", "move_index", "m1", "m2"] ) wins = 0 losses = 0 def print_centered(msg): spaces = " " * ((PAGE_WIDTH - len(msg)) // 2) print(spaces + msg) def print_header(title): print_centered(title) print_centered("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY") print() print() print() def print_instructions(): print( """ THIS PROGRAM PLAYS THE GAME OF HEXAPAWN. HEXAPAWN IS PLAYED WITH CHESS PAWNS ON A 3 BY 3 BOARD. THE PAWNS ARE MOVED AS IN CHESS - ONE SPACE FORWARD TO AN EMPTY SPACE OR ONE SPACE FORWARD AND DIAGONALLY TO CAPTURE AN OPPOSING MAN. ON THE BOARD, YOUR PAWNS ARE 'O', THE COMPUTER'S PAWNS ARE 'X', AND EMPTY SQUARES ARE '.'. TO ENTER A MOVE, TYPE THE NUMBER OF THE SQUARE YOU ARE MOVING FROM, FOLLOWED BY THE NUMBER OF THE SQUARE YOU WILL MOVE TO. THE NUMBERS MUST BE SEPERATED BY A COMMA. THE COMPUTER STARTS A SERIES OF GAMES KNOWING ONLY WHEN THE GAME IS WON (A DRAW IS IMPOSSIBLE) AND HOW TO MOVE. IT HAS NO STRATEGY AT FIRST AND JUST MOVES RANDOMLY. HOWEVER, IT LEARNS FROM EACH GAME. THUS, WINNING BECOMES MORE AND MORE DIFFICULT. ALSO, TO HELP OFFSET YOUR INITIAL ADVANTAGE, YOU WILL NOT BE TOLD HOW TO WIN THE GAME BUT MUST LEARN THIS BY PLAYING. THE NUMBERING OF THE BOARD IS AS FOLLOWS: 123 456 789 FOR EXAMPLE, TO MOVE YOUR RIGHTMOST PAWN FORWARD, YOU WOULD TYPE 9,6 IN RESPONSE TO THE QUESTION 'YOUR MOVE ?'. SINCE I'M A GOOD SPORT, YOU'LL ALWAYS GO FIRST. """ ) def prompt_yes_no(msg): while True: print(msg) response = input().upper() if response[0] == "Y": return True elif response[0] == "N": return False def reverse_space_name(space_name): # reverse a space name in the range 1-9 left to right assert 1 <= space_name <= 9 reflections = {1: 3, 2: 2, 3: 1, 4: 6, 5: 5, 6: 4, 7: 9, 8: 8, 9: 7} return reflections[space_name] def is_space_in_center_column(space_name): return reverse_space_name(space_name) == space_name class BoardLayout: def __init__(self, cells, move_list): self.cells = cells self.moves = move_list def _check_match_no_mirror(self, cell_list): for space_index, board_contents in enumerate(self.cells): if board_contents != cell_list[space_index]: return False return True def _check_match_with_mirror(self, cell_list): for space_index, board_contents in enumerate(self.cells): reversed_space_index = reverse_space_name(space_index + 1) - 1 if board_contents != cell_list[reversed_space_index]: return False return True def check_match(self, cell_list): if self._check_match_with_mirror(cell_list): return True, True elif self._check_match_no_mirror(cell_list): return True, False return False, None def get_random_move(self, reverse_board): if not self.moves: return None move_index = random.randrange(len(self.moves)) m1, m2 = self.moves[move_index] if reverse_board: m1 = reverse_space_name(m1) m2 = reverse_space_name(m2) return move_index, m1, m2 boards = [ BoardLayout([-1, -1, -1, 1, 0, 0, 0, 1, 1], [(2, 4), (2, 5), (3, 6)]), BoardLayout([-1, -1, -1, 0, 1, 0, 1, 0, 1], [(1, 4), (1, 5), (3, 6)]), BoardLayout([-1, 0, -1, -1, 1, 0, 0, 0, 1], [(1, 5), (3, 5), (3, 6), (4, 7)]), BoardLayout([0, -1, -1, 1, -1, 0, 0, 0, 1], [(3, 6), (5, 8), (5, 9)]), BoardLayout([-1, 0, -1, 1, 1, 0, 0, 1, 0], [(1, 5), (3, 5), (3, 6)]), BoardLayout([-1, -1, 0, 1, 0, 1, 0, 0, 1], [(2, 4), (2, 5), (2, 6)]), BoardLayout([0, -1, -1, 0, -1, 1, 1, 0, 0], [(2, 6), (5, 7), (5, 8)]), BoardLayout([0, -1, -1, -1, 1, 1, 1, 0, 0], [(2, 6), (3, 5)]), BoardLayout([-1, 0, -1, -1, 0, 1, 0, 1, 0], [(4, 7), (4, 8)]), BoardLayout([0, -1, -1, 0, 1, 0, 0, 0, 1], [(3, 5), (3, 6)]), BoardLayout([0, -1, -1, 0, 1, 0, 1, 0, 0], [(3, 5), (3, 6)]), BoardLayout([-1, 0, -1, 1, 0, 0, 0, 0, 1], [(3, 6)]), BoardLayout([0, 0, -1, -1, -1, 1, 0, 0, 0], [(4, 7), (5, 8)]), BoardLayout([-1, 0, 0, 1, 1, 1, 0, 0, 0], [(1, 5)]), BoardLayout([0, -1, 0, -1, 1, 1, 0, 0, 0], [(2, 6), (4, 7)]), BoardLayout([-1, 0, 0, -1, -1, 1, 0, 0, 0], [(4, 7), (5, 8)]), BoardLayout([0, 0, -1, -1, 1, 0, 0, 0, 0], [(3, 5), (3, 6), (4, 7)]), BoardLayout([0, -1, 0, 1, -1, 0, 0, 0, 0], [(2, 8), (5, 8)]), BoardLayout([-1, 0, 0, -1, 1, 0, 0, 0, 0], [(1, 5), (4, 7)]), ] def get_move(board_index, move_index): assert board_index >= 0 and board_index < len(boards) board = boards[board_index] assert move_index >= 0 and move_index < len(board.moves) return board.moves[move_index] def remove_move(board_index, move_index): assert board_index >= 0 and board_index < len(boards) board = boards[board_index] assert move_index >= 0 and move_index < len(board.moves) del board.moves[move_index] def init_board(): return [COMPUTER_PIECE] * 3 + [EMPTY_SPACE] * 3 + [HUMAN_PIECE] * 3 def print_board(board): piece_dict = {COMPUTER_PIECE: "X", EMPTY_SPACE: ".", HUMAN_PIECE: "O"} space = " " * 10 print() for row in range(3): line = "" for column in range(3): line += space space_number = row * 3 + column space_contents = board[space_number] line += piece_dict[space_contents] print(line) print() def get_coordinates(): while True: try: print("YOUR MOVE?") response = input() m1, m2 = [int(c) for c in response.split(",")] return m1, m2 except ValueError as ve: print_illegal() def print_illegal(): print("ILLEGAL MOVE.") def board_contents(board, space_number): return board[space_number - 1] def set_board(board, space_number, new_value): board[space_number - 1] = new_value def is_legal_human_move(board, m1, m2): if board_contents(board, m1) != HUMAN_PIECE: # Start space doesn't contain player's piece return False if board_contents(board, m2) == HUMAN_PIECE: # Destination space contains player's piece (can't capture your own piece) return False is_capture = m2 - m1 != -3 if is_capture and board_contents(board, m2) != COMPUTER_PIECE: # Destination does not contain computer piece return False if m2 > m1: # can't move backwards return False if (not is_capture) and board_contents(board, m2) != EMPTY_SPACE: # Destination is not open return False if m2 - m1 < -4: # too far return False if m1 == 7 and m2 == 3: # can't jump corner to corner (wrapping around the board) return False return True def player_piece_on_back_row(board): for space in range(1, 4): if board_contents(board, space) == HUMAN_PIECE: return True return False def computer_piece_on_front_row(board): for space in range(7, 10): if board_contents(board, space) == COMPUTER_PIECE: return True return False def all_human_pieces_captured(board): return len(list(get_human_spaces(board))) == 0 def all_computer_pieces_captured(board): return len(list(get_computer_spaces(board))) == 0 def human_win(last_computer_move): print("YOU WIN") remove_move(last_computer_move.board_index, last_computer_move.move_index) global losses losses += 1 def computer_win(has_moves): if not has_moves: msg = "YOU CAN'T MOVE, SO " else: msg = "" msg += "I WIN" print(msg) global wins wins += 1 def show_scores(): print(f"I HAVE WON {wins} AND YOU {losses} OUT OF {wins + losses} GAMES.") print() def human_has_move(board): for i in get_human_spaces(board): if board_contents(board, i - 3) == EMPTY_SPACE: # can move piece forward return True elif is_space_in_center_column(i): if (board_contents(board, i - 2) == COMPUTER_PIECE) or ( board_contents(board, i - 4) == COMPUTER_PIECE ): # can capture from center return True else: continue elif i < 7: assert (i == 4) or (i == 6) if board_contents(board, 2) == COMPUTER_PIECE: # can capture computer piece at 2 return True else: continue elif board_contents(board, 5) == COMPUTER_PIECE: assert (i == 7) or (i == 9) # can capture computer piece at 5 return True else: continue return False def get_board_spaces(): """ generates the space names (1-9)""" yield from range(1, 10) def get_board_spaces_with(board, val): """ generates spaces containing pieces of type val""" for i in get_board_spaces(): if board_contents(board, i) == val: yield i def get_human_spaces(board): yield from get_board_spaces_with(board, HUMAN_PIECE) def get_empty_spaces(board): yield from get_board_spaces_with(board, EMPTY_SPACE) def get_computer_spaces(board): yield from get_board_spaces_with(board, COMPUTER_PIECE) def has_computer_move(board): for i in get_computer_spaces(board): found_move = False if board_contents(board, i + 3) == EMPTY_SPACE: # can move forward (down) return True if is_space_in_center_column(i): # i is in the middle column if (board_contents(board, i + 2) == HUMAN_PIECE) or ( board_contents(board, i + 4) == HUMAN_PIECE ): return True else: if i > 3: # beyond the first row if board_contents(board, 8) == HUMAN_PIECE: # can capture on 8 return True else: continue else: if board_contents(board, 5) == HUMAN_PIECE: # can capture on 5 return True else: continue return False def find_board_index_that_matches_board(board): for board_index, board_layout in enumerate(boards): matches, is_reversed = board_layout.check_match(board) if matches: return board_index, is_reversed # THE TERMINATION OF THIS LOOP IS IMPOSSIBLE print("ILLEGAL BOARD PATTERN.") assert False def pick_computer_move(board): if not has_computer_move(board): return None board_index, reverse_board = find_board_index_that_matches_board(board) m = boards[board_index].get_random_move(reverse_board) if m is None: print("I RESIGN") return None move_index, m1, m2 = m return ComputerMove(board_index, move_index, m1, m2) def get_human_move(board): while True: m1, m2 = get_coordinates() if not is_legal_human_move(board, m1, m2): print_illegal() else: return m1, m2 def apply_move(board, m1, m2, piece_value): set_board(board, m1, EMPTY_SPACE) set_board(board, m2, piece_value) def play_game(): last_computer_move = None board = init_board() while True: print_board(board) m1, m2 = get_human_move(board) apply_move(board, m1, m2, HUMAN_PIECE) print_board(board) if player_piece_on_back_row(board) or all_computer_pieces_captured(board): human_win(last_computer_move) return computer_move = pick_computer_move(board) if computer_move is None: human_win(last_computer_move) return last_computer_move = computer_move m1, m2 = last_computer_move.m1, last_computer_move.m2 print(f"I MOVE FROM {m1} TO {m2}") apply_move(board, m1, m2, COMPUTER_PIECE) print_board(board) if computer_piece_on_front_row(board): computer_win(True) return elif (not human_has_move(board)) or (all_human_pieces_captured(board)): computer_win(False) return def main(): print_header("HEXAPAWN") if prompt_yes_no("INSTRUCTIONS (Y-N)?"): print_instructions() global wins, losses wins = 0 losses = 0 while True: play_game() show_scores() if __name__ == "__main__": main()