Two Numbers Under Every Cell
The cell store
The terrain grid has answered one question since it was written: what kind of ground is this cell. That byte is enough for tiles, fire and wind, but it cannot remember that a root drank here last tick. Under the terrain kind goes a store: moisture and nutrient per cell, spent by every root that reaches the cell, never drawn below zero, and refilled by rules the world owns.
The previous plant budget used allowances. A cell handed the plant the same water every tick, and anything not taken vanished at the edge of the calculation. A store changes that bargain: what a plant takes is gone, what it leaves remains, and the next plant reads the history already written into the ground.
A second number joins moisture because water alone cannot explain growth. Nutrient spends down at its own rate and refills on its own terms. The tick fixes only the amount that the scarcest of light, moisture and nutrient can pay for.
The zero floor
Before any code, one cell and one root and a pencil. The cell holds 1.250 units of water, which is a full cell in this world. A root takes 0.250 of it every tick. Something puts 0.050 back every tick. The order of those two is a decision and not an accident: the root draws first, and the refill happens after it.
Tick 1, then. The cell holds 1.250, hands over 0.250, is left with 1.000, and the refill carries it to 1.050. Tick 2 opens at 1.050 and closes at 0.850. Every tick costs the cell 0.200 net, so at the start of tick n it is holding 1.250 minus 0.200 multiplied by one less than n. Five ticks of that leaves 0.250 at the top of tick 6, which the root takes to the last thousandth. Tick 7 opens with 0.050 in the ground and a root asking for 0.250.
That tick is what the chapter turns on, and there are two ways to write the subtraction. Take the demand and move on, and the cell reads −0.200 while the root has been handed water that was never in the ground. Or hand over what is there and stop: the root gets 0.050, the cell reads 0.000, and every number stays inside what the world can account for.
The second rule follows from a pattern already built once in this book. Drag got a cap for exactly this reason: a term that works against a quantity may take it to zero and may not take it past. There the quantity was a body's speed and the term was a force sampled once and then spent for a whole tick. Here the quantity is water in the ground and the term is a root. The sentence carries over unchanged.
What follows the empty tick matters more than the clamp does. From tick 7 on, the cell opens holding exactly the refill, hands all of it over, and is empty again. The root asks for 0.250 and receives 0.050, and it will keep receiving 0.050 for as long as it stands there. A root on an emptied cell lives at the refill rate and not at its own demand. A fifth of what it wanted, every tick, indefinitely. Growth does not stop when the ground runs out. It drops to whatever the ground can put back, and that is a much more interesting number.
// internal/terra/terra.go
// Ground is what one cell hands a plant in a single tick. The three
// numbers are allowances rather than stores: whatever is not taken this
// tick is not saved for the next one.
type Ground struct {
Light float64 // energy units of daylight landing on the cell
Water float64 // water units the soil here can give up
Nutrient float64 // nutrient units the soil here can give up
}
// Plant is one plant's whole physiology. Mass is the only field a tick
// changes; the others are what kind of plant this is.
type Plant struct {
Mass float64 // grams of tissue standing right now
Catch float64 // the share of its cell's light the crown intercepts
Thirst float64 // water units spent per energy unit fixed
Hunger float64 // nutrient units spent per energy unit fixed
Upkeep float64 // energy units one gram of tissue costs per tick
Root float64 // how far the roots reach, in cells
}
// Ledger is one tick's numbers, in the order they are worked out.
type Ledger struct {
Caught float64 // energy the crown fixed
Drawn float64 // water the roots pulled to pay for it
Ate float64 // nutrient the roots pulled to pay for it
Paid float64 // energy the standing tissue charged
Grown float64 // grams the leftover bought; negative when short
Limit string // the resource that decided Caught
}
Five fields are new and everything else is untouched. Ground grows a
nutrient allowance beside its water. Plant grows a Hunger, the
nutrient twin of the thirst it already had, and a Root, which is the first
number on a plant that is about distance instead of physiology: how far from its own
cell it can take anything. The ledger gains a line for the nutrient pulled and a name
for whatever decided the tick. Nutrient earns its place beside water because the two run
out at different speeds, and a resource that never becomes the scarce one might as well
be a constant.
// internal/terra/bed.go
// Cell is what one cell of ground is holding. Where a Ground is an
// allowance for a single tick, both of these are stores: what a root
// leaves behind is still there next tick, and what it takes is gone
// until something puts it back.
type Cell struct {
Moisture float64
Nutrient float64
}
// Bed is the store underneath the allowances: two numbers for every
// cell of a terrain grid, kept in the same flat row-order slice the
// terrain itself lives in.
type Bed struct {
W, H int
kind *sim.Grid
cells []Cell
}
// NewBed lays soil over a terrain grid. Soil cells start full, open
// water holds all the moisture there is and no nutrient, and rock holds
// nothing at all: a root in rock gets nothing because there is nothing
// there.
func NewBed(g *sim.Grid, full Cell) *Bed {
b := &Bed{W: g.W, H: g.H, kind: g, cells: make([]Cell, g.W*g.H)}
for y := 0; y < g.H; y++ {
for x := 0; x < g.W; x++ {
c := sim.Coord{X: x, Y: y}
t, err := g.At(c)
if err != nil {
continue
}
switch t {
case sim.Soil:
b.cells[b.index(c)] = full
case sim.Water:
b.cells[b.index(c)] = Cell{Moisture: full.Moisture}
}
}
}
return b
}
The bed is a second grid laid over the first, in the same flat row-order slice, so the arithmetic that turns a coordinate into a slot is the one already learned. What it does not do is copy the terrain. It keeps a pointer to the grid and asks whenever it needs to know what kind of ground a cell is, so there is one authority on that and no chance of two copies disagreeing after something edits one of them.
Cell and Ground hold nearly the same nouns and mean opposite
things, so the comment on each of them says which it is. A Ground is what
arrives this tick and expires at the end of it. A Cell is a balance that
survives every tick until something spends it. Nearly all the mistakes available in this
chapter come from treating one as the other.
Root radius in cells
A root is not a point. It occupies a patch of ground, and the cheapest honest way to say how much is a radius in cells: every cell whose centre lies within r cells of the plant's own belongs to the root. Two questions come out of that with numeric answers. How many cells is it, and how much does spreading further actually buy.
Numbers before symbols. A radius of 1 cell reaches the plant's own cell and its four neighbours along a row or a column; the diagonals sit 1.414 cells away, further than 1, so they are out. Five cells. Stretch the radius to 1.5 and the diagonals come inside, making nine. At radius 2 the four cells two steps along a row or a column join, making thirteen.
A circle of radius r covers an area of π multiplied by r multiplied by r, and one cell is one unit of area, so that product predicts the count. Set them side by side: 5 against 3.142, 9 against 7.069, 13 against 12.566, 21 against 19.635, 29 against 28.274, 49 against 50.265. The count runs above the area at small radii, because a cell is counted whole the moment its centre is inside and the corners that hang outside are counted with it. By radius 4 the two have nearly met.
cells ≈ π × r × r
What doubling does is the part to keep. From radius 1 to radius 2 the count goes 5 to 13; from 2 to 4, 13 to 49. Area says the factor should be 4 both times, and by the second doubling it very nearly is. A root twice as wide reaches about four times as much ground. Spread the same draw across four times the cells and each one is asked for a quarter as much, and takes four times as long to empty. A wide-rooted plant is not thirstier than a narrow one; it is a plant spreading the same thirst thinner.
// internal/terra/bed.go
// Rootable reports whether anything can put a root into this cell. Soil
// can be rooted in; open water and bare rock cannot.
func (b *Bed) Rootable(c sim.Coord) bool {
t, err := b.kind.At(c)
return err == nil && t == sim.Soil
}
// Reach is the geometry alone: every cell of the grid whose centre lies
// within r cells of c, walked in row order so two runs visit them in
// the same sequence. Coordinates off the grid are dropped here, which
// is the only place that check has to happen.
func (b *Bed) Reach(c sim.Coord, r float64) []sim.Coord {
n := int(r)
var out []sim.Coord
for dy := -n; dy <= n; dy++ {
for dx := -n; dx <= n; dx++ {
if float64(dx*dx+dy*dy) > r*r {
continue
}
p := c.Offset(dx, dy)
if b.In(p) {
out = append(out, p)
}
}
}
return out
}
// Roots is the reach filtered down to the cells that answer: the disc
// is geometry, and the bed decides which of it is ground a root can
// live in.
func (b *Bed) Roots(c sim.Coord, r float64) []sim.Coord {
var out []sim.Coord
for _, p := range b.Reach(c, r) {
if b.Rootable(p) {
out = append(out, p)
}
}
return out
}
$ go run ./cmd/bed -mode disc
bed: a root of radius r, and a draw of 1.500 split evenly across it
r cells pi*r*r cells/pi*r*r per cell ticks to dry
1.0 5 3.142 1.592 0.3000 4.2
1.5 9 7.069 1.273 0.1667 7.5
2.0 13 12.566 1.035 0.1154 10.8
2.5 21 19.635 1.070 0.0714 17.5
3.0 29 28.274 1.026 0.0517 24.2
4.0 49 50.265 0.975 0.0306 40.8
Two methods and not one, because two different questions are being asked.
Reach is geometry: which cells are near enough. Roots is the
ground's answer: which of those hold anything a root can take. Keeping them apart makes
the number of cells a plant actually feeds from a fact about where it is standing, so a
plant against the rim with a third of its disc in rock has a third less soil to draw on
and not one line of special-casing anywhere.
Reach is also where the off-grid check happens, once. A plant near an edge
has part of its disc outside the world, and each of those coordinates would index past
the end of a slice. Dropping them here means Roots, Draw and
everything above them are handed only coordinates that name real cells. The last column
divides a full cell by what one cell gives up and says how many ticks that lasts with
nothing coming back: a ranking, not a forecast, and it says that quadrupling the cells
quadruples the patience of the ground.
Figure 36.1 — the disc is geometry; the bed decides how much of it is ground.
// internal/terra/bed.go
// Offer is the Ground under a plant this tick: the light landing on its
// own cell, and everything its roots could pull out of every cell they
// reach. It changes nothing, so two plants asking in the same tick both
// get told the truth as it stands when they ask.
func (b *Bed) Offer(at sim.Coord, r, light float64) Ground {
g := Ground{Light: light}
for _, c := range b.Roots(at, r) {
s := b.cells[b.index(c)]
g.Water += s.Moisture
g.Nutrient += s.Nutrient
}
return g
}
// Draw pulls want out of the ground under a plant and reports what it
// got. Every root cell gives up a share in proportion to what it is
// holding, so a nearly empty cell is asked for nearly nothing and no
// cell is ever asked for more than it has.
func (b *Bed) Draw(at sim.Coord, r float64, want Cell) Cell {
cells := b.Roots(at, r)
var have Cell
for _, c := range cells {
s := b.cells[b.index(c)]
have.Moisture += s.Moisture
have.Nutrient += s.Nutrient
}
var got Cell
for _, c := range cells {
s := &b.cells[b.index(c)]
got.Moisture += bite(&s.Moisture, want.Moisture, have.Moisture)
got.Nutrient += bite(&s.Nutrient, want.Nutrient, have.Nutrient)
}
return got
}
// bite takes one cell's share of a draw: what the cell is holding
// divided by what the whole root can reach, multiplied by what was
// asked for, and never more than the cell has.
func bite(store *float64, want, have float64) float64 {
if want <= 0 || have <= 0 || *store <= 0 {
return 0
}
take := want * (*store / have)
if take > *store {
take = *store
}
*store -= take
return take
}
Offer adds up what the roots can reach and changes nothing, so it can be
called as often as anyone likes and two plants asking in the same tick are both told the
truth as it stands when they ask. Draw is the half that moves numbers, and
it takes from each cell in proportion to what that cell is holding. A cell with half the
water gives up half the share; a cell with none gives up none. That rule is what makes
the clamp inside bite almost never fire, because a proportional split
cannot overdraw a cell unless the total was short to begin with.
An even split is the obvious alternative and it is the one that breaks, further down this page.
// internal/terra/bed.go
// Regen trickles a fixed amount of both resources back into every cell
// a root could reach, never past the cap. This is a placeholder rate
// and nothing more: the amount is the same in the driest corner of the
// valley as it is on the shore of the pond.
func (b *Bed) Regen(rate, cap Cell) {
for y := 0; y < b.H; y++ {
for x := 0; x < b.W; x++ {
c := sim.Coord{X: x, Y: y}
if !b.Rootable(c) {
continue
}
s := &b.cells[b.index(c)]
s.Moisture = math.Min(cap.Moisture, s.Moisture+rate.Moisture)
s.Nutrient = math.Min(cap.Nutrient, s.Nutrient+rate.Nutrient)
}
}
}
$ go run ./cmd/bed -mode cell -ticks 10
bed: one cell holding 1.250, one root asking 0.250 a tick,
the trickle putting back 0.050
tick holding asked got left after trickle
1 1.250 0.250 0.250 1.000 1.050
2 1.050 0.250 0.250 0.800 0.850
3 0.850 0.250 0.250 0.600 0.650
4 0.650 0.250 0.250 0.400 0.450
5 0.450 0.250 0.250 0.200 0.250
6 0.250 0.250 0.250 0.000 0.050
7 0.050 0.250 0.050 0.000 0.050
8 0.050 0.250 0.050 0.000 0.050
9 0.050 0.250 0.050 0.000 0.050
10 0.050 0.250 0.050 0.000 0.050
Six full draws, one short draw of 0.050 at tick 7, and the refill rate from then on. That is the pencil arithmetic from the top of the chapter, printed by the code, and every column matches to the last digit.
Regen is the weakest thing on this page and its comment says so. It adds
the same amount to every soil cell on the grid, capped at full, whether the cell is on
the shore of the pond or in the driest corner under the rim. Ground does not work like
that: water arrives somewhere in particular, and where it goes after that is decided by
the heights. The constant is a placeholder holding the position until moisture is
allowed to move, and it is labelled as one where somebody editing the file will see it.
The limiting factor
Three things can now hold a plant back, counted in three units: energy from light, water units from the ground, nutrient units from the same ground. Nothing can be compared until they are in one unit, and the previous chapter already chose it. Everything is priced in the energy it can pay for. Light's price is the share of the cell's daylight the crown intercepts. Water's is however much the roots can reach divided by the water the plant spends per unit of energy. Nutrient's is the same division with its own rate.
One question remains: what does the plant do with three prices. Averaging them is tempting, since all three contributed and an average sounds even-handed. It fails hardest at the extreme, so start there.
Stand a plant in full sun on bone-dry ground. Its crown intercepts enough light to fix 2.500 energy units. Its nutrient can pay for 24.000, which is far more than it can use. Its water can pay for nothing at all, because there is no water: 0.000, which is not small but absent. Average the three and the answer is 8.833, so the plant fixes more than three times the energy that lands on its entire crown, on dry ground, this tick. Nothing in the run will complain about it.
Take the smallest of the three and the answer is 0.000. That is the law, and it fits in one line:
fixed = min(light, water, nutrient)
The reason is that the three do not substitute for one another. A gram of tissue needs some of each, and no amount of one covers a shortage of another: sunlight will not wet a root and minerals will not either. Whichever resource covers the least energy sets the energy, and the surplus of the others goes unspent. An average quietly assumes the three are interchangeable, and every row above is that assumption billing the plant for resources it does not have.
The law's second half is that the scarcest resource is not fixed. A seedling in wet
ground is short of light and nothing else. A few ticks later the same plant, bigger and
drinking harder, has spent its cells down and is short of water. Later still the water
trickles back faster than the nutrient does, and the shortage changes hands again. One
min covers all three regimes with no flag to switch between them.
// internal/terra/terra.go
// Limit is the resource holding this plant back on this ground, and the
// energy it can pay for. Light pays for whatever the crown intercepts;
// water and nutrient each pay for as much energy as the plant's rate of
// spending them stretches to. Growth follows the smallest of the three
// and never their average.
func (p Plant) Limit(g Ground) (float64, string) {
e, name := g.Light*p.Catch, "light"
if w := g.Water / p.Thirst; w < e {
e, name = w, "water"
}
if n := g.Nutrient / p.Hunger; n < e {
e, name = n, "nutrient"
}
return e, name
}
// Fix is the energy this plant can turn over in one tick: its share of
// the light landing on the cell, or as much of that as the ground's
// water or nutrient will pay for, whichever runs out first.
func (p Plant) Fix(g Ground) float64 { e, _ := p.Limit(g); return e }
// Tick charges one plant one tick against one cell and hands back the
// numbers that moved. The plant's mass is the only thing it changes,
// and it changes it exactly once.
func (p *Plant) Tick(g Ground) Ledger {
l := Ledger{}
l.Caught, l.Limit = p.Limit(g)
l.Drawn = l.Caught * p.Thirst
l.Ate = l.Caught * p.Hunger
l.Paid = p.Mass * p.Upkeep
l.Grown = l.Caught - l.Paid
p.Mass += l.Grown
return l
}
$ go run ./cmd/bed -mode law
bed: three resources, each priced in the energy it can pay for
light water nutrient min mean
full soil, full sun 2.500 12.500 24.000 2.500 13.000 <- light
most of the water gone 2.500 1.667 24.000 1.667 9.389 <- water
nutrients nearly out 2.500 12.500 0.667 0.667 5.222 <- nutrient
deep shade, wet ground 0.250 12.500 24.000 0.250 12.250 <- light
bone-dry ground 2.500 0.000 24.000 0.000 8.833 <- water
The previous chapter wrote this comparison as an if between two resources,
which is what two resources deserve. Three of them turn it into a running minimum, and
Limit carries out a name along with the number because that name costs
nothing. The comparison that found the smallest already knows which one it was, and a
growth figure alone says how fast while the name says what to fix.
Read the mean column against the min column and the strength of the objection is in the gap. Averaging never produces a number a resource can actually deliver; it produces the average of a bottleneck and two surpluses, which is a quantity nothing in the world corresponds to.
// cmd/bed/main.go
// full is what an untouched soil cell holds, and the cap the trickle
// refills towards.
var full = terra.Cell{Moisture: 1.25, Nutrient: 0.60}
// regen is the placeholder: the same small amount put back into every
// soil cell every tick, wherever the cell is.
var regen = terra.Cell{Moisture: 0.050, Nutrient: 0.004}
// even switches the draw from proportional to an equal split, which is
// this chapter's worked failure.
var even bool
// seedling is the plant every run in this chapter starts with: the
// previous chapter's physiology, plus a nutrient rate and a root that
// reaches beyond the cell it stands on.
func seedling() terra.Plant {
return terra.Plant{
Mass: 1, Catch: 0.25, Thirst: 0.6, Hunger: 0.15, Upkeep: 0.02, Root: 1.5,
}
}
// step is one plant's whole tick against the ground it stands on: ask
// the bed what is there, run the books against that offer, and charge
// the bed for exactly what the books spent.
func step(b *terra.Bed, p *terra.Plant, at sim.Coord) terra.Ledger {
g := b.Offer(at, p.Root, Light)
l := p.Tick(g)
want := terra.Cell{Moisture: l.Drawn, Nutrient: l.Ate}
if even {
b.DrawEven(at, p.Root, want)
} else {
b.Draw(at, p.Root, want)
}
return l
}
$ go run ./cmd/bed -mode grow -ticks 40
bed: a 12x8 valley from seed 5, one seedling at 4,1, root radius 1.5
the disc covers 9 cells and 6 of them are soil
tick caught drawn ate paid grown mass ceiling limit under the plant
1 2.500 1.500 0.375 0.020 2.480 3.480 125.00 light 1.050 0.541
2 2.500 1.500 0.375 0.070 2.430 5.910 125.00 light 0.850 0.483
3 2.500 1.500 0.375 0.118 2.382 8.292 125.00 light 0.650 0.424
4 2.500 1.500 0.375 0.166 2.334 10.626 125.00 light 0.450 0.366
5 2.500 1.500 0.375 0.213 2.287 12.914 125.00 light 0.250 0.307
6 2.500 1.500 0.375 0.258 2.242 15.156 125.00 light 0.050 0.249
7 0.500 0.300 0.075 0.303 0.197 15.352 25.00 water 0.050 0.240
8 0.500 0.300 0.075 0.307 0.193 15.545 25.00 water 0.050 0.232
9 0.500 0.300 0.075 0.311 0.189 15.734 25.00 water 0.050 0.223
10 0.500 0.300 0.075 0.315 0.185 15.920 25.00 water 0.050 0.215
15 0.500 0.300 0.075 0.332 0.168 16.792 25.00 water 0.050 0.172
20 0.500 0.300 0.075 0.349 0.151 17.581 25.00 water 0.050 0.130
25 0.500 0.300 0.075 0.363 0.137 18.294 25.00 water 0.050 0.087
30 0.500 0.300 0.075 0.376 0.124 18.938 25.00 water 0.050 0.045
35 0.440 0.264 0.066 0.388 0.052 19.460 22.00 nutrient 0.056 0.004
40 0.160 0.096 0.024 0.371 -0.211 18.359 8.00 nutrient 0.226 0.004
the bed holds 68.856 of moisture and 22.824 of nutrient, numbers 0b3d4e61f1836901
step is three lines and each is a different kind of thing: ask the ground,
run the books, charge the ground. The books are the previous chapter's, unchanged in
what they do. What is new is that their input is now computed from state that the same
plant spent down last tick.
The column under the plant is the pencil arithmetic again: 1.050, 0.850, 0.650, 0.450, 0.250, and 0.050 at tick 6, which is the one-cell walk from the top of the chapter appearing inside a six-cell root without being told to. Six cells hold 7.500 of water, the refill puts back 0.300 a tick, the plant draws 1.500, so the store loses 1.200 a tick and covers six ticks and no more.
At tick 7 the limit column changes to water and everything falls at once. Caught goes from 2.500 to 0.500, the whole income the refill can support, and the ceiling with it: from 125 grams to 25. The plant is at 15.352 grams and still under the new ceiling, so it keeps growing, slowly. At tick 35 the nutrient runs short as well and takes the limit over, and by tick 40 the ceiling is down to 8 grams with the plant at more than twice that. Growth has turned negative and mass is coming off: 19.460 at tick 35, 18.359 at tick 40. Nothing in the code makes a plant shrink. It falls out of an income that collapsed while the upkeep bill did not.
$ go run ./cmd/bed -mode grow -ticks 40 -maps | tail -10
the bed holds 68.856 of moisture and 22.824 of nutrient, numbers 0b3d4e61f1836901
moisture, ' ' empty . : o O @ full
############
#@@...@@@@@#
#@@...@@@@@#
#@~~~~~@@@@#
#@~~~~~~@@@#
#@@@~~~~@@@#
#@@@~@@@@@@#
############
One plant, forty ticks, and a rectangle of spent ground where the rest of the valley is untouched. Six cells, three across and two down, which is a disc of radius 1.5 with its top row cut off by the rim. That patch is the plant's biography written into the world: anything arriving later can read it, because it is state on the grid and not a private field inside the plant.
Offer hands over the entire contents of every reachable cell as this tick's
allowance, so nothing but the crown stops a plant emptying its root zone in a single
tick. Ground is slower than that: water has to travel through soil to reach a root, and
how fast it travels is a property of the soil. Modelling that means the offer becomes a
rate and not a total, which turns the cliff at tick 7 into a slope. The cliff is honest
for what is written here and the slope needs machinery this page does not have.
The shared cell
Nothing in Plant mentions another plant. There is no neighbour list, no
contest, no arbitration anywhere. Put two seedlings two cells apart and their discs of
radius 1.5 overlap in one column, two soil cells out of the six each of them reaches, and
the competition runs itself.
$ go run ./cmd/bed -mode pair -ticks 40 -maps
bed: two seedlings at 4,1 and 6,1, root radius 1.5, 40 ticks
6 rootable cells each, 2 of them the same cells
tick west mass east mass west limit east limit shared cell 5,1
1 3.480 3.480 light light 0.836 0.483
2 5.910 5.910 light light 0.499 0.377
5 12.914 12.053 light water 0.050 0.136
10 14.101 12.496 water water 0.050 0.099
15 15.148 12.897 water water 0.050 0.069
20 16.094 13.259 water water 0.050 0.044
25 16.950 13.586 water water 0.050 0.023
30 17.512 13.953 nutrient water 0.050 0.004
35 16.598 14.883 nutrient water 0.050 0.004
40 15.772 14.948 nutrient nutrient 0.128 0.004
alone they would weigh 18.359 and 18.359
together they weigh 15.772 and 14.948, numbers 6b4a2d415d413f15
moisture, ' ' empty . : o O @ full
############
#@@::...@@@#
#@@::...@@@#
#@~~~~~@@@@#
#@~~~~~~@@@#
#@@@~~~~@@@#
#@@@~@@@@@@#
############
Alone, either plant reaches 18.359 grams. Together they reach 15.772 and 14.948, and the shared column is where the difference went. The western plant draws first every tick, so when the shared cells hold the last water useful, it is the one that gets it: at tick 5 it is still limited by light while the eastern plant is already short of water. Neither plant is doing anything about the other. The only asymmetry between them is which of two calls happens first.
That makes the call order part of the physics, so it has to be an order the program
controls. A slice walked from the front is one. A map[sim.Coord]*Plant is
not: Go randomises map iteration on purpose, so a world walking its plants out of a map
would give the shared cell to a different plant on every run and never replay twice. The
plants live in a slice and they will keep living in one.
Splitting the draw evenly across the root's cells is the first thing anyone writes. It is one division instead of a loop and a ratio, and with a single plant on untouched ground it is indistinguishable from the proportional version, because every cell holds the same amount and the same share is the right share.
// internal/terra/bed.go
// DrawEven is the obvious version: split the draw equally across the
// root's cells and subtract. Kept so the two can be run against each
// other.
func (b *Bed) DrawEven(at sim.Coord, r float64, want Cell) Cell {
cells := b.Roots(at, r)
n := float64(len(cells))
for _, c := range cells {
s := &b.cells[b.index(c)]
s.Moisture -= want.Moisture / n
s.Nutrient -= want.Nutrient / n
}
return want
}
$ go run ./cmd/bed -mode pair -ticks 40 -even -maps
bed: two seedlings at 4,1 and 6,1, root radius 1.5, 40 ticks
6 rootable cells each, 2 of them the same cells
tick west mass east mass west limit east limit shared cell 5,1
1 3.480 3.480 light light 0.800 0.479
2 5.910 5.910 light light 0.350 0.358
5 11.580 11.192 water water -0.694 0.071
10 12.130 11.964 water water -0.809 0.000
15 12.766 12.616 water water -0.934 -0.074
20 13.341 13.206 water water -1.059 -0.147
25 13.860 13.738 water water -1.184 -0.221
30 14.330 14.220 water water -1.309 -0.295
35 14.695 14.615 nutrient nutrient -1.424 -0.366
40 13.785 13.812 nutrient nutrient -1.289 -0.375
alone they would weigh 18.359 and 18.359
together they weigh 13.785 and 13.812, numbers 84a529bbe9ca4e5d
moisture, ' ' empty . : o O @ full
############
#@@OO OO@@@#
#@@OO OO@@@#
#@~~~~~@@@@#
#@~~~~~~@@@#
#@@@~~~~@@@#
#@@@~@@@@@@#
############
Nothing panics and no number is NaN. Both plants live, both grow, and the summary reads like a slightly worse version of the run above. The tell is the shared cell: at tick 5 it holds −0.694, and by tick 35 it is at −1.424 and still falling. There is no such quantity as negative moisture. What the number records is water handed to a plant that was never in the ground.
Work out how it got there. The shared cell is drawn on twice a tick, once by each plant, while the four private cells on either side are drawn on once. An even split asks all six of a plant's cells for the same amount regardless of what any of them is holding, so the shared cell pays double and hits zero first. Nothing stops it there, so it goes on paying after it is empty, on credit.
Then the second half, which is the surprising one. Look at the map: the private cells
read O, nearly full, and the shared cell reads blank. Both plants are
water-limited from tick 5 in a root zone that is mostly full water. The debt did it.
Offer sums what the root can reach, and it is summing a negative number
with five positive ones, so both plants are told they have far less water than they do,
both cut their draw to match, and the private cells refill to the cap while the plants
starve beside them. Together they weigh 13.785 and 13.812 against 15.772 and 14.948, and
the cells on either side of the debt read two and three bands wetter than the same cells
in the correct run.
The fix is the proportional draw, which cannot overdraw a cell that has less than its
neighbours, because it asks such a cell for proportionally less. There is a quieter
sibling of this bug that survives a clamp, and it is the reason Draw hands
back what it gave. Clamp each cell so no store goes below zero, then let the caller
assume it got what it asked for, and the map looks perfect while the plant is still being
credited with water the ground could not supply. A store that can go below zero is a
store that is not being conserved. A taker that is credited more than the store lost is
the same failure wearing a clean map.
The generalisation is one the wind field already paid for once, and the second payment is where it becomes a rule. A quantity that varies from place to place lives on the grid and not inside whoever needs it. The wind was a read-only field: many things sample it, none of them change it. The bed is a field that is written to, and everything hard about it comes from that one difference. Two readers of a wind get the same answer. Two takers from a store do not, and the store has to say so.
Three properties keep a written field honest, and this page has all three. It is conserved: what a plant receives is exactly what the ground lost. It is floored: a store may reach zero and may not pass it. And it is ordered: the sequence in which takers are served is fixed by the program and not by the runtime. Break the first and resources come from nowhere. Break the second and the first goes with it. Break the third and the world stops replaying, which is the most expensive of the three to find, because it does not show up until the run you were trying to reproduce.
The soil grid after roots
- say what separates a
Groundfrom aCellin one sentence, and name which of the two a number belongs on when you meet a new one. - Given a cell's store, a root's draw and a refill rate, you can walk the water down tick by tick, name the tick the cell first comes up short, and state what the root receives on every tick after that.
- count the cells inside a root of a given radius, check the count against π × r × r, and say what doubling the radius does to the count and to the draw on any one cell.
- Given light, water and nutrient priced in energy, you can produce the energy fixed and name the limiting factor, and say what an average lets a plant do on dry ground.
- Reading a run whose limiting factor moves from light to water to nutrient, you can point at the tick each handover happened, work out the new ceiling, and explain why a plant above its ceiling starts losing mass.
- Shown a soil grid holding a negative number, you know the draw was split evenly across cells that were not equal, and you know what that debt does to everything that reads the cell afterwards.
Exercise 1 — predict the tick the ceiling falls. Using only
the numbers in full and regen, the six rootable cells and a
draw of 1.500 a tick, work out the last tick the plant can pay full price for its
light. Then widen the root to 2.5 and predict the tick again before running
it.
Six cells hold 6 multiplied by 1.250, or 7.500 of water. The refill puts back 6 multiplied by 0.050, or 0.300 a tick, against a draw of 1.500, so the store loses 1.200 net a tick and 7.500 divided by 1.200 is 6.25. Six ticks are covered and the seventh is not, which is what the run shows: light through tick 6, water from tick 7.
At radius 2.5 the disc covers 21 cells, three of them off the top of the grid and eight of the remaining 18 either rim or pond, so 10 answer. That is 12.500 of water and 0.500 a tick coming back, so the net loss is 1.000 and the store covers 12.5 ticks. Predict the handover at tick 13.
$ go run ./cmd/bed -mode grow -ticks 13 -radius 2.5 | tail -4
9 2.500 1.500 0.375 0.390 2.110 21.615 125.00 light 0.350 0.299
10 2.500 1.500 0.375 0.432 2.068 23.683 125.00 light 0.250 0.265
13 0.833 0.500 0.125 0.554 0.279 27.975 41.67 water 0.050 0.190
the bed holds 63.000 of moisture and 22.295 of nutrient, numbers 62db488c78eeeb49
Tick 13 on the nose. Note the bed total: over the same thirteen ticks the wide root left 63.000 of moisture in the valley where the narrow one leaves 67.800, so spreading the roots took more water out and not less. It took it from more places, and the plant is heavier for it, 27.975 grams against the narrow root's 16.454.
Exercise 2 — hunt the bug that leaves no trace. Run the even split with a single plant instead of two and compare every printed column against the correct run. Then compare the digests. What does that say about which runs are worth hashing?
Every printed column is identical, because one plant on untouched ground draws the same amount from six cells that all hold the same amount, so an even share and a proportional share are the same share. The bed totals agree to three decimals as well.
$ go run ./cmd/bed -mode grow -ticks 40 | tail -2
40 0.160 0.096 0.024 0.371 -0.211 18.359 8.00 nutrient 0.226 0.004
the bed holds 68.856 of moisture and 22.824 of nutrient, numbers 0b3d4e61f1836901
$ go run ./cmd/bed -mode grow -ticks 40 -even | tail -2
40 0.160 0.096 0.024 0.371 -0.211 18.359 8.00 nutrient 0.226 0.004
the bed holds 68.856 of moisture and 22.824 of nutrient, numbers 9565cbec90af28ad
The digests differ, and they are the only thing that does. Folding the raw bits of every stored number catches a divergence too small to print and in cells nobody printed, which is exactly the class of bug this page shipped and the pair run exposed. A summary line that agrees proves only that two runs agree about the six numbers somebody chose to look at.
Exercise 3 — stand the seedling in the pond. Move it from 4,1
to 4,4, in the middle of the water. Before running it, use the map from the terrain
chapter to predict how many of its nine disc cells Roots will return, and
what the limiting factor will be on the very first tick.
Rows 3, 4 and 5 around cell 4,4 are almost entirely pond, and Rootable
refuses open water, so of the nine cells in the disc exactly one is soil: 3,5, the
corner where the pond narrows. One cell, 1.250 of water, and a plant whose crown
could fix 2.500 energy if the ground could pay for it.
$ go run ./cmd/bed -mode grow -ticks 10 -x 4 -y 4
bed: a 12x8 valley from seed 5, one seedling at 4,4, root radius 1.5
the disc covers 9 cells and 1 of them are soil
tick caught drawn ate paid grown mass ceiling limit under the plant
1 2.083 1.250 0.312 0.020 2.063 3.063 104.17 water 1.250 0.000
2 0.083 0.050 0.013 0.061 0.022 3.085 4.17 water 1.250 0.000
3 0.083 0.050 0.013 0.062 0.022 3.107 4.17 water 1.250 0.000
the bed holds 73.800 of moisture and 26.015 of nutrient, numbers 7db4ec80d2c060f0
Water on the first tick and every tick after. The single cell empties inside one
tick, and the plant then lives on that cell's refill: 0.083 of energy a tick against
the 2.500 its crown is entitled to, and a ceiling of 4.17 grams instead of 125. The
column under the plant is the joke: 1.250 of moisture, sitting in the cell it is
standing on, and 0.000 of nutrient, because NewBed gives water cells
moisture and no nutrient and Rootable will not let a root touch either.
A plant dying of thirst in a pond is what the code says and it is plainly wrong about the world. It is also a decision rather than a bug, and it is a decision you can read: roots live in soil is one method, and which plants can stand in water is a trait that belongs to a species and no species exists yet.
Every cell in this valley refills at the same rate, and that constant is now the least defensible number on the grid. A valley has a spring in it, and water arriving at one cell does not sit where it lands: it runs downhill, soaks into ground that will take it, and leaves from the top wherever the sun reaches. Give the ground heights and the moisture numbers stop being a placeholder and start being a record of where the water actually goes, which is what makes one seedling's cell worth twice its neighbour's. Next: the spring, the slope, and moisture that moves.