Appendix A — The way across the river
Deutsche Fassung · Translated from the German original.

The fruit lies on the other side of the river. The queen knows how to get across.
Goal: Three new things come into the game:
- water that no ant can cross,
- a plank that an ant can pick up,
- and a crossing that appears where it has put the plank down.
Everything else has stayed unchanged.
The level is the second level of the game.
You will find the finished program at anhang/app_mit_planke.py, and the level
next to it as anhang/level-planke.json:
uv run streamlit run anhang/app_mit_planke.py
The second level

The task is not in the carrying but in the recognising.
No ant knows that there is fruit behind the river. None knows that a plank would help.
And none sees both — three squares of sight, and the two things lie half a board's width apart.
Only you see the whole picture, and one sentence is all it takes.
From anhang/level-planke.json, shortened by the name, the briefing and where
the fruit lies:
{
"columns": 30,
"rows": 20,
"nest": { "x": 13, "y": 8, "size": 4 },
"water": [[7, 0], [7, 1], [7, 2], [7, 3], [7, 4], [7, 5], [7, 6],
[6, 6], [5, 6], [4, 6], [3, 6], [2, 6], [1, 6], [0, 6]],
"plank": [22, 15],
"target": 5
}
The river is one square wide everywhere. That is enough, because nobody wades.
A hand instead of a switch
Until now the ant's attribute carrying was a truth value — carrying a fruit,
yes or no.
Now there are two things to carry, so it becomes a string:
# What this ant has hold of: nothing, "a fruit" or "the plank".
# A word rather than a flag, because there are two things now and
# because this is what goes into the state as it stands.
"holding": None,
That pays off, because the value goes into the state unchanged:
"the ant is carrying": ant["holding"] or "nothing",
"a fruit" becomes "the plank", and the question understands that without
further ado.
Water is taken, the crossing is not
def taken_cells() -> set[tuple[int, int]]:
blocked = {(ant["x"], ant["y"]) for ant in st.session_state.ants}
blocked |= set(st.session_state.fruit)
blocked |= st.session_state.water - {st.session_state.bridge}
if st.session_state.plank:
blocked.add(st.session_state.plank)
return blocked
A single line makes the river impassable, and the set difference
- {st.session_state.bridge} makes exactly one square of it walkable again.
Because free_directions builds on this set, it takes effect everywhere at
once: no neighbouring square in the water is offered, the square with the
plank is.
Introducing an obstacle costs one line in this program. That is no accident but the harvest of step 3: everything that means “something is already here” gathers in one place.
The one option that is not a direction
TAKE_PLANK = "take the plank"
In options():
plank = st.session_state.plank
if plank and not ant["holding"] and steps_away(plank[0] - ant["x"], plank[1] - ant["y"]) == 1:
extra[TAKE_PLANK] = "pick up the plank lying beside this ant"
At the end it joins the directions:
return {way: towards.get(way) for way in ways} | extra
The rule is the same as for all other options: it exists only when it is possible. An ant with its hands full is never offered it, and neither is one far away.
Why is picking up a decision and putting down not? In chapter 4 both were a consequence, and the reasoning was: a question whose answer is almost always the same is a request paid for and thrown away.
With the plank it is half different. An ant on its way to a fruit should not pick up a plank in passing — here the answer is open, so it is asked. On the other hand, an ant arriving at the water with the plank has exactly one sensible option, so it is not asked:
if ant["holding"] == "the plank":
water = water_beside(ant)
if water is not None:
ant["holding"] = None
st.session_state.bridge = water
return
The Rust version picks the square more carefully:
nearest_crossingonly takes water with at least two walkable neighbours, so that the plank is not laid into nowhere. Here the river is one square wide everywhere, so every water square meets that condition, and the check can be left out.
Three exceptions, all for the same reason
While building this I fell into the same pit three times. It is worth writing down, because it is the same one as in chapter 5.
First: the plank has to be visible from further away than anything else.
PLANK_VISIBLE_FROM = 8
With the usual three squares of sight the level did not work. Over thirty rounds not a single ant walked to the plank — none could know it was there. The Rust version has the same constant with the same value. A plank is a big thing lying about in the open.
Second: whoever carries the plank must not learn where the nest is.
In the first attempt every ant with its hands full was told where the nest was. So the carrier dutifully took the plank home and walked in circles with it for thirty rounds. Now every load learns only its own destination:
if ant["holding"] == "a fruit":
... # the nest
elif ant["holding"] == "the plank":
... # the nearest water
Third: the crossing has to be visible from everywhere.
Otherwise the plank is laid — and afterwards nobody finds the spot again. Measured: eighty rounds, one fruit. Now the crossing is named to every ant, no matter how far away it stands, as a landmark like the nest:
f"the crossing over the water, {phrase(bx - ant['x'], by - ant['y'])}"
Three times the same lesson, and it is the most important one of this tutorial:
What is in the state is the behaviour. What is not in it does not exist for the ant — no matter how obvious it looks on the screen.
What comes of it
The sentence I played with:
The fruit is all in the far north-west, behind a river you cannot wade. A plank lies in the south-east: whoever sees it, fetch it, carry it west to the river and lay it down there. Everyone else go north-west, cross where the plank lies, and carry fruit back to the nest.

The course of events, measured: the plank is picked up around round 6 to 10, carried north-west — one of those answers with 0.96 confidence — and put down between round 20 and 30. After that the colony streams across the one square.
With that the puzzle is solved, and by one sentence. No ant could have worked it out.
Where this appendix reaches its limit
And then it gets slow going. Four full runs, in the order I did them:
| Bridge down | Stores | Cost | |
|---|---|---|---|
| crossing visible only from three squares | round 30 | 1 of 5 after 80 rounds | $0.016 |
| crossing as a landmark | round 25 | 1 of 5 after 70 rounds | $0.015 |
| plus the scent trail, half-life 12 | round 25 | 0 of 5 after 60 rounds | $0.013 |
| half-life 30 | round 30 | 0 of 5 after 60 rounds | $0.012 |
One fruit, then none. The puzzle is solved, the carrying away is not — and with the scent trail it even gets worse.
That can be explained and, honestly, was to be expected. On the first board the trail is as good as perfect information (chapter 5). Here the way home is three times as long and leads through one square — and a trail that nobody refreshes, because nobody happens to walk the same way, has faded before the carrier is back.
So my first guess was the half-life. It is now in the level instead of in the code:
"scent_half_life": 30
It brought nothing — line four of the table. The setting has stayed anyway, because the reasoning behind it is sound: how long a trail has to last depends on how far the colony walks. It just was not the problem here.
The reason is not a weakness of the model but the way this port is built: every single step is a question of its own. An ant that is to fetch fruit and come back over one particular square needs twenty decisions in a row for it, each without any memory of the previous one. On top of that, eight ants squeeze through a single square.
That is exactly why the Rust version has intents instead of single steps: there you ask once “fetch the fruit in the north-west”, and the ant works on it for several seconds. That is the point from chapter 8 where carrying on pays off — and this appendix is the proof that it is not theoretical.
Try it
- First give an order that mentions neither river nor plank, say “Search for fruit and bring it home”, and play twenty rounds. The colony finds the river, and that is where it ends.
- Switch on the dice and let it run for a hundred rounds. Does it ever pick up the plank? And if so, does it put it down somewhere sensible?
- Say only “Fetch the plank in the south-east”. Is it fetched? And where is it carried to?
- Set
PLANK_VISIBLE_FROMback to 3 and play again. - Move the plank closer to the river in
level-planke.json. How much easier does the level get? - Add a second water square, so that the river is two squares wide in one
place. What happens if the plank is laid there? (And now you know what the
Rust version has
nearest_crossingfor.) - Watch the board after the bridge is down: where does it turn green? The trail shows which way the colony really takes — and how quickly it fades again when nobody uses it.
This tutorial was created by Gerhard Völkl using Claude Code (Anthropic). It is meant to help you learn more about what happens when ants ask a decision model. Suggestions for improvement are welcome at gkvoelkl@nelson-games.de.