diff --git a/46 Hexapawn/python/hexapawn.py b/46 Hexapawn/python/hexapawn.py index 50f2abc8..e89e3426 100644 --- a/46 Hexapawn/python/hexapawn.py +++ b/46 Hexapawn/python/hexapawn.py @@ -14,6 +14,38 @@ 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 @@ -23,12 +55,19 @@ HUMAN_PIECE = 1 EMPTY_SPACE = 0 COMPUTER_PIECE = -1 -ComputerMove = collections.namedtuple('ComputerMove', ['x', 'y', 'm1', 'm2']) +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") @@ -36,8 +75,10 @@ def print_header(title): print() print() + def print_instructions(): - print(""" + 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 @@ -67,7 +108,8 @@ 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): @@ -79,208 +121,239 @@ def prompt_yes_no(msg): elif response[0] == "N": return False -def reverse_board_position(x): - assert(x >= 1 and x < 10) - - score = {1: 3, - 2: 2, - 3: 1, - 4: 6, - 5: 5, - 6: 4, - 7: 9, - 8: 8, - 9: 7} - return score[x] -def get_b(x, y): - data = [[-1, -1, -1, 1, 0, 0, 0, 1, 1], - [-1, -1, -1, 0, 1, 0, 1, 0, 1], - [-1, 0, -1, -1, 1, 0, 0, 0, 1], - [ 0, -1, -1, 1, -1, 0, 0, 0, 1], - [-1, 0, -1, 1, 1, 0, 0, 1, 0], - [-1, -1, 0, 1, 0, 1, 0, 0, 1], - [ 0, -1, -1, 0, -1, 1, 1, 0, 0], - [ 0, -1, -1, -1, 1, 1, 1, 0, 0], - [-1, 0, -1, -1, 0, 1, 0, 1, 0], - [ 0, -1, -1, 0, 1, 0, 0, 0, 1], - [ 0, -1, -1, 0, 1, 0, 1, 0, 0], - [-1, 0, -1, 1, 0, 0, 0, 0, 1], - [ 0, 0, -1, -1, -1, 1, 0, 0, 0], - [-1, 0, 0, 1, 1, 1, 0, 0, 0], - [ 0, -1, 0, -1, 1, 1, 0, 0, 0], - [-1, 0, 0, -1, -1, 1, 0, 0, 0], - [ 0, 0, -1, -1, 1, 0, 0, 0, 0], - [ 0, -1, 0, 1, -1, 0, 0, 0, 0], - [-1, 0, 0, -1, 1, 0, 0, 0, 0]] +def reverse_space_name(space_name): + # reverse a space name in the range 1-9 left to right + assert 1 <= space_name <= 9 - assert(x >= 1 and x < 20) - assert(y >= 1 and y < 10) + reflections = {1: 3, 2: 2, 3: 1, 4: 6, 5: 5, 6: 4, 7: 9, 8: 8, 9: 7} + return reflections[space_name] - return data[x-1][y-1] -m_data = [[24, 25, 36, 0], - [14, 15, 36, 0], - [15, 35, 36, 47], - [36, 58, 59, 0], - [15, 35, 36, 0], - [24, 25, 26, 0], - [26, 57, 58, 0], - [26, 35, 0, 0], - [47, 48, 0, 0], - [35, 36, 0, 0], - [35, 36, 0, 0], - [36, 0, 0, 0], - [47, 58, 0, 0], - [15, 0, 0, 0], - [26, 47, 0, 0], - [47, 58, 0, 0], - [35, 36, 47, 0], - [28, 58, 0, 0], - [15, 47, 0, 0]] +def is_space_in_center_column(space_name): + return reverse_space_name(space_name) == space_name -def get_m(x, y): - assert(x >= 1 and x < 20) - assert(y >= 1 and y < 5) - return m_data[x-1][y-1] +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 set_m(x, y, value): - m_data[x-1][y-1] = value def init_board(): - return ([COMPUTER_PIECE] * 3 + - [EMPTY_SPACE] * 3 + - [HUMAN_PIECE] * 3) + return [COMPUTER_PIECE] * 3 + [EMPTY_SPACE] * 3 + [HUMAN_PIECE] * 3 + def print_board(board): - piece_dict = {COMPUTER_PIECE: 'X', - EMPTY_SPACE: '.', - HUMAN_PIECE: 'O'} + piece_dict = {COMPUTER_PIECE: "X", EMPTY_SPACE: ".", HUMAN_PIECE: "O"} - space = " "*10 + space = " " * 10 print() - for i in range(3): + for row in range(3): line = "" - for j in range(3): + for column in range(3): line += space - space_number = i * 3 + j + 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?") + print("YOUR MOVE?") response = input() - m1, m2 = [int(c) for c in response.split(',')] + 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_move(board, m1, m2): + +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 - # line 160 - is_capture = (m2-m1 != -3) + + is_capture = m2 - m1 != -3 if is_capture and board_contents(board, m2) != COMPUTER_PIECE: # Destination does not contain computer piece return False - # line 170 + if m2 > m1: # can't move backwards return False - # line 180 + if (not is_capture) and board_contents(board, m2) != EMPTY_SPACE: # Destination is not open return False - # line 185 - if m2-m1 < -4: + + if m2 - m1 < -4: # too far return False - # line 186 + 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): + 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") - set_m(last_computer_move.x, last_computer_move.y, 0) - global l - l += 1 + remove_move(last_computer_move.board_index, last_computer_move.move_index) + global losses + losses += 1 + def computer_win(has_moves): - if has_moves: + if not has_moves: msg = "YOU CAN'T MOVE, SO " else: msg = "" msg += "I WIN" print(msg) - global w - w += 1 + global wins + wins += 1 + def show_scores(): - print(f"I HAVE WON {w} AND YOU {l} OUT OF {w+l} GAMES.") + print(f"I HAVE WON {wins} AND YOU {losses} OUT OF {wins + losses} GAMES.") print() + def human_has_move(board): - # line 690 for i in get_human_spaces(board): - if board_contents(board, i-3) == EMPTY_SPACE: + if board_contents(board, i - 3) == EMPTY_SPACE: # can move piece forward return True - elif reverse_board_position(i) == i: - # line 780 - # can capture from center - if ((board_contents(board, i-2) == COMPUTER_PIECE) or - (board_contents(board, i-4) == COMPUTER_PIECE)): + 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 + continue elif i < 7: - # Line 760 - assert((i == 4) or (i == 6)) - # can capture computer piece at 2 + 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)) + assert (i == 7) or (i == 9) # can capture computer piece at 5 return True else: @@ -289,38 +362,43 @@ def human_has_move(board): 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: + if board_contents(board, i + 3) == EMPTY_SPACE: # can move forward (down) return True - # line 260 - if reverse_board_position(i) == i: + 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)): + if (board_contents(board, i + 2) == HUMAN_PIECE) or ( + board_contents(board, i + 4) == HUMAN_PIECE + ): return True else: - # line 270 if i > 3: # beyond the first row if board_contents(board, 8) == HUMAN_PIECE: @@ -329,7 +407,6 @@ def has_computer_move(board): else: continue else: - # line 280 if board_contents(board, 5) == HUMAN_PIECE: # can capture on 5 return True @@ -337,104 +414,48 @@ def has_computer_move(board): continue return False -def get_flipped_table(b_line): # TODO remove table altogether - t = {} - # line 360 - for row in range(1, 4): - for column in range(1, 4): - # line 380 - flipped_column = 4 - column - - # fill out t to represent the data from b flipped left to right - space = (row-1) * 3 + column - flipped_space = (row - 1) * 3 + flipped_column - - t[space] = get_b(b_line, flipped_space) - return t -def board_matches_b(b_line, board): - for s in get_board_spaces(): - if get_b(b_line, s) != board_contents(board, s): - return False - return True +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 -def board_matches_flipped_b(b_line, board): - flipped_table = get_flipped_table(b_line) - - for s in get_board_spaces(): - if flipped_table[s] != board_contents(board, s): - return False - return True - -def does_b_line_match(b_line, board): - if board_matches_b(b_line, board): - return True, False - elif board_matches_flipped_b(b_line, board): - return True, True - else: - return False, None - -def has_any_m_table(x): - for i in range(1,5): - if get_m(x, i) != 0: - return True - return False - -def pick_from_m_table(x): - valid_y_list = [y for y in range(1,5) if get_m(x, y) != 0] - assert(len(valid_y_list) > 0) - return random.choice(valid_y_list) - - -def get_move_for_b_line(b_line, reverse_board): - # line 540 - x = b_line - - if not has_any_m_table(x): - return None - - # line 600 - y = pick_from_m_table(x) - - # line 610 - mxy = get_m(x, y) - m1 = mxy // 10 - m2 = mxy % 10 - if reverse_board: - m1 = reverse_board_position(m1) - m2 = reverse_board_position(m2) - - return ComputerMove(x, y, m1, m2) - - -def find_b_line_that_matches_board(board): - for b_line in range(1,20): - matches, reverse_board = does_b_line_match(b_line, board) - if matches: - return b_line, reverse_board - # THE TERMINATION OF THIS LOOP IS IMPOSSIBLE print("ILLEGAL BOARD PATTERN.") - assert(False) - - + assert False + + def pick_computer_move(board): if not has_computer_move(board): - # Line 340 return None - # line 350 - b_line, reverse_board = find_b_line_that_matches_board(board) + board_index, reverse_board = find_board_index_that_matches_board(board) - m = get_move_for_b_line(b_line, reverse_board) + m = boards[board_index].get_random_move(reverse_board) - if m == None: + if m is None: print("I RESIGN") return None - return m - - + 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(): @@ -444,149 +465,52 @@ def play_game(): while True: print_board(board) - - has_legal_move = False - while not has_legal_move: - m1, m2 = get_coordinates() - - if not is_legal_move(board, m1, m2): - print_illegal() - else: - # otherwise, acceptable move - has_legal_move = True - - set_board(board, m1, 0) - set_board(board, m2, 1) - - # line 205 + + 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)): + + if player_piece_on_back_row(board) or all_computer_pieces_captured(board): human_win(last_computer_move) return - - # line 230 + 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}") - set_board(board, m1, 0) - set_board(board, m2, -1) - - # line 640 + apply_move(board, m1, m2, COMPUTER_PIECE) + print_board(board) - - if (computer_piece_on_front_row(board) or - all_human_pieces_captured(board)): + + if computer_piece_on_front_row(board): computer_win(True) return - elif not human_has_move(board): + 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 w, l - w = 0 - l = 0 + global wins, losses + wins = 0 + losses = 0 while True: play_game() show_scores() - + + if __name__ == "__main__": main() - -""" -230 FOR I=1 TO 9 -240 IF S(I)<>-1 THEN 330 -250 IF S(I+3)=0 THEN 350 -260 IF FNR(I)=I THEN 320 -270 IF I>3 THEN 300 -280 IF S(5)=1 THEN 350 -290 GOTO 330 -300 IF S(8)=1 THEN 350 -310 GOTO 330 -320 IF S(I+2)=1 OR S(I+4)=1 THEN 350 -330 NEXT I -340 GOTO 820 -350 FOR I=1 TO 19 -360 FOR J=1 TO 3 -370 FOR K=3 TO 1 STEP -1 -380 T((J-1)*3+K)=B(I,(J-1)*3+4-K) -390 NEXT K -400 NEXT J -410 FOR J=1 TO 9 -420 IF S(J)<>B(I,J) THEN 460 -430 NEXT J -440 R=0 -450 GOTO 540 -460 FOR J=1 TO 9 -470 IF S(J)<>T(J) THEN 510 -480 NEXT J -490 R=1 -500 GOTO 540 -510 NEXT I -511 REMEMBER THE TERMINATION OF THIS LOOP IS IMPOSSIBLE -512 PRINT "ILLEGAL BOARD PATTERN." -530 STOP -540 X=I -550 FOR I=1 TO 4 -560 IF M(X,I)<>0 THEN 600 -570 NEXT I -580 PRINT "I RESIGN." -590 GOTO 820 -600 Y=INT(RND(1)*4+1) -601 IF M(X,Y)=0 THEN 600 -610 IF R<>0 THEN 630 -620 PRINT "I MOVE FROM ";STR$(INT(M(X,Y)/10));" TO ";STR$(FNM(M(X,Y))) -622 S(INT(M(X,Y)/10))=0 -623 S(FNM(M(X,Y)))=-1 -624 GOTO 640 -630 PRINT "I MOVE FROM ";STR$(FNR(INT(M(X,Y)/10)));" TO "; -631 PRINT STR$(FNR(FNM(M(X,Y)))) -632 S(FNR(INT(M(X,Y)/10)))=0 -633 S(FNR(FNM(M(X,Y))))=-1 -640 GOSUB 1000 -641 IF S(7)=-1 OR S(8)=-1 OR S(9)=-1 THEN 870 -650 FOR I=1 TO 9 -660 IF S(I)=1 THEN 690 -670 NEXT I -680 GOTO 870 -690 FOR I=1 TO 9 -700 IF S(I)<>1 THEN 790 -710 IF S(I-3)=0 THEN 120 -720 IF FNR(I)=I THEN 780 -730 IF I<7 THEN 760 -740 IF S(5)=-1 THEN 120 -750 GOTO 790 -760 IF S(2)=-1 THEN 120 -770 GOTO 790 -780 IF S(I-2)=-1 OR S(I-4)=-1 THEN 120 -790 NEXT I -800 PRINT "YOU CAN'T MOVE, SO "; -810 GOTO 870 -820 PRINT "YOU WIN." -830 M(X,Y)=0 -840 L=L+1 -850 PRINT "I HAVE WON";W;"AND YOU";L;"OUT OF";L+W;"GAMES." -851 PRINT -860 GOTO 100 -870 PRINT "I WIN." -880 W=W+1 -890 GOTO 850 -"""