# Python solution to The Hardest Logic Puzzle Ever

The The Hardest Logic Puzzle Ever recently came to my attention:

Three gods A, B, and C are called, in no particular order, True, False, and Random. True always speaks truly, False always speaks falsely, but whether Random speaks truly or falsely is a completely random matter. Your task is to determine the identities of A, B, and C by asking three yes-no questions; each question must be put to exactly one god. The gods understand English, but will answer all questions in their own language, in which the words for yes and no are da and ja, in some order. You do not know which word means which.

Furthermore, a single god may be asked more than one question, questions are permitted to depend on the answers to earlier questions, and the nature of Random's response should be thought of as depending on the flip of a fair coin hidden in his brain: if the coin comes down heads, he speaks truly; if tails, falsely.

I quickly wrote a Python solution. The code is meant to be mostly PEP8 compliant but was not intended to be very formal — just a straight port from word problem to program. If there is a problem, the program should immediately crash to prevent undiscovered bugs.

Given that the code shown below was meant to simply test the ideas provided in the solution for the puzzle, would it be best to rewrite the program and clean it up if code quality is to be improved without having regards for how the problem and its solution reads?

#! /usr/bin/env python3
# Reference: https://en.wikipedia.org/wiki/The_Hardest_Logic_Puzzle_Ever
from random import choice, sample

def question_1(god):
if god == 'Random':
return choice((yes, no))
truth = a == 'Random'   # part 1
if god == 'True':
answer = yes if truth else no
elif god == 'False':
answer = no if truth else yes
truth = answer == 'ja'  # part 2
if god == 'True':
return yes if truth else no
elif god == 'False':
return no if truth else yes

def question_2(god):
if god == 'Random':
return choice((yes, no))
truth = god == 'False'  # part 1
if god == 'True':
answer = yes if truth else no
elif god == 'False':
answer = no if truth else yes
truth = answer == 'ja'  # part 2
if god == 'True':
return yes if truth else no
elif god == 'False':
return no if truth else yes

def question_3(god):
if god == 'Random':
return choice((yes, no))
truth = b == 'Random'   # part 1
if god == 'True':
answer = yes if truth else no
elif god == 'False':
answer = no if truth else yes
truth = answer == 'ja'  # part 2
if god == 'True':
return yes if truth else no
elif god == 'False':
return no if truth else yes

for _ in range(10000):
# setup
a, b, c = sample(('True', 'False', 'Random'), 3)
da, ja = sample(('yes', 'no'), 2)
temp = {y: x for x, y in globals().items() if isinstance(y, str)}
yes, no = temp['yes'], temp['no']
del temp
# question
not_random = 'c'
not_random = 'a'
not_random_id = 'True'
not_random_id = 'False'
b_id = 'Random'
if not_random != 'a':
a_id = 'Random'
elif not_random != 'c':
c_id = 'Random'
# decide
if not_random == 'a':
a_id = not_random_id
elif not_random == 'c':
c_id = not_random_id
try:
a_id
except NameError:
a_id = ({'True', 'False', 'Random'} - {b_id, c_id}).pop()
else:
try:
b_id
except NameError:
b_id = ({'True', 'False', 'Random'} - {a_id, c_id}).pop()
else:
try:
c_id
except NameError:
c_id = ({'True', 'False', 'Random'} - {a_id, b_id}).pop()
# verify
try:
assert (a, b, c) == (a_id, b_id, c_id)
except AssertionError:
print(f'a, b, c = {a!r}, {b!r}, {c!r}')
print(f'a_id, b_id, c_id = {a_id!r}, {b_id!r}, {c_id!r}')
raise
else:
del a, b, c, da, ja, yes, no, \
a_id, b_id, c_id

• @Carcigenicate The mapping between "yes" and "no" with "da" and "ja" is meant to be unknown. This is reflected in the code as you found it to be unclear which is on purpose. In the questions, yes and no are used to make the code readable while leaving the meaning of "da" and "ja" as ambiguous. – Noctis Skytower Jan 10 at 21:29
• In case there are any questions about the assignment to not_random on lines 61 and 63: the purpose of not assigning c or a directly is to avoid gaining any knowledge that has not been earned yet. By using an indirect reference and then having to pull the value out of globals(), the true identity of god A or god C is not stolen and cannot be used accidently without merit. – Noctis Skytower Jan 10 at 22:34
• Is there any difference between question_1, question_2, and question_3? – vnp Jan 10 at 22:44
• @vnp # part 1 of the question is always different. Each question function is meant to evaluate the related question as defined in the riddle. – Noctis Skytower Jan 10 at 22:46
• Oh I see now. Consider it a first phase of the review: the reviewer is stumbled. An immediate recommendation is to factor out everything after # part 1 into a function. – vnp Jan 11 at 1:56

The code is not well organized. The 3 questions are very similar and does not explain what is being asked, and the top-level code mix together the setup logic, the solving logic and the testing logic.

Besides, the whole logic is convoluted: using strings to store state is error-prone; and relying so much on the global state (and even the name of some variables) is even more.

You should:

1. Define some constant using enums:

import enum

JA = 'ja'
DA = 'da'

class God(enum.Enum):
TRUE = True
FALSE = False
RANDOM = None

2. Extract out the question structure so it is easier to understand what is being asked:

def answer_truthy(question):
return yes if question else no

return no if question else yes

if god is God.RANDOM:
return choice((yes, no))



So you can would_you_say_ja_if_asked(a is God.RANDOM, b) for instance, which is clearer what is being asked.

To make it read even better, you can define this last function as a method on the God class:

class God(enum.Enum):
TRUE = True
FALSE = False
RANDOM = None

if self is God.RANDOM:
return choice((yes, no))



Which is used as b.would_you_say_ja_if_asked(a is God.RANDOM) and reads very well.

3. Extract out your solving logic out of the setup + test one:

def solve():
"""Solve the puzzle by asking three questions"""
a = b = c = None

# Determine which of A or C is not the Random god

# Determine the identity of previously selected god
if non_random_god is C:
else:

# Determine which god is Random
b = God.RANDOM
elif non_random_god is C:
a = God.RANDOM
else:
c = God.RANDOM

# Deduct the identity of the third god
last_one = (set(God) - {a, b, c}).pop()
if a is None:
a = last_one
elif b is None:
b = last_one
else:
c = last_one

return a, b, c

4. Stop relying on global variables and pass state explicitly around:

#! /usr/bin/env python3

"""Setup and find an answer to the hardest puzzle ever.

The puzzle statement is as follows:

Three gods A, B, and C are called, in no particular
order, True, False, and Random. True always speaks
truly, False always speaks falsely, but whether Random
speaks truly or falsely is a completely random matter.
C by asking three yes-no questions; each question must
be put to exactly one god. The gods understand English,
but will answer all questions in their own language, in
which the words for yes and no are da and ja, in some
order. You do not know which word means which.

Furthermore, a single god may be asked more than one
question, questions are permitted to depend on the
answers to earlier questions, and the nature of Random's
response should be thought of as depending on the flip
of a fair coin hidden in his brain: if the coin comes
down heads, he speaks truly; if tails, falsely.

Reference: https://en.wikipedia.org/wiki/The_Hardest_Logic_Puzzle_Ever

The solution is found as follows:

* ask B if they would say ja if asked 'is A Random?';
depending on the answer either A or C is not Random.
* ask the non-Random god if they would say ja if asked
'are you False?'; the answer will tell who they are.
* ask the same god if they would say ja if asked
'is B Random?'; the answer will tell who Random is.
* the third god can be deduced without further questions.
"""

import sys
import enum
from random import choice, sample

JA = 'ja'
DA = 'da'

class God(enum.Enum):
TRUE = True
FALSE = False
RANDOM = None

if self is God.RANDOM:
return choice(yes_no_meaning)

yes, no = yes_no_meaning
return yes if question else no

yes, no = yes_no_meaning
return no if question else yes

def solve(A, B, C, yes_no_meaning):
"""Solve the puzzle by asking three questions"""
a = b = c = None

# Determine which of A or C is not the Random god

# Determine the identity of previously selected god
if non_random_god is C:
else:

# Determine which god is Random
b = God.RANDOM
elif non_random_god is C:
a = God.RANDOM
else:
c = God.RANDOM

# Deduct the identity of the third god
last_one = (set(God) - {a, b, c}).pop()
if a is None:
a = last_one
elif b is None:
b = last_one
else:
c = last_one

return a, b, c

def setup_puzzle():
a, b, c = sample(list(God), 3)
return yes, no, a, b, c

def test(test_cases=10_000):
for _ in range(test_cases):
yes, no, A, B, C = setup_puzzle()
a, b, c = solve(A, B, C, (yes, no))

if (a, b, c) != (A, B, C):
print(f'a, b, c = {a}, {b}, {c}', file=sys.stderr)
print(f'A, B, C = {A}, {B}, {C}', file=sys.stderr)
sys.exit('Found a failing case')

print('All tests passed')

if __name__ == '__main__':
test()

1. The code needs a lot more comments to say why. Why is each question_x written as it is written? Why is there a loop over 10000? My best guess is that the questions hard-code a strategy and you test it with 10000 random configurations, but (a) that should be made explicit (and so should the strategy itself); and (b) you should explain why you test 10000 random configurations when there are only 12 possible configurations.

2. The use of globals is very confusing. I strongly suggest passing around an explicit state.

@Mathias Ettinger's answer is very comprehensive for the big picture design changes, so I would probably follow that one for my general design (though your question has piqued my interest in programming this question, so I may come back with my own variant at some point). However, I do have some useful tidbits to offer in my answer, and they can hopefully help you improve your knowledge, even if they aren't the big picture for this problem.

In what cases would this implementation of THLPE require more formality?

...

Given that the code shown below was meant to simply test the ideas provided in the solution for the puzzle, it what case(s) would it be best to rewrite the program and clean it up if code quality is to be improved without having regards for how the problem and its solution reads?

Code Review is a place for general review of code quality as described in Do I want the code to be good code? in our on-topic page. I will be reviewing just as described there. I am not approaching the problem in this way just because of how our rules are phrased. In my experience in general coding, I've found that following best practices in general helps to achieve better reliability, and I think most would agree with me on this. There is really not any conflict between best practices and reliability; they go hand in hand.

On the other hand, best practices and performance can sometimes come into conflict. However, for many (most?) problems, optimizing performance beyond best practices is not a real issue, hence the common programming aphorism of premature optimization. And when this becomes a common enough issue in an active programming language (like Python), the language can be revised to address it, or alternate solutions can be developed by the community (like Cython for C-optimized Python code).

# Review

## Don't repeat yourself!

There's one extremely noticeable bad practice in your program. question_1, question_2, and question_3 are all verbatim identical aside from their names. Instead of having repeated code, there should be a single question function, which I would probably renamed to ask_question for more clarity. If the number of the question was important, then it should be a parameter to that one function (probably named num, unless it has a more specific designation). However, the number does not matter in this problem. The only information that matters for this puzzle is the

Repeating code is a bad practice because there's more code to keep track of when revising the code, so it's easier to introduce a continuity error. It is also generally easier to read shorter code.

## Don't abuse globals()

You should be wary of using globals() because it introduces another layer of complexity into a program. Unless you're sure that you need it, you should try to find another way to accomplish your goal. In your case, you could have a dict or even an iterable to store the gods, so there's no reason to haphazardly query globals.

## Use the right function for the situation

The following two lines come to my attention:

    a, b, c = sample(('True', 'False', 'Random'), 3)
da, ja = sample(('yes', 'no'), 2)


Here, you're using random.sample to randomly order a tuple. However, there is some implicit repetition here: you're entering the length of each tuple (3 and 2 respectively), when that information could be retrieved from the tuple itself through the len built-in function. This repetition occurs because sample is the wrong function for this situation: you actually want random.shuffle if you're trying to reorder an iterable (though it modifies its argument in-place, so it would need a list instead of a tuple).

However, for the big picture, I wouldn't even use a randomly generated list like this; I would probably use enums in a way similar to @Mathias Ettinger's answer.

A minor comment for efficiency, for example, you may want to change

def question_1(creature):
if creature == 'Random':
return choice((yes, no))
truth = a == 'Random'   # part 1
if creature == 'True':
answer = yes if truth else no
elif creature == 'False':
answer = no if truth else yes
truth = answer == 'ja'  # part 2
if creature == 'True':
return yes if truth else no
elif creature == 'False':
return no if truth else yes


to

def question_1(creature):
if creature == 'Random':
return choice((yes, no))

truth = a == 'Random'   # part 1

if creature == 'True':
answer = yes if truth else no
truth = answer == 'ja'  # part 2
return yes if truth else no
else:
answer = no if truth else yes
truth = answer == 'ja'  # part 2
return no if truth else yes

• Since there are only 3 possibilities for creature: 'Random', 'False', or 'True'. So after if creature == 'True', else is for when creature == False.

• The previous one has two if creature == 'True':..., but you can make it into just one to be more compact.