mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-12 07:40:50 -08:00
233 lines
7.8 KiB
Python
233 lines
7.8 KiB
Python
from enum import IntEnum
|
|
from typing import Any, Tuple
|
|
|
|
|
|
class WinOptions(IntEnum):
|
|
Undefined = 0
|
|
TakeLast = 1
|
|
AvoidLast = 2
|
|
|
|
@classmethod
|
|
def _missing_(cls, value: Any) -> "WinOptions":
|
|
try:
|
|
int_value = int(value)
|
|
except Exception:
|
|
return WinOptions.Undefined
|
|
if int_value == 1:
|
|
return WinOptions.TakeLast
|
|
elif int_value == 2:
|
|
return WinOptions.AvoidLast
|
|
else:
|
|
return WinOptions.Undefined
|
|
|
|
|
|
class StartOptions(IntEnum):
|
|
Undefined = 0
|
|
ComputerFirst = 1
|
|
PlayerFirst = 2
|
|
|
|
@classmethod
|
|
def _missing_(cls, value: Any) -> "StartOptions":
|
|
try:
|
|
int_value = int(value)
|
|
except Exception:
|
|
return StartOptions.Undefined
|
|
if int_value == 1:
|
|
return StartOptions.ComputerFirst
|
|
elif int_value == 2:
|
|
return StartOptions.PlayerFirst
|
|
else:
|
|
return StartOptions.Undefined
|
|
|
|
|
|
def print_intro() -> None:
|
|
"""Print out the introduction and rules for the game."""
|
|
print("BATNUM".rjust(33, " "))
|
|
print("CREATIVE COMPUTING MORRISSTOWN, NEW JERSEY".rjust(15, " "))
|
|
print()
|
|
print()
|
|
print()
|
|
print("THIS PROGRAM IS A 'BATTLE OF NUMBERS' GAME, WHERE THE")
|
|
print("COMPUTER IS YOUR OPPONENT.")
|
|
print()
|
|
print("THE GAME STARTS WITH AN ASSUMED PILE OF OBJECTS. YOU")
|
|
print("AND YOUR OPPONENT ALTERNATELY REMOVE OBJECTS FROM THE PILE.")
|
|
print("WINNING IS DEFINED IN ADVANCE AS TAKING THE LAST OBJECT OR")
|
|
print("NOT. YOU CAN ALSO SPECIFY SOME OTHER BEGINNING CONDITIONS.")
|
|
print("DON'T USE ZERO, HOWEVER, IN PLAYING THE GAME.")
|
|
print("ENTER A NEGATIVE NUMBER FOR NEW PILE SIZE TO STOP PLAYING.")
|
|
print()
|
|
return
|
|
|
|
|
|
def get_params() -> Tuple[int, int, int, StartOptions, WinOptions]:
|
|
"""This requests the necessary parameters to play the game.
|
|
|
|
Returns a set with the five game parameters:
|
|
pile_size - the starting size of the object pile
|
|
min_select - minimum selection that can be made on each turn
|
|
max_select - maximum selection that can be made on each turn
|
|
start_option - 1 if the computer is first
|
|
or 2 if the player is first
|
|
win_option - 1 if the goal is to take the last object
|
|
or 2 if the goal is to not take the last object
|
|
"""
|
|
pile_size = get_pile_size()
|
|
if pile_size < 0:
|
|
return (-1, 0, 0, StartOptions.Undefined, WinOptions.Undefined)
|
|
win_option = get_win_option()
|
|
min_select, max_select = get_min_max()
|
|
start_option = get_start_option()
|
|
return (pile_size, min_select, max_select, start_option, win_option)
|
|
|
|
|
|
def get_pile_size() -> int:
|
|
# A negative number will stop the game.
|
|
pile_size = 0
|
|
while pile_size == 0:
|
|
try:
|
|
pile_size = int(input("ENTER PILE SIZE "))
|
|
except ValueError:
|
|
pile_size = 0
|
|
return pile_size
|
|
|
|
|
|
def get_win_option() -> WinOptions:
|
|
win_option: WinOptions = WinOptions.Undefined
|
|
while win_option == WinOptions.Undefined:
|
|
win_option = WinOptions(input("ENTER WIN OPTION - 1 TO TAKE LAST, 2 TO AVOID LAST: ")) # type: ignore
|
|
return win_option
|
|
|
|
|
|
def get_min_max() -> Tuple[int, int]:
|
|
min_select = 0
|
|
max_select = 0
|
|
while min_select < 1 or max_select < 1 or min_select > max_select:
|
|
(min_select, max_select) = (
|
|
int(x) for x in input("ENTER MIN AND MAX ").split(" ")
|
|
)
|
|
return min_select, max_select
|
|
|
|
|
|
def get_start_option() -> StartOptions:
|
|
start_option: StartOptions = StartOptions.Undefined
|
|
while start_option == StartOptions.Undefined:
|
|
start_option = StartOptions(input("ENTER START OPTION - 1 COMPUTER FIRST, 2 YOU FIRST ")) # type: ignore
|
|
return start_option
|
|
|
|
|
|
def player_move(
|
|
pile_size: int, min_select: int, max_select: int, win_option: WinOptions
|
|
) -> Tuple[bool, int]:
|
|
"""This handles the player's turn - asking the player how many objects
|
|
to take and doing some basic validation around that input. Then it
|
|
checks for any win conditions.
|
|
|
|
Returns a boolean indicating whether the game is over and the new pile_size."""
|
|
player_done = False
|
|
while not player_done:
|
|
player_move = int(input("YOUR MOVE "))
|
|
if player_move == 0:
|
|
print("I TOLD YOU NOT TO USE ZERO! COMPUTER WINS BY FORFEIT.")
|
|
return (True, pile_size)
|
|
if player_move > max_select or player_move < min_select:
|
|
print("ILLEGAL MOVE, REENTER IT")
|
|
continue
|
|
pile_size = pile_size - player_move
|
|
player_done = True
|
|
if pile_size <= 0:
|
|
if win_option == WinOptions.AvoidLast:
|
|
print("TOUGH LUCK, YOU LOSE.")
|
|
else:
|
|
print("CONGRATULATIONS, YOU WIN.")
|
|
return (True, pile_size)
|
|
return (False, pile_size)
|
|
|
|
|
|
def computer_pick(
|
|
pile_size: int, min_select: int, max_select: int, win_option: WinOptions
|
|
) -> int:
|
|
"""This handles the logic to determine how many objects the computer
|
|
will select on its turn.
|
|
"""
|
|
q = pile_size - 1 if win_option == WinOptions.AvoidLast else pile_size
|
|
c = min_select + max_select
|
|
computer_pick = q - (c * int(q / c))
|
|
computer_pick = max(computer_pick, min_select)
|
|
return min(computer_pick, max_select)
|
|
|
|
|
|
def computer_move(
|
|
pile_size: int, min_select: int, max_select: int, win_option: WinOptions
|
|
) -> Tuple[bool, int]:
|
|
"""This handles the computer's turn - first checking for the various
|
|
win/lose conditions and then calculating how many objects
|
|
the computer will take.
|
|
|
|
Returns a boolean indicating whether the game is over and the new pile_size."""
|
|
# First, check for win conditions on this move
|
|
# In this case, we win by taking the last object and
|
|
# the remaining pile is less than max select
|
|
# so the computer can grab them all and win
|
|
if win_option == WinOptions.TakeLast and pile_size <= max_select:
|
|
print(f"COMPUTER TAKES {pile_size} AND WINS.")
|
|
return (True, pile_size)
|
|
# In this case, we lose by taking the last object and
|
|
# the remaining pile is less than minsize and the computer
|
|
# has to take all of them.
|
|
if win_option == WinOptions.AvoidLast and pile_size <= min_select:
|
|
print(f"COMPUTER TAKES {min_select} AND LOSES.")
|
|
return (True, pile_size)
|
|
|
|
# Otherwise, we determine how many the computer selects
|
|
curr_sel = computer_pick(pile_size, min_select, max_select, win_option)
|
|
pile_size -= curr_sel
|
|
print(f"COMPUTER TAKES {curr_sel} AND LEAVES {pile_size}")
|
|
return (False, pile_size)
|
|
|
|
|
|
def play_game(
|
|
pile_size: int,
|
|
min_select: int,
|
|
max_select: int,
|
|
start_option: StartOptions,
|
|
win_option: WinOptions,
|
|
) -> None:
|
|
"""This is the main game loop - repeating each turn until one
|
|
of the win/lose conditions is met.
|
|
"""
|
|
game_over = False
|
|
# players_turn is a boolean keeping track of whether it's the
|
|
# player's or computer's turn
|
|
players_turn = start_option == StartOptions.PlayerFirst
|
|
|
|
while not game_over:
|
|
if players_turn:
|
|
(game_over, pile_size) = player_move(
|
|
pile_size, min_select, max_select, win_option
|
|
)
|
|
players_turn = False
|
|
if game_over:
|
|
return
|
|
if not players_turn:
|
|
(game_over, pile_size) = computer_move(
|
|
pile_size, min_select, max_select, win_option
|
|
)
|
|
players_turn = True
|
|
|
|
|
|
def main() -> None:
|
|
while True:
|
|
print_intro()
|
|
(pile_size, min_select, max_select, start_option, win_option) = get_params()
|
|
|
|
if pile_size < 0:
|
|
return
|
|
|
|
# Just keep playing the game until the user kills it with ctrl-C
|
|
play_game(pile_size, min_select, max_select, start_option, win_option)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|