mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-05 20:40:25 -08:00
227 lines
8.5 KiB
Python
227 lines
8.5 KiB
Python
import random
|
|
import sys
|
|
from typing import List, Union, Tuple
|
|
|
|
|
|
# define some parameters for the game which should not be modified.
|
|
def setup_game() -> Tuple[int, int, int, int]:
|
|
print("""
|
|
MASTERMIND
|
|
CREATIVE COMPUTING MORRISTOWN, NEW JERSEY
|
|
|
|
|
|
|
|
""")
|
|
# get user inputs for game conditions
|
|
num_colors: int = len(COLOR_LETTERS) + 1
|
|
while num_colors > len(COLOR_LETTERS):
|
|
num_colors = int(input("Number of colors (max 8): ")) # C9 in BASIC
|
|
num_positions = int(input("Number of positions: ")) # P9 in BASIC
|
|
num_rounds = int(input("Number of rounds: ")) # R9 in BASIC
|
|
possibilities = num_colors**num_positions
|
|
|
|
print(f"Number of possibilities {possibilities}")
|
|
print("Color\tLetter")
|
|
print("=====\t======")
|
|
for element in range(0, num_colors):
|
|
print(f"{COLORS[element]}\t{COLORS[element][0]}")
|
|
return num_colors, num_positions, num_rounds, possibilities
|
|
|
|
|
|
# Global variables
|
|
COLORS = ["BLACK", "WHITE", "RED", "GREEN", "ORANGE", "YELLOW", "PURPLE", "TAN"]
|
|
COLOR_LETTERS = "BWRGOYPT"
|
|
NUM_COLORS, NUM_POSITIONS, NUM_ROUNDS, POSSIBILITIES = setup_game()
|
|
human_score = 0
|
|
computer_score = 0
|
|
|
|
|
|
def main() -> None:
|
|
current_round = 1
|
|
while current_round <= NUM_ROUNDS:
|
|
print(f"Round number {current_round}")
|
|
human_turn()
|
|
computer_turn()
|
|
current_round += 1
|
|
print_score(is_final_score=True)
|
|
sys.exit()
|
|
|
|
|
|
def human_turn() -> None:
|
|
global human_score
|
|
num_moves = 1
|
|
guesses: List[List[Union[str, int]]] = []
|
|
print("Guess my combination ...")
|
|
secret_combination = int(POSSIBILITIES * random.random())
|
|
answer = possibility_to_color_code(secret_combination)
|
|
while True:
|
|
print(f"Move # {num_moves} Guess : ")
|
|
user_command = input("Guess ")
|
|
if user_command == "BOARD":
|
|
print_board(guesses) # 2000
|
|
elif user_command == "QUIT": # 2500
|
|
print(f"QUITTER! MY COMBINATION WAS: {answer}")
|
|
print("GOOD BYE")
|
|
quit()
|
|
elif len(user_command) != NUM_POSITIONS: # 410
|
|
print("BAD NUMBER OF POSITIONS")
|
|
else:
|
|
invalid_letters = get_invalid_letters(user_command)
|
|
if invalid_letters > "":
|
|
print(f"INVALID GUESS: {invalid_letters}")
|
|
else:
|
|
guess_results = compare_two_positions(user_command, answer)
|
|
if guess_results[1] == NUM_POSITIONS: # correct guess
|
|
print(f"You guessed it in {num_moves} moves!")
|
|
human_score = human_score + num_moves
|
|
print_score()
|
|
return # from human turn, triumphant
|
|
else:
|
|
print(f"You have {guess_results[1]} blacks and {guess_results[2]} whites")
|
|
guesses.append(guess_results)
|
|
num_moves += 1
|
|
|
|
if num_moves > 10: # RAN OUT OF MOVES
|
|
print("YOU RAN OUT OF MOVES! THAT'S ALL YOU GET!")
|
|
print(f"THE ACTUAL COMBINATION WAS: {answer}")
|
|
human_score = human_score + num_moves
|
|
print_score()
|
|
return # from human turn, defeated
|
|
|
|
|
|
def computer_turn() -> None:
|
|
global computer_score
|
|
while True:
|
|
all_possibilities = [1] * POSSIBILITIES
|
|
num_moves = 1
|
|
print("NOW I GUESS. THINK OF A COMBINATION.")
|
|
input("HIT RETURN WHEN READY: ")
|
|
while True:
|
|
possible_guess = find_first_solution_of(all_possibilities)
|
|
if possible_guess < 0: # no solutions left :(
|
|
print("YOU HAVE GIVEN ME INCONSISTENT INFORMATION.")
|
|
print("TRY AGAIN, AND THIS TIME PLEASE BE MORE CAREFUL.")
|
|
break # out of inner while loop, restart computer turn
|
|
|
|
computer_guess = possibility_to_color_code(possible_guess)
|
|
print(f"My guess is: {computer_guess}")
|
|
blacks_str, whites_str = input(
|
|
"ENTER BLACKS, WHITES (e.g. 1,2): "
|
|
).split(",")
|
|
blacks = int(blacks_str)
|
|
whites = int(whites_str)
|
|
if blacks == NUM_POSITIONS: # Correct guess
|
|
print(f"I GOT IT IN {num_moves} MOVES")
|
|
computer_score = computer_score + num_moves
|
|
print_score()
|
|
return # from computer turn
|
|
|
|
# computer guessed wrong, deduce which solutions to eliminate.
|
|
for i in range(0, POSSIBILITIES):
|
|
if all_possibilities[i] == 0: # already ruled out
|
|
continue
|
|
possible_answer = possibility_to_color_code(i)
|
|
comparison = compare_two_positions(
|
|
possible_answer, computer_guess
|
|
)
|
|
if (blacks != comparison[1]) or (whites != comparison[2]):
|
|
all_possibilities[i] = 0
|
|
|
|
if num_moves == 10:
|
|
print("I USED UP ALL MY MOVES!")
|
|
print("I GUESS MY CPU IS JUST HAVING AN OFF DAY.")
|
|
computer_score = computer_score + num_moves
|
|
print_score()
|
|
return # from computer turn, defeated.
|
|
num_moves += 1
|
|
|
|
|
|
def find_first_solution_of(all_possibilities: List[int]) -> int:
|
|
"""Scan through all_possibilities for first remaining non-zero marker,
|
|
starting from some random position and wrapping around if needed.
|
|
If not found return -1."""
|
|
start = int(POSSIBILITIES * random.random())
|
|
for i in range(0, POSSIBILITIES):
|
|
solution = (i + start) % POSSIBILITIES
|
|
if all_possibilities[solution]:
|
|
return solution
|
|
return -1
|
|
|
|
|
|
# 470
|
|
def get_invalid_letters(user_command) -> str:
|
|
"""Makes sure player input consists of valid colors for selected game configuration."""
|
|
valid_colors = COLOR_LETTERS[:NUM_COLORS]
|
|
invalid_letters = ""
|
|
for letter in user_command:
|
|
if letter not in valid_colors:
|
|
invalid_letters = invalid_letters + letter
|
|
return invalid_letters
|
|
|
|
|
|
# 2000
|
|
def print_board(guesses) -> None:
|
|
"""Print previous guesses within the round."""
|
|
print("Board")
|
|
print("Move\tGuess\tBlack White")
|
|
for idx, guess in enumerate(guesses):
|
|
print(f"{idx + 1}\t{guess[0]}\t{guess[1]} {guess[2]}")
|
|
|
|
|
|
def possibility_to_color_code(possibility: int) -> str:
|
|
"""Accepts a (decimal) number representing one permutation in the realm of
|
|
possible secret codes and returns the color code mapped to that permutation.
|
|
This algorithm is essentially converting a decimal number to a number with
|
|
a base of #num_colors, where each color code letter represents a digit in
|
|
that #num_colors base."""
|
|
color_code: str = ""
|
|
pos: int = NUM_COLORS ** NUM_POSITIONS # start with total possibilities
|
|
remainder = possibility
|
|
for _ in range(NUM_POSITIONS - 1, 0, -1): # process all but the last digit
|
|
pos = pos // NUM_COLORS
|
|
color_code += COLOR_LETTERS[remainder // pos]
|
|
remainder = remainder % pos
|
|
color_code += COLOR_LETTERS[remainder] # last digit is what remains
|
|
return color_code
|
|
|
|
|
|
# 4500
|
|
def compare_two_positions(guess: str, answer: str) -> List[Union[str, int]]:
|
|
"""Returns blacks (correct color and position) and whites (correct color
|
|
only) for candidate position (guess) versus reference position (answer)."""
|
|
blacks = 0
|
|
whites = 0
|
|
initial_guess = guess
|
|
increment = 0
|
|
for pos in range(0, NUM_POSITIONS):
|
|
if guess[pos] != answer[pos]:
|
|
for pos2 in range(0, NUM_POSITIONS):
|
|
if guess[pos] == answer[pos2] and guess[pos2] != answer[pos2]: # correct color but not correct place
|
|
whites = whites + 1
|
|
answer = answer[:pos2] + chr(increment) + answer[pos2 + 1:]
|
|
guess = guess[:pos] + chr(increment + 1) + guess[pos + 1:]
|
|
increment = increment + 2
|
|
else: # correct color and placement
|
|
blacks = blacks + 1
|
|
# THIS IS DEVIOUSLY CLEVER
|
|
guess = guess[:pos] + chr(increment + 1) + guess[pos + 1:]
|
|
answer = answer[:pos] + chr(increment) + answer[pos + 1:]
|
|
increment = increment + 2
|
|
return [initial_guess, blacks, whites]
|
|
|
|
|
|
# 5000 + logic from 1160
|
|
def print_score(is_final_score: bool = False) -> None:
|
|
"""Print score after each turn ends, including final score at end of game."""
|
|
if is_final_score:
|
|
print("GAME OVER")
|
|
print("FINAL SCORE:")
|
|
else:
|
|
print("SCORE:")
|
|
print(f" COMPUTER {computer_score}")
|
|
print(f" HUMAN {human_score}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|