Wolf, goat and cabbage

The river crossing puzzle, with the shortest answer shown to be shortest.

wolf-goat-cabbage.py · output · proof · check · try it in the playground


The question

A farmer must get a wolf, a goat and a cabbage across a river. The boat holds the farmer and at most one passenger. Left alone, the wolf eats the goat, and the goat eats the cabbage. Finding a plan is one thing; showing that no shorter plan exists is another.

How can he do it, and what is the fewest number of crossings?


What we tell eyekind: crossings

A situation, or state, lists which bank, west ('w') or east ('e'), the farmer, wolf, goat and cabbage are on, in that order. Everyone starts west: ['w', 'w', 'w', 'w'].

# A plan from a state is a safe move followed by a plan from the next state;
# everyone on the east bank needs no more moves.
fact(solution(['e', 'e', 'e', 'e'], []))
implied_by(
  solution(State, [Move, *Rest]),
  [move(State, Move, Next), safe(Next), solution(Next, Rest)],
)

# The farmer crosses alone or with one passenger that is on his bank.
implied_by(move([X, X, Goat, Cabbage], 'wolf', [Y, Y, Goat, Cabbage]), change(X, Y))
implied_by(move([X, Wolf, X, Cabbage], 'goat', [Y, Wolf, Y, Cabbage]), change(X, Y))
implied_by(move([X, Wolf, Goat, X], 'cabbage', [Y, Wolf, Goat, Y]), change(X, Y))
implied_by(move([X, Wolf, Goat, Cabbage], 'nothing', [Y, Wolf, Goat, Cabbage]), change(X, Y))

fact(change('e', 'w'))
fact(change('w', 'e'))

# Safe when the goat is with the farmer, or with neither the wolf nor the cabbage.
implied_by(
  safe([Farmer, Wolf, Goat, Cabbage]),
  [one_eq(Farmer, Goat, Wolf), one_eq(Farmer, Goat, Cabbage)],
)
fact(one_eq(X, X, _))
fact(one_eq(X, _, X))

What we tell eyekind: the shortest plan

# Some plan of 0 to 6 crossings works.
implied_by(
  'shorter_solution',
  [in_range(0, 6, N), moves(N, Plan), solution(['w', 'w', 'w', 'w'], Plan)],
)

# once(...) stops at the first seven-crossing plan; shortest_crossing lists all.
implied_by(
  wolf_goat_cabbage_verified(7),
  [not_('shorter_solution'), moves(7, Plan), once(solution(['w', 'w', 'w', 'w'], Plan))],
)
implied_by(
  shortest_crossing(Plan),
  [not_('shorter_solution'), moves(7, Plan), solution(['w', 'w', 'w', 'w'], Plan)],
)

query(wolf_goat_cabbage_verified(7))
query(shortest_crossing(_))

What eyekind concludes

// Answers: each instance of a query() that holds.
wolf_goat_cabbage_verified(7)
shortest_crossing(['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'])
shortest_crossing(['goat', 'nothing', 'cabbage', 'goat', 'wolf', 'nothing', 'goat'])

Seven crossings is the minimum, and there are exactly two seven-crossing plans. Both start and end by taking the goat; they differ only in whether the wolf or the cabbage goes over first. The first plan, crossing by crossing:

  1. Take the goat over. (Wolf and cabbage are safe together.)
  2. Come back alone.
  3. Take the wolf over.
  4. Bring the goat back, or the wolf would eat it.
  5. Take the cabbage over.
  6. Come back alone.
  7. Take the goat over. Everyone is east.

The proof, line by line

eyekind --proof writes down how it reached each answer. The whole proof has 72 steps; see the full proof. Here are its claims, its clauses, and the complete chain of 55 steps for the first answer:

// Answers: each instance of a query() that holds.
wolf_goat_cabbage_verified(7)
shortest_crossing(['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'])
shortest_crossing(['goat', 'nothing', 'cabbage', 'goat', 'wolf', 'nothing', 'goat'])

// Clauses: the program clauses the steps cite, by clause number.
clause(1, fact(solution(['e', 'e', 'e', 'e'], [])))
clause(2, implied_by(solution(State, [Move, ...Rest]), [move(State, Move, Next), safe(Next), solution(Next, Rest)]))
clause(3, implied_by(move([X, X, Goat, Cabbage], 'wolf', [Y, Y, Goat, Cabbage]), change(X, Y)))
clause(4, implied_by(move([X, Wolf, X, Cabbage], 'goat', [Y, Wolf, Y, Cabbage]), change(X, Y)))
clause(5, implied_by(move([X, Wolf, Goat, X], 'cabbage', [Y, Wolf, Goat, Y]), change(X, Y)))
clause(6, implied_by(move([X, Wolf, Goat, Cabbage], 'nothing', [Y, Wolf, Goat, Cabbage]), change(X, Y)))
clause(7, fact(change('e', 'w')))
clause(8, fact(change('w', 'e')))
clause(9, implied_by(safe([Farmer, Wolf, Goat, Cabbage]), [one_eq(Farmer, Goat, Wolf), one_eq(Farmer, Goat, Cabbage)]))
clause(10, fact(one_eq(X, X, _0)))
clause(11, fact(one_eq(X, _0, X)))
clause(12, fact(moves(0, [])))
clause(13, implied_by(moves(N, [_0, ...Rest]), [gt(N, 0), is(M, sub(N, 1)), moves(M, Rest)]))
clause(17, implied_by(wolf_goat_cabbage_verified(7), [not('shorter_solution'), moves(7, Plan), once(solution(['w', 'w', 'w', 'w'], Plan))]))
clause(18, implied_by(shortest_crossing(Plan), [not('shorter_solution'), moves(7, Plan), solution(['w', 'w', 'w', 'w'], Plan)]))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(wolf_goat_cabbage_verified(7), clause(17), {Plan: ['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat']}, [not('shorter_solution'), moves(7, ['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat']), once(solution(['w', 'w', 'w', 'w'], ['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat']))])
step(not('shorter_solution'), 'absent', {}, [])
step(moves(7, ['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat']), clause(13), {N: 7, _0: 'goat', Rest: ['nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'], M: 6}, [gt(7, 0), is(6, sub(7, 1)), moves(6, ['nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'])])
step(gt(7, 0), 'builtin', {}, [])
step(is(6, sub(7, 1)), 'builtin', {}, [])
step(moves(6, ['nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat']), clause(13), {N: 6, _0: 'nothing', Rest: ['wolf', 'goat', 'cabbage', 'nothing', 'goat'], M: 5}, [gt(6, 0), is(5, sub(6, 1)), moves(5, ['wolf', 'goat', 'cabbage', 'nothing', 'goat'])])
step(gt(6, 0), 'builtin', {}, [])
step(is(5, sub(6, 1)), 'builtin', {}, [])
step(moves(5, ['wolf', 'goat', 'cabbage', 'nothing', 'goat']), clause(13), {N: 5, _0: 'wolf', Rest: ['goat', 'cabbage', 'nothing', 'goat'], M: 4}, [gt(5, 0), is(4, sub(5, 1)), moves(4, ['goat', 'cabbage', 'nothing', 'goat'])])
step(gt(5, 0), 'builtin', {}, [])
step(is(4, sub(5, 1)), 'builtin', {}, [])
step(moves(4, ['goat', 'cabbage', 'nothing', 'goat']), clause(13), {N: 4, _0: 'goat', Rest: ['cabbage', 'nothing', 'goat'], M: 3}, [gt(4, 0), is(3, sub(4, 1)), moves(3, ['cabbage', 'nothing', 'goat'])])
step(gt(4, 0), 'builtin', {}, [])
step(is(3, sub(4, 1)), 'builtin', {}, [])
step(moves(3, ['cabbage', 'nothing', 'goat']), clause(13), {N: 3, _0: 'cabbage', Rest: ['nothing', 'goat'], M: 2}, [gt(3, 0), is(2, sub(3, 1)), moves(2, ['nothing', 'goat'])])
step(gt(3, 0), 'builtin', {}, [])
step(is(2, sub(3, 1)), 'builtin', {}, [])
step(moves(2, ['nothing', 'goat']), clause(13), {N: 2, _0: 'nothing', Rest: ['goat'], M: 1}, [gt(2, 0), is(1, sub(2, 1)), moves(1, ['goat'])])
step(gt(2, 0), 'builtin', {}, [])
step(is(1, sub(2, 1)), 'builtin', {}, [])
step(moves(1, ['goat']), clause(13), {N: 1, _0: 'goat', Rest: [], M: 0}, [gt(1, 0), is(0, sub(1, 1)), moves(0, [])])
step(gt(1, 0), 'builtin', {}, [])
step(is(0, sub(1, 1)), 'builtin', {}, [])
step(moves(0, []), clause(12), {}, [])
step(once(solution(['w', 'w', 'w', 'w'], ['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'])), 'control', {}, [solution(['w', 'w', 'w', 'w'], ['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'])])
step(solution(['w', 'w', 'w', 'w'], ['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat']), clause(2), {State: ['w', 'w', 'w', 'w'], Move: 'goat', Rest: ['nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'], Next: ['e', 'w', 'e', 'w']}, [move(['w', 'w', 'w', 'w'], 'goat', ['e', 'w', 'e', 'w']), safe(['e', 'w', 'e', 'w']), solution(['e', 'w', 'e', 'w'], ['nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'])])
step(move(['w', 'w', 'w', 'w'], 'goat', ['e', 'w', 'e', 'w']), clause(4), {X: 'w', Wolf: 'w', Cabbage: 'w', Y: 'e'}, [change('w', 'e')])
step(change('w', 'e'), clause(8), {}, [])
step(safe(['e', 'w', 'e', 'w']), clause(9), {Farmer: 'e', Wolf: 'w', Goat: 'e', Cabbage: 'w'}, [one_eq('e', 'e', 'w'), one_eq('e', 'e', 'w')])
step(one_eq('e', 'e', 'w'), clause(10), {X: 'e', _0: 'w'}, [])
step(solution(['e', 'w', 'e', 'w'], ['nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat']), clause(2), {State: ['e', 'w', 'e', 'w'], Move: 'nothing', Rest: ['wolf', 'goat', 'cabbage', 'nothing', 'goat'], Next: ['w', 'w', 'e', 'w']}, [move(['e', 'w', 'e', 'w'], 'nothing', ['w', 'w', 'e', 'w']), safe(['w', 'w', 'e', 'w']), solution(['w', 'w', 'e', 'w'], ['wolf', 'goat', 'cabbage', 'nothing', 'goat'])])
step(move(['e', 'w', 'e', 'w'], 'nothing', ['w', 'w', 'e', 'w']), clause(6), {X: 'e', Wolf: 'w', Goat: 'e', Cabbage: 'w', Y: 'w'}, [change('e', 'w')])
step(change('e', 'w'), clause(7), {}, [])
step(safe(['w', 'w', 'e', 'w']), clause(9), {Farmer: 'w', Wolf: 'w', Goat: 'e', Cabbage: 'w'}, [one_eq('w', 'e', 'w'), one_eq('w', 'e', 'w')])
step(one_eq('w', 'e', 'w'), clause(11), {X: 'w', _0: 'e'}, [])
step(solution(['w', 'w', 'e', 'w'], ['wolf', 'goat', 'cabbage', 'nothing', 'goat']), clause(2), {State: ['w', 'w', 'e', 'w'], Move: 'wolf', Rest: ['goat', 'cabbage', 'nothing', 'goat'], Next: ['e', 'e', 'e', 'w']}, [move(['w', 'w', 'e', 'w'], 'wolf', ['e', 'e', 'e', 'w']), safe(['e', 'e', 'e', 'w']), solution(['e', 'e', 'e', 'w'], ['goat', 'cabbage', 'nothing', 'goat'])])
step(move(['w', 'w', 'e', 'w'], 'wolf', ['e', 'e', 'e', 'w']), clause(3), {X: 'w', Goat: 'e', Cabbage: 'w', Y: 'e'}, [change('w', 'e')])
step(safe(['e', 'e', 'e', 'w']), clause(9), {Farmer: 'e', Wolf: 'e', Goat: 'e', Cabbage: 'w'}, [one_eq('e', 'e', 'e'), one_eq('e', 'e', 'w')])
step(one_eq('e', 'e', 'e'), clause(10), {X: 'e', _0: 'e'}, [])
step(solution(['e', 'e', 'e', 'w'], ['goat', 'cabbage', 'nothing', 'goat']), clause(2), {State: ['e', 'e', 'e', 'w'], Move: 'goat', Rest: ['cabbage', 'nothing', 'goat'], Next: ['w', 'e', 'w', 'w']}, [move(['e', 'e', 'e', 'w'], 'goat', ['w', 'e', 'w', 'w']), safe(['w', 'e', 'w', 'w']), solution(['w', 'e', 'w', 'w'], ['cabbage', 'nothing', 'goat'])])
step(move(['e', 'e', 'e', 'w'], 'goat', ['w', 'e', 'w', 'w']), clause(4), {X: 'e', Wolf: 'e', Cabbage: 'w', Y: 'w'}, [change('e', 'w')])
step(safe(['w', 'e', 'w', 'w']), clause(9), {Farmer: 'w', Wolf: 'e', Goat: 'w', Cabbage: 'w'}, [one_eq('w', 'w', 'e'), one_eq('w', 'w', 'w')])
step(one_eq('w', 'w', 'e'), clause(10), {X: 'w', _0: 'e'}, [])
step(one_eq('w', 'w', 'w'), clause(10), {X: 'w', _0: 'w'}, [])
step(solution(['w', 'e', 'w', 'w'], ['cabbage', 'nothing', 'goat']), clause(2), {State: ['w', 'e', 'w', 'w'], Move: 'cabbage', Rest: ['nothing', 'goat'], Next: ['e', 'e', 'w', 'e']}, [move(['w', 'e', 'w', 'w'], 'cabbage', ['e', 'e', 'w', 'e']), safe(['e', 'e', 'w', 'e']), solution(['e', 'e', 'w', 'e'], ['nothing', 'goat'])])
step(move(['w', 'e', 'w', 'w'], 'cabbage', ['e', 'e', 'w', 'e']), clause(5), {X: 'w', Wolf: 'e', Goat: 'w', Y: 'e'}, [change('w', 'e')])
step(safe(['e', 'e', 'w', 'e']), clause(9), {Farmer: 'e', Wolf: 'e', Goat: 'w', Cabbage: 'e'}, [one_eq('e', 'w', 'e'), one_eq('e', 'w', 'e')])
step(one_eq('e', 'w', 'e'), clause(11), {X: 'e', _0: 'w'}, [])
step(solution(['e', 'e', 'w', 'e'], ['nothing', 'goat']), clause(2), {State: ['e', 'e', 'w', 'e'], Move: 'nothing', Rest: ['goat'], Next: ['w', 'e', 'w', 'e']}, [move(['e', 'e', 'w', 'e'], 'nothing', ['w', 'e', 'w', 'e']), safe(['w', 'e', 'w', 'e']), solution(['w', 'e', 'w', 'e'], ['goat'])])
step(move(['e', 'e', 'w', 'e'], 'nothing', ['w', 'e', 'w', 'e']), clause(6), {X: 'e', Wolf: 'e', Goat: 'w', Cabbage: 'e', Y: 'w'}, [change('e', 'w')])
step(safe(['w', 'e', 'w', 'e']), clause(9), {Farmer: 'w', Wolf: 'e', Goat: 'w', Cabbage: 'e'}, [one_eq('w', 'w', 'e'), one_eq('w', 'w', 'e')])
step(solution(['w', 'e', 'w', 'e'], ['goat']), clause(2), {State: ['w', 'e', 'w', 'e'], Move: 'goat', Rest: [], Next: ['e', 'e', 'e', 'e']}, [move(['w', 'e', 'w', 'e'], 'goat', ['e', 'e', 'e', 'e']), safe(['e', 'e', 'e', 'e']), solution(['e', 'e', 'e', 'e'], [])])
step(move(['w', 'e', 'w', 'e'], 'goat', ['e', 'e', 'e', 'e']), clause(4), {X: 'w', Wolf: 'e', Cabbage: 'e', Y: 'e'}, [change('w', 'e')])
step(safe(['e', 'e', 'e', 'e']), clause(9), {Farmer: 'e', Wolf: 'e', Goat: 'e', Cabbage: 'e'}, [one_eq('e', 'e', 'e'), one_eq('e', 'e', 'e')])
step(solution(['e', 'e', 'e', 'e'], []), clause(1), {}, [])
// … 17 more steps, 72 in total, prove the two shortest_crossing answers.
// They reuse the steps above, so only the second plan needs new ones.

It has three parts:

Four kinds of justification occur. clause(N) means “by line N of the program”. 'builtin' means a test or calculation, such as gt(7, 0), that the checker redoes itself. 'control' means once(...), which the checker rebuilds from the goal inside it. 'absent' means something was concluded because it could not be found.

The 55 steps say:

Steps already shown, such as change('w', 'e'), are reused rather than repeated.


Checked: what the report says

A proof is only worth something if someone else can check it. eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Answers checked subject to the obligations below.
wolf_goat_cabbage_verified(7)
shortest_crossing(['goat', 'nothing', 'wolf', 'goat', 'cabbage', 'nothing', 'goat'])
shortest_crossing(['goat', 'nothing', 'cabbage', 'goat', 'wolf', 'nothing', 'goat'])

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 56)
condition('C2', 'well_founded', 'ok', 72)
condition('C3', 'justification', 'ok', 72)
condition('C4', 'coverage', 'ok', 113)
condition('C5', 're_decision', 'ok', 15)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 75)
condition('C8', 'witness_integrity', 'ok', 0)

// Obligations: absence and collection completeness are taken on trust.
obligation('absent', 'theory_scoped', not('shorter_solution'))

// Proof statistics: recorded steps and how they were checked.
steps(72)
verified(56)
recomputed(14)
composed(1)
trusted(1)
claims(3)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked_with_obligations')

Each condition line names a check, says 'ok', and counts what it looked at:

The statistics: 72 steps, 56 verified against the program, 14 recomputed, 1 composed (the once), 1 trusted, 3 claims.

The obligation line is the step taken on trust: there is no shorter solution. 'theory_scoped' means “with respect to this program”: the search covered every plan of 0 to 6 crossings the program allows. The verdict is checked with obligations: the two plans are fully checked, move by move; the claim that 7 is the minimum rests on that exhaustive search, and the report says so. The verdict does not say the program has no other conclusions.


Try it

eyekind examples/wolf-goat-cabbage.py            # the answers
eyekind --proof examples/wolf-goat-cabbage.py    # answers with their proof

Or open it in the playground. Change in_range(0, 6, N) to in_range(0, 7, N). Now “a shorter solution” includes seven-crossing plans, which exist, so not_('shorter_solution') fails and eyekind reports // No answers: no query() instance holds.


Takeaway

“Here is a plan” and “no better plan exists” are different kinds of claim. eyekind proves the first step by step, and labels the second clearly as the part that rests on an exhaustive search.