The World Vol 4 · The Terrarium
ch 36 / 105
Chapter 36

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.

▣ Build · stage 1 — the ledger's third resource
// 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.

▣ Build · stage 2 — a store under every cell
// 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.

∑ Interlude — counting the cells in a circle

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.

rthe root radius, in cells: how far from its own cell a plant can take anything
πpi, 3.14159…: how many times a circle's width fits around its edge, and the constant in its area
π × r × rthe area of a circle of radius r, counted in cells, since one cell is one unit of area
dthe draw: what the plant's roots take of one resource in a tick, before it is divided up
d ÷ cellswhat a single cell inside the root gives up, if the draw is split evenly
▣ Build · stage 3 — the disc, and the part of it that answers
// 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.

A root of radius 2.5 at cell 4,1, and which of its cells answer A five by five block of cells, columns 2 to 6 and rows minus 1 to 3, centred on the plant at column 4 row 1. The four corner cells fall outside the circle of radius 2.5. Three cells in the top row lie off the grid and are marked off. The five cells of row 0 are the rock rim. Three cells of row 3 are open water. The ten cells of rows 1 and 2 are soil and are the only ones that answer, with the plant's own cell among them. A legend on the right counts 21 cells within radius 2.5, minus 3 off the grid, minus 5 rock rim, minus 3 open water, leaving 10 cells that answer the root. A ROOT OF RADIUS 2.5 AT CELL 4,1 2 3 4 5 6 -1 0 1 2 3 off off off # # # # # . . * . . . . . . . ~ ~ ~ WHAT THE ROOT ACTUALLY GETS 21 cells lie within radius 2.5 − 3 of them are off the grid − 5 are the rock rim − 3 are open water = 10 cells answer the root the four corners fall outside the circle and were never asked

Figure 36.1 — the disc is geometry; the bed decides how much of it is ground.

▣ Build · stage 4 — what the ground offers, and what it hands over
// 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.

∑ Interlude — the law of the scarcest, and what an average lets through

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.

lightenergy the crown intercepts: the cell's daylight multiplied by the plant's share of it
waterenergy the reachable water can pay for: water units divided by water spent per energy unit
nutrientenergy the reachable nutrient can pay for, by the same division with its own rate
min(a, b, c)the smallest of three numbers; here two comparisons and no arithmetic at all
limiting factorthe name of whichever resource the min picked: the one currently holding growth back
▣ Build · stage 5 — two comparisons, and a name for the answer
// 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.

▣ Build · stage 6 — one seedling, one valley, forty ticks
// 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.

◆ Note — a root that could drink its whole cell in one tick

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.

⚠ Worked failure — the cell that owed water to two plants

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

✓ Checkpoint — what you can now do
  • say what separates a Ground from a Cell in 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.
⚡ Exercises — try first, then reveal
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.