From bacad32a618bc6b87c354b0e4757b3c74ad42eb7 Mon Sep 17 00:00:00 2001 From: Joe Nellis Date: Sun, 24 Apr 2022 17:39:03 -0700 Subject: [PATCH 1/7] Off by one error in for/range. Previously if the computer chose a secret code of zero (all blacks) it would skip initialization entirely and the player would not be able to finish the puzzle. Alternatively, if the computer chose the highest secret code (e.g. all Red in a three color puzzle), the actual secret code would be initialized to one less (RRW). --- 60_Mastermind/python/mastermind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index b37477fb..4c53439a 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -46,7 +46,7 @@ def main() -> None: print("Guess my combination ...") answer = int(possibilities * random.random()) numeric_answer = [-1] * num_positions - for _ in range(0, answer): + for _ in range(0, answer + 1): numeric_answer = get_possibility(numeric_answer) # human_readable_answer = make_human_readable(numeric_answer, color_letters) while num_moves < 10 and not turn_over: From 44983bb25fc9c121753005553581494aae16ee81 Mon Sep 17 00:00:00 2001 From: Joe Nellis Date: Fri, 29 Apr 2022 13:54:53 -0700 Subject: [PATCH 2/7] More off by 1 errors of the same type as the previous commit for the computer guessing the secret code. All errors are involved in the algortihm that converts an empty list to a guess. --- 60_Mastermind/python/mastermind.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index 4c53439a..843466ea 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -132,7 +132,7 @@ def main() -> None: inconsistent_information = True else: numeric_guess = [-1] * num_positions - for _ in range(0, guess): + for _ in range(0, guess+1): numeric_guess = get_possibility(numeric_guess) human_readable_guess = make_human_readable( numeric_guess, color_letters @@ -154,7 +154,7 @@ def main() -> None: if all_possibilities[i] == 0: # already ruled out continue numeric_possibility = [-1] * num_positions - for _ in range(0, i): + for _ in range(0, i+1): numeric_possibility = get_possibility( numeric_possibility ) From 4308713ec28156c9a574c5b2823a7d509d7d3b99 Mon Sep 17 00:00:00 2001 From: Joe Nellis Date: Fri, 29 Apr 2022 17:10:10 -0700 Subject: [PATCH 3/7] Refactoring of the #get_possibility and #make_human_readable methods into one method, #possibility_to_color_code, that just converts a solution permutation (out of the possible permutations) straight into a string code of letters. Both of these methods are essentially a flow of the first method being fed into the second and thus they are combined and the intermediate data structure (List[int]) is no longer needed. --- 60_Mastermind/python/mastermind.py | 103 +++++++++-------------------- 1 file changed, 32 insertions(+), 71 deletions(-) diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index 843466ea..9f98866f 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -6,7 +6,7 @@ from typing import List, Union colors = ["BLACK", "WHITE", "RED", "GREEN", "ORANGE", "YELLOW", "PURPLE", "TAN"] color_letters = "BWRGOYPT" num_positions = 0 -num_colors = 100 +num_colors: int = 100 human_score = 0 computer_score = 0 @@ -44,21 +44,16 @@ def main() -> None: guesses: List[List[Union[str, int]]] = [] turn_over = False print("Guess my combination ...") - answer = int(possibilities * random.random()) + secret_combination = int(possibilities * random.random()) + answer = possibility_to_color_code(secret_combination) numeric_answer = [-1] * num_positions - for _ in range(0, answer + 1): - 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(f"QUITTER! MY COMBINATION WAS: {answer}") print("GOOD BYE") quit() elif len(user_command) != num_positions: # 410 @@ -68,9 +63,7 @@ def main() -> None: if invalid_letters > "": print(f"INVALID GUESS: {invalid_letters}") else: - guess_results = compare_two_positions( - user_command, make_human_readable(numeric_answer, color_letters) - ) + guess_results = compare_two_positions(user_command, answer) print(f"Results: {guess_results}") if guess_results[1] == num_positions: # correct guess turn_over = True @@ -87,11 +80,7 @@ def main() -> None: 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) - ) - ) + print(f"THE ACTUAL COMBINATION WAS: {answer}") human_score = human_score + num_moves print_score(computer_score, human_score) @@ -107,23 +96,22 @@ def main() -> None: 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()) + possible_guess = int(possibilities * random.random()) if ( - all_possibilities[computer_guess] == 1 + all_possibilities[possible_guess] == 1 ): # random guess is possible, use it found_guess = True - guess = computer_guess else: - for i in range(computer_guess, possibilities): + for i in range(possible_guess + 1, possibilities): if all_possibilities[i] == 1: found_guess = True - guess = i + possible_guess = i break if not found_guess: - for i in range(0, computer_guess): + for i in range(0, possible_guess): if all_possibilities[i] == 1: found_guess = True - guess = i + possible_guess = i break if not found_guess: # inconsistent info from user print("YOU HAVE GIVEN ME INCONSISTENT INFORMATION.") @@ -131,13 +119,8 @@ def main() -> None: turn_over = True inconsistent_information = True else: - numeric_guess = [-1] * num_positions - for _ in range(0, guess+1): - numeric_guess = get_possibility(numeric_guess) - human_readable_guess = make_human_readable( - numeric_guess, color_letters - ) - print(f"My guess is: {human_readable_guess}") + 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(",") @@ -153,19 +136,12 @@ def main() -> None: 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+1): - numeric_possibility = get_possibility( - numeric_possibility - ) - human_readable_possibility = make_human_readable( - numeric_possibility, color_letters - ) # 4000 + possible_answer = possibility_to_color_code(i) comparison = compare_two_positions( - human_readable_possibility, human_readable_guess + possible_answer, computer_guess ) print(comparison) - if ((blacks != comparison[1]) or (whites != comparison[2])): # type: ignore + if (blacks != comparison[1]) or (whites != comparison[2]): # type: ignore all_possibilities[i] = 0 if not turn_over: # COMPUTER DID NOT GUESS print("I USED UP ALL MY MOVES!") @@ -197,27 +173,21 @@ 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 +# 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. +def possibility_to_color_code(possibility: int) -> str: + color_code: str = "" + pos: int = num_colors ** num_positions # start with total possibilities + remainder = possibility + for i 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 @@ -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() From cca3f941d2f696d770c5e1e5b1080fa02cc545a2 Mon Sep 17 00:00:00 2001 From: Joe Nellis Date: Fri, 29 Apr 2022 17:34:31 -0700 Subject: [PATCH 4/7] More cleanup refactoring, losing unused variables, shadowed globals, unnecessary spaces, and long lines, and debug prints. --- 60_Mastermind/python/mastermind.py | 32 ++++++++++++------------------ 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index 9f98866f..fb0ab7db 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -13,8 +13,6 @@ computer_score = 0 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" num_colors = 100 human_score = 0 @@ -28,7 +26,6 @@ def main() -> None: 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") @@ -46,7 +43,6 @@ def main() -> None: print("Guess my combination ...") secret_combination = int(possibilities * random.random()) answer = possibility_to_color_code(secret_combination) - numeric_answer = [-1] * num_positions while num_moves < 10 and not turn_over: print(f"Move # {num_moves} Guess : ") user_command = input("Guess ") @@ -64,12 +60,11 @@ def main() -> None: print(f"INVALID GUESS: {invalid_letters}") else: guess_results = compare_two_positions(user_command, answer) - 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) + print_score() else: print( "You have {} blacks and {} whites".format( @@ -82,10 +77,9 @@ def main() -> None: 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(computer_score, human_score) + print_score() # COMPUTER TURN - guesses = [] turn_over = False inconsistent_information = False while not turn_over and not inconsistent_information: @@ -130,7 +124,7 @@ def main() -> None: print(f"I GOT IT IN {num_moves} MOVES") turn_over = True computer_score = computer_score + num_moves - print_score(computer_score, human_score) + print_score() else: num_moves += 1 for i in range(0, possibilities): @@ -140,16 +134,15 @@ def main() -> None: comparison = compare_two_positions( possible_answer, computer_guess ) - print(comparison) - if (blacks != comparison[1]) or (whites != comparison[2]): # type: ignore + if (blacks != comparison[1]) or (whites != comparison[2]): all_possibilities[i] = 0 if not turn_over: # COMPUTER DID NOT GUESS 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) + print_score() current_round += 1 - print_score(computer_score, human_score, is_final_score=True) + print_score(is_final_score=True) sys.exit() @@ -192,7 +185,8 @@ def possibility_to_color_code(possibility: int) -> str: # 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 @@ -204,20 +198,20 @@ def compare_two_positions(guess: str, answer: str) -> List[Union[str, int]]: 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") From 8bc2b33a581f9726cac463649cd39dcccc507fbf Mon Sep 17 00:00:00 2001 From: Joe Nellis Date: Fri, 29 Apr 2022 22:20:22 -0700 Subject: [PATCH 5/7] Refactoring to distinguish setup of the game from the actual game. Changing globals to uppercase to signal constancy while assigning them via return from #setup_game. The two remaining mutable globals are for score keeping. Maintain commenting style of multistring inside method body. --- 60_Mastermind/python/mastermind.py | 91 ++++++++++++++++-------------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index fb0ab7db..cb89a97b 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -1,27 +1,20 @@ 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: int = 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 - - num_colors = 100 - human_score = 0 - computer_score = 0 - +# 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 - 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 @@ -31,17 +24,30 @@ def main() -> None: 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: + global human_score, computer_score 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 ...") - secret_combination = int(possibilities * random.random()) + secret_combination = int(POSSIBILITIES * random.random()) answer = possibility_to_color_code(secret_combination) while num_moves < 10 and not turn_over: print(f"Move # {num_moves} Guess : ") @@ -52,7 +58,7 @@ def main() -> None: print(f"QUITTER! MY COMBINATION WAS: {answer}") print("GOOD BYE") quit() - elif len(user_command) != num_positions: # 410 + elif len(user_command) != NUM_POSITIONS: # 410 print("BAD NUMBER OF POSITIONS") else: invalid_letters = get_invalid_letters(user_command) @@ -60,7 +66,7 @@ def main() -> None: print(f"INVALID GUESS: {invalid_letters}") else: guess_results = compare_two_positions(user_command, answer) - if guess_results[1] == num_positions: # correct guess + 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 @@ -83,20 +89,20 @@ def main() -> None: turn_over = False inconsistent_information = False while not turn_over and not inconsistent_information: - all_possibilities = [1] * possibilities + 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 - possible_guess = int(possibilities * random.random()) + possible_guess = int(POSSIBILITIES * random.random()) if ( all_possibilities[possible_guess] == 1 ): # random guess is possible, use it found_guess = True else: - for i in range(possible_guess + 1, possibilities): + for i in range(possible_guess + 1, POSSIBILITIES): if all_possibilities[i] == 1: found_guess = True possible_guess = i @@ -120,14 +126,14 @@ def main() -> None: ).split(",") blacks = int(blacks_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") turn_over = True computer_score = computer_score + num_moves print_score() else: num_moves += 1 - for i in range(0, possibilities): + for i in range(0, POSSIBILITIES): if all_possibilities[i] == 0: # already ruled out continue possible_answer = possibility_to_color_code(i) @@ -149,7 +155,7 @@ def main() -> None: # 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: @@ -166,20 +172,21 @@ def print_board(guesses) -> None: print(f"{idx + 1}\t{guess[0]}\t{guess[1]} {guess[2]}") -# 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. + 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 + pos: int = NUM_COLORS ** NUM_POSITIONS # start with total possibilities remainder = possibility - for i in range(num_positions-1, 0, -1): # process all but the last digit - pos = pos // num_colors - color_code += color_letters[remainder // pos] + for i 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 + color_code += COLOR_LETTERS[remainder] # last digit is what remains return color_code @@ -191,9 +198,9 @@ def compare_two_positions(guess: str, answer: str) -> List[Union[str, int]]: 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 From f74367fa59aafae89b848d9dd3f871e9959dc599 Mon Sep 17 00:00:00 2001 From: Joe Nellis Date: Sat, 30 Apr 2022 00:42:24 -0700 Subject: [PATCH 6/7] Refactoring player turn and computer turn to separate methods. Computer turn logic previously ignored counting turns. Computer turn previously gave up the round if the user enters "inconsistent information" about the computers guess when it should have restarted the computers turn. Refactoring to remove usage of 'flag' variables to control program flow. --- 60_Mastermind/python/mastermind.py | 215 ++++++++++++++--------------- 1 file changed, 107 insertions(+), 108 deletions(-) diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index cb89a97b..181a3e7e 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -37,121 +37,121 @@ computer_score = 0 def main() -> None: - global human_score, computer_score - current_round = 1 - 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 ...") - secret_combination = int(POSSIBILITIES * random.random()) - answer = possibility_to_color_code(secret_combination) - 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 - 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 - turn_over = True - print(f"You guessed it in {num_moves} moves!") - human_score = human_score + num_moves - print_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(f"THE ACTUAL COMBINATION WAS: {answer}") - human_score = human_score + num_moves - print_score() - - # COMPUTER TURN - 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 - possible_guess = int(POSSIBILITIES * random.random()) - if ( - all_possibilities[possible_guess] == 1 - ): # random guess is possible, use it - found_guess = True - else: - for i in range(possible_guess + 1, POSSIBILITIES): - if all_possibilities[i] == 1: - found_guess = True - possible_guess = i - break - if not found_guess: - for i in range(0, possible_guess): - if all_possibilities[i] == 1: - found_guess = True - possible_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: - 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") - turn_over = True - computer_score = computer_score + num_moves - print_score() - else: - num_moves += 1 - 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 not turn_over: # COMPUTER DID NOT GUESS - 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() + 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( + "You have {} blacks and {} whites".format( + guess_results[1], guess_results[2] + ) + ) + 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.""" @@ -172,7 +172,6 @@ def print_board(guesses) -> None: 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. From 84fce13f359685b3104f9a8179d73e3371a63299 Mon Sep 17 00:00:00 2001 From: Joe Nellis Date: Sat, 30 Apr 2022 00:42:24 -0700 Subject: [PATCH 7/7] Refactoring player turn and computer turn to separate methods. Computer turn logic previously ignored counting turns. Computer turn previously gave up the round if the user enters "inconsistent information" about the computers guess when it should have restarted the computers turn. Refactoring to remove usage of 'flag' variables to control program flow. --- 60_Mastermind/python/mastermind.py | 209 ++++++++++++++--------------- 1 file changed, 104 insertions(+), 105 deletions(-) diff --git a/60_Mastermind/python/mastermind.py b/60_Mastermind/python/mastermind.py index cb89a97b..cc77153c 100644 --- a/60_Mastermind/python/mastermind.py +++ b/60_Mastermind/python/mastermind.py @@ -5,15 +5,15 @@ 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(""" + print(""" MASTERMIND CREATIVE COMPUTING MORRISTOWN, NEW JERSEY - - - + + + """) # get user inputs for game conditions - num_colors: int = len(COLOR_LETTERS)+1 + 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 @@ -37,119 +37,119 @@ computer_score = 0 def main() -> None: - global human_score, computer_score - current_round = 1 - 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 ...") - secret_combination = int(POSSIBILITIES * random.random()) - answer = possibility_to_color_code(secret_combination) - 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 - print(f"QUITTER! MY COMBINATION WAS: {answer}") - print("GOOD BYE") - quit() - elif len(user_command) != NUM_POSITIONS: # 410 - print("BAD NUMBER OF POSITIONS") + 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: - invalid_letters = get_invalid_letters(user_command) - if invalid_letters > "": - print(f"INVALID GUESS: {invalid_letters}") + 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: - guess_results = compare_two_positions(user_command, answer) - 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() - else: - print( - "You have {} blacks and {} whites".format( - guess_results[1], guess_results[2] - ) + 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 + ) + 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 - # COMPUTER TURN - 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 - possible_guess = int(POSSIBILITIES * random.random()) - if ( - all_possibilities[possible_guess] == 1 - ): # random guess is possible, use it - found_guess = True - else: - for i in range(possible_guess + 1, POSSIBILITIES): - if all_possibilities[i] == 1: - found_guess = True - possible_guess = i - break - if not found_guess: - for i in range(0, possible_guess): - if all_possibilities[i] == 1: - found_guess = True - possible_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: - 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") - turn_over = True - computer_score = computer_score + num_moves - print_score() - else: - num_moves += 1 - 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 not turn_over: # COMPUTER DID NOT GUESS + +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() - current_round += 1 - print_score(is_final_score=True) - sys.exit() + 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 @@ -172,7 +172,6 @@ def print_board(guesses) -> None: 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. @@ -182,7 +181,7 @@ def possibility_to_color_code(possibility: int) -> str: color_code: str = "" pos: int = NUM_COLORS ** NUM_POSITIONS # start with total possibilities remainder = possibility - for i in range(NUM_POSITIONS - 1, 0, -1): # process all but the last digit + 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