Mastering Mastermind
Intro
Blah blah
Modeling The Game of Hangman
We’re going to model a game such as Hangman in python with four parts:
code_word
: This is the code word we’re trying to break. Aka, the word that the opponent has chosen, and the player must determine.dictionary
: The entire set of code words that the opponent can choose from.get_next_guess
: The player’s strategy given the current state of the game.get_response
: The response that the opponent will given the player’s next guess.
An Example
Given our dictionary is cat
, can
, and car
, and the code word is cat
.
A logical first guess may be t
, which the opponent would respond with --t
.
After some time, the player will determine the code word and may use cat
as their next guess.
An example game log may look like the following:
[
{'guess': 't', 'response': '--t'},
{'guess': 'n', 'response': '!'},
{'guess': 'c', 'response': 'c-t'},
{'guess': 'cat', 'response': 'cat'},
]
Game implementation
In python, we can define a function play_game
that accepts our four parts defined above.
def play_game(code_word, dictionary, get_next_guess, get_response):
"""
Arguments:
code_word -- The opponent's chosen code word.
dictionary -- The set of all possible code words the opponent could've chosen.
get_next_guess -- A function that will be invoked to return the next guess.
get_response -- A function that will be invoked to return the opponent's response to the guess.
Returns:
game_log -- A list of game turns containing each turn's guess and response.
"""
game_log = GameLog()
possible_code_words = list(dictionary)
while True:
next_guess, code_words_by_response = get_next_guess(
possible_code_words,
get_response,
game_log)
response = get_response(
code_word,
next_guess,
game_log)
game_log.log(next_guess, response)
if next_guess == code_word:
return game_log
possible_code_words = code_words_by_response.get(response)
Line by line game walkthrough
On the first loop, we are starting with the full dictionary. The responsibility of
get_next_guess
is twofold: Return the next guess and return possible sets
of remaining words indexed by the opponent’s response. This index will be
used on the last line of the loop to whittle down the set of remaining words
that still match the current state of the game.
next_guess, code_words_by_response = get_next_guess(
possible_code_words, # ['bat', 'cat', 'can', 'car']
get_response, # <function get_response>
game_log # []
)
assert next_guess == 't'
assert code_words_by_response == {
'b--;': ['bat'],
'c--;': ['cat', 'can', 'car'],
'-a-;': ['bat', 'cat', 'can', 'car'],
'--t;': ['bat', 'cat'],
'--n;': ['can'],
'--r;': ['car'],
'---;s': ['bat', 'cat', 'can', 'car'],
}
Since the next guess is t
, the response is --t
. The player now knows
that t
was a correct guess AND what position the t
is in the code word.
response = get_response(
code_word, # 'cat'
next_guess, # 't'
game_log # []
)
assert response == '--t;'
Simple line to capture the turn in the game log.
game_log.log(next_guess, response)
assert game_log == [{'guess': 't', 'response': '--t'}]
Since the next guess (t
) is not the code word, we move on.
if next_guess == code_word: # False
return game_log
After the turn is complete, we whittle down the set of possible words based
on the response (--t
) we received back from get_response
. Looking at
the code_words_by_response
assertion above, we see that we now only have
two remaining words that could match the current state of the game. We’re now
ready to start the second loop.
possible_code_words = code_words_by_response.get(response)
assert possible_code_words == ['bat', 'cat']
The second loop is exactly the same except we have a shorter list of possible code words and an entry in the game log. We’ll only show the first step in the loop, since the rest should be self explanatory.
next_guess, code_words_by_response = get_next_guess(
possible_code_words, # ['bat', 'cat']
get_response, # <function get_response>
game_log # [{'guess': 't', 'response': '--t'}]
)
assert next_guess == 'c'
assert code_words_by_response == {
'b-t;': ['bat'],
'c-t;': ['cat'],
'-at;': ['bat', 'cat'],
'---;s': ['cat'],
}
And so on…
Implementing a basic strategy in get_next_guess
An example of a naive solution get_next_guess
that we most often used as kids is shown below.
It knows the order of the most commonly occurring letters in the english dictionary
and tries them one by one. We will ignore the _get_remaining_code_words_by_response()
implementation for now to keep things simple.
Don’t worry, we’ll revisit it when we start building more complex strategies.
def get_next_guess(remaining_words, get_response, game_log)
# If we've whittled down to return one remaining word,
# return that word as our next guess to finish the game.
if len(remaining_words) == 1:
return remaining_words[0]
next_guess = None
potential_guesses = 'esiarntolcdupmghbyfvkwzxqj'
for guess in potential_guesses:
if guess in game_log.previous_guesses:
continue
if guess in remaining_words:
next_guess = guess
return next_guess, _get_remaining_code_words_by_response()
Implementing the opponent’s get_response
def get_response(code_word, next_guess, game_log)
"""
Example:
Returns `ba-a-a` where the code word is `banana` and guesses include `b` and `a`
"""
all_guesses = game_log.previous_guesses + next_guess
result = []
for symbol in code_word:
if symbol in all_guesses or code_word in guesses:
result.append(symbol)
else:
result.append('-')
return ''.join(symbol)
Exploring optimal strategies
Naive, success, info gain (maybe others in mastermind paper?)
Exploring optimal strategies in Mastermind
TODO
Exploring optimal strategies in Battleship
TODO
Wrapping Up
2D optimization problem, what if we wanted to add a third? In next post, we’ll add time as a third dimension.