One Action a Tick
The six-action table
A creature can fill twenty-four numbers about the cell it stands on, then the row is thrown away. Nothing yet turns those readings into a tick spent resting, walking, sprinting, turning or biting.
A creature does exactly one thing a tick, chosen from a table of six entries, and every entry states what it charges the store, how many ticks it occupies, and what has to be true here for it to be allowed. The check runs before the action, so a refusal costs a tick and never half happens.
The table keeps price and legality apart. Travelling charges the store by mass times speed squared at the row's work price. Biting needs tissue inside reach. Walking into water fails because the ground refuses it, not because the price changed.
Six is the count a body with legs, a heading and a mouth can tell apart: rest, walk, sprint, turn left, turn right, bite. The count is fixed because any controller above the table produces one score per entry and compares those scores with each other.
A hand-written driver sits in front of the table for this page. It turns toward the nearest plant a ray found, walks until the plant is inside reach, and bites. The driver plugs in through one method because the table and the legality check are the parts that stay.
The six priced entries
An entry needs a name, a price, a duration, and a test. The price is a fact about a kind of creature: what a sprint costs depends on how heavy the animal is and what its legs charge, and not at all on which cell it happens to be on. The test is the opposite. It is about this particular animal on this particular tick, because whether a step is allowed depends entirely on what is in front of it. So the two take different arguments, and the difference is on the page rather than in somebody's head.
// internal/beast/act.go
// Act names one of the six things a creature may do with a tick. The
// order is written down once here and indexed by everything that reads
// the table, so it is fixed the way the five hit codes are fixed.
type Act uint8
const (
Rest Act = iota
Walk
Sprint
Left
Right
Bite
)
// Acts is how many entries the table has. A creature does exactly one
// of them per tick and there is no seventh thing it can do.
const Acts = 6
// Move is one row of the action table: what the action charges the
// store, how many ticks it occupies before the creature may choose
// again, and the test that says whether the ground permits it here this
// tick. Price takes a row and not a creature, because a price is a fact
// about a kind; Allow takes the creature, because permission is a fact
// about where that particular animal is standing.
type Move struct {
Name string
Ticks int
Price func(k Kind) float64
Allow func(b *Beast, a Act, v *View) error
Needs string // the precondition in words, for anything that prints the table
}
// Table is the six, in table order. Nothing outside this file decides
// what a creature can do.
var Table = [Acts]Move{
Rest: {Name: "rest", Ticks: 1, Price: nothing, Allow: always, Needs: "nothing"},
Walk: {Name: "walk", Ticks: 1, Price: amble, Allow: ground, Needs: "the cell ahead is not open water"},
Sprint: {Name: "sprint", Ticks: 1, Price: dash, Allow: ground, Needs: "the cell ahead is not open water"},
Left: {Name: "turn left", Ticks: 1, Price: Kind.Wheel, Allow: always, Needs: "nothing"},
Right: {Name: "turn right", Ticks: 1, Price: Kind.Wheel, Allow: always, Needs: "nothing"},
Bite: {Name: "bite", Ticks: 2, Price: Kind.Chew, Allow: mouth, Needs: "tissue standing inside Reach"},
}
The constants run 0 to 5 through iota and the order is a fact other code
indexes by, so it is settled here and never re-sorted for tidiness, the same way the
five hit codes were settled when the rays learned to report what they met.
Acts is the width of the table and Table is an array of
exactly that many entries, so a seventh action cannot be added by accident: it fails
to compile. Kind.Wheel and Kind.Chew in the price column are
methods used as plain functions, which is what a method name without a receiver in
front of it means in Go, and it is how a price written as a method on a row ends up
sitting in a table of functions.
Now the prices. Two of them are already written: a walk asks for half the top speed and a sprint for all of it, and the movement charge for one tick held at a stated speed is a function the row already has. The other two are new, and each one spends a number that has been sitting unused in the creature row since the row was first typed out.
// internal/beast/act.go, continued
// The three prices that are not one-line methods on Kind. A walk asks
// for half the top speed and a sprint for all of it, so both are the
// travel charge from the movement chapter read at a stated speed.
func nothing(k Kind) float64 { return 0 }
func amble(k Kind) float64 { return k.Travel(k.Top / 2) }
func dash(k Kind) float64 { return k.Travel(k.Top) }
// Wheel is what one turn action costs. Getting the whole body round
// once is priced at one tick of the hardest thing the legs can do,
// because it is the legs that do it; one turn action covers Swing of a
// whole turn and pays Swing of that bill.
func (k Kind) Wheel() float64 { return k.Swing * k.Travel(k.Top) }
// Chew is what one bite costs to take, as opposed to what it is worth
// once it is inside. The mouth carries Bite grams over Reach cells in
// one tick, and this world prices that the way it prices every other
// movement: the mass moved, times the square of the speed it moved at,
// at the row's price for a unit of work.
func (k Kind) Chew() float64 { return k.Work * k.Bite * k.Reach * k.Reach }
// Turn is how far one turn action swings the heading, in radians:
// Swing of a whole turn.
func (k Kind) Turn() float64 { return k.Swing * 2 * math.Pi }
Swing does two jobs and they are the same job seen twice. It says how far
one turn action swings the heading, a twelfth of a whole turn for this row, and it says
what share of a whole turn's bill that action pays. Getting the whole body round once
is priced at one tick of the hardest thing the legs can do, because it is the legs that
do it, so a twelfth of a turn costs a twelfth of a sprinting tick.
Reach does the same trick. It is a distance the mouth covers, and pricing
that distance needs no new law: the mouth carries Bite grams over
Reach cells inside one tick, which is a mass moved at a speed, and this
world already knows what a mass moved at a speed costs. The grams are the mouthful and
not the body, because a browser reaching for a leaf is not throwing forty grams of
itself at it.
Every number below comes off the browser row printed in the first chapter of this volume: 40 grams of body, a store of 400, 0.004 energy units a gram a tick, 0.60 energy units for a unit of movement work, a bite of 0.25 grams worth 4.0 units a gram, a reach of 1 cell, a top speed of 0.45 cells a tick, and a swing of a twelfth of a turn.
Start with the sprint, because two of the other five are priced as a share of it. One tick of movement work is the mass times the square of the speed: 40 × 0.45 × 0.45 = 8.10. At 0.60 energy units apiece that is 4.8600 units for one tick held flat out. A walk is half that speed, so 40 × 0.225 × 0.225 = 2.025 units of work and 1.2150 units of energy: half the speed, a quarter of the charge.
A whole turn is priced at that same 4.8600, and one turn action covers a twelfth of a turn, so it charges 4.8600 ÷ 12 = 0.4050. A bite carries a quarter of a gram over one cell: 0.60 × 0.25 × 1 × 1 = 0.1500. Resting charges nothing at all, and the standing body is charged 40 × 0.004 = 0.1600 every tick whatever the creature chose.
Only one of the six brings anything in. A quarter of a gram at four units a gram is 1.0000 units, so a bite nets 1.0000 − 0.1500 = 0.8500 before the body's own upkeep. Divide the upkeep into that and you get the pace a creature has to keep to stay level: 0.8500 ÷ 0.1600 = 5.3125 ticks between bites. Since a bite occupies two ticks, the fastest a mouth can go is one bite every two, comfortably inside the pace it needs.
sprint = w·m·t·t
walk = w·m·(t÷2)·(t÷2)
turn = s × sprint
bite = w·g·r·r
pace = (g·c − bite) ÷ (B·a)
The bench prints the table and redoes every one of those divisions from the row, so nothing above has to be taken on trust.
$ go run ./cmd/acts -mode table
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
the six, priced for the browser: bulk 40, work 0.60, top 0.45,
bite 0.25 grams, convert 4.0, reach 1, swing 0.0833 of a whole turn
action price ticks what has to be true here
0 rest 0.0000 1 nothing
1 walk 1.2150 1 the cell ahead is not open water
2 sprint 4.8600 1 the cell ahead is not open water
3 turn left 0.4050 1 nothing
4 turn right 0.4050 1 nothing
5 bite 0.1500 2 tissue standing inside Reach
a walk asks for 0.2250 cells a tick and a sprint for 0.4500, so the two
travelling prices are the movement charge read at those speeds
a whole turn is priced at one sprinting tick, 4.8600, and one turn action
covers 0.0833 of a turn and pays 0.0833 of that bill
a bite carries 0.25 grams over 1 cell in a tick: 0.60 x 0.25 x 1 x 1 = 0.1500
the standing body is charged 0.1600 a tick whatever the action is
one bite is worth 1.0000 and costs 0.1500 to take, so it nets 0.8500
against the upkeep that is one bite every 5.3125 ticks to hold a store level,
and a bite occupies 2 ticks, so the rule is inside what a mouth can do
Read the price column top to bottom and the whole of a creature's economics is in it. A sprinting tick costs thirty times what the body costs to keep. A turn costs about two and a half ticks of upkeep, so looking around is cheap and running about is not. And the one entry that pays anything back pays 0.8500, which is five and a bit ticks of standing still bought with one mouthful. Set the two against each other and one tick of sprinting costs what nearly six bites bring in, which is the whole argument for an animal that walks about slowly and eats often.
The Refusal value
A price on a table is a claim, and something has to test it against a particular animal before the animal acts. That test has two halves. Can the store pay the price, and does this cell permit the action at all. Neither is interesting on its own. What the design turns on is what the test hands back when the answer is no.
The obvious answer is a bool. It is one bit, it costs nothing, and every
caller knows what to do with it. It is also the wrong answer here, and the reason is the
log. This world keeps its memory as one JSON line per event, opened for append and never
rewritten, and a refused action is an event: something happened to a creature this tick
and the tick was spent on nothing. A bool cannot be written into that log, because the
log needs a sentence and a bool is not one.
// internal/beast/act.go, continued
// The three rules an action can be refused by. They are values and not
// strings so a caller can test which one it hit without reading
// English: errors.Is(err, beast.ErrBroke) is a question about the
// store, whatever the sentence around it says.
var (
// ErrBroke is the store refusing to pay the price on the table.
ErrBroke = errors.New("the store cannot pay for it")
// ErrNoGround is the ground refusing to carry a step.
ErrNoGround = errors.New("the cell ahead is open water or off the grid")
// ErrNoReach is a mouth with nothing standing inside its reach.
ErrNoReach = errors.New("no standing tissue inside reach")
)
// Refusal is one action the check would not let happen. It is an error
// value and not a false, because four things have to reach the log and
// a bool carries none of them: who was refused, what they tried, which
// rule stopped them, and the cell the rule was about. Want and Got are
// the two numbers that rule compared, in whatever unit it works in;
// they are both nothing for a rule that compares no numbers.
type Refusal struct {
Who sim.EntityID // the creature the check refused
Act Act // what it tried to do
Why error // ErrBroke, ErrNoGround or ErrNoReach
At sim.Coord // the cell the rule was about
Want float64 // what the rule asked for
Got float64 // what it found there
}
// Error is the one-line sentence the log carries.
func (r *Refusal) Error() string {
return fmt.Sprintf("creature %d at %d,%d cannot %s: %v",
r.Who, r.At.X, r.At.Y, r.Act, r.Why)
}
// Unwrap hands out the rule underneath, so errors.Is answers questions
// about which rule fired without anything unpacking the struct.
func (r *Refusal) Unwrap() error { return r.Why }
// Legal is the check, in the order it asks. The store first, because it
// is one subtraction and every one of the six is subject to it; then
// the entry's own test, which is the only part that has to look at the
// world. It hands back nil when the creature may take the action and a
// *Refusal when it may not.
//
// The upkeep is not in this check. A body is charged for standing there
// whatever it does with the tick, so a store that cannot cover the
// upkeep is not something choosing a cheaper action can mend.
func (b *Beast) Legal(a Act, v *View) error {
m := Table[a]
if p := m.Price(*b.Kind); p > b.Store {
return &Refusal{Who: b.ID, Act: a, Why: ErrBroke, At: b.Cell(), Want: p, Got: b.Store}
}
return m.Allow(b, a, v)
}
The three sentinels are values and not strings, so a caller asks which rule fired by
comparing values: errors.Is(err, beast.ErrBroke) is a question about the
store and stays true however the sentence around it is reworded.
Refusal wraps one of them and adds the four things the log actually wants,
and its Unwrap method is what lets errors.Is see through the
struct to the rule inside. So one returned value answers three different questions:
print it and you get a sentence, test it with errors.Is and you get the
rule, unpack it with errors.As and you get the numbers.
Legal asks the store first on purpose. It is one subtraction, it applies
to all six entries, and it needs nothing from the world; the entry's own test is the
half that has to go and look. The upkeep is deliberately not part of it. A body is
charged for standing there whatever it does, so a store that cannot cover the upkeep
is not a situation choosing a cheaper action can mend, and refusing every action on
those grounds would only mean the creature could not even rest.
Two of the three tests need to look at the ground, and both need a small piece of geometry that has not existed until now: which cells count as here.
// internal/beast/act.go, continued
// always is the test for an action nothing about the ground can forbid.
func always(b *Beast, a Act, v *View) error { return nil }
// ground refuses a step that would put weight on open water or over the
// rim. It tests the cell ahead and not the cell underfoot, because the
// cell underfoot has already been walked on.
func ground(b *Beast, a Act, v *View) error {
c := b.Ahead()
if !v.Valley.Bed.In(c) || v.Valley.Bed.Kind(c) == sim.Water {
return &Refusal{Who: b.ID, Act: a, Why: ErrNoGround, At: c}
}
return nil
}
// mouth refuses a bite with nothing to bite. The two numbers on the
// refusal are the grams the mouth wanted and the grams the richest cell
// inside its reach was holding.
func mouth(b *Beast, a Act, v *View) error {
st, grams := b.Meal(v)
if st == nil {
return &Refusal{Who: b.ID, Act: a, Why: ErrNoReach, At: b.Cell(),
Want: b.Kind.Bite, Got: grams}
}
return nil
}
// Ahead is the cell one cell along the heading from where the body is
// standing: the ground a step would put weight on.
func (b *Beast) Ahead() sim.Coord {
p := b.Pos.Scale(1.0 / terra.Tile)
return sim.Coord{
X: int(math.Floor(p.X + math.Cos(b.Face))),
Y: int(math.Floor(p.Y + math.Sin(b.Face))),
}
}
// Meal is the stand this creature's mouth can get to and the grams
// standing on it: of every cell inside Reach, the one holding the most
// tissue. Reach is counted in cells crossed in the worse of the two
// directions, so a reach of one cell is the cell underfoot and the
// eight touching it, and the diagonal neighbours are as close to a
// mouth as the orthogonal ones. Nothing about the answer depends on
// which way the creature is facing: a mouth on a body a cell wide can
// reach round it.
//
// Ties go to the first cell in row order, so two cells holding exactly
// the same grams are settled the same way on every machine.
func (b *Beast) Meal(v *View) (*terra.Stand, float64) {
n := int(b.Kind.Reach)
here := b.Cell()
var best *terra.Stand
most := 0.0
for dy := -n; dy <= n; dy++ {
for dx := -n; dx <= n; dx++ {
st, ok := v.Stand(here.Offset(dx, dy))
if !ok || st.Plant.Mass <= most {
continue
}
best, most = st, st.Plant.Mass
}
}
return best, most
}
Ahead is the body's position in cells plus one cell along the heading,
floored. Note the 1.0 in the division. Writing 1 / terra.Tile
with two whole numbers is a constant division that Go does in whole numbers, and it
comes out as nothing at all, so every creature reports the cell ahead of the top left
corner of the valley and no test fails anywhere.
Meal reads Reach as a ring rather than a straight line. A
reach of one cell is the cell underfoot and the eight touching it, so a mouth does not
have to be pointed at its dinner and a plant on the diagonal is no further away than a
plant to the east. That choice is what makes the rule at the end of this chapter safe:
a ray sample sitting less than one cell out has landed either underfoot or in one of
those eight, so a bite asked for on the strength of a near sample is a bite the check
allows. It also picks the richest cell inside the ring, which costs nine map lookups
and settles ties by row order, so two identical cells are resolved the same way on
every machine that ever runs it.
Stand a creature on 3,1, at the north-west of the valley, with soil ahead of it and a plant beside it, and ask the check all six questions.
$ go run ./cmd/acts -mode check -at 3,1
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
creature 1 on 3,1, facing 0 degrees, 400.0000 in the store
the cell ahead of it is 4,1, soil
the richest cell inside its reach is 2,1 at 383.5560 grams
action price ticks what the check says
rest 0.0000 1 allowed
walk 1.2150 1 allowed
sprint 4.8600 1 allowed
turn left 0.4050 1 allowed
turn right 0.4050 1 allowed
bite 0.1500 2 allowed
Six allowed, which is the boring answer and the one to see first. Now move it four rows down to the north shore of the pond, where the ground ahead is water and there is not a gram of tissue standing within reach of it.
$ go run ./cmd/acts -mode check -at 3,5
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
creature 1 on 3,5, facing 0 degrees, 400.0000 in the store
the cell ahead of it is 4,5, water
the richest cell inside its reach holds 0.0000 grams
action price ticks what the check says
rest 0.0000 1 allowed
walk 1.2150 1 creature 1 at 4,5 cannot walk: the cell ahead is open water or off the grid
sprint 4.8600 1 creature 1 at 4,5 cannot sprint: the cell ahead is open water or off the grid
turn left 0.4050 1 allowed
turn right 0.4050 1 allowed
bite 0.1500 2 creature 1 at 3,5 cannot bite: no standing tissue inside reach
Four of the six are still allowed, and the two refusals name different rules, different cells and different reasons. Both travelling entries are refused about cell 4,5, which is the cell ahead and not the cell the creature is on: the creature is standing perfectly safely and it is the step that is the problem. The bite is refused about 3,5, which is where the creature is, because the mouth searched the ring around that cell and came back with nothing. Turning is allowed, and it is the only useful thing left, which is exactly the behaviour an animal at a water's edge ought to have available.
The store gate is the one that closes gradually. Walk a store down and watch the table empty from the top, since the entries are refused in order of what they cost.
$ go run ./cmd/acts -mode broke -at 3,1
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
the same creature on 3,1 asked all six at falling stores
a tick of the standing body costs 0.1600 whatever it does
store rest walk sprint turn left turn right bite
400.0000 yes yes yes yes yes yes
5.0000 yes yes yes yes yes yes
4.8600 yes yes yes yes yes yes
1.3000 yes yes - yes yes yes
1.2149 yes - - yes yes yes
0.4050 yes - - yes yes yes
0.1500 yes - - - - yes
0.1499 yes - - - - -
0.0000 yes - - - - -
the walk at 1.2149 in the store, read three ways
as a sentence creature 1 at 3,1 cannot walk: the store cannot pay for it
as a rule errors.Is(err, beast.ErrBroke) is true
as numbers it wanted 1.2150 and the store held 1.2149, short by 0.0001
The sprint goes first, at some store between 4.86 and 1.30. Then the walk, then the two
turns, and last the bite, which is the cheapest thing a creature can do that is not
nothing. Below 0.1500 in the store the only entry left is rest, and rest is free precisely
so that this row exists: an animal with an empty store can still be asked what it wants to
do and still has an answer. The three lines at the bottom are one error value read three
ways. As a sentence it names the creature, the cell and the rule. As a rule it answers
errors.Is without any string being compared. As numbers it says the walk
wanted 1.2150 and the store held 1.2149, short by a ten-thousandth, which is the sort of
detail that turns a puzzling run into a one-line fix.
One tick from senses to bite
Three fields go on the creature before the tick can be written, and between them they answer two questions. Who is this, because the log names actors and a pointer is not a name. And what is it part way through, because the table has a duration column and a duration column nothing ever reads is decoration.
// internal/beast/body.go
type Beast struct {
ID sim.EntityID // what the log calls it, for as long as it lives
Name string
Kind *Kind
Store float64 // energy units in the store right now
Born int // the tick it was born on
Ate float64 // grams it has taken off the valley since it was born
Dead bool // its store reached nothing and it stopped
Last Ledger
field.Body // where it is and how fast, in world pixels
Face float64 // the heading its eyes look along, radians clockwise of east
// The action under way and the ticks it still owes. While Owed is
// above nothing the creature is not asked what it wants to do: the
// action it is part-way through takes the tick.
Busy Act
Owed int
// Greedy is the bite as it was first written: a whole mouthful
// taken whether or not the plant has one to give. Blind is the
// caster without the line that lets a ray leave the cell it starts
// on. Both are kept behind flags so the failures can be run instead
// of described, and both are false in every creature this book
// ships.
Greedy bool
Blind bool
// Unpaid is the bite with the repayment switched off: the grams
// still leave the plant, but the minerals they were holding go
// nowhere instead of landing on the cell that lent them. It is
// named for the ground's books and not for the animal, because a
// creature carrying it eats exactly as it always did and the whole
// of the loss is the bed's. It is kept behind a flag so what that
// missing line costs can be run instead of described, and it is
// false in every creature this book ships.
Unpaid bool
}
// internal/beast/act.go, continued
// Tag hands out the identities a roster of creatures is known by: one
// each, in founding order, never reused, skipping anything already
// carrying one. It hands back the next free identity so a population
// that grows later goes on counting from where this one stopped.
//
// Identities are handed out here and not in Spawn because being
// somebody is a fact about a population, not about a body: two
// creatures made by two different benches would both be number one
// until something put them in the same valley.
func Tag(herd []*Beast, next sim.EntityID) sim.EntityID {
for _, b := range herd {
if b.ID == 0 {
b.ID = next
next++
}
}
return next
}
ID is the same sim.EntityID the world has handed out to
everything it has ever needed to name: a number given once, in order, never reused
after a death. It is handed out by Tag and not by Spawn,
because being somebody is a fact about a population and not about a body. Two creatures
stamped by two different benches would both be number one until something put them in
the same valley, and Tag is that something.
Now the tick itself. It has one branch for a creature that is still busy, one for a choice the check refuses, and one for a choice it allows, and all three of them end in the same place.
// internal/beast/act.go, continued
// Tally is one creature's tick under the table, in the order the
// numbers are worked out.
type Tally struct {
Act Act // what the creature actually did with the tick
Held bool // the tick went to an action already under way
Grams float64 // grams taken off a stand
Gained float64 // energy units those grams were worth
Spilt float64 // energy units of that the store had no room for
Price float64 // energy units the action itself charged
Rent float64 // energy units the standing body charged
Haul float64 // energy units the tick's actual travel charged
Left float64 // what is in the store when the tick closes
}
// Do is one creature's whole tick: the action it was asked for, if the
// check allows it and the creature is free to choose, and then the same
// Step every creature takes whatever it did.
//
// Three things happen in a fixed order, and the order is the design. A
// creature part-way through an action it took earlier is not asked
// again: the action under way gets the tick and the choice is dropped.
// A choice the check refuses costs the creature its tick and nothing
// else, and the refusal comes back as an error so the caller can put it
// in the log instead of losing it. And every path ends in Step, so the
// movement charge is not something an action can dodge: a creature that
// rests while still moving is charged for the moving.
func (b *Beast) Do(a Act, v *View) (Tally, error) {
var t Tally
var err error
if b.Owed > 0 {
b.Owed--
t = Tally{Act: b.Busy, Held: true}
b.aim(0)
} else {
if err = b.Legal(a, v); err != nil {
a = Rest
}
t = b.apply(a, v)
b.Busy, b.Owed = a, Table[a].Ticks-1
}
toll := b.Step()
t.Rent, t.Haul = toll.Rent, toll.Haul
t.Left = b.Store
return t, err
}
Every path through Do ends in Step, and that is the load
bearing line. Step is the movement chapter's: it moves the body by
whatever went into the accumulator and then charges the store for being a body and for
moving one, at the speed the body actually reached. Putting it at the bottom of every
branch means the travel charge is not something an action can dodge. A creature that
chooses to rest while it is still gliding is charged for the gliding, which is the same
fact the movement chapter printed when a walker's store emptied and the drift went on
being charged past nothing.
The refusal branch is three lines and carries the boundary. The action becomes
Rest, the tick proceeds, and the error is carried out of the function
beside a perfectly valid Tally. Nothing is rolled back, because nothing
was done. The creature really did spend the tick, and the caller really does get told
why.
// internal/beast/act.go, continued
// apply is the effect of one action and the charge that goes with it.
// The two travelling entries are the exception: the table quotes what
// they will cost at the speed they ask for, and Step charges the speed
// the body actually reached, which is never more than the speed it was
// asked for. Everything else is charged here, because nothing else in
// the tick would.
func (b *Beast) apply(a Act, v *View) Tally {
k := b.Kind
t := Tally{Act: a}
switch a {
case Walk:
b.aim(k.Top / 2)
case Sprint:
b.aim(k.Top)
case Left:
t.Price = k.Wheel()
b.Face = wrap(b.Face - k.Turn())
b.aim(0)
case Right:
t.Price = k.Wheel()
b.Face = wrap(b.Face + k.Turn())
b.aim(0)
case Bite:
t.Price = k.Chew()
if st, _ := b.Meal(v); st != nil {
t.Grams = b.Graze(st, v.Valley.Bed)
t.Gained = t.Grams * k.Convert
b.Store += t.Gained
if b.Store > k.Full {
t.Spilt = b.Store - k.Full
b.Store = k.Full
}
}
b.aim(0)
default:
b.aim(0)
}
b.Store -= t.Price
return t
}
// aim puts one tick of legs into the accumulator: the force that steers
// the body toward travelling at speed cells a tick along the heading.
// A speed of nothing is a real request and the one four of the six
// entries make: it asks the legs to stop the body, which takes them the
// same handful of ticks that starting it did.
func (b *Beast) aim(speed float64) {
want := field.Vec2{
X: math.Cos(b.Face) * speed * terra.Tile,
Y: math.Sin(b.Face) * speed * terra.Tile,
}
b.Body.ApplyForce(b.Push(want))
}
Four of the six ask the legs for no velocity at all, and that is a real request rather than an absence of one. Nothing in this world slows a body down by itself, so a creature that stopped asking to travel would carry on travelling for ever; asking for zero puts a braking force into the accumulator through the same steering the walking entries use, and the body sheds its speed over the same handful of ticks it took to build it.
The two travelling entries charge nothing in apply, and that is not an
oversight. Their price on the table is a quote, used by the check to ask whether the
store could stand the tick; the money actually leaves in Step, priced off
the speed the body reached. A creature that asks for a walk from a standstill does not
reach a walk on the first tick and is not charged for one. The quote is an upper bound
and the charge is the truth.
The bite is the only entry that moves grams, and it moves them through
Graze, unchanged since the chapter that wrote it: no bite is bigger than
the plant it comes off, the grams leave the stand's standing mass, and the store has a
ceiling that anything past it spills over. What is new is the second argument. The
benches that priced a bite had one plant and no valley under it, so they passed
nil and there was no ground for the minerals in a mouthful to go back to.
Here there is: v.Valley.Bed is the real one, and the nutrient the eaten
tissue was holding lands on the cell the plant is standing on as the bite is taken.
Which leaves the question of who does the choosing. The answer is one method, and the narrowness of it is the point: whatever chooses is handed the twenty-four numbers and hands back one of six, and there is no third thing in the signature for it to reach the valley through.
// internal/beast/act.go, continued
// Mind is anything that can look at one reading of the world and name
// one of the six. Twenty-four numbers in, one action out, and nothing
// else crosses: a Mind cannot see the valley, cannot reach a plant, and
// cannot spend anything. Everything that ever drives a creature in this
// world goes through this one method.
type Mind interface {
Pick(row *Senses) Act
}
// Hunt is the rule this chapter drives its creatures with, written out
// by hand. Find the ray that stopped nearest on a plant. If it is off
// to one side, turn that way. If it is ahead and the plant is further
// off than Near, walk. Otherwise bite.
//
// Near is in cells, and half a cell is the value that makes the bite
// safe rather than lucky: a sample sitting less than one cell from the
// body is inside the cell underfoot or one of the eight touching it,
// and every one of those is inside a reach of one cell. A bite this
// rule asks for is a bite the check allows.
//
// Front is how many rays count as straight ahead, and it is three and
// not one for a reason that is arithmetic rather than taste. The rays
// are Gap apart and one turn action swings the heading by Swing of a
// whole turn, which for this row is two rays' worth. A target one ray
// left of the middle would be turned to one ray right of it, then back
// again, for as long as the creature lived. Aiming can only ever be as
// fine as the coarser of the two steps.
//
// Sight is the span the row's ray distances were divided by, and it is
// on the rule because a Mind is handed shares of a span and never the
// span itself. A rule that wants to think in cells has to be told what
// a whole one was.
//
// Blind is what to do on a tick where no ray found a plant at all. A
// Mind that reads nothing but the row cannot invent a direction, and
// the one this package ships turns right until something shows up;
// anything better belongs to the caller, which is why it is a field.
type Hunt struct {
Near float64
Sight float64
Front int
Blind Mind
}
// Pick reads the fan out of the row and returns one of the six.
func (h Hunt) Pick(row *Senses) Act {
mid, half := (Fan-1)/2, h.Front/2
best, near := -1, math.Inf(1)
for i := 0; i < Fan; i++ {
if row[Eye(i)+1] != Worth[Plant] {
continue
}
if d := row[Eye(i)]; d < near {
best, near = i, d
}
}
switch {
case best < 0:
if h.Blind != nil {
return h.Blind.Pick(row)
}
return Right
case best < mid-half:
return Left
case best > mid+half:
return Right
case near*h.Sight <= h.Near:
return Bite
}
return Walk
}
Pick reads eighteen of the twenty-four slots and ignores the rest. It
looks for the ray whose code says plant, takes the nearest of them, and answers with a
turn, a walk or a bite. It cannot see the valley, cannot follow a pointer to a plant,
and cannot spend anything, because the only argument it has is an array of numbers.
Front is the detail that took a run to find. The rays sit fifteen degrees
apart and one turn action swings the heading thirty, so a turn moves the target two
rays. A rule that insisted on the middle ray would push a target one ray left over to
one ray right, then back, for as long as the creature lived. Aiming is only ever as
fine as the coarser of the two steps, so the middle three rays count as ahead.
Twelve ticks of it, from the same cell the check was asked about, with the valley held still so that nothing grows back underneath the experiment.
$ go run ./cmd/acts -mode hunt -at 3,1 -ticks 12
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
creature 1 put on 3,1 by hand, facing 0 degrees, 400.0000 in the store
driven by the rule: turn to the nearest plant ray, walk it down, bite inside 0.50 of a cell,
with the middle 3 rays counted as straight ahead
the valley is held still, so nothing grows back while the creature eats
0 other creatures on stream 12
tick cell facing action price rent haul grams store
1 3,1 0.0 walk 0.0000 0.1600 0.3037 0.0000 399.5362
2 3,1 0.0 walk 0.0000 0.1600 1.2150 0.0000 398.1612
3 4,1 0.0 walk 0.0000 0.1600 1.2150 0.0000 396.7862
4 4,1 0.0 walk 0.0000 0.1600 1.2150 0.0000 395.4112
5 4,1 0.0 walk 0.0000 0.1600 1.2150 0.0000 394.0362
6 4,1 0.0 bite 0.1500 0.1600 0.3037 0.2500 394.4225
7 4,1 0.0 (bite) 0.0000 0.1600 0.0000 0.0000 394.2625
8 4,1 330.0 turn left 0.4050 0.1600 0.0000 0.0000 393.6975
9 4,1 330.0 bite 0.1500 0.1600 0.0000 0.2500 394.3875
10 4,1 330.0 (bite) 0.0000 0.1600 0.0000 0.0000 394.2275
11 4,1 330.0 bite 0.1500 0.1600 0.0000 0.2500 394.9175
12 4,1 330.0 (bite) 0.0000 0.1600 0.0000 0.0000 394.7575
12 ticks, 12 lines printed: 5 walk, 1 turn left, 6 bite, 0 refused
0.7500 grams eaten, 394.7575 left in the store, standing on 4,1 facing 330.0
the log took 12 lines, one an action and one a refusal:
{"t":1,"ev":"act","id":1,"what":"walk"}
at the last tick 5 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 19.5576 grams
The first five ticks are a walk and they show the ramp: 0.3037 charged on the first tick, then 1.2150 on every one after, because a body cannot go from nothing to a walk inside one tick and is charged for the speed it actually held. On tick 6 the plant is inside half a cell, the rule asks for a bite, and the tally shows both sides of it at once: a price of 0.1500 out, a quarter of a gram in, and a store that goes up instead of down for the first time in the run. Tick 7 is the bite's second tick, printed in brackets because the creature was never asked what it wanted; it pays the upkeep and nothing else. Then a turn, and then the pattern the animal settles into.
Twelve ticks is not enough to say whether the rule makes a living. Two hundred is, and starting it on a tenth of a store makes the answer visible.
$ go run ./cmd/acts -mode hunt -at 3,1 -ticks 200 -every 40 -store 40
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
creature 1 put on 3,1 by hand, facing 0 degrees, 40.0000 in the store
driven by the rule: turn to the nearest plant ray, walk it down, bite inside 0.50 of a cell,
with the middle 3 rays counted as straight ahead
the valley is held still, so nothing grows back while the creature eats
0 other creatures on stream 12
tick cell facing action price rent haul grams store
1 3,1 0.0 walk 0.0000 0.1600 0.3037 0.0000 39.5363
6 4,1 0.0 bite 0.1500 0.1600 0.3037 0.2500 34.4225
8 4,1 330.0 turn left 0.4050 0.1600 0.0000 0.0000 33.6975
9 4,1 330.0 bite 0.1500 0.1600 0.0000 0.2500 34.3875
40 4,1 330.0 (bite) 0.0000 0.1600 0.0000 0.0000 42.1775
80 4,1 330.0 (bite) 0.0000 0.1600 0.0000 0.0000 52.7775
120 4,1 330.0 (bite) 0.0000 0.1600 0.0000 0.0000 63.3775
160 4,1 330.0 (bite) 0.0000 0.1600 0.0000 0.0000 73.9775
171 4,1 330.0 rest 0.0000 0.1600 0.0000 0.0000 75.6979
200 4,1 330.0 rest 0.0000 0.1600 0.0000 0.0000 71.0579
200 ticks, 10 lines printed: 5 walk, 1 turn left, 164 bite, 30 refused
20.3076 grams eaten, 71.0579 left in the store, standing on 4,1 facing 330.0
the log took 200 lines, one an action and one a refusal:
{"t":1,"ev":"act","id":1,"what":"walk"}
{"t":171,"ev":"refused","id":1,"what":"creature 1 at 4,1 cannot bite: no standing tissue inside reach"}
at the last tick 5 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 0.0000 grams
The store climbs from 40 to nearly 76 over a hundred and seventy ticks of steady eating, which is the pace arithmetic from the Interlude turning up in a run: one bite every two ticks brings in 0.8500 and costs 0.3200 of upkeep, so the store gains about a quarter of a unit a tick. Then, on tick 171, the bite starts being refused, and the last two lines of the run say why in a way no argument could. Five of the nine rays still stop on a plant. The richest cell inside the creature's reach holds 0.0000 grams. It has eaten out every cell it can get to, the stands are still standing there holding nothing, and a ray brings back a code and never a quantity, so the fan goes on reporting plants in five directions while the mouth finds nothing at all. The check is the only thing in the creature that knows the difference, and the last thirty ticks of the run are a store falling again while the rule keeps asking.
One more thing that run says quietly. The creature walked east and left the largest plant in the valley behind it. The check on that same cell reported 383.5560 grams standing at 2,1, one cell west of where it started, and it went the other way for twenty grams. Its rays cannot look behind it, and even if they could, a ray that stops on a plant has no opinion at all about how much of it is there.
The bool is still tempting, because the two versions of the check ask exactly the same questions. Here it is, written against the same exported pieces the shipped check uses, with everything except the answer thrown away.
// cmd/acts/main.go
// okay is the check as it was first written: two questions, one bit,
// no reason. It asks exactly what beast.Legal asks and throws away
// everything except the answer.
func okay(b *beast.Beast, a beast.Act, v *beast.View) bool {
if beast.Table[a].Price(*b.Kind) > b.Store {
return false
}
switch a {
case beast.Walk, beast.Sprint:
c := b.Ahead()
return v.Valley.Bed.In(c) && v.Valley.Bed.Kind(c) != sim.Water
case beast.Bite:
st, _ := b.Meal(v)
return st != nil
}
return true
}
The bench keeps it behind a mode so the two can be run against each other. Drive a creature with a rule that sprints whatever the row says, give it twenty units to spend, and watch it with the bool version.
$ go run ./cmd/acts -mode bool -at 3,1 -mind sprint -store 20 -ticks 10
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
creature 1 put on 3,1 by hand, facing 0 degrees, 20.0000 in the store
driven by a rule that sprints whatever the row says
the valley is held still, so nothing grows back while the creature eats
the check is the bool version: a refused action is a rest and says nothing
0 other creatures on stream 12
tick cell facing action price rent haul grams store
1 3,1 0.0 sprint 0.0000 0.1600 0.3037 0.0000 19.5362
2 3,1 0.0 sprint 0.0000 0.1600 1.2150 0.0000 18.1612
3 4,1 0.0 sprint 0.0000 0.1600 2.7338 0.0000 15.2675
4 4,1 0.0 sprint 0.0000 0.1600 4.8600 0.0000 10.2475
5 5,1 0.0 sprint 0.0000 0.1600 4.8600 0.0000 5.2275
6 5,1 0.0 sprint 0.0000 0.1600 4.8600 0.0000 0.2075
7 5,1 0.0 rest 0.0000 0.1600 2.7338 0.0000 -2.6863
8 6,1 0.0 rest 0.0000 0.1600 1.2150 0.0000 -4.0613
9 6,1 0.0 rest 0.0000 0.1600 0.3038 0.0000 -4.5250
10 6,1 0.0 rest 0.0000 0.1600 0.0000 0.0000 -4.6850
10 ticks, 10 lines printed: 4 rest, 6 sprint, 0 refused
0.0000 grams eaten, -4.6850 left in the store, standing on 6,1 facing 0.0
the log took 10 lines, one an action and one a refusal:
{"t":1,"ev":"act","id":1,"what":"sprint"}
at the last tick 4 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 354.5717 grams
Six sprints at up to 5.0200 a tick empty a store of twenty in six ticks, and then the
tape says rest, four times, and stops. Nothing in that run is a lie. The
creature really did rest on those four ticks. What the run cannot tell you is whether it
rested because a rule chose to or because something refused it, and those are opposite
situations: the first is an animal husbanding a store and the second is an animal stuck.
The summary line makes the confusion official by counting four rests, and the log has one
line per tick saying the same thing.
Now the same ten ticks with the shipped check.
$ go run ./cmd/acts -mode hunt -at 3,1 -mind sprint -store 20 -ticks 10 | tail -7
10 ticks, 10 lines printed: 6 sprint, 4 refused
0.0000 grams eaten, -4.6850 left in the store, standing on 6,1 facing 0.0
the log took 10 lines, one an action and one a refusal:
{"t":1,"ev":"act","id":1,"what":"sprint"}
{"t":7,"ev":"refused","id":1,"what":"creature 1 at 5,1 cannot sprint: the store cannot pay for it"}
at the last tick 4 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 354.5717 grams
Every number in the tape is identical, because it is the same simulation: the same six sprints, the same overdraft to −4.6850, the same finish at 6,1. The difference is entirely in what was recorded. The summary now reads six sprints and four refusals rather than four rests, and the log carries a line naming the creature, the cell, the action and the rule: creature 1 at 5,1 could not sprint because the store could not pay for it. The bool version was not wrong about what happened. It had nothing to say about why, and by the time anybody wanted to know, the tick that knew was long gone.
The rule underneath is not about creatures. A test that refuses something has to hand back the reason at the moment it refuses, because that is the only moment the reason exists. Everything the refusal knows is on the stack right then: which entry, which cell, which rule, and the two numbers that were compared. A bool discards all of it, and no amount of logging further out can get it back, because further out is a place where the only thing known is that a creature rested.
The last two lines of the run are a bonus the failure did not intend. The sprinting creature finished at 6,1 with 354.5717 grams of plant inside its reach and an overdrawn store, having spent every unit it had running past its dinner.
Why the driver is a field
Strip the valley out and two mechanisms are left, and they need to stay separate because they solve different problems.
The first is that the cost, the duration and the precondition of every option live in one table, as data, next to each other. That is what makes them comparable. Nothing has to call a function and see what happens to find out whether an action is affordable, so the question "which of these can this creature do right now" is answered by six cheap tests rather than by six attempts and five rollbacks. It also means the answer to "what does a sprint cost" has exactly one place to live, and a second kind of creature with different legs gets a different sprint price without a line of the table changing.
The second is the seam. Mind is one method taking one array and returning one
small integer, and the rule this chapter wrote sits behind it like anything else would.
Swap the field and the whole apparatus underneath goes on working: the same table, the
same check, the same tick, the same log. The bench proves that by driving the same creature
with a rule that has no plan at all, only the wandering the walkers used, reduced to one
draw a tick against the six entries.
$ go run ./cmd/acts -mode hunt -at 3,5 -mind wander -ticks 12
acts: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
creature 1 put on 3,5 by hand, facing 0 degrees, 400.0000 in the store
driven by the wander alone, one draw off stream 13 a tick
the valley is held still, so nothing grows back while the creature eats
0 other creatures on stream 12
tick cell facing action price rent haul grams store
1 3,5 0.0 rest 0.0000 0.1600 0.0000 0.0000 399.8400
2 3,5 30.0 turn right 0.4050 0.1600 0.0000 0.0000 399.2750
3 3,5 60.0 turn right 0.4050 0.1600 0.0000 0.0000 398.7100
4 3,5 60.0 rest 0.0000 0.1600 0.0000 0.0000 398.5500
5 3,5 30.0 turn left 0.4050 0.1600 0.0000 0.0000 397.9850
6 3,5 30.0 rest 0.0000 0.1600 0.0000 0.0000 397.8250
7 3,5 60.0 turn right 0.4050 0.1600 0.0000 0.0000 397.2600
8 3,5 30.0 turn left 0.4050 0.1600 0.0000 0.0000 396.6950
9 3,5 30.0 rest 0.0000 0.1600 0.0000 0.0000 396.5350
10 3,5 30.0 rest 0.0000 0.1600 0.0000 0.0000 396.3750
11 3,5 30.0 rest 0.0000 0.1600 0.0000 0.0000 396.2150
12 3,5 0.0 turn left 0.4050 0.1600 0.0000 0.0000 395.6500
12 ticks, 12 lines printed: 3 turn left, 3 turn right, 6 refused
0.0000 grams eaten, 395.6500 left in the store, standing on 3,5 facing 0.0
the log took 12 lines, one an action and one a refusal:
{"t":2,"ev":"act","id":1,"what":"turn right"}
{"t":1,"ev":"refused","id":1,"what":"creature 1 at 4,5 cannot walk: the cell ahead is open water or off the grid"}
at the last tick 0 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 0.0000 grams
Twelve ticks at the pond's edge and the creature never moves a cell. It draws off its own numbered stream, asks to walk six times, and the ground refuses all six because the cell ahead is water. The tape shows those six as rests; the summary counts them as refusals; the log names the rule. Nothing about the table, the check or the tick changed to accommodate a different driver, and nothing about the driver had to know that a pond existed.
There is a cost to the seam and it is visible in that run. A rule that reads nothing but the row cannot learn that walking into the pond does not work, because the refusal comes back to the caller and not to the rule. The row does carry the evidence, in the ray codes that say water, and the wander never looks at them, which is a property of this particular driver and not of the design. The design is the part that makes it swappable.
A creature standing on a plant sees no plant. The rays skip the cell underfoot, on
purpose, since a ray that reported the ground its own body is on would report it in every
direction; the gradients never read the cell underfoot either, since a difference across
a cell has no use for its middle. So there is no slot anywhere in the twenty-four that
says "you are standing on dinner", and a rule reading only the row will walk off a plant
it could have eaten. The check knows, because Meal searches the ring
including the middle. The row does not.
- Given the browser row, produce all six prices with a calculator, including the
0.4050 for a turn from
Swingand the 0.1500 for a bite fromReach, and say which single number each one spends. - Say why the check asks about the store before it asks about the ground, and why the body's upkeep is deliberately not one of the things it can refuse an action for.
- Handed a
*Refusal, get three different answers out of it, and name the method that letserrors.Issee the rule through the struct. - Explain why the walk and the sprint charge nothing in
apply, and what the number in their price column is actually used for. - Shown a tape where a creature rests for four ticks, know that "rested" and "was refused" look identical from outside and that only the tick that refused could have told them apart.
- Say why the middle three rays count as straight ahead when only one of them is, working from the fifteen degrees between rays and the thirty degrees in a turn.
- Given a run whose rays report plants in five directions and whose mouth finds nothing, name which of the two is telling the truth and why both are working correctly.
Exercise 1 — take the dead zone out. The rule counts three
rays as straight ahead. Set it to one with
go run ./cmd/acts -mode hunt -at 3,1 -ticks 8 -front 1 and predict what
the creature does before you look.
It turns left, then right, then left, then right, and pays 0.4050 every time. One walk on tick 1 brings a plant into ray 3, which is one ray left of the middle, so the rule turns left; a turn is two rays wide, so the plant lands on ray 5, one ray right of the middle, so the rule turns right and puts it back on ray 3. Seven ticks of that cost 2.8350 units and cover no ground at all. The fix is not a better turn: it is admitting that a mechanism that moves in thirty-degree steps cannot aim to fifteen.
$ go run ./cmd/acts -mode hunt -at 3,1 -ticks 8 -front 1 | tail -13
3 3,1 0.0 turn right 0.4050 0.1600 0.0000 0.0000 398.4062
4 3,1 330.0 turn left 0.4050 0.1600 0.0000 0.0000 397.8413
5 3,1 0.0 turn right 0.4050 0.1600 0.0000 0.0000 397.2763
6 3,1 330.0 turn left 0.4050 0.1600 0.0000 0.0000 396.7113
7 3,1 0.0 turn right 0.4050 0.1600 0.0000 0.0000 396.1463
8 3,1 330.0 turn left 0.4050 0.1600 0.0000 0.0000 395.5813
8 ticks, 8 lines printed: 1 walk, 4 turn left, 3 turn right, 0 refused
0.0000 grams eaten, 395.5813 left in the store, standing on 3,1 facing 330.0
the log took 8 lines, one an action and one a refusal:
{"t":1,"ev":"act","id":1,"what":"walk"}
at the last tick 4 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 383.5560 grams
Exercise 2 — let it bite from further off. The rule bites
when a plant is inside half a cell. Raise that to one and a half with
go run ./cmd/acts -mode hunt -at 3,1 -near 1.5 -ticks 200 -every 40 -store 40
and compare the two-hundred-tick run against the one in the chapter.
It never moves, and the reason is odder than it looks. The middle ray stops on a plant 1.50 cells east, which is now inside the threshold, so the rule asks for a bite. The mouth then searches the ring around the cell and finds the 383-gram plant one cell west, which no ray has ever reported because rays do not look backwards. So the creature bites something it cannot see while aiming at something it cannot reach, and spends two hundred ticks doing it. It takes 25.0000 grams against 20.3076, finishes on 93.0000 against 71.0579, and is refused nothing where the other run was refused thirty times. One number in the rule, moved by a cell, and the difference is whether the animal eats the biggest plant in the valley or walks past it to a small one.
$ go run ./cmd/acts -mode hunt -at 3,1 -near 1.5 -ticks 200 -every 40 -store 40 | tail -13
1 3,1 0.0 bite 0.1500 0.1600 0.0000 0.2500 40.6900
40 3,1 0.0 (bite) 0.0000 0.1600 0.0000 0.0000 50.6000
80 3,1 0.0 (bite) 0.0000 0.1600 0.0000 0.0000 61.2000
120 3,1 0.0 (bite) 0.0000 0.1600 0.0000 0.0000 71.8000
160 3,1 0.0 (bite) 0.0000 0.1600 0.0000 0.0000 82.4000
200 3,1 0.0 (bite) 0.0000 0.1600 0.0000 0.0000 93.0000
200 ticks, 6 lines printed: 200 bite, 0 refused
25.0000 grams eaten, 93.0000 left in the store, standing on 3,1 facing 0.0
the log took 200 lines, one an action and one a refusal:
{"t":1,"ev":"act","id":1,"what":"bite"}
at the last tick 3 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 358.5560 grams
Exercise 3 — write the refusals down. Point the bench at
a file with
go run ./cmd/acts -mode hunt -at 3,5 -mind wander -ticks 12 -log acts.jsonl,
then read the file. What does a run of this world leave behind that a tape on a
terminal does not?
Twelve lines of JSON, one per tick, appended to a file that is opened for append and never rewritten. Six of them are actions and six are refusals carrying the sentence the check produced. Run it again and there are twenty-four, because the log is the world's memory and nothing in it is ever overwritten. The count the bench prints is its own appends, not the file's length, which is why the line reads the same on the second run as on the first.
$ go run ./cmd/acts -mode hunt -at 3,5 -mind wander -ticks 12 -log acts.jsonl | tail -3
at the last tick 0 of the 9 rays still stopped on a plant, and the
richest cell inside its reach held 0.0000 grams
appended 12 lines to acts.jsonl
A creature can now perceive the valley, choose one of six things, be told it may not, and pay for the one it may. What nothing in here can do is stop. Every run in this chapter finished with an animal still standing, one of them 4.6850 units under nothing and still charged 0.1600 a tick for a body with nothing left to spend, and the forty grams that body was built out of are grams the valley grew and has not had back.