Files
basic-computer-games/72_Queen/python/queen.py
Martin Thoma c500424956 Simplify Python Code
print_with_tab / print_with_whitespace is trivial with Python
string formatting and was mostly used in only 2 lines.
2022-04-02 07:32:09 +02:00

277 lines
9.0 KiB
Python
Executable File

#!/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 <https://chrisphan.com>
Supports Python version 3.8 or later.
"""
from random import random
from typing import Final, FrozenSet, Optional, Tuple
########################################################################################
# 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 intro() -> None:
"""Print the intro and print instructions if desired."""
print(" " * 33 + "Queen")
print(" " * 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.")