Light In, Mass Out
One plant's tick budget
One plant on one cell has four accounts open every tick: light caught, water drawn, upkeep paid, and mass added or lost. A plant is a budget kept once a tick; income comes from light, the water bill prices that income, upkeep charges every gram already standing, and only the difference becomes new tissue.
The point of starting this small is that the ceiling is visible before the run starts. A square of ground does not get larger as the plant grows. Income stays flat. Upkeep grows with every gram the plant has already bought, so the two lines meet at one mass the code can predict.
That is the first living rule in The Hollow. Rock, water and soil have moved by seeded laws, walkers have crossed the map for free, and fire has consumed fuel and gone out. None of those things kept an account of what it needed to remain standing.
The new package is internal/terra. It keeps the terrarium's books: what a
cell of ground offers in a tick, what the plant standing there can afford, and how much
tissue remains after the bill. No random stream is read here, so the
printed numbers are the same numbers wherever the code runs.
The 150.00-gram ceiling
Five constants and one definition. Twelve units of light land on the cell each tick. The plant's crown intercepts 0.25 of what lands. Fixing one unit of energy costs it 0.60 units of water, drawn out of the soil, and the cell can give up 2.50 units of water in a tick. Every gram of standing tissue charges 0.02 units a tick just to stay alive. The definition is the energy unit itself: one unit of leftover energy buys exactly one gram of tissue, because that is how the unit was picked. Real numbers for real plants come later; these were chosen so you can check the arithmetic.
Income first. 12.00 landing, 0.25 intercepted, so 12.00 × 0.25 = 3.00 units a tick. Paying for it costs 3.00 × 0.60 = 1.80 units of water, and the cell has 2.50 to give, so the water goes through without argument and the plant earns its full 3.00. Note what the income does not depend on: not the season, not the neighbours, and not the size of the plant. It is 3.00 on the first tick and 3.00 on the ten-thousandth.
Now the outgoings, starting from a seedling of 1.00 gram. Upkeep on tick 1 is 0.02 × 1.00 = 0.02 units. Income 3.00 minus upkeep 0.02 leaves 2.98, and 2.98 units buys 2.98 grams, so the plant ends the tick at 3.98 grams. Tick 2 charges upkeep on the new figure: 0.02 × 3.98 = 0.0796, leaving 2.9204 to build with, for a mass of 6.9004. Tick 3 charges 0.02 × 6.9004 = 0.138008 and builds 2.861992, reaching 9.762392. Tick 4 charges 0.19524784, builds 2.80475216, and stands at 12.56714416 grams.
The decimals stop being tidy on the third tick, which is what a charge proportional to a quantity that keeps changing does to a column of numbers. Something else is happening in that column too, and it is easy to miss at this size: the amount built fell from 2.98 to 2.9204 to 2.861992 to 2.80475216. Every tick the plant gets a little richer and every tick it builds a little less, because it is paying to keep the tissue it bought last time.
So ask the question the table is heading toward. When does the building stop? The plant grows by income minus upkeep, which is 3.00 minus 0.02 × mass, and that is zero when 0.02 × mass equals 3.00. Divide: mass = 3.00 ÷ 0.02 = 150.00 grams. Check it from both sides. At 149 grams the plant earns 3.00 and is charged 2.98, so it puts on two hundredths of a gram. At 151 grams it is charged 3.02 against the same 3.00 and loses two hundredths. 150 grams is where a plant on this cell settles whether it climbs to it or falls to it, and every number in that division came from the constants at the top of this section.
Everything above was done in numbers. The shorthand earns its place only because the same five quantities are about to appear for four different species on thousands of cells. Give the light landing on a cell the name L, the crown's share c, the cell's water allowance w, the water spent per unit fixed t, the upkeep per gram u, and the plant's standing mass m. A dot between two letters means multiply, so L·c is 12.00 × 0.25.
The income is the smaller of two figures: what the crown can catch, and what the water
will pay for. min(a, b) is the usual way to write "whichever of these two
is smaller", and it is exactly one if in code.
income = min(L·c, w ÷ t)
Check it: L·c is 3.00, and w ÷ t is 2.50 ÷ 0.60 = 4.1667, so the income is 3.00 and the water is not what is holding this plant back. The change in mass over one tick gets the mark Δm, said "delta m", where the triangle means "how much this changed":
Δm = income − u·m
And the mass where that change is zero, written m*, is the one the last paragraph divided out: set Δm to 0, which puts u·m equal to the income, and divide both sides by u.
m* = income ÷ u
3.00 ÷ 0.02 = 150.00, which is the crossing in Figure 35.1 and the number the program is about to print before it takes a single tick.
internal/terra ledger
Two types, because the numbers come from two different places. Light and water are facts about a square of ground and would be true if nothing were growing there. The crown's share, the thirst and the upkeep are facts about a kind of plant and travel with it wherever it is planted. Keeping them apart costs nothing today and is the difference between a small edit and a rewrite the day one cell has to answer to several plants.
One more thing to have in mind before the code. The world advances in fixed steps of a
tenth of a second of wall time, and "once a tick" below means once per one of those steps,
the same step the terrain and the walkers already move on. No hour, no second and no frame
rate appears anywhere in terra. The books balance per tick, so playing the
sim faster or slower changes how long you wait for a plant and changes nothing about the
plant.
// internal/terra/terra.go
// Package terra keeps the terrarium's books: what one cell of ground
// offers in a tick, what the plant standing on it can afford out of
// that, and the tissue it has to show for the difference. Nothing in
// here draws anything, and nothing in here knows how long a tick is.
package terra
// Ground is what one cell hands a plant in a single tick. Both 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
}
// Plant is one plant's whole physiology. Mass is the only field a tick
// changes; the other three 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
Upkeep float64 // energy units one gram of tissue costs per tick
}
// Ledger is one tick's four 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
Paid float64 // energy the standing tissue charged
Grown float64 // grams the leftover bought; negative when short
}
// 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 cell's water
// will pay for, whichever runs out first.
func (p Plant) Fix(g Ground) float64 {
caught := g.Light * p.Catch
if caught*p.Thirst > g.Water {
return g.Water / p.Thirst
}
return caught
}
// Tick charges one plant one tick against one cell and hands back the
// four 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{Caught: p.Fix(g)}
l.Drawn = l.Caught * p.Thirst
l.Paid = p.Mass * p.Upkeep
l.Grown = l.Caught - l.Paid
p.Mass += l.Grown
return l
}
// Ceiling is the mass at which upkeep swallows the whole income, so
// nothing is left to build with. It is worked out from the four numbers
// and never measured from a run.
func (p Plant) Ceiling(g Ground) float64 { return p.Fix(g) / p.Upkeep }
Three decisions in that file matter. Fix hangs off a
Plant value and Tick hangs off a pointer to one, and the split
is the whole safety story of the package: asking a plant what it could earn can never
alter it, and there is exactly one method in the world that may move
Mass. Tick returns a Ledger instead of printing
one, so the arithmetic and the reporting are separable, and a caller that wants a table
and a caller that wants a census are the same tick underneath. And
Ceiling takes no ticks at all. It is the paper division from the last
section, in one line, available before the simulation starts and useful precisely
because it never consults the run it is predicting.
This is physiology at the depth a valley needs, and it is a long way from the biochemistry. One number stands for all the tissue, so a plant that spent everything on roots and a plant that spent everything on leaves are the same plant here. A real leaf does not turn light into sugar by one multiplication: it saturates in bright light, slows down when it is cold, and burns some of its own sugar in the dark, none of which appears above. Nitrogen and phosphorus are not in the books at all. Nothing here can age, die, or set seed. The efficiency numbers were chosen to be legible, not measured; a real leaf converts a few percent of the light that strikes it, while 0.25 here is a crown's share of a cell's daylight, which is a friendlier quantity with a friendlier number. What the model does have to get right is the direction of the arrows and the fact that upkeep scales with size, because those two are what make a plant stop. The rest can be added a term at a time, and some of it is.
// cmd/terra/main.go — the bench: no window, no world, one plant
// head prints the cell, the plant and the two numbers that can be
// worked out before a single tick is taken.
func head(g terra.Ground, p terra.Plant) {
fmt.Printf("one plant on one cell: %.2f light and %.2f water a tick\n", g.Light, g.Water)
fmt.Printf(" crown catches %.2f of the light, thirst %.2f water a unit, upkeep %.2f a gram\n",
p.Catch, p.Thirst, p.Upkeep)
fmt.Printf(" income %.4f a tick, wanting %.4f of the %.2f water on offer\n",
p.Fix(g), p.Fix(g)*p.Thirst, g.Water)
fmt.Printf(" upkeep swallows that at %.4f / %.2f = %.4f grams\n",
p.Fix(g), p.Upkeep, p.Ceiling(g))
}
// budget runs the books tick by tick and prints the four numbers plus
// the running mass, with the first four rows always shown because they
// are the four the reader worked out on paper.
func budget(g terra.Ground, p terra.Plant, ticks, every int, settle float64) {
head(g, p)
fmt.Printf("\n %4s %10s %10s %10s %10s %11s\n",
"tick", "caught", "drawn", "paid", "grown", "mass")
stalled, at := 0, 0.0
for t := 1; t <= ticks; t++ {
l := p.Tick(g)
if stalled == 0 && math.Abs(l.Grown) < settle {
stalled, at = t, p.Mass
}
if t > 4 && t%every != 0 && t != ticks {
continue
}
fmt.Printf(" %4d %10.4f %10.4f %10.4f %10.4f %11.4f\n",
t, l.Caught, l.Drawn, l.Paid, l.Grown, p.Mass)
}
if stalled == 0 {
fmt.Printf(" the tick's change is still over %.4f grams after %d ticks\n", settle, ticks)
return
}
fmt.Printf(" the tick's change fell under %.4f grams at tick %d, %.4f grams standing\n",
settle, stalled, at)
}
$ go run ./cmd/terra
one plant on one cell: 12.00 light and 2.50 water a tick
crown catches 0.25 of the light, thirst 0.60 water a unit, upkeep 0.02 a gram
income 3.0000 a tick, wanting 1.8000 of the 2.50 water on offer
upkeep swallows that at 3.0000 / 0.02 = 150.0000 grams
tick caught drawn paid grown mass
1 3.0000 1.8000 0.0200 2.9800 3.9800
2 3.0000 1.8000 0.0796 2.9204 6.9004
3 3.0000 1.8000 0.1380 2.8620 9.7624
4 3.0000 1.8000 0.1952 2.8048 12.5671
10 3.0000 1.8000 0.5154 2.4846 28.2562
20 3.0000 1.8000 0.9699 2.0301 50.5264
30 3.0000 1.8000 1.3413 1.6587 68.7228
40 3.0000 1.8000 1.6447 1.3553 83.5906
50 3.0000 1.8000 1.8926 1.1074 95.7387
60 3.0000 1.8000 2.0952 0.9048 105.6646
70 3.0000 1.8000 2.2607 0.7393 113.7747
80 3.0000 1.8000 2.3959 0.6041 120.4013
90 3.0000 1.8000 2.5064 0.4936 125.8157
100 3.0000 1.8000 2.5967 0.4033 130.2397
the tick's change is still over 0.0100 grams after 100 ticks
The first four rows are the four you did by hand, printed to four places: 3.9800,
6.9004, 9.7624 and 12.5671. Nothing was fitted and nothing was tuned. The two left-hand
columns never move, because the light landing on the cell does not care how big the
plant gets and the water bill is a fixed multiple of an income that is itself fixed. All
the motion is in paid, climbing toward 3.0000, and in grown,
falling away from it. The mass column is the two of them added up over and over, and at
tick 100 it stands at 130.2397 with 19.76 grams of headroom left under the 150.0000 the
header predicted on the first line of output.
Run it for three hundred ticks and the interesting part is how it arrives.
// cmd/terra/main.go — every constant in the chapter is a flag, so a
// prediction can be checked by changing one of them
light := flag.Float64("light", 12, "energy units landing on the cell each tick")
water := flag.Float64("water", 2.5, "water units the cell can give up each tick")
catch := flag.Float64("catch", 0.25, "share of the cell's light the crown intercepts")
thirst := flag.Float64("thirst", 0.6, "water units spent per energy unit fixed")
upkeep := flag.Float64("upkeep", 0.02, "energy one gram of tissue costs a tick")
mass := flag.Float64("mass", 1, "grams of tissue the plant starts with")
ticks := flag.Int("ticks", 100, "how many ticks to run")
every := flag.Int("every", 10, "print a row every this many ticks")
settle := flag.Float64("settle", 0.01, "the growth per tick counted as stalled")
$ go run ./cmd/terra -ticks 300 -every 50
one plant on one cell: 12.00 light and 2.50 water a tick
crown catches 0.25 of the light, thirst 0.60 water a unit, upkeep 0.02 a gram
income 3.0000 a tick, wanting 1.8000 of the 2.50 water on offer
upkeep swallows that at 3.0000 / 0.02 = 150.0000 grams
tick caught drawn paid grown mass
1 3.0000 1.8000 0.0200 2.9800 3.9800
2 3.0000 1.8000 0.0796 2.9204 6.9004
3 3.0000 1.8000 0.1380 2.8620 9.7624
4 3.0000 1.8000 0.1952 2.8048 12.5671
50 3.0000 1.8000 1.8926 1.1074 95.7387
100 3.0000 1.8000 2.5967 0.4033 130.2397
150 3.0000 1.8000 2.8531 0.1469 142.8039
200 3.0000 1.8000 2.9465 0.0535 147.3794
250 3.0000 1.8000 2.9805 0.0195 149.0457
300 3.0000 1.8000 2.9929 0.0071 149.6525
the tick's change fell under 0.0100 grams at tick 283, 149.5100 grams standing
$ go run ./cmd/terra -water 1.2 -ticks 300 -every 50
one plant on one cell: 12.00 light and 1.20 water a tick
crown catches 0.25 of the light, thirst 0.60 water a unit, upkeep 0.02 a gram
income 2.0000 a tick, wanting 1.2000 of the 1.20 water on offer
upkeep swallows that at 2.0000 / 0.02 = 100.0000 grams
tick caught drawn paid grown mass
1 2.0000 1.2000 0.0200 1.9800 2.9800
2 2.0000 1.2000 0.0596 1.9404 4.9204
3 2.0000 1.2000 0.0984 1.9016 6.8220
4 2.0000 1.2000 0.1364 1.8636 8.6856
50 2.0000 1.2000 1.2642 0.7358 63.9472
100 2.0000 1.2000 1.7321 0.2679 86.8707
150 2.0000 1.2000 1.9024 0.0976 95.2187
200 2.0000 1.2000 1.9645 0.0355 98.2588
250 2.0000 1.2000 1.9871 0.0129 99.3659
300 2.0000 1.2000 1.9953 0.0047 99.7691
the tick's change fell under 0.0100 grams at tick 263, 99.5124 grams standing
Be careful about the word "stall", because the long run shows the honest version. The plant never reaches 150.0000 grams. Each tick it closes 0.02 of whatever gap is left, so the gap is multiplied by 0.98 every tick and gets smaller forever without getting to zero: 7.20 grams short at tick 150, 0.95 short at tick 250, 0.35 short at tick 300. A program cannot report a stall it never has, so it reports the thing it can defend, which is the first tick where a whole tick of growth came to less than a hundredth of a gram. That is tick 283. The threshold is a flag because it is a judgement, and the ceiling above it is not a judgement at all.
The second run is the water column earning its place. Turn the cell's allowance down to
1.20 and the crown's 3.00 is unaffordable: 1.20 ÷ 0.60 pays for exactly 2.00
units, the caught column drops to 2.00 without a single leaf changing, and
the header prints a ceiling of 100.0000 grams before the loop starts. Two-thirds of the
water buys two-thirds of the plant. The plant itself is the same plant, standing on
worse ground.
The runaway crown
One line in terra.go invites a fair objection: a bigger plant has more leaf, so surely it catches more light.
Catch being a constant seems to say a seedling and a full-grown plant
intercept the same share of the same square, which cannot be true. So change it. Let the
share grow with the mass, at 0.05 for every gram standing, and the seedling starts at 0.05
and earns its way up.
// cmd/terra/main.go — the crown's share recomputed from the mass at
// the top of every tick, and an extra column reporting how much of the
// cell's whole daylight the plant walked off with
func runaway(g terra.Ground, p terra.Plant, share float64, ticks, every int) {
fmt.Printf("crown share %.2f a gram, on %.2f light and %.2f water a tick\n",
share, g.Light, g.Water)
fmt.Printf(" %5s %9s %9s %9s %9s %9s %11s\n",
"tick", "share", "caught", "of light", "paid", "grown", "mass")
over := 0
for t := 1; t <= ticks; t++ {
p.Catch = share * p.Mass
if over == 0 && p.Catch > 1 {
over = t
}
l := p.Tick(g)
if t > 4 && t%every != 0 && t != ticks {
continue
}
fmt.Printf(" %5d %9.4f %9.4f %9.4f %9.4f %9.4f %11.4f\n",
t, p.Catch, l.Caught, l.Caught/g.Light, l.Paid, l.Grown, p.Mass)
}
fmt.Printf(" the crown's share passed 1.0000 at tick %d\n", over)
fmt.Printf(" it settles at %.4f grams, harvesting %.4f of the %.2f that lands\n",
p.Ceiling(g), p.Fix(g), g.Light)
}
$ go run ./cmd/terra -mode runaway -ticks 120 -every 20
crown share 0.05 a gram, on 12.00 light and 2.50 water a tick
tick share caught of light paid grown mass
1 0.0500 0.6000 0.0500 0.0200 0.5800 1.5800
2 0.0790 0.9480 0.0790 0.0316 0.9164 2.4964
3 0.1248 1.4978 0.1248 0.0499 1.4479 3.9443
4 0.1972 2.3666 0.1972 0.0789 2.2877 6.2320
20 2.9373 4.1667 0.3472 1.1749 2.9918 61.7372
40 5.4234 4.1667 0.3472 2.1693 1.9973 110.4646
60 7.0831 4.1667 0.3472 2.8332 1.3334 142.9954
80 8.1911 4.1667 0.3472 3.2765 0.8902 164.7132
100 8.9309 4.1667 0.3472 3.5724 0.5943 179.2122
120 9.4248 4.1667 0.3472 3.7699 0.3968 188.8918
the crown's share passed 1.0000 at tick 9
it settles at 208.3333 grams, harvesting 4.1667 of the 12.00 that lands
Read that run the way somebody who wanted the change would read it, and it is a success. The seedling grows slowly at first and then faster, which is what real seedlings do. The harvest settles at 4.1667 units, comfortably under the 12.00 that lands on the cell. The plant stops growing, at 208.3333 grams instead of 150, and if 208 grams is the number you wanted then here is a model that produces it. Nothing on the page says "wrong".
The share column does, once you know what it is a share of. It
passed 1.0000 at tick 9 and reads 9.4248 by tick 120, and a share is a fraction of
something. This crown claims 942% of its cell's daylight. It is not doing so in the
caught column only because the water allowance stepped in first:
2.50 ÷ 0.60 caps the harvest at 4.1667, and it was pinned there long before tick
20, which is why "of light" sits at a respectable 0.3472 while the share behind it
climbs with nothing to stop it. The water bill is hiding a broken model, and a hidden bug is worse
than one that crashes. To see what it was hiding, stand the same plant on wet ground.
$ go run ./cmd/terra -mode runaway -water 1000 -ticks 120 -every 20
crown share 0.05 a gram, on 12.00 light and 1000.00 water a tick
tick share caught of light paid grown mass
1 0.0500 0.6000 0.0500 0.0200 0.5800 1.5800
2 0.0790 0.9480 0.0790 0.0316 0.9164 2.4964
3 0.1248 1.4978 0.1248 0.0499 1.4479 3.9443
4 0.1972 2.3666 0.1972 0.0789 2.2877 6.2320
20 267.8449 1666.6667 138.8889 107.1380 1559.5287 6916.4271
40 1563.7822 1666.6667 138.8889 625.5129 1041.1538 32316.7976
60 2428.9602 1666.6667 138.8889 971.5841 695.0826 49274.2874
80 3006.5600 1666.6667 138.8889 1202.6240 464.0427 60595.2427
100 3392.1702 1666.6667 138.8889 1356.8681 309.7986 68153.2028
120 3649.6067 1666.6667 138.8889 1459.8427 206.8240 73198.9572
the crown's share passed 1.0000 at tick 8
it settles at 83333.3333 grams, harvesting 1666.6667 of the 12.00 that lands
83 kilograms of plant on one square of ground, harvesting 1666.6667 units of light off a cell that receives 12.00. The "of light" column says 138.8889, which is the plant eating a hundred and thirty-nine times the daylight that arrives. The two runs together give the diagnosis: a plant's final size should not be decided by how much water the ground has going spare, and here it is, because the only thing capping the harvest was the water. Take the cap off and there is nothing underneath it.
The rule the mistake teaches is small and general. A share is bounded by the thing
it is a share of. Any quantity a model takes out of a place has to be checked
against what that place holds, and the check belongs where the taking happens, not
wherever the numbers happen to run out. A crown's share is a fraction between 0 and 1
and a line that computes it needs a ceiling of 1 written into it. This page goes the
other way and keeps Catch a constant, because one plant's crown growing
over its own cell is a different question from the one the budget is answering, and
mixing them is how you end up with two ceilings and no idea which one you are looking
at.
Mass buys the drawing
A number in a table is not a plant. The valley already knows how to grow a picture of one: a grammar that rewrites a short string into a long one, and a turtle that reads the long one back as a list of straight segments. That machinery takes a generation count and draws whatever that count produces, and until now the count was typed by a person. The budget can supply it instead, and the link between them is a price.
The sprig grammar puts five F's where each F was, so a plant at generation 0 draws 1 segment, at generation 1 draws 5, at 2 draws 25, at 3 draws 125, at 4 draws 625. Decide that one drawn segment of stalk stands for one gram of tissue and the generations acquire prices: 1, 5, 25, 125 and 625 grams. A plant draws the largest generation it can pay for out of the mass it is standing up.
// internal/terra/form.go
// Form is what a grammar costs to build. One drawn segment stands for
// SegMass grams of stalk, and every generation multiplies the number of
// segments by Fan, so generation n needs Fan^n segments standing. Max is
// the generation the grammar is never grown past, so a runaway number
// cannot ask the turtle for a million strokes.
type Form struct {
SegMass float64
Fan float64
Max int
}
// Cost is the tissue generation n needs standing before it can be drawn.
func (f Form) Cost(n int) float64 {
c := f.SegMass
for i := 0; i < n; i++ {
c *= f.Fan
}
return c
}
// Stage is the largest generation this much tissue can pay for.
func (f Form) Stage(mass float64) int {
n := 0
for n < f.Max && f.Cost(n+1) <= mass {
n++
}
return n
}
// cmd/terra/main.go — draw the plant on every tick its tissue can pay
// for one more generation of stalk, and hash the frame each time
// picture draws one plant into a fresh buffer and hands back the
// segments and the frame hash. twig picks one of three greens from how
// many brackets deep the turtle was, the way the plant chapter painted
// its stalks.
func picture(gen int, seg float64, w, h int) ([]grow.Segment, grow.Box, string) {
n := grow.Named["sprig"]
pl := grow.Plant{Axiom: n.Axiom, Rules: n.Rules, Gen: gen, Seg: seg,
Turn: field.Turn / 8, Up: field.Turn / 4}
_, segs := pl.Sprout(vec.Vec2{X: float64(w) / 2, Y: float64(h) - 2})
b := render.NewBuffer(w, h)
b.Fill(render.Void)
for _, s := range segs {
b.Line(int(math.Round(s.A.X)), int(math.Round(s.A.Y)),
int(math.Round(s.B.X)), int(math.Round(s.B.Y)), twig(s.Depth))
}
return segs, grow.Bounds(segs), b.Hash()
}
The run below names no geometry, because all of it sits in flag defaults, and the four
digests it prints cannot be checked without it. The frame is 128 pixels across and 128
down. One segment carries the turtle 3 pixels. The turtle starts at
(w/2, h-2), the middle of the bottom edge and two rows up from it, so the
plant grows up into the frame from the ground it is standing on. Change any one of those
three and all four digests change with them.
$ go run ./cmd/terra -mode form -ticks 300
one plant on one cell: 12.00 light and 2.50 water a tick
crown catches 0.25 of the light, thirst 0.60 water a unit, upkeep 0.02 a gram
income 3.0000 a tick, wanting 1.8000 of the 2.50 water on offer
upkeep swallows that at 3.0000 / 0.02 = 150.0000 grams
one segment of stalk stands for 1.00 grams, 5 segments a generation
tick mass gen gen costs segments w by h frame
0 1.0000 0 1.00 1 0 by 3 a82dc9e70a3846be
2 6.9004 1 5.00 5 4 by 9 e71f02e589d35eed
9 25.7716 2 25.00 25 14 by 27 97c796700671c9f0
89 125.3222 3 125.00 125 41 by 81 a4ae5f902f5d4f06
generation 4 would need 625.00 grams, and the ceiling is 150.0000
Four drawings in three hundred ticks, and the gaps between them stretch out because each generation costs five times the one before while the plant's income is flat. Generation 1 arrives on tick 2, generation 2 on tick 9, generation 3 on tick 89. Generation 4 never arrives at all, and the last line says why in numbers already on the page: it wants 625 grams standing and the ceiling is 150. This plant is a three-generation plant forever, and nobody wrote a 3 anywhere. The 3 is what 12.00 units of light, a quarter of them caught, and two hundredths of upkeep a gram add up to on a grammar that quintuples.
The frame hash is over the finished pixels: buffer size first, then every pixel in reading order. Since the frame, the segment length, the turn and the grammar are all fixed here, the picture is a function of the generation and nothing else, so those four digests pin four drawings that must come out identical on any machine. The last one measures 41 by 81 pixels: twenty-seven times the height of the single three-pixel stroke the seedling was, bought with a hundred and twenty-five times the tissue, both of those facts coming out of the same column of arithmetic.
The predicted ceiling
Strip the plant out and what is left is a pattern that will run through the rest of this book. A quantity accumulates. Something adds to it at a rate that does not depend on how much is already there. Something else takes away at a rate proportional to how much is already there. Those two facts alone guarantee a settling point, at inflow divided by the proportional rate, approached by closing the same fraction of the remaining distance every step. A cup of tea cooling in a room does it. A bucket with a hole in it under a running tap does it. A population whose deaths scale with its size and whose births do not does it. The plant is the first one this book builds, and it will not be the last.
The reason to care about the pattern rather than the plant is that it tells you where to look when a number comes out wrong. If a plant settles somewhere unexpected, exactly two things can be responsible: the income, or the per-gram charge. Both are visible in the header line before the run starts. The worked failure above is the same reasoning run backwards, where a ceiling of 208.3333 was traceable to an income that had quietly become a function of water instead of light.
There is a standard hiding in that, and the rest of this volume holds to it. If a number decides how big something gets or how many of them there are, it should be computable from the constants and checkable against the run, not discovered by watching and then written down. 150.0000 was on the first line of output before a tick was taken, and the run spent 283 ticks agreeing with it. A model whose important numbers can only be found by running it is a model you cannot reason about, and an ecology is hard enough to reason about already.
- Given light on a cell, a crown's share, an upkeep per gram and a starting mass, I can fill in four ticks of caught, paid, grown and mass with a calculator, and check them against the printed table digit for digit.
- compute a plant's ceiling as income divided by upkeep, and show that it is the same number whether the plant climbs to it from 1 gram or falls to it from 200.
- explain why the plant never actually reaches 150.0000, and defend reporting a stall as the first tick whose growth fell under a stated threshold.
- Handed a cell with a water allowance of 1.20 and a thirst of 0.60, you can work out the income of 2.00 and the ceiling of 100.0000 before running anything.
- Shown a plant harvesting 1666.6667 units on a cell that receives 12.00, check the crown's share first, and you can name the bound it broke.
- price each generation of a grammar that quintuples its segments, and say which generations a given ceiling puts permanently out of reach.
Exercise 1 — start it too big. Run
go run ./cmd/terra -mass 200 -ticks 100 -every 25. Predict the sign of
the grown column and the value the mass column is heading for, then
check.
Income is still 3.0000 and upkeep on 200 grams is 4.0000, so the first tick's
grown is −1.0000 and the plant ends the tick at 199.0000. Every
row after it is negative and shrinking: −0.9800, −0.9604, −0.9412,
and −0.1353 by tick 100 with 156.6310 grams standing. The target is the same
150.0000 the header prints, approached from above at the same 0.98 a tick. A plant
too big for its cell does not sit there being too big; it burns tissue it cannot
pay for until it fits. The line in Tick that allows it is
p.Mass += l.Grown with no floor under it, and nothing special had to be
written for the shrinking case.
Exercise 2 — make it twice as expensive to be alive. Double
the upkeep with go run ./cmd/terra -upkeep 0.04 -ticks 300 -every 100.
Predict the ceiling and say whether the plant reaches its stall sooner or later than
the 283 ticks the default took.
The ceiling halves to 3.0000 ÷ 0.04 = 75.0000, and the run prints exactly that on its fourth line. The stall arrives at tick 141, roughly half of 283, which catches people out: the expensive plant is only half the size but it gets there in half the time. The reason is that the same constant sets both. Each tick now closes 0.04 of the gap instead of 0.02, so the distance to the ceiling falls twice as fast while the ceiling itself is twice as close. Cheap tissue makes a big plant slowly; expensive tissue makes a small one quickly.
Exercise 3 — find the water that costs a branching. Work out
the water allowance whose ceiling lands exactly on the 125.00 grams generation 3
costs, run -mode form at that allowance for 3,000 ticks, then run it
again a tenth of a unit wetter.
Set the ceiling to 125: income must be 125 × 0.02 = 2.50, and an income of
2.50 needs 2.50 × 0.60 = 1.50 units of water. Run
go run ./cmd/terra -mode form -water 1.5 -ticks 3000 and the plant
stops at generation 2 with 25 segments, having spent three thousand ticks creeping
toward a ceiling that is exactly the price of generation 3 and can therefore never
quite pay it. Run it at
-water 1.6 and the ceiling moves to 133.3333, generation 3 arrives on
tick 137, and the frame hash is a4ae5f902f5d4f06: the same digest as
the default run's generation 3, because the drawing depends on the generation and
not on the route the plant took to afford it. A tenth of a unit of water a tick is
the difference between a plant that branches three times and one that never does,
and that is the whole argument for what comes next.