Merge branch 'coding-horror:main' into fixes_for_Mastermind

This commit is contained in:
Anthony Rubick
2022-05-01 11:25:22 -07:00
committed by GitHub

View File

@@ -1,186 +1,161 @@
import random import random
import sys import sys
from typing import List, Union from typing import List, Union, Tuple
# Global variables
colors = ["BLACK", "WHITE", "RED", "GREEN", "ORANGE", "YELLOW", "PURPLE", "TAN"]
color_letters = "BWRGOYPT"
num_positions = 0
num_colors = 100
human_score = 0
computer_score = 0
def main() -> None: # define some parameters for the game which should not be modified.
global colors, color_letters, num_positions, num_colors, human_score, computer_score def setup_game() -> Tuple[int, int, int, int]:
colors = ["BLACK", "WHITE", "RED", "GREEN", "ORANGE", "YELLOW", "PURPLE", "TAN"] print("""
color_letters = "BWRGOYPT" MASTERMIND
CREATIVE COMPUTING MORRISTOWN, NEW JERSEY
num_colors = 100
human_score = 0
computer_score = 0
""")
# get user inputs for game conditions # get user inputs for game conditions
print("Mastermind") num_colors: int = len(COLOR_LETTERS) + 1
print("Creative Computing Morristown, New Jersey") while num_colors > len(COLOR_LETTERS):
while num_colors > 8:
num_colors = int(input("Number of colors (max 8): ")) # C9 in BASIC num_colors = int(input("Number of colors (max 8): ")) # C9 in BASIC
num_positions = int(input("Number of positions: ")) # P9 in BASIC num_positions = int(input("Number of positions: ")) # P9 in BASIC
num_rounds = int(input("Number of rounds: ")) # R9 in BASIC num_rounds = int(input("Number of rounds: ")) # R9 in BASIC
possibilities = num_colors**num_positions possibilities = num_colors**num_positions
all_possibilities = [1] * possibilities
print(f"Number of possibilities {possibilities}") print(f"Number of possibilities {possibilities}")
print("Color\tLetter") print("Color\tLetter")
print("=====\t======") print("=====\t======")
for element in range(0, num_colors): for element in range(0, num_colors):
print(f"{colors[element]}\t{colors[element][0]}") 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 current_round = 1
while current_round <= NUM_ROUNDS:
while current_round <= num_rounds:
print(f"Round number {current_round}") print(f"Round number {current_round}")
num_moves = 1 human_turn()
guesses: List[List[Union[str, int]]] = [] computer_turn()
turn_over = False current_round += 1
print("Guess my combination ...") print_score(is_final_score=True)
answer = int(possibilities * random.random()) sys.exit()
numeric_answer = [-1] * num_positions
for _ in range(0, answer):
numeric_answer = get_possibility(numeric_answer)
# human_readable_answer = make_human_readable(numeric_answer, color_letters)
while num_moves < 10 and not turn_over:
print(f"Move # {num_moves} Guess : ")
user_command = input("Guess ")
if user_command == "BOARD":
print_board(guesses) # 2000
elif user_command == "QUIT": # 2500
human_readable_answer = make_human_readable(
numeric_answer, color_letters
)
print(f"QUITTER! MY COMBINATION WAS: {human_readable_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, make_human_readable(numeric_answer, color_letters)
)
print(f"Results: {guess_results}")
if guess_results[1] == num_positions: # correct guess
turn_over = True
print(f"You guessed it in {num_moves} moves!")
human_score = human_score + num_moves
print_score(computer_score, human_score)
else:
print(
"You have {} blacks and {} whites".format(
guess_results[1], guess_results[2]
)
)
num_moves = num_moves + 1
guesses.append(guess_results)
if not turn_over: # RAN OUT OF MOVES
print("YOU RAN OUT OF MOVES! THAT'S ALL YOU GET!")
print(
"THE ACTUAL COMBINATION WAS: {}".format(
make_human_readable(numeric_answer, color_letters)
)
)
human_score = human_score + num_moves
print_score(computer_score, human_score)
# COMPUTER TURN
guesses = [] def human_turn() -> None:
turn_over = False global human_score
inconsistent_information = False num_moves = 1
while not turn_over and not inconsistent_information: guesses: List[List[Union[str, int]]] = []
all_possibilities = [1] * possibilities print("Guess my combination ...")
num_moves = 1 secret_combination = int(POSSIBILITIES * random.random())
inconsistent_information = False answer = possibility_to_color_code(secret_combination)
print("NOW I GUESS. THINK OF A COMBINATION.") while True:
input("HIT RETURN WHEN READY: ") print(f"Move # {num_moves} Guess : ")
while num_moves < 10 and not turn_over and not inconsistent_information: user_command = input("Guess ")
found_guess = False if user_command == "BOARD":
computer_guess = int(possibilities * random.random()) print_board(guesses) # 2000
if ( elif user_command == "QUIT": # 2500
all_possibilities[computer_guess] == 1 print(f"QUITTER! MY COMBINATION WAS: {answer}")
): # random guess is possible, use it print("GOOD BYE")
found_guess = True quit()
guess = computer_guess 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: else:
for i in range(computer_guess, possibilities): print(
if all_possibilities[i] == 1: "You have {} blacks and {} whites".format(
found_guess = True guess_results[1], guess_results[2]
guess = i )
break
if not found_guess:
for i in range(0, computer_guess):
if all_possibilities[i] == 1:
found_guess = True
guess = i
break
if not found_guess: # inconsistent info from user
print("YOU HAVE GIVEN ME INCONSISTENT INFORMATION.")
print("TRY AGAIN, AND THIS TIME PLEASE BE MORE CAREFUL.")
turn_over = True
inconsistent_information = True
else:
numeric_guess = [-1] * num_positions
for _ in range(0, guess):
numeric_guess = get_possibility(numeric_guess)
human_readable_guess = make_human_readable(
numeric_guess, color_letters
) )
print(f"My guess is: {human_readable_guess}") guesses.append(guess_results)
blacks_str, whites_str = input( num_moves += 1
"ENTER BLACKS, WHITES (e.g. 1,2): "
).split(",") if num_moves > 10: # RAN OUT OF MOVES
blacks = int(blacks_str) print("YOU RAN OUT OF MOVES! THAT'S ALL YOU GET!")
whites = int(whites_str) print(f"THE ACTUAL COMBINATION WAS: {answer}")
if blacks == num_positions: # Correct guess human_score = human_score + num_moves
print(f"I GOT IT IN {num_moves} MOVES") print_score()
turn_over = True return # from human turn, defeated
computer_score = computer_score + num_moves
print_score(computer_score, human_score)
else: def computer_turn() -> None:
num_moves += 1 global computer_score
for i in range(0, possibilities): while True:
if all_possibilities[i] == 0: # already ruled out all_possibilities = [1] * POSSIBILITIES
continue num_moves = 1
numeric_possibility = [-1] * num_positions print("NOW I GUESS. THINK OF A COMBINATION.")
for _ in range(0, i): input("HIT RETURN WHEN READY: ")
numeric_possibility = get_possibility( while True:
numeric_possibility possible_guess = find_first_solution_of(all_possibilities)
) if possible_guess < 0: # no solutions left :(
human_readable_possibility = make_human_readable( print("YOU HAVE GIVEN ME INCONSISTENT INFORMATION.")
numeric_possibility, color_letters print("TRY AGAIN, AND THIS TIME PLEASE BE MORE CAREFUL.")
) # 4000 break # out of inner while loop, restart computer turn
comparison = compare_two_positions(
human_readable_possibility, human_readable_guess computer_guess = possibility_to_color_code(possible_guess)
) print(f"My guess is: {computer_guess}")
print(comparison) blacks_str, whites_str = input(
if ((blacks != comparison[1]) or (whites != comparison[2])): # type: ignore "ENTER BLACKS, WHITES (e.g. 1,2): "
all_possibilities[i] = 0 ).split(",")
if not turn_over: # COMPUTER DID NOT GUESS 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 USED UP ALL MY MOVES!")
print("I GUESS MY CPU IS JUST HAVING AN OFF DAY.") print("I GUESS MY CPU IS JUST HAVING AN OFF DAY.")
computer_score = computer_score + num_moves computer_score = computer_score + num_moves
print_score(computer_score, human_score) print_score()
current_round += 1 return # from computer turn, defeated.
print_score(computer_score, human_score, is_final_score=True) num_moves += 1
sys.exit()
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 # 470
def get_invalid_letters(user_command) -> str: def get_invalid_letters(user_command) -> str:
"""Makes sure player input consists of valid colors for selected game configuration.""" """Makes sure player input consists of valid colors for selected game configuration."""
valid_colors = color_letters[:num_colors] valid_colors = COLOR_LETTERS[:NUM_COLORS]
invalid_letters = "" invalid_letters = ""
for letter in user_command: for letter in user_command:
if letter not in valid_colors: if letter not in valid_colors:
@@ -197,57 +172,52 @@ def print_board(guesses) -> None:
print(f"{idx + 1}\t{guess[0]}\t{guess[1]} {guess[2]}") print(f"{idx + 1}\t{guess[0]}\t{guess[1]} {guess[2]}")
# 3500 def possibility_to_color_code(possibility: int) -> str:
# Easily the place for most optimization, since they generate every possibility """Accepts a (decimal) number representing one permutation in the realm of
# every time when checking for potential solutions possible secret codes and returns the color code mapped to that permutation.
# From the original article: This algorithm is essentially converting a decimal number to a number with
# "We did try a version that kept an actual list of all possible combinations a base of #num_colors, where each color code letter represents a digit in
# (as a string array), which was significantly faster than this versionn but that #num_colors base."""
# which ate tremendous amounts of memory." color_code: str = ""
def get_possibility(possibility) -> List[int]: pos: int = NUM_COLORS ** NUM_POSITIONS # start with total possibilities
# print(possibility) remainder = possibility
if possibility[0] > -1: # 3530 for _ in range(NUM_POSITIONS - 1, 0, -1): # process all but the last digit
current_position = 0 # Python arrays are zero-indexed pos = pos // NUM_COLORS
while True: color_code += COLOR_LETTERS[remainder // pos]
if possibility[current_position] < num_colors - 1: # zero-index again remainder = remainder % pos
possibility[current_position] += 1 color_code += COLOR_LETTERS[remainder] # last digit is what remains
return possibility return color_code
else:
possibility[current_position] = 0
current_position += 1
else: # 3524
possibility = [0] * num_positions
return possibility
# 4500 # 4500
def compare_two_positions(guess: str, answer: str) -> List[Union[str, int]]: 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).""" """Returns blacks (correct color and position) and whites (correct color
only) for candidate position (guess) versus reference position (answer)."""
increment = 0 increment = 0
blacks = 0 blacks = 0
whites = 0 whites = 0
initial_guess = guess initial_guess = guess
for pos in range(0, num_positions): for pos in range(0, NUM_POSITIONS):
if guess[pos] != answer[pos]: if guess[pos] != answer[pos]:
for pos2 in range(0, num_positions): for pos2 in range(0, NUM_POSITIONS):
if not ( if not (
guess[pos] != answer[pos2] or guess[pos2] == answer[pos2] guess[pos] != answer[pos2] or guess[pos2] == answer[pos2]
): # correct color but not correct place ): # correct color but not correct place
whites = whites + 1 whites = whites + 1
answer = answer[:pos2] + chr(increment) + answer[pos2 + 1 :] answer = answer[:pos2] + chr(increment) + answer[pos2 + 1:]
guess = guess[:pos] + chr(increment + 1) + guess[pos + 1 :] guess = guess[:pos] + chr(increment + 1) + guess[pos + 1:]
increment = increment + 2 increment = increment + 2
else: # correct color and placement else: # correct color and placement
blacks = blacks + 1 blacks = blacks + 1
# THIS IS DEVIOUSLY CLEVER # THIS IS DEVIOUSLY CLEVER
guess = guess[:pos] + chr(increment + 1) + guess[pos + 1 :] guess = guess[:pos] + chr(increment + 1) + guess[pos + 1:]
answer = answer[:pos] + chr(increment) + answer[pos + 1 :] answer = answer[:pos] + chr(increment) + answer[pos + 1:]
increment = increment + 2 increment = increment + 2
return [initial_guess, blacks, whites] return [initial_guess, blacks, whites]
# 5000 + logic from 1160 # 5000 + logic from 1160
def print_score(computer_score, human_score, is_final_score: bool = False) -> None: def print_score(is_final_score: bool = False) -> None:
"""Print score after each turn ends, including final score at end of game.""" """Print score after each turn ends, including final score at end of game."""
if is_final_score: if is_final_score:
print("GAME OVER") print("GAME OVER")
@@ -258,14 +228,5 @@ def print_score(computer_score, human_score, is_final_score: bool = False) -> No
print(f" HUMAN {human_score}") print(f" HUMAN {human_score}")
# 4000, 5500, 6000 subroutines are all identical
def make_human_readable(num: List[int], color_letters) -> str:
"""Make the numeric representation of a position human readable."""
retval = ""
for i in range(0, len(num)):
retval = retval + color_letters[int(num[i])]
return retval
if __name__ == "__main__": if __name__ == "__main__":
main() main()