For this problem, an explicit time is not necessary, so we will represent the history as a list of actions. On the other hand, you need to explicitly represent the state of your system, i.e. the current content of the three buckets. The reason is that Prolog datastructures (i.e. terms) cannot be changed, once they are created. Since there are a lot of meaningless terms, it is good practice to define datatypes first via an is_type/1
predicate. Because you will be using arithmetic at some point (when you pour water from one bucket to another), I will use arithmetic constraints instead of the ancient is/2
predicate.
:- use_module(library(clpfd)).
First we state that there are 3 buckets, represented by the atoms b1, b2 and b3:
is_bucket(b1).
is_bucket(b2).
is_bucket(b3).
Then we need to define our state. We just use a term buckets/3
where the first argument holds the capacity of b1 and likewise for the other two.
is_state(buckets(X,Y,Z)) :-
% each bucket contains at least 0 liters
[X,Y,Z] ins 0 .. sup.
All containers may not become negative, so we set their domain to range from zero to (positive) infinity.
Now what's an action? So far you described emptying and pouring:
is_action(empty(B)) :-
is_bucket(B).
is_action(pour(From, To)) :-
is_bucket(From),
is_bucket(To).
To empty a bucket, we only need to know which one. If we pour water from one to another, we need to describe both. Since we already have a predicate describing a bucket, we can just state a rule as "If From
and To
are buckets, then pour(From, To)
is an action.
Now we need to explain how an action transforms a state. This is a relation between the old state, the new state, and because we'd like to know what happens, also the history.
% initial state
state_goesto_action(buckets(7,5,3), buckets(7,5,3), []).
The transition for the initial state does not change anything and has an empty history (the []
).
% state transitions for moving
state_goesto_action(buckets(X,Y,Z), buckets(0,Y,Z), [empty(b1) | History]) :-
state_goesto_action(_S0, buckets(X,Y,Z), History).
This rule can be read as "If we had an action coming from some state _S0
leading to the state buckets(X,Y,Z)
with some History
, then we can perform the empty(b1)
action next, and we will reach the state buckets(0,Y,Z)
". In other words, the state is updated and the action is prepended to the history. A symmetrical rule works for the other buckets:
state_goesto_action(buckets(X,Y,Z), buckets(X,0,Z), [empty(b2) | History]) :-
state_goesto_action(_S0, buckets(X,Y,Z), History).
state_goesto_action(buckets(X,Y,Z), buckets(X,Y,0), [empty(b3) | History]) :-
state_goesto_action(_S0, buckets(X,Y,Z), History).
How can we check if this makes sense? Let's look at histories of length 2:
?- state_goesto_action(_,S1,[H1,H2]).
S1 = buckets(0, 3, 5),
H1 = H2, H2 = empty(b1) .
Ah nice, if both actions are empty(b1)
, the first bucket is empty and the others untouched. Let's look at further solutions:
S1 = buckets(0, 0, 5),
H1 = empty(b1),
H2 = empty(b2) ;
S1 = buckets(0, 3, 0),
H1 = empty(b1),
H2 = empty(b3) ;
S1 = buckets(0, 0, 5),
H1 = empty(b2),
H2 = empty(b1) ;
S1 = buckets(7, 0, 5),
H1 = H2, H2 = empty(b2) ;
S1 = buckets(7, 0, 0),
H1 = empty(b2),
H2 = empty(b3) ;
S1 = buckets(0, 3, 0),
H1 = empty(b3),
H2 = empty(b1) ;
S1 = buckets(7, 0, 0),
H1 = empty(b3),
H2 = empty(b2) ;
S1 = buckets(7, 3, 0),
H1 = H2, H2 = empty(b3).
Looks like we get all possibilities of emptying buckets (and nothing more :-)). Now you need to add rules for pouring from one bucket to the other. Good luck!
(Edit: typos, mistake in the second rule)