mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-12 15:50:20 -08:00
Unfixes the fix introduced in a previous commit regarding how the computer deduces the answer. ReadMe updated with a thorough explanation of the deduction process.
This commit is contained in:
@@ -28,20 +28,126 @@ As published in Basic Computer Games (1978):
|
||||
Downloaded from Vintage Basic at
|
||||
http://www.vintage-basic.net/games.html
|
||||
|
||||
#### Porting Notes
|
||||
###How the computer deduces your guess.
|
||||
|
||||
in [#613](https://github.com/coding-horror/basic-computer-games/pull/613)
|
||||
The computer takes the number of black pegs and white pegs that the user reports
|
||||
and uses that information as a target. It then assumes its guess is the answer
|
||||
and proceeds to compare the black and white pegs against all remaining possible
|
||||
answers. For each set of black and white pegs it gets in these comparisons, if
|
||||
they don't match what the user reported, then they can not be part of the solution.
|
||||
This can be a non-intuitive assumption, so we'll walk through it with a three color,
|
||||
three position example (27 possible solutions.)
|
||||
|
||||
Let's just suppose our secret code we're hiding from the computer is `BWB`
|
||||
|
||||
First let's point out the commutative property of comparing two codes for their
|
||||
black and white pegs. A black peg meaning correct color and correct position, and
|
||||
a white peg meaning correct color and wrong position. If the computer guesses
|
||||
`RBW` then the black/white peg report is 0 black, 2 white. But if `RBW` is the
|
||||
secret code and the computer guesses `BWB` the reporting for `BWB` is going to be
|
||||
the same, 0 black, 2 white.
|
||||
|
||||
Now lets look at a table with the reporting for every possible guess the computer
|
||||
can make while our secret code is `BWB`.
|
||||
|
||||
| Guess | Black | White | | Guess | Black | White | | Guess | Black | White |
|
||||
|-------|-------|-------|-----|-------|-------|-------|-----|-------|-------|-------|
|
||||
| BBB | 2 | 0 | | WBB | 1 | 2 | | RBB | 1 | 1 |
|
||||
| BBW | 1 | 2 | | WBW | 0 | 2 | | RBW | 0 | 2 |
|
||||
| BBR | 1 | 1 | | WBR | 0 | 2 | | RBR | 0 | 1 |
|
||||
| BWB | 3 | 0 | | WWB | 2 | 0 | | RWB | 2 | 0 |
|
||||
| BWW | 2 | 0 | | WWW | 1 | 0 | | RWW | 1 | 0 |
|
||||
| BWR | 2 | 0 | | WWR | 1 | 0 | | RWR | 1 | 0 |
|
||||
| BRB | 2 | 0 | | WRB | 1 | 1 | | RRB | 1 | 0 |
|
||||
| BRW | 1 | 1 | | WRW | 0 | 1 | | RRW | 0 | 1 |
|
||||
| BRR | 1 | 0 | | WRR | 0 | 1 | | RRR | 0 | 0 |
|
||||
|
||||
The computer has guessed `RBW` and the report on it is 0 black, 2 white. The code
|
||||
used to eliminate other solutions looks like this:
|
||||
|
||||
`1060 IF B1<>B OR W1<>W THEN I(X)=0`
|
||||
|
||||
was changed to:
|
||||
which says set `RBW` as the secret and compare it to all remaining solutions and
|
||||
get rid of any that don't match the same black and white report, 0 black and 2 white.
|
||||
So let's do that.
|
||||
|
||||
`1060 IF B1>B OR W1>W THEN I(X)=0`
|
||||
Remember, `RBW` is pretending to be the secret code here. These are the remaining
|
||||
solutions reporting their black and white pegs against `RBW`.
|
||||
|
||||
This was done because of a bug:
|
||||
| Guess | Black | White | | Guess | Black | White | | Guess | Black | White |
|
||||
|-------|-------|-------|-----|-------|-------|-------|-----|-------|-------|-------|
|
||||
| BBB | 1 | 0 | | WBB | 1 | 1 | | RBB | 2 | 0 |
|
||||
| BBW | 2 | 0 | | WBW | 2 | 0 | | RBW | 3 | 0 |
|
||||
| BBR | 1 | 1 | | WBR | 1 | 2 | | RBR | 2 | 0 |
|
||||
| BWB | 0 | 2 | | WWB | 0 | 2 | | RWB | 1 | 2 |
|
||||
| BWW | 1 | 1 | | WWW | 1 | 0 | | RWW | 2 | 0 |
|
||||
| BWR | 0 | 3 | | WWR | 1 | 1 | | RWR | 1 | 1 |
|
||||
| BRB | 0 | 2 | | WRB | 0 | 3 | | RRB | 1 | 1 |
|
||||
| BRW | 1 | 2 | | WRW | 1 | 1 | | RRW | 2 | 0 |
|
||||
| BRR | 0 | 2 | | WRR | 0 | 2 | | RRR | 1 | 0 |
|
||||
|
||||
Originally, after guessing and getting feedback, the computer would look through every possible combination, and for all that haven't previously been marked as impossible it would check whether or not the black and white pins that that combination should get are not-equal to what its previous guess got and, if they are equal, the combination would be marked as possible, and if they aren't equal then the combination would be marked as impossible. This results in a bug where the computer eliminates the correct answer as a possible solution after the first guess, unless the first guess just happens to be correct.
|
||||
Now we are going to eliminate every solution that **DOESN'T** matches 0 black and 2 white.
|
||||
|
||||
this was discussed in more detail in [issue #611](https://github.com/coding-horror/basic-computer-games/issues/611)
|
||||
| Guess | Black | White | | Guess | Black | White | | Guess | Black | White |
|
||||
|----------|-------|-------|-----|----------|-------|-------|-----|----------|-------|-------|
|
||||
| ~~~BBB~~ | 1 | 0 | | ~~~WBB~~ | 1 | 1 | | ~~~RBB~~ | 2 | 0 |
|
||||
| ~~~BBW~~ | 2 | 0 | | ~~~WBW~~ | 2 | 0 | | ~~~RBW~~ | 3 | 0 |
|
||||
| ~~~BBR~~ | 1 | 1 | | ~~~WBR~~ | 1 | 2 | | ~~~RBR~~ | 2 | 0 |
|
||||
| BWB | 0 | 2 | | WWB | 0 | 2 | | ~~~RWB~~ | 1 | 2 |
|
||||
| ~~~BWW~~ | 1 | 1 | | ~~~WWW~~ | 1 | 0 | | ~~~RWW~~ | 2 | 0 |
|
||||
| ~~~BWR~~ | 0 | 3 | | ~~~WWR~~ | 1 | 1 | | ~~~RWR~~ | 1 | 1 |
|
||||
| BRB | 0 | 2 | | ~~~WRB~~ | 0 | 3 | | ~~~RRB~~ | 1 | 1 |
|
||||
| ~~~BRW~~ | 1 | 2 | | ~~~WRW~~ | 1 | 1 | | ~~~RRW~~ | 2 | 0 |
|
||||
| BRR | 0 | 2 | | WRR | 0 | 2 | | ~~~RRR~~ | 1 | 0 |
|
||||
|
||||
That wipes out all but five solutions. Notice how the entire right column of solutions
|
||||
is eliminated, including our original guess of `RBW`, therefore eliminating any
|
||||
special case to specifically eliminate this guess from the solution set when we first find out
|
||||
its not the answer.
|
||||
|
||||
Continuing on, we have the following solutions left of which our secret code, `BWB`
|
||||
is one of them. Remember our commutative property explained previously.
|
||||
|
||||
additionally, it's recommended that you have the computer elimate it's previous guess as possible unless that guess was correct. (the rust port does this)
|
||||
| Guess | Black | White |
|
||||
|-------|-------|-------|
|
||||
| BWB | 0 | 2 |
|
||||
| BRB | 0 | 2 |
|
||||
| BRR | 0 | 2 |
|
||||
| WWB | 0 | 2 |
|
||||
| WRR | 0 | 2 |
|
||||
|
||||
So for its second pick, the computer will randomly pick one of these remaining solutions. Let's pick
|
||||
the middle one, `BRR`, and perform the same ritual. Our user reports to the computer
|
||||
that it now has 1 black, 0 whites when comparing to our secret code `BWB`. Let's
|
||||
now compare `BRR` to the remaining five solutions and eliminate any that **DON'T**
|
||||
report 1 black and 0 whites.
|
||||
|
||||
| Guess | Black | White |
|
||||
|----------|-------|-------|
|
||||
| BWB | 1 | 0 |
|
||||
| ~~~BRB~~ | 2 | 0 |
|
||||
| ~~~BRR~~ | 3 | 0 |
|
||||
| ~~~WWB~~ | 0 | 1 |
|
||||
| ~~~WRR~~ | 2 | 0 |
|
||||
|
||||
Only one solution matches and its our secret code! The computer will guess this
|
||||
one next as it's the only choice left, for a total of three moves.
|
||||
Coincidentally, I believe the expected maximum number of moves the computer will
|
||||
make is the number of positions plus one for the initial guess with no information.
|
||||
This is because it is winnowing down the solutions
|
||||
logarithmically on average. You noticed on the first pass, it wiped out 22
|
||||
solutions. If it was doing this logarithmically the worst case guess would
|
||||
still eliminate 18 of the solutions leaving 9 (3<sup>2</sup>). So we have as
|
||||
a guideline:
|
||||
|
||||
Log<sub>(# of Colors)</sub>TotalPossibilities
|
||||
|
||||
but TotalPossibilities = (# of Colors)<sup># of Positions</sup>
|
||||
|
||||
so you end up with the number of positions as a guess limit. If you consider the
|
||||
simplest non-trivial puzzle, two colors with two positions, and you guess BW or
|
||||
WB first, the most you can logically deduce if you get 1 black and 1 white is
|
||||
that it is either WW, or BB which could bring your total guesses up to three
|
||||
which is the number of positions plus one. So if your computer's turn is taking
|
||||
longer than the number of positions plus one to find the answer then something
|
||||
is wrong with your code.
|
||||
@@ -152,7 +152,7 @@ namespace Game
|
||||
if (isCandidate[index])
|
||||
{
|
||||
var (candidateBlacks, candidateWhites) = guess.Compare(candidate);
|
||||
if (blacks > candidateBlacks || whites > candidateWhites)
|
||||
if (blacks != candidateBlacks || whites != candidateWhites)
|
||||
isCandidate[index] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ async function main()
|
||||
copy_hs();
|
||||
convert_qa();
|
||||
get_number();
|
||||
if (b1 > b || w1 > w)
|
||||
if (b1 != b || w1 != w)
|
||||
ia[x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
1035 GOSUB 6500
|
||||
1040 GOSUB 4000
|
||||
1050 GOSUB 4500
|
||||
1060 IF B1>B OR W1>W THEN I(X)=0
|
||||
1060 IF B1<>B OR W1<>W THEN I(X)=0
|
||||
1070 NEXT X
|
||||
1080 NEXT M
|
||||
1090 PRINT "I USED UP ALL MY MOVES!"
|
||||
|
||||
@@ -1,271 +1,271 @@
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
computer_score = 0
|
||||
|
||||
# get user inputs for game conditions
|
||||
print("Mastermind")
|
||||
print("Creative Computing Morristown, New Jersey")
|
||||
while num_colors > 8:
|
||||
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]}")
|
||||
|
||||
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 ...")
|
||||
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)
|
||||
|
||||
# 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
|
||||
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(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
|
||||
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()
|
||||
|
||||
|
||||
# 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]}")
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# 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)."""
|
||||
increment = 0
|
||||
blacks = 0
|
||||
whites = 0
|
||||
initial_guess = guess
|
||||
for pos in range(0, num_positions):
|
||||
if guess[pos] != answer[pos]:
|
||||
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 :]
|
||||
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(computer_score, human_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}")
|
||||
|
||||
|
||||
# 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()
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
computer_score = 0
|
||||
|
||||
# get user inputs for game conditions
|
||||
print("Mastermind")
|
||||
print("Creative Computing Morristown, New Jersey")
|
||||
while num_colors > 8:
|
||||
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]}")
|
||||
|
||||
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 ...")
|
||||
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)
|
||||
|
||||
# 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
|
||||
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(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
|
||||
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()
|
||||
|
||||
|
||||
# 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]}")
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# 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)."""
|
||||
increment = 0
|
||||
blacks = 0
|
||||
whites = 0
|
||||
initial_guess = guess
|
||||
for pos in range(0, num_positions):
|
||||
if guess[pos] != answer[pos]:
|
||||
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 :]
|
||||
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(computer_score, human_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}")
|
||||
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -325,7 +325,7 @@ fn main() {
|
||||
if *b.1 { //filter out ones we already know aren't possible
|
||||
let mut tmp_guess = GUESS::new(CODE::new_from_int(b.0, num_colors, num_positions));
|
||||
tmp_guess.evaluate(&answer);
|
||||
if blacks > tmp_guess.blacks || whites > tmp_guess.whites { //if number of blacks/whites is different, set it to false
|
||||
if blacks != tmp_guess.blacks || whites != tmp_guess.whites { //if number of blacks/whites is different, set it to false
|
||||
*b.1 = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user