3
votes

I recently started learning Prolog for fun. I found the following murder mystery puzzle. Since I don't know much about Prolog except the basics, I cannot really evaluate the solution provided in the link, however, it didn't seem particularly nice to me. My solution is not enough to generate the correct answers so I'm looking for some pointers as to how to get there or if it's at all possible to get there with my approach. Here's the puzzle just in case the link goes down:

To discover who killed Mr. Boddy, you need to learn where each person was, and what weapon was in the room. Clues are scattered throughout the quiz (you cannot solve question 1 until all 10 are read).

To begin, you need to know the suspects. There are three men (George, John, Robert) and three women (Barbara, Christine, Yolanda). Each person was in a different room (Bathroom, Dining Room, Kitchen, Living Room, Pantry, Study). A suspected weapon was found in each room (Bag, Firearm, Gas, Knife, Poison, Rope). Who was found in the kitchen?

Clue 1: The man in the kitchen was not found with the rope, knife, or bag. Which weapon, then, which was not the firearm, was found in the kitchen?

Clue 2: Barbara was either in the study or the bathroom; Yolanda was in the other. Which room was Barbara found in?

Clue 3: The person with the bag, who was not Barbara nor George, was not in the bathroom nor the dining room. Who had the bag in the room with them?

Clue 4: The woman with the rope was found in the study. Who had the rope?

Clue 5: The weapon in the living room was found with either John or George. What weapon was in the living room?

Clue 6: The knife was not in the dining room. So where was the knife?

Clue 7: Yolanda was not with the weapon found in the study nor the pantry. What weapon was found with Yolanda?

Clue 8: The firearm was in the room with George. In which room was the firearm found?

It was discovered that Mr. Boddy was gassed in the pantry. The suspect found in that room was the murderer. Who, then, do you point the finger towards?

Here's the link to the author's solution.

Here's my attempted solution:

male(george).
male(john).
male(robert).

female(barbara).
female(christine).
female(yolanda).

person(X) :- male(X).
person(X) :- female(X).

room(kitchen).
room(bathroom).
room(diningroom).
room(livingroom).
room(pantry).
room(study).

weapon(bag).
weapon(firearm).
weapon(gas).
weapon(knife).
weapon(poison).
weapon(rope).

/*
Clue 1: The man in the kitchen was not found with 
        the rope, knife, or bag. 
        Which weapon, then, which was not the firearm, 
        was found in the kitchen?
*/

/* X is Weapon, Y is Room, Z is Person */

killer(X, Y, Z) :-
    room(Y) = room(kitchen),
    male(Z),
    dif(weapon(X), weapon(rope)),
    dif(weapon(X), weapon(knife)),
    dif(weapon(X), weapon(bag)),
    dif(weapon(X), weapon(firearm)).

/*
Clue 2: Barbara was either in the study or the bathroom; 
        Yolanda was in the other. 
        Which room was Barbara found in?
*/

/* It was easy to deduce the following from other data */

killer(X, Y, Z) :-
    female(Z) = female(barbara),
    room(study) = room(Y).

killer(X, Y, Z) :-
    female(Z) = female(yolanda),
    room(bathroom) = room(Y).

/*
Clue 3: The person with the bag, who was not Barbara nor 
        George, was not in the bathroom nor the dining room. 
        Who had the bag in the room with them?
*/
killer(X, Y, Z) :-
    weapon(bag) = weapon(X),
    dif(room(Y), room(bathroom)),
    dif(room(Y), room(diningroom)),
    dif(person(Z), male(george)),
    dif(person(Z), female(barbara)).


/*
Clue 4: The woman with the rope was found in the study. 
        Who had the rope?
*/
killer(X, Y, Z) :- 
    weapon(rope) = weapon(X),
    room(study) = room(Y),
    female(Z).

/*
Clue 5: The weapon in the living room was found with either 
        John or George. What weapon was in the living room?
*/

killer(X, Y, Z) :-
    room(Y) = room(livingroom),
    dif(male(Z), male(robert)).

/*
Clue 6: The knife was not in the dining room. 
        So where was the knife?
*/

killer(X, Y, Z) :-
    weapon(knife) = weapon(X),
    room(Y) \= room(diningroom).

/*
Clue 7: Yolanda was not with the weapon found 
        in the study nor the pantry. 
        What weapon was found with Yolanda?
*/

killer(X, Y, Z) :-
    female(yolanda) = female(Z),
    dif(room(study), room(Y)),
    dif(room(pantry), room(Y)).

/*
Clue 8: The firearm was in the room with George. 
        In which room was the firearm found?
*/

killer(X, Y, Z) :-
    weapon(firearm) = weapon(X),
    male(george) = male(Z).

/*
It was discovered that Mr. Boddy was gassed in the pantry. 
The suspect found in that room was the murderer. 
Who, then, do you point the finger towards?
*/

killer(X, Y, Z) :-
    room(Y) = room(pantry),
    weapon(X) = weapon(gas).
3
You mentioned that my proposed solution was not easy to read (I agree, actually it should only demonstrate the approach). You may have a look to the updated solution.CoronA

3 Answers

4
votes

I took a more positive approach to this problem. Rather than trying any form of negation I went with just plain unification.

Key is this predicate pair:

members([],_).
members([M|Ms],Xs) :- select(M,Xs,Ys),members(Ms,Ys).

This is a basic permutation predicate. It will take a list of the first argument and try to unify against all permutations of second list.

Now a lot of the rules became quite easy to express:

For example, clue 1:

clue1(House) :- members([[P,kitchen,_],[_,_,rope],[_,_,knife],[_,_,bag],[_,_,firearm]],House),man(P).

So this meant that the rope, knife, bag and firearm were all members of the house, but in different rooms than the kitchen. Prolog would keep backtracking util it found a fit for these items.

Here's my full solution:

man(george).
man(john).
man(robert).
woman(barbara).
woman(christine).
woman(yolanda).

members([],_).
members([M|Ms],Xs) :- select(M,Xs,Ys),members(Ms,Ys).

clue1(House) :- members([[P,kitchen,_],[_,_,rope],[_,_,knife],[_,_,bag],[_,_,firearm]],House),man(P).
clue2(House) :- member([barbara,study,_],House), member([yolanda,bathroom,_],House).
clue2(House) :- member([barbara,bathroom,_],House), member([yolanda,study,_],House).
clue3(House) :- members([[_,_,bag],[barbara,_,_],[george,_,_]],House),members([[_,_,bag],[_,bathroom,_],[_,dining_room,_]],House).
clue4(House) :- members([[P,study,rope]],House),woman(P).
clue5(House) :- members([[john,living_room,_]],House).
clue5(House) :- members([[george,living_room,_]],House).
clue6(House) :- members([[_,_,knife],[_,dining_room,_]],House).
clue7(House) :- members([[yolanda,_,_],[_,study,_],[_,pantry,_]],House).
clue8(House) :- member([george,_,firearm],House).
clue9(House,P) :- members([[P,pantry,gas]],House).

solve(X) :-
    House = [[_,bathroom,_],[_,dining_room,_],[_,kitchen,_],[_,living_room,_],[_,pantry,_],[_,study,_]],
    clue1(House),
    clue2(House),
    clue3(House),
    clue4(House),
    clue5(House),
    clue6(House),
    clue7(House),
    clue8(House),
    clue9(House,X),
    members([[george,_,_],[john,_,_],[robert,_,_],[barbara,_,_],[christine,_,_],[yolanda,_,_]],House),
    members([[_,_,bag],[_,_,firearm],[_,_,gas],[_,_,knife],[_,_,poison],[_,_,rope]],House),
    write(House),
    true.

That gave me:

?- solve(X).
[[yolanda,bathroom,knife],[george,dining_room,firearm],[robert,kitchen,poison],[john,living_room,bag],[christine,pantry,gas],[barbara,study,rope]]
X = christine .
1
votes

Edit: See an improved version of the reference solution at https://swish.swi-prolog.org/p/crime_constraints.pl.

I agree that the solution you linked to is ugly, but it does use the right approach. Yours isn't quite going in the right direction. Some remarks:

/* X is Weapon, Y is Room, Z is Person */

Why not use the variable names Weapon, Room, and Person then? It makes your program much easier to read.

weapon(rope) = weapon(X)

This is exactly equivalent to just writing X = rope or rope = X.

But apart from these there are other two big problems with the way you are approaching this puzzle:

First, you are not modeling relationships between your objects as data. For example, for "The woman with the rope was found in the study." you have this clause:

killer(X, Y, Z) :- 
    weapon(rope) = weapon(X),
    room(study) = room(Y),
    female(Z).

This does indeed have three solutions that you can interpret as "a relation killer(rope, study, barbara), killer(rope, study, christine), or killer(rope, study, yolanda)", but your program doesn't know how to interpret it that way. You don't actually construct data that expresses this relationship. This is what the solution you linked to does correctly: It models rooms and weapons as variables which can be bound to atoms representing persons. Thus it can express this clue as woman(Rope) ("the person with the Rope is a woman") and Rope = Study ("the rope and the study are associated with the same person").

The second big problem is that you are modeling all clues as different clauses of the same predicate. This is wrong because in Prolog the different clauses of a predicate express a choice: Something holds if the first clause holds or the second clause holds or the third clause holds, etc. But you want to express that the first clue holds and the second clue holds and the third clue holds, etc. And "and" is expressed by combining the different conditions with , in the body of one clause. This is why the linked solution has different predicates clue1, clue2, etc., all of which are called from the body of one big predicate.

0
votes

Derive Rules from the clues in sequence

Each person was in a different room (Bathroom, Dining Room, Kitchen, Living Room, Pantry, Study). A suspected weapon was found in each room (Bag, Firearm, Gas, Knife, Poison, Rope).

unique(A,B,C,D,E,F) :- 
     A \= B, A \= C, A \= D, A \= E, A \= F,
     B \= C, B \= D, B \= E, B \= F,
     C \= D, C \= E, C \= F,
     D \= E, D \= F,
     E \= F.

suspicious(pwr(george,WA,RA), pwr(john,WB,RB), pwr(robert,WC,RC), pwr(barbara,WD,RD), pwr(christine,WE,RE), pwr(yolanda,WF,RF)) :- 
    weapon(WA), weapon(WB), weapon(WC), weapon(WD), weapon(WE), weapon(WF),
    unique(WA,WB,WC,WD,WE,WF),
    room(RA), room(RB), room(RC), room(RD), room(RE), room(RF),
    unique(RA,RB,RC,RD,RE,RF).

Now let us examine

Clue 1: The man in the kitchen was not found with the rope, knife, or bag. Which weapon, then, which was not the firearm, was found in the kitchen?

clue1(L) :-
    oneof(pwr(P,W,kitchen),L),
    male(P),
    weapon(W),
    W \= rope, W \= knife, W \= bag, W \= firearm.

We do this for each of the 8 clues and finally

It was discovered that Mr. Boddy was gassed in the pantry. The suspect found in that room was the murderer. Who, then, do you point the finger towards?

killer(X, L) :- member(pwr(X,gas,pantry),L).

resolved(X) :-
    suspicious(A,B,C,D,E,F),
    L = [A,B,C,D,E,F],
    clue1(L),
    clue2(L),
    clue3(L),
    clue4(L),
    clue5(L),
    clue6(L),
    clue7(L),
    clue8(L),
    killer(X, L).

The full program could be found and run. The inference is rather slow (but faster than the authors solution).

Why consider it a better design to use relations instead of Variable bindings?

I understand a prolog program as a ruleset to derive knowledge. That means:

  • Each relation in prolog should describe a relation in the domain
  • Adding entities (Weapons, Persons, Rooms) to the world should not make the ruleset obsolete. The problem has not changed (we only extended the world) so the rules and queries need not to be touched.
  • Extending the problem (e.g. by adding a seventh location) should have minimal impact

Not every aspect is optimal in the referenced solution, some may be better expressed if one is more familiar with prolog.

Why do I think that a ruleset should be robust to world changes?

I used datalog in program analysis. That means that each relation in source code (or byte code) was modeled as facts and the rules inferred types, security vulnerabilities, design patterns etc. There were multiple millions of facts and multiple thousands of ruleset code. Adding an entity (e.g. a source code line, a type annotation) should not drive me to reimplement the ruleset code (which was quite hard to write it correctly).

Why do I think that using implicit relations is bad code?

Consider this code from the reference solution, it is totally misleading:

clue1(Bathroom, Dining, Kitchen, Livingroom, Pantry, Study, Bag, Firearm, Gas, Knife, Poison, Rope) :-
   man(Kitchen),       // a man is a kitchen?
   \+Kitchen=Rope,     // a kitchen is not a rope?
   \+Kitchen=Knife,    // a kitchen is not a knife?
   \+Kitchen=Bag,      // a kitchen is not a bag
   \+Kitchen=Firearm.  // a kitchen is not a firearm

Ok the variable names are ugly, better readable would be

clue1(InBathroom, InDiningroom, InKitchen, InLivingroom, InPantry, InStudy, WithBag, WithFirearm, WithGas, WithKnife, WithPoison, WithRope) :-
   man(InKitchen),     // (person) in the kitchen is a man - ok
   \+Kitchen=Rope,     // (person) in the kitchen is not 
                          (person) with a rope - better than above
   \+Kitchen=Knife,    // ...
   \+Kitchen=Bag,      // ...
   \+Kitchen=Firearm.  // ...

But we misuse the equal relation for an explicit one. There is a clear indicator: Variables containing predicates in their names are probably implicit relations. "personInKitchen" is a (logical) predicate "in" connecting two substantives "person" and "kitchen".

As comparison a model with lists and function symbols (suspect/3 is the relational function that connects persons to weapons and rooms, Suspects is the list of suspects):

clue1(Suspects) :-    
    member(suspect(Person,Weapon,Room),Suspects), 
    male(Person),     // The man (Person)
    Room = kitchen,   // in the Kitchen (Room)
    Weapon \= rope,   // was not found with the (Weapon) rope
    Weapon \= knife,  // (Weapon) knife
    Weapon \= bag,    // (Weapon) bag
    Weapon \= firearm.// (Weapon) firearm

Summary

So if you use prolog for private purpose, I do not mind "misusing" Variables to come to a quick solution. But if your ruleset and your data grows it seems to me quite essential to model all relations explicitly.