From 87aebad1dbf1a63c34617ce06710d002dc3a34d4 Mon Sep 17 00:00:00 2001 From: Christopher Phan Date: Thu, 10 Mar 2022 16:24:08 -0600 Subject: [PATCH 1/3] Add Python implementation of Queen --- 72_Queen/python/.flake8 | 4 + 72_Queen/python/queen.py | 273 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 72_Queen/python/.flake8 create mode 100644 72_Queen/python/queen.py diff --git a/72_Queen/python/.flake8 b/72_Queen/python/.flake8 new file mode 100644 index 00000000..6f094a9d --- /dev/null +++ b/72_Queen/python/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 88 +select = C,E,F,W,B,B950 +extend-ignore = E203, E501 diff --git a/72_Queen/python/queen.py b/72_Queen/python/queen.py new file mode 100644 index 00000000..4a87de87 --- /dev/null +++ b/72_Queen/python/queen.py @@ -0,0 +1,273 @@ +"""Implementation of queen.bas to Python.""" + +from typing import Final, Optional +from random import random + + +######################################################################################## +# Optional configs +######################################################################################## +# You can edit these variables to change the behavior of the game. +# +# The original implementation has a bug that allows a player to move off the board, +# e.g. start at the nonexistant space 91. Change the variable FIX_BOARD_BUG to ``True`` +# to fix this behavior. +# + +FIX_BOARD_BUG: Final[bool] = False + +# In the original implementation, the board is only printed once. Change the variable +# SHOW_BOARD_ALWAYS to ``True`` to display the board every time. + +SHOW_BOARD_ALWAYS: Final[bool] = False + +# In the original implementaiton, the board is printed a bit wonky because of the +# differing widths of the numbers. Change the variable ALIGNED_BOARD to ``True`` to +# fix this. + +ALIGNED_BOARD: Final[bool] = False + +######################################################################################## + +INSTR_TXT: Final[ + str +] = """WE ARE GOING TO PLAY A GAME BASED ON ONE OF THE CHESS +MOVES. OUR QUEEN WILL BE ABLE TO MOVE ONLY TO THE LEFT, +DOWN, OR DIAGONALLY DOWN AND TO THE LEFT. + +THE OBJECT OF THE GAME IS TO PLACE THE QUEEN IN THE LOWER +LEFT HAND SQUARE BY ALTERNATING MOVES BETWEEN YOU AND THE +COMPUTER. THE FIRST ONE TO PLACE THE QUEEN THERE WINS. + +YOU GO FIRST AND PLACE THE QUEEN IN ANY ONE OF THE SQUARES +ON THE TOP ROW OR RIGHT HAND COLUMN. +THAT WILL BE YOUR FIRST MOVE. +WE ALTERNATE MOVES. +YOU MAY FORFEIT BY TYPING '0' AS YOUR MOVE. +BE SURE TO PRESS THE RETURN KEY AFTER EACH RESPONSE. + +""" + + +WIN_MSG: Final[ + str +] = """C O N G R A T U L A T I O N S . . . + +YOU HAVE WON--VERY WELL PLAYED. +IT LOOKS LIKE I HAVE MET MY MATCH. +THANKS FOR PLAYING---I CAN'T WIN ALL THE TIME. + +""" + +LOSE_MSG: Final[ + str +] = """ +NICE TRY, BUT IT LOOKS LIKE I HAVE WON. +THANKS FOR PLAYING. + +""" + + +def loc_to_num(location: tuple[int, int], fix_align: bool = False) -> str: + """Convert a position given by row, column into a space number.""" + row, col = location + out_str: str = f"{row + 8 - col}{row + 1}" + if not fix_align or len(out_str) == 3: + return out_str + else: + return out_str + " " + + +GAME_BOARD: Final[str] = ( + "\n" + + "\n\n\n".join( + "".join(f" {loc_to_num((row, col), ALIGNED_BOARD)} " for col in range(8)) + for row in range(8) + ) + + "\n\n\n" +) + + +def num_to_loc(num: int) -> tuple[int, int]: + """Convert a space number into a position given by row, column.""" + row: int = num % 10 - 1 + col: int = row + 8 - (num - row - 1) // 10 + return row, col + + +# The win location +WIN_LOC: Final[tuple[int, int]] = (7, 0) + +# These are the places (other than the win condition) that the computer will always +# try to move into. +COMPUTER_SAFE_SPOTS: Final[frozenset[tuple[int, int]]] = frozenset( + [ + (2, 3), + (4, 5), + (5, 1), + (6, 2), + ] +) + +# These are the places that the computer will always try to move into. +COMPUTER_PREF_MOVES: Final[ + frozenset[tuple[int, int]] +] = COMPUTER_SAFE_SPOTS | frozenset([WIN_LOC]) + +# These are the locations (not including the win location) from which either player can +# force a win (but the computer will always choose one of the COMPUTER_PREF_MOVES). +SAFE_SPOTS: Final[frozenset[tuple[int, int]]] = COMPUTER_SAFE_SPOTS | frozenset( + [ + (0, 4), + (3, 7), + ] +) + + +def str_with_tab(indent: int, text: str, uppercase: bool = True) -> str: + """Create a string with ``indent`` spaces followed by ``text``.""" + if uppercase: + text = text.upper() + return " " * indent + text + + +def intro(): + """Print the intro and print instructions if desired.""" + print(str_with_tab(33, "Queen")) + print(str_with_tab(15, "Creative Computing Morristown, New Jersey")) + print("\n" * 2) + if ask("DO YOU WANT INSTRUCTIONS"): + print(INSTR_TXT) + + +def get_move(current_loc: Optional[tuple[int, int]]) -> tuple[int, int]: + """Get the next move from the player.""" + prompt: str + player_resp: str + move_raw: int + new_row: int + new_col: int + if current_loc is None: # It's the first turn + prompt = "WHERE WOULD YOU LIKE TO START? " + else: + prompt = "WHAT IS YOUR MOVE? " + row, col = current_loc + while True: + player_resp = input(prompt).strip() + try: + move_raw = int(player_resp) + if move_raw == 0: # Forfeit + return 8, 8 + new_row, new_col = num_to_loc(move_raw) + if current_loc is None: + if (new_row == 0 or new_col == 7) and ( + not FIX_BOARD_BUG or (new_col >= 0 and new_row < 8) + ): + return new_row, new_col + else: + prompt = ( + "PLEASE READ THE DIRECTIONS AGAIN.\n" + + "YOU HAVE BEGUN ILLEGALLY.\n\n" + + "WHERE WOULD YOU LIKE TO START? " + ) + else: + if ( + (new_row == row and new_col < col) # move left + or (new_col == col and new_row > row) # move down + or (new_row - row == col - new_col) # move diag left and down + ) and (not FIX_BOARD_BUG or (new_col >= 0 and new_row < 8)): + return new_row, new_col + else: + prompt = "Y O U C H E A T . . . TRY AGAIN? " + + except ValueError: + prompt = "!NUMBER EXPECTED - RETRY INPUT LINE\n? " + + +def random_computer_move(location: tuple[int, int]) -> tuple[int, int]: + """Make a random move.""" + row, col = location + if (z := random()) > 0.6: + # Move down one space + return row + 1, col + elif z > 0.3: + # Move diagonaly (left and down) one space + return row + 1, col - 1 + else: + # Move left one space + return row, col - 1 + + +def computer_move(location: tuple[int, int]) -> tuple[int, int]: + """Get the computer's move.""" + # If the player has made an optimal move, then choose a random move + if location in SAFE_SPOTS: + return random_computer_move(location) + # We don't need to implmement the logic of checking for the player's win, + # because that is checked before this function is called. + row, col = location + for k in range(7, 0, -1): + # If the computer can move left k spaces and end in up in a safe spot or win, + # do it. + if (new_loc := (row, col - k)) in COMPUTER_PREF_MOVES: + return new_loc + # If the computer can move down k spaces and end up in a safe spot or win, do it. + if (new_loc := (row + k, col)) in COMPUTER_PREF_MOVES: + return new_loc + # If the computer can move diagonally k spaces and end up in a safe spot or win, + # do it. + if (new_loc := (row + k, col - k)) in COMPUTER_PREF_MOVES: + return new_loc + # As a fallback, do a random move. (NOTE: This shouldn't actally happen--it + # should always be possible to make an optimal move if the player doesn't play + # in a location in SAFE_SPOTS. + return random_computer_move(location) + + +def main_game() -> None: + """Execute the main game.""" + game_over: bool = False + location: Optional[tuple[int, int]] = None # Indicate it is the first turn + while not game_over: + location = get_move(location) + if location == (8, 8): # (8, 8) is returned when the player enters 0 + print("\nIT LOOKS LIKE I HAVE WON BY FORFEIT.\n") + game_over = True + elif location == WIN_LOC: # Player wins (in lower left corner) + print(WIN_MSG) + game_over = True + else: + location = computer_move(location) + print(f"COMPUTER MOVES TO SQUARE {loc_to_num(location)}") + if location == WIN_LOC: # Computer wins (in lower left corner) + print(LOSE_MSG) + game_over = True + # The default behavior is not to show the board each turn, but + # this can be modified by changing a flag at the start of the file. + if not game_over and SHOW_BOARD_ALWAYS: + print(GAME_BOARD) + + +def ask(prompt: str) -> bool: + """Ask a yes/no question until user gives an understandable response.""" + inpt: str + while True: + # Normalize input to uppercase, no whitespace, then get first character + inpt = input(prompt + "? ").upper().strip()[0] + print() + if inpt == "Y": + return True + elif inpt == "N": + return False + print("PLEASE ANSWER 'YES' OR 'NO'.") + return False + + +if __name__ == "__main__": + intro() + still_playing: bool = True + while still_playing: + print(GAME_BOARD) + main_game() + still_playing = ask("ANYONE ELSE CARE TO TRY") + print("\nOK --- THANKS AGAIN.") From f0a31894efda99a1e1f9e64a124126a015b7c127 Mon Sep 17 00:00:00 2001 From: Christopher Phan Date: Thu, 10 Mar 2022 22:32:22 -0600 Subject: [PATCH 2/3] Make executible, add more info to docstring --- 72_Queen/python/queen.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) mode change 100644 => 100755 72_Queen/python/queen.py diff --git a/72_Queen/python/queen.py b/72_Queen/python/queen.py old mode 100644 new mode 100755 index 4a87de87..625f121c --- a/72_Queen/python/queen.py +++ b/72_Queen/python/queen.py @@ -1,4 +1,13 @@ -"""Implementation of queen.bas to Python.""" +#!/usr/bin/env python3 +""" +Implementation of Queens game in Python 3. + +Original game in BASIC by David Ahl in _BASIC Comuter Games_, published in 1978, +as reproduced here: + https://www.atariarchives.org/basicgames/showpage.php?page=133 + +Port to Python 3 by Christopher L. Phan +""" from typing import Final, Optional from random import random From 7f2ae3a2aaeae03b307edd76c3369dacc46ff2e2 Mon Sep 17 00:00:00 2001 From: Christopher Phan Date: Fri, 11 Mar 2022 09:16:13 -0600 Subject: [PATCH 3/3] Add support for Python 3.8 --- 72_Queen/python/README.md | 3 ++- 72_Queen/python/queen.py | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/72_Queen/python/README.md b/72_Queen/python/README.md index 781945ec..bfa388b7 100644 --- a/72_Queen/python/README.md +++ b/72_Queen/python/README.md @@ -1,3 +1,4 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) -Conversion to [Python](https://www.python.org/about/) +Conversion to [Python](https://www.python.org/about/) by Christopher Phan. +Supports Python version 3.8 or later. diff --git a/72_Queen/python/queen.py b/72_Queen/python/queen.py index 625f121c..5473a958 100755 --- a/72_Queen/python/queen.py +++ b/72_Queen/python/queen.py @@ -6,11 +6,13 @@ Original game in BASIC by David Ahl in _BASIC Comuter Games_, published in 1978, as reproduced here: https://www.atariarchives.org/basicgames/showpage.php?page=133 -Port to Python 3 by Christopher L. Phan +Port to Python 3 by Christopher L. Phan + +Supports Python version 3.8 or later. """ -from typing import Final, Optional from random import random +from typing import Final, FrozenSet, Optional, Tuple ######################################################################################## @@ -77,7 +79,7 @@ THANKS FOR PLAYING. """ -def loc_to_num(location: tuple[int, int], fix_align: bool = False) -> str: +def loc_to_num(location: Tuple[int, int], fix_align: bool = False) -> str: """Convert a position given by row, column into a space number.""" row, col = location out_str: str = f"{row + 8 - col}{row + 1}" @@ -97,7 +99,7 @@ GAME_BOARD: Final[str] = ( ) -def num_to_loc(num: int) -> tuple[int, int]: +def num_to_loc(num: int) -> Tuple[int, int]: """Convert a space number into a position given by row, column.""" row: int = num % 10 - 1 col: int = row + 8 - (num - row - 1) // 10 @@ -105,11 +107,11 @@ def num_to_loc(num: int) -> tuple[int, int]: # The win location -WIN_LOC: Final[tuple[int, int]] = (7, 0) +WIN_LOC: Final[Tuple[int, int]] = (7, 0) # These are the places (other than the win condition) that the computer will always # try to move into. -COMPUTER_SAFE_SPOTS: Final[frozenset[tuple[int, int]]] = frozenset( +COMPUTER_SAFE_SPOTS: Final[FrozenSet[Tuple[int, int]]] = frozenset( [ (2, 3), (4, 5), @@ -120,12 +122,12 @@ COMPUTER_SAFE_SPOTS: Final[frozenset[tuple[int, int]]] = frozenset( # These are the places that the computer will always try to move into. COMPUTER_PREF_MOVES: Final[ - frozenset[tuple[int, int]] + FrozenSet[Tuple[int, int]] ] = COMPUTER_SAFE_SPOTS | frozenset([WIN_LOC]) # These are the locations (not including the win location) from which either player can # force a win (but the computer will always choose one of the COMPUTER_PREF_MOVES). -SAFE_SPOTS: Final[frozenset[tuple[int, int]]] = COMPUTER_SAFE_SPOTS | frozenset( +SAFE_SPOTS: Final[FrozenSet[Tuple[int, int]]] = COMPUTER_SAFE_SPOTS | frozenset( [ (0, 4), (3, 7), @@ -149,7 +151,7 @@ def intro(): print(INSTR_TXT) -def get_move(current_loc: Optional[tuple[int, int]]) -> tuple[int, int]: +def get_move(current_loc: Optional[Tuple[int, int]]) -> Tuple[int, int]: """Get the next move from the player.""" prompt: str player_resp: str @@ -193,7 +195,7 @@ def get_move(current_loc: Optional[tuple[int, int]]) -> tuple[int, int]: prompt = "!NUMBER EXPECTED - RETRY INPUT LINE\n? " -def random_computer_move(location: tuple[int, int]) -> tuple[int, int]: +def random_computer_move(location: Tuple[int, int]) -> Tuple[int, int]: """Make a random move.""" row, col = location if (z := random()) > 0.6: @@ -207,7 +209,7 @@ def random_computer_move(location: tuple[int, int]) -> tuple[int, int]: return row, col - 1 -def computer_move(location: tuple[int, int]) -> tuple[int, int]: +def computer_move(location: Tuple[int, int]) -> Tuple[int, int]: """Get the computer's move.""" # If the player has made an optimal move, then choose a random move if location in SAFE_SPOTS: @@ -236,7 +238,7 @@ def computer_move(location: tuple[int, int]) -> tuple[int, int]: def main_game() -> None: """Execute the main game.""" game_over: bool = False - location: Optional[tuple[int, int]] = None # Indicate it is the first turn + location: Optional[Tuple[int, int]] = None # Indicate it is the first turn while not game_over: location = get_move(location) if location == (8, 8): # (8, 8) is returned when the player enters 0