7
\$\begingroup\$

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
 answer = question_1(b)
 if answer == 'ja':
 not_random = 'c'
 elif answer == 'da':
 not_random = 'a'
 answer = question_2(globals()[not_random])
 if answer == 'da':
 not_random_id = 'True'
 elif answer == 'ja':
 not_random_id = 'False'
 answer = question_3(globals()[not_random])
 if answer == 'ja':
 b_id = 'Random'
 elif answer == 'da':
 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, \
 answer, not_random, not_random_id, \
 a_id, b_id, c_id
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Jan 10, 2019 at 21:06
\$\endgroup\$
5
  • \$\begingroup\$ @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. \$\endgroup\$ Commented Jan 10, 2019 at 21:29
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Jan 10, 2019 at 22:34
  • \$\begingroup\$ Is there any difference between question_1, question_2, and question_3? \$\endgroup\$ Commented Jan 10, 2019 at 22:44
  • \$\begingroup\$ @vnp # part 1 of the question is always different. Each question function is meant to evaluate the related question as defined in the riddle. \$\endgroup\$ Commented Jan 10, 2019 at 22:46
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Jan 11, 2019 at 1:56

4 Answers 4

7
\$\begingroup\$

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
    class Answer(enum.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
    def answer_falsey(question):
     return no if question else yes
    def would_you_say_ja_if_asked(question, god):
     if god is God.RANDOM:
     return choice((yes, no))
     god_answer = answer_truthy if god is God.TRUE else answer_falsey
     answer = god_answer(question)
     return god_answer(answer is Answer.JA)
    

    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
     def would_you_say_ja_if_asked(self, question):
     if self is God.RANDOM:
     return choice((yes, no))
     god_answer = answer_truthy if self is God.TRUE else answer_falsey
     answer = god_answer(question)
     return god_answer(answer is Answer.JA)
    

    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
     answer = B.would_you_say_ja_if_asked(A is God.RANDOM)
     non_random_god = C if answer is Answer.JA else A
     # Determine the identity of previously selected god
     answer = non_random_god.would_you_say_ja_if_asked(non_random_god is God.FALSE)
     if non_random_god is C:
     c = God.FALSE if answer is Answer.JA else God.TRUE
     else:
     a = God.FALSE if answer is Answer.JA else God.TRUE
     # Determine which god is Random
     answer = non_random_god.would_you_say_ja_if_asked(B is God.RANDOM)
     if answer is Answer.JA:
     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.
     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.
    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
    class Answer(enum.Enum):
     JA = 'ja'
     DA = 'da'
    class God(enum.Enum):
     TRUE = True
     FALSE = False
     RANDOM = None
     def would_you_say_ja_if_asked(self, question, yes_no_meaning):
     if self is God.RANDOM:
     return choice(yes_no_meaning)
     god_answer = answer_truthy if self is God.TRUE else answer_falsey
     answer = god_answer(question, yes_no_meaning)
     return god_answer(answer is Answer.JA, yes_no_meaning)
    def answer_truthy(question, yes_no_meaning):
     yes, no = yes_no_meaning
     return yes if question else no
    def answer_falsey(question, yes_no_meaning):
     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
     answer = B.would_you_say_ja_if_asked(A is God.RANDOM, yes_no_meaning)
     non_random_god = C if answer is Answer.JA else A
     # Determine the identity of previously selected god
     answer = non_random_god.would_you_say_ja_if_asked(non_random_god is God.FALSE, yes_no_meaning)
     if non_random_god is C:
     c = God.FALSE if answer is Answer.JA else God.TRUE
     else:
     a = God.FALSE if answer is Answer.JA else God.TRUE
     # Determine which god is Random
     answer = non_random_god.would_you_say_ja_if_asked(B is God.RANDOM, yes_no_meaning)
     if answer is Answer.JA:
     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():
     yes, no = sample(list(Answer), 2)
     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()
    
Toby Speight
87.1k14 gold badges104 silver badges322 bronze badges
answered Jan 11, 2019 at 10:08
\$\endgroup\$
0
5
\$\begingroup\$
  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.

answered Jan 11, 2019 at 9:25
\$\endgroup\$
5
\$\begingroup\$

Best general answer

@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.

Answering the meta question

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.

answered Jan 11, 2019 at 12:01
\$\endgroup\$
3
\$\begingroup\$

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.

answered Jan 15, 2019 at 9:40
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.