Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 23175

Moshe Zadka: The Hardest Logic Puzzle Ever (In Python)

$
0
0

The Labyrinth is a children’s movie. The main character is 16 years old, and solving a logic puzzle that will literally decide if she lives or dies. In fiction, characters are faced with realistic challenges: ones they can solve, even if they have to make an effort.

So, it makes sense that the designer of the eponymous labyrinth did not consult logicians Richard Smullyan, George Boolos (no relation to the inventor of boolean algebra), and John McCarthy (yes, the same person who invented Lisp and suggested that a “2-month, 10-(person) study” would make significant headway in the study of Artificial Intelligence). Those three would suggest that the designer use The Hardest Logic Puzzle Ever.

There are persistent rumors of the movie getting a reboot, or perhaps a sequel. Like any good sequel, the protagonists should face newer and bigger challenges. In the interests of helping the screen writers for the sequel/reboot, here is my explanation of the “Hardest Logic Puzzle Ever”, together with clear code.

from__future__importannotations

Do you remember movies from your childhood that just did not age that well? You cringe at some tasteless joke, you say “I can’t believe it was acceptable to show this back then”? I think we can agree we want to future-proof the script for a bit.

Using modern-style annotations will make our code much easier to maintain.

importattr

This movie will come out in the 2020s, and we should use 2020-era code to model it. The attrs library is almost always the right solution to implement classes.

importrandom

The protagonist will face unknown challenges. Randomness is a state of knowledge. From her perspective, the true state of the world is random.

fromzopeimportinterfacefromtypingimportCallable,Mapping,Tuple

This is already a hard logic puzzle, no reason to make it harder on ourselves. Clear interface and type hints will make the logic easier to understand.

This is why zope.interface is appropriate for The Hardest Logic Puzzle Ever. We also need the Callable interface, since our protagonist will be asking the Gods questions in the form of functions, and Mapping, since she will eventually need to answer the question “which God is which”.

The Tuple type will most be used by the careful protagonist in her internal type hints. This is high stakes code, and she wants to make sure she gets it right.

In the HLPE (Hardest Logic Puzzle Ever), the ones who answer the questions are Gods. Just like in the Marvel Cinematic Universe, they might not be literal “Gods”, but clearly powerful aliens. As aliens, they have their own language. The words for “yes” and “no” are “da” and “ja” – but we do not know which is which.

I suggest that this will not be revealed in the script. This is good fodder for endless fan discussions later on Reddit. As such, the best way is to make sure we do not know the answer ourselves: make it a random language!

importenum@enum.uniqueclassGodWords(enum.Enum):ja="ja"da="da"defmake_god_language()->Mapping[bool,GodWords]:words=list(GodWords)random.shuffle(words)ret={}return{True:words.pop(),False:words.pop()}

Shuffling the words for “yes” and “no” means that the .pop() call will get a random one. Now we have the language: it maps an abstract concept (a Python Boolean) to a string.

In the HLPE, there are three Gods, called “A”, “B”, and “C”. They can be asked any question that refers to the Gods.

@enum.uniqueclassGodNames(enum.Enum):A="A"B="B"C="C"classIGod(interface.Interface):defask(question:Question)->GodWords:"""Ask"""

But what questions is the protagonist allowed to ask? Questions are something the audience-identification protagonist will ask. For this, a Protocol is appropriate. (Also, those are more convenient for describing functions, which we do not want to annotate with explicit implementation declarations.)

fromtyping_extensionsimportProtocolclassQuestion(Protocol):def__call__(self,gods:Dict[GodName,IGod])->bool:pass

Because of how annotations work, at this point we need not know what GodName or IGod are.

The simplest of the Gods is the one who speaks always truly (in the God language).

@interface.implementer(IGod)@attr.s(auto_attribs=True,frozen=True)classTrueGod:_gods:Mapping[GodNames,IGod]_language:Mapping[bool,GodWords]defask(self,question:Question)->GodWords:returnself._language[bool(question(self._gods))]

The next God always lies.

@attr.s(auto_attribs=True,frozen=True)classFalseGod:_gods:Mapping[GodNames,IGod]_language:Mapping[bool,GodWords]defask(self,question:Question)->GodWords:returnself._language[notbool(question(self._gods))]

But how to implement Random? This is a harder question than it seems.

The original statement just said “whether Random speaks truly or falsely is a completely random matter”. What does that mean? If you ask it two questions, can it answer truthfully to one and lie to the other?

Boolos wrote a “clarification”: “Whether Random speaks truly or not should be thought of as depending on the flip of a coin hidden in (their) brain: if the coin comes down heads, (they) speaks truly; if tails, falsely.” This clarification fails to elucidate much: it does not answer, for example, the question above.

Finally, based on the suggested solution, and assuming that the obvious simpler solutions do not work, Raben and Rabensuggested suggested the clear guideline: “Whether Random says ja or da should be thought of as depending on the flip of a coin hidden in his brain: if the coin comes down heads, he says ja; if tails, he says da.”

This is the guideline I have chosen to implement here.

@attr.s(auto_attribs=True,frozen=True)classRandomGod:_gods:Mapping[GodNames,IGod]_language:Mapping[bool,GodWords]defask(self,question:Question)->GodWords:returnself._language[random.choice([True,False])]

So much back and forth discussion, over two millenia, for such simple code.

I’m sure the God-like aliens would have used code to describe the puzzle from the beginning, avoiding the messiness of natural language.

Accessing the God classes themselves would be the height of hubris, but the goal of the protagonist is to answer “which God is which”. We will build a special enumeration of the Gods’ identities, to be used in the solution.

GodIdentities=enum.Enum('God Identities',{klass.__name__:klass.__name__forklassin[TrueGod,FalseGod,RandomGod]})

With the Gods’ personalities implemented, we can write the code that creates a random world that complies with the terms of the puzzle. Three Gods, known as “A”, “B”, and “C”, assigned to the names randomly, and speaking in a randomly generated language.

defmake_gods()->Mapping[GodNames,IGod]:language=make_god_language()gods={}god_list=[klass(gods=gods,language=language)forklassin[TrueGod,FalseGod,RandomGod]]random.shuffle(god_list)fornameinGodNames:gods[name]=god_list.pop()returngods

The code so far corresponds to the first part of the scene: where the protagonist comes to the place of the Gods, learns the local rules, and realizes that she must either solve the puzzle correctly, or fail.

This time she is granted the chance to ask three questions. However, there are many more unknowns: there are 6 possible assignments for the Gods, and two possible meanings for the language. Frankly, I doubt that I would be able to solve the puzzle on my feet, when I am afraid for my life.

But luckily, I have Wikipedia and time, and so I have written up the code to find a solution here.

Part of the solution is to ask a God a question about themselves: “if I asked you SOME QUESTION, would you say ja”. While no question asked of Random is useful (the answer is random) for either True or False, this question would result in “ja” the right answer is “yes”, and “da* if the right answer is”no". This means that wrapping questions like this means we have to care neither the identity of the God, nor about the details of the God language.

This makes it a useful abstraction!

defif_asked_a_question_would_you_say_ja_is_ja(god_name:GodNames,gods:Mapping[GodNames,IGod],question:Callable[[IGod,Mapping[GodNames,IGod]],bool],)->bool:you=gods[god_name]defadd_you(gods):returnquestion(you,gods)returnyou.ask(lambdagods:you.ask(add_you)==GodWords.ja)==GodWords.ja

This would be a wonderful chance for a flash-“forward” into a hypothetical scene: the movie goes into black-and-white, and the protagonist voice-overs: if “A” is “True”, what would happen if I ask them “is 1 equal 1”? What would happen if I ask them “if I asked you, ‘is 1 equal 1?’, would you answer ’ja? What if”A" is “False”?

defhypothetical():language=make_god_language()gods={}hypothetical_true_god=TrueGod(gods=gods,language=language)hypothetical_false_god=FalseGod(gods=gods,language=language)gods[GodNames.A]=hypothetical_true_godgods[GodNames.B]=hypothetical_false_godfor(name,identity)in[(GodNames.A,GodIdentities.TrueGod),(GodNames.B,GodIdentities.FalseGod)]:forquestionin[lambdax,y:1==1,lambdax,y:1==0]:objective_value=question(None,None)print(f"{identity}, asked {objective_value} question:",if_asked_a_question_would_you_say_ja_is_ja(name,gods,question))hypothetical()delhypothetical
God Identities.TrueGod, asked True question: True
God Identities.TrueGod, asked False question: False
God Identities.FalseGod, asked True question: True
God Identities.FalseGod, asked False question: False

Movie goes back to color. A smile spreads on the protagonist’s face. She knows how to solve this.

The next challenge is to find a God that is not Random. If you ask God B about whether A is Random, then if the answer is “ja”, then it means either the correct answer is that A is Random or it means B is Random. Either way, C is not Random.

For similar reasons, if B answers “da”, then “A” is not Random.

Either way, we have found a non-Random God. Now we know that we can find the truth from them by using the wrapper!

So we ask them whether they’re True. Now we ask them about whether “B” is Random.

So we know:

  • One God who is not Random (who we will call “the interlocutor”, since we’ll spend the rest of the conversation with them)
  • Whether the interlocutor is True
  • Whether B is Random
defask_questions(gods:Mapping[GodNames,IGod])->Tuple[GodNames,bool,bool]:is_a_random_according_to_b=if_asked_a_question_would_you_say_ja_is_ja(GodNames.B,gods,lambdayou,gods:isinstance(gods[GodNames.A],RandomGod))interlocutor=GodNames.Cifis_a_random_according_to_belseGodNames.Ais_interlocutor_true=if_asked_a_question_would_you_say_ja_is_ja(interlocutor,gods,lambdayou,gods:isinstance(you,TrueGod))is_b_random=if_asked_a_question_would_you_say_ja_is_ja(interlocutor,gods,lambdayou,gods:isinstance(gods[GodNames.B],RandomGod))returninterlocutor,is_interlocutor_true,is_b_random

Once again, the movie goes into a black and white flash-forward as the protagonist plans her move.

defhypothetical():forexperimentinrange(1,5):print("Experiment",experiment)gods=make_gods()interlocutor,is_interlocutor_true,is_b_random=ask_questions(gods)print(f"I think {interlocutor} is {is_interlocutor_true}God. They're {type(gods[interlocutor]).__name__}.")print(f"B is {'' if is_b_random else 'not '}RandomGod. They're {type(gods[GodNames.B]).__name__}.")hypothetical()delhypothetical
Experiment 1
I think GodNames.C is FalseGod. They're FalseGod.
B is not RandomGod. They're TrueGod.
Experiment 2
I think GodNames.A is FalseGod. They're FalseGod.
B is not RandomGod. They're TrueGod.
Experiment 3
I think GodNames.A is TrueGod. They're TrueGod.
B is RandomGod. They're RandomGod.
Experiment 4
I think GodNames.A is FalseGod. They're FalseGod.
B is not RandomGod. They're TrueGod.

Now that we know the answers, it is time to put them all together. We first record whether the interlocutor is True or False. If B is Random, we mark them as such. If not, we know that neither the interlocutor or B is Random, so the other God must be Random.

Now that we know two Gods’ identities, the last name that remains belongs to the last possible identity.

defanalyze_answers(interlocutor:GodNames,is_interlocutor_true:bool,is_b_random:bool):solution={}solution[interlocutor]=(GodIdentities.TrueGodifis_interlocutor_trueelseGodIdentities.FalseGod)[other_god]=set(GodNames)-set([interlocutor,GodNames.B])random_god=GodNames.Bifis_b_randomelseother_godsolution[random_god]=GodIdentities.RandomGod[last_god]=set(GodNames)-set(solution.keys())[last_god_value]=set(GodIdentities)-set(solution.values())solution[last_god]=last_god_valuereturnsolution

Putting the questioning and the analysis together is straightforward.

deffind_solution(gods:Mapping[GodNames,IGod])->Mapping[GodNames,GodIdentities]:interlocutor,is_interlocutor_true,is_b_random=ask_questions(gods)returnanalyze_answers(interlocutor,is_interlocutor_true,is_b_random)

Our little checker returns both a description of the situation, as well as whether the solution was correct.

defcheck_solution()->Tuple[Mapping[str,str],Mapping[str,str],bool]:gods=make_gods()solution=find_solution(gods)reality=sorted([(name.value,type(god).__name__)forname,godingods.items()])deduced=sorted([(name.value,identity.value)forname,identityinsolution.items()])returnreality,deduced,reality==deduced

The last step is to test our solution multiple times. Again, remember that there are 6 * 2 = 12 possible situations. However, if “B” is Random, we can get two different answers. This means the total number of options for a path is more than 12, but less than 24.

If we run the solution for a 1000 times, the probability that a given path will not be taken is less than (23/24)**1000 * 24. How much is it?

(23/24)**1000*24
7.885069901070409e-18

Good enough!

Now we can see if the script works. We lack Hollywood’s professional script doctors, but we do have a powerful Python interpreter.

foriinrange(1000):reality,deduced,correct=check_solution()ifnotcorrect:raiseValueError("Solution is incorrect",reality,deduced)ifi%200==0:print("Solved correctly for",reality)
Solved correctly for [('A', 'RandomGod'), ('B', 'TrueGod'), ('C', 'FalseGod')]
Solved correctly for [('A', 'RandomGod'), ('B', 'FalseGod'), ('C', 'TrueGod')]
Solved correctly for [('A', 'RandomGod'), ('B', 'TrueGod'), ('C', 'FalseGod')]
Solved correctly for [('A', 'TrueGod'), ('B', 'FalseGod'), ('C', 'RandomGod')]
Solved correctly for [('A', 'TrueGod'), ('B', 'RandomGod'), ('C', 'FalseGod')]

We printed out every 200th situation, to have some nice output!

Python confirms it. Hollywood should buy our script.

Thanks to Glyph Lefkowitz for his feedback on the Labyrinth post, some of which inspired changes in this post. Thanks to Mark Williams for his feedback on an early draft. Any mistakes or issues that remain are my responsibility.


Viewing all articles
Browse latest Browse all 23175

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>