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}")
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 num_moves = 1
guesses: List[List[Union[str, int]]] = [] guesses: List[List[Union[str, int]]] = []
turn_over = False
print("Guess my combination ...") print("Guess my combination ...")
answer = int(possibilities * random.random()) secret_combination = int(POSSIBILITIES * random.random())
numeric_answer = [-1] * num_positions answer = possibility_to_color_code(secret_combination)
for _ in range(0, answer): while True:
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 : ") print(f"Move # {num_moves} Guess : ")
user_command = input("Guess ") user_command = input("Guess ")
if user_command == "BOARD": if user_command == "BOARD":
print_board(guesses) # 2000 print_board(guesses) # 2000
elif user_command == "QUIT": # 2500 elif user_command == "QUIT": # 2500
human_readable_answer = make_human_readable( print(f"QUITTER! MY COMBINATION WAS: {answer}")
numeric_answer, color_letters
)
print(f"QUITTER! MY COMBINATION WAS: {human_readable_answer}")
print("GOOD BYE") print("GOOD BYE")
quit() quit()
elif len(user_command) != num_positions: # 410 elif len(user_command) != NUM_POSITIONS: # 410
print("BAD NUMBER OF POSITIONS") print("BAD NUMBER OF POSITIONS")
else: else:
invalid_letters = get_invalid_letters(user_command) invalid_letters = get_invalid_letters(user_command)
if invalid_letters > "": if invalid_letters > "":
print(f"INVALID GUESS: {invalid_letters}") print(f"INVALID GUESS: {invalid_letters}")
else: else:
guess_results = compare_two_positions( guess_results = compare_two_positions(user_command, answer)
user_command, make_human_readable(numeric_answer, color_letters) if guess_results[1] == NUM_POSITIONS: # correct guess
)
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!") print(f"You guessed it in {num_moves} moves!")
human_score = human_score + num_moves human_score = human_score + num_moves
print_score(computer_score, human_score) print_score()
return # from human turn, triumphant
else: else:
print( print(
"You have {} blacks and {} whites".format( "You have {} blacks and {} whites".format(
guess_results[1], guess_results[2] guess_results[1], guess_results[2]
) )
) )
num_moves = num_moves + 1
guesses.append(guess_results) guesses.append(guess_results)
if not turn_over: # RAN OUT OF MOVES num_moves += 1
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 if num_moves > 10: # RAN OUT OF MOVES
guesses = [] print("YOU RAN OUT OF MOVES! THAT'S ALL YOU GET!")
turn_over = False print(f"THE ACTUAL COMBINATION WAS: {answer}")
inconsistent_information = False human_score = human_score + num_moves
while not turn_over and not inconsistent_information: print_score()
all_possibilities = [1] * possibilities return # from human turn, defeated
def computer_turn() -> None:
global computer_score
while True:
all_possibilities = [1] * POSSIBILITIES
num_moves = 1 num_moves = 1
inconsistent_information = False
print("NOW I GUESS. THINK OF A COMBINATION.") print("NOW I GUESS. THINK OF A COMBINATION.")
input("HIT RETURN WHEN READY: ") input("HIT RETURN WHEN READY: ")
while num_moves < 10 and not turn_over and not inconsistent_information: while True:
found_guess = False possible_guess = find_first_solution_of(all_possibilities)
computer_guess = int(possibilities * random.random()) if possible_guess < 0: # no solutions left :(
if (
all_possibilities[computer_guess] == 1
): # random guess is possible, use it
found_guess = True
guess = computer_guess
else:
for i in range(computer_guess, possibilities):
if all_possibilities[i] == 1:
found_guess = True
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("YOU HAVE GIVEN ME INCONSISTENT INFORMATION.")
print("TRY AGAIN, AND THIS TIME PLEASE BE MORE CAREFUL.") print("TRY AGAIN, AND THIS TIME PLEASE BE MORE CAREFUL.")
turn_over = True break # out of inner while loop, restart computer turn
inconsistent_information = True
else: computer_guess = possibility_to_color_code(possible_guess)
numeric_guess = [-1] * num_positions print(f"My guess is: {computer_guess}")
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}")
blacks_str, whites_str = input( blacks_str, whites_str = input(
"ENTER BLACKS, WHITES (e.g. 1,2): " "ENTER BLACKS, WHITES (e.g. 1,2): "
).split(",") ).split(",")
blacks = int(blacks_str) blacks = int(blacks_str)
whites = int(whites_str) whites = int(whites_str)
if blacks == num_positions: # Correct guess if blacks == NUM_POSITIONS: # Correct guess
print(f"I GOT IT IN {num_moves} MOVES") print(f"I GOT IT IN {num_moves} MOVES")
turn_over = True
computer_score = computer_score + num_moves computer_score = computer_score + num_moves
print_score(computer_score, human_score) print_score()
else: return # from computer turn
num_moves += 1
for i in range(0, possibilities): # computer guessed wrong, deduce which solutions to eliminate.
for i in range(0, POSSIBILITIES):
if all_possibilities[i] == 0: # already ruled out if all_possibilities[i] == 0: # already ruled out
continue continue
numeric_possibility = [-1] * num_positions possible_answer = possibility_to_color_code(i)
for _ in range(0, i):
numeric_possibility = get_possibility(
numeric_possibility
)
human_readable_possibility = make_human_readable(
numeric_possibility, color_letters
) # 4000
comparison = compare_two_positions( comparison = compare_two_positions(
human_readable_possibility, human_readable_guess possible_answer, computer_guess
) )
print(comparison) if (blacks != comparison[1]) or (whites != comparison[2]):
if ((blacks != comparison[1]) or (whites != comparison[2])): # type: ignore
all_possibilities[i] = 0 all_possibilities[i] = 0
if not turn_over: # COMPUTER DID NOT GUESS
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,39 +172,34 @@ 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
@@ -247,7 +217,7 @@ def compare_two_positions(guess: str, answer: str) -> List[Union[str, int]]:
# 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()