diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index b37477fb..cc77153c 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -1,186 +1,161 @@ import random import sys -from typing import List, Union - -# 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 +from typing import List, Union, Tuple -def main() -> None: - global colors, color_letters, num_positions, num_colors, human_score, computer_score - colors = ["BLACK", "WHITE", "RED", "GREEN", "ORANGE", "YELLOW", "PURPLE", "TAN"] - color_letters = "BWRGOYPT" +# 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 - num_colors = 100 - human_score = 0 - computer_score = 0 + + """) # get user inputs for game conditions - print("Mastermind") - print("Creative Computing Morristown, New Jersey") - while num_colors > 8: + 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 - all_possibilities = [1] * possibilities 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]}") + 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: + while current_round <= NUM_ROUNDS: print(f"Round number {current_round}") - num_moves = 1 - guesses: List[List[Union[str, int]]] = [] - turn_over = False - print("Guess my combination ...") - answer = int(possibilities * random.random()) - 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) + human_turn() + computer_turn() + current_round += 1 + print_score(is_final_score=True) + sys.exit() - # COMPUTER TURN - guesses = [] - turn_over = False - inconsistent_information = False - while not turn_over and not inconsistent_information: - all_possibilities = [1] * possibilities - num_moves = 1 - inconsistent_information = False - print("NOW I GUESS. THINK OF A COMBINATION.") - input("HIT RETURN WHEN READY: ") - while num_moves < 10 and not turn_over and not inconsistent_information: - found_guess = False - computer_guess = int(possibilities * random.random()) - if ( - all_possibilities[computer_guess] == 1 - ): # random guess is possible, use it - found_guess = True - guess = computer_guess + +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: - 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("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( + "You have {} blacks and {} whites".format( + guess_results[1], guess_results[2] + ) ) - print(f"My guess is: {human_readable_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") - turn_over = True - computer_score = computer_score + num_moves - print_score(computer_score, human_score) - else: - num_moves += 1 - for i in range(0, possibilities): - if all_possibilities[i] == 0: # already ruled out - continue - numeric_possibility = [-1] * num_positions - 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( - human_readable_possibility, human_readable_guess - ) - print(comparison) - if ((blacks != comparison[1]) or (whites != comparison[2])): # type: ignore - all_possibilities[i] = 0 - if not turn_over: # COMPUTER DID NOT GUESS + 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(computer_score, human_score) - current_round += 1 - print_score(computer_score, human_score, is_final_score=True) - sys.exit() + 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] + valid_colors = COLOR_LETTERS[:NUM_COLORS] invalid_letters = "" for letter in user_command: 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]}") -# 3500 -# Easily the place for most optimization, since they generate every possibility -# every time when checking for potential solutions -# From the original article: -# "We did try a version that kept an actual list of all possible combinations -# (as a string array), which was significantly faster than this versionn but -# which ate tremendous amounts of memory." -def get_possibility(possibility) -> List[int]: - # print(possibility) - if possibility[0] > -1: # 3530 - current_position = 0 # Python arrays are zero-indexed - while True: - if possibility[current_position] < num_colors - 1: # zero-index again - possibility[current_position] += 1 - return possibility - else: - possibility[current_position] = 0 - current_position += 1 - else: # 3524 - possibility = [0] * num_positions - return possibility +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).""" + """Returns blacks (correct color and position) and whites (correct color + only) for candidate position (guess) versus reference position (answer).""" increment = 0 blacks = 0 whites = 0 initial_guess = guess - for pos in range(0, num_positions): + for pos in range(0, NUM_POSITIONS): if guess[pos] != answer[pos]: - for pos2 in range(0, num_positions): + for pos2 in range(0, NUM_POSITIONS): if not ( guess[pos] != answer[pos2] or 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 :] + 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 :] + 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(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.""" if is_final_score: 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}") -# 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__": main()