Tall Plants Eat First
The light field
Two plants can stand side by side, share no roots, and still fight over the same daylight. A one-gram seedling under a 150-gram neighbour should not earn the same 3.00 units as a seedling in the open. Light is a field the plants write before any plant eats: each crown casts shade tallest first, and a plant reads the light left at its cell immediately before its own crown is written into the grid.
Root competition is even-handed because two roots can both draw from the same store until it reaches the refill rate. Light arrives from above once. The crown that intercepts it takes its share before the shorter plant below has a budget at all.
The chapter holds the bed steady so the measurement names shade and nothing else. Water and nutrient stay full; the only moving number is the daylight a plant is allowed to fix.
The half-light handoff
A crown needs three numbers, and all three turn grams into something about the plant's top half. How far it reaches: 1.5 cells, so its own cell and the eight around it, which is the same nine-cell disc a root of that radius covers. How much leaf a gram of tissue carries: 0.03 of a cell's worth. How tall a gram stands: 0.04 of a cell.
Take the plant at its ceiling, 150 grams. It carries 0.03 × 150 = 4.50 cells of leaf, spread across the 9 cells it reaches, so every cell under it lies beneath 0.50 of a cell's worth of leaf. Half of each cell is covered and half is not, so half the light arriving from above carries on through. Height comes out of the same mass: 0.04 × 150 = 6.00 cells tall, which is the number that decides who reads first and takes no part in the arithmetic after that.
Now the seedling underneath. It has 12.00 × 0.50 = 6.00 units of daylight overhead instead of 12.00, so its crown catches 6.00 × 0.25 = 1.50 units a tick, and upkeep still charges 0.02 for every gram it is carrying. Its ceiling is income divided by upkeep, the same division as ever: 1.50 ÷ 0.02 = 75.00 grams. Put a second grown crown over it and the light halves again to 3.00, income falls to 0.75, and the ceiling lands on 37.50. A third takes it to 18.75. Every crown overhead costs the plant below half of everything it will ever be.
Walk the shaded seedling out by hand for three ticks and it grows the way the open one did, only smaller. Tick 1: upkeep 0.02 × 1.00 = 0.02, income 1.50, so 1.48 grams go on and it stands at 2.48. Tick 2 charges 0.049600 and builds 1.450400, reaching 3.930400. Tick 3 charges 0.078608, builds 1.421392, and leaves it at 5.351792 grams.
The pace is the surprise. This plant is heading for 75 instead of 150, and it is not taking any longer about it. Its distance from the ceiling at the start of tick 1 is 74.00 grams, and 74.00 × 0.98 = 72.52, which is exactly 75.00 − 2.48. Every tick closes 0.02 of whatever gap is left, whichever ceiling the plant is heading for, because the gap shrinks by upkeep multiplied by the gap and the income cancels out of that entirely. Shade decides where a plant stops. It has nothing to say about how long the stopping takes.
Three quantities and one multiplication. Write the leaf a gram carries as f, the number of cells the crown reaches as k, and the plant's mass as m, all of which are already numbers: 0.03, 9 and 150. The leaf the crown is carrying is f·m, and spread over k cells the amount lying over any one of them is:
cover = f·m ÷ k
0.03 × 150 ÷ 9 = 0.50. A cover of 0.50 means half a cell's worth of leaf over one cell, so half the light hits leaf and half of it misses. What gets past is everything not covered, and a cell cannot be more than entirely covered, so the figure is capped at 1:
pass = 1 − min(cover, 1)
Two crowns over one cell multiply: the second one gets the half the first let through and passes half of that, leaving 0.50 × 0.50 = 0.25 of the sky. Three leave 0.125. The general form for n identical crowns is pass multiplied by itself n times, written with a raised number:
light = L × passⁿ
And the ceiling follows from the ledger without a new idea in it. Income is the light reaching the plant multiplied by its crown's share c, and the ceiling is income divided by upkeep u:
m* = L × passⁿ × c ÷ u
12.00 × 0.5 × 0.25 ÷ 0.02 = 75.00 under one crown, and every further crown halves it. Multiplication is what makes shade stack the way it does: adding the covers up would let four crowns of 0.30 block 120% of the daylight, and multiplying their passes can never take a cell below zero however many of them there are.
$ go run ./cmd/canopy -mode shade
canopy: a seedling under grown neighbours, each standing at 150.00 grams
each crown spreads 4.50 cells of leaf over 9 cells, so 0.5000 of the light gets through
crowns light income ceiling grams at 100
0 12.0000 3.0000 150.0000 130.2397
1 6.0000 1.5000 75.0000 65.1862
2 3.0000 0.7500 37.5000 32.6594
3 1.5000 0.3750 18.7500 16.3960
4 0.7500 0.1875 9.3750 8.2643
Read the last column against the one before it. The ceilings halve exactly, by construction. The masses at tick 100 nearly halve and miss by a little: 65.1862 where half of 130.2397 would be 65.1198. The difference is the gram the seed started with. After a hundred ticks 0.1326 of that original gram is still in the plant, unscaled by anything, because no amount of shade can take away tissue the plant did not have to earn. Everything else in the column is bought with light and everything bought with light halves.
The model above is a disc of evenly spread leaf that dims the cells beneath it, and real canopies are less obliging. Sunlight arrives at an angle, so a real shadow falls to one side and walks across the ground through the day and through the year; this one falls straight down and never moves. Leaves inside a crown shade each other, so doubling a tree's leaf does not double what it intercepts, and the honest curve for light through a canopy falls away steeply at first and then flattens, where the straight subtraction here keeps its slope until the cell is covered. Nothing here has a trunk, which is a large object that casts shade and carries no leaf at all. And leaf area does not really track mass by one multiplication: a sapling is nearly all leaf and an old tree is nearly all wood.
What the model has to get right is smaller than any of that and it gets it right: shade depends on how much leaf is up there, it stacks when crowns overlap, it can never remove more light than arrived, and who it falls on is decided by height. Those four are what make a canopy a canopy. The rest are corrections to a curve, and a curve has to exist before anybody can correct it.
The shade pass
The grid comes first, and it is deliberately thin: one number per cell, wiped and rebuilt every tick. Shade is not a store and must never behave like one. The soil holds water from one tick to the next because water that was not drunk is still there; shade holds nothing, because it is a fact about what is standing right now and evaporates the instant the plant casting it does.
// internal/terra/sky.go
// Sky is the light field: one number for every cell of the valley,
// wiped and rebuilt from scratch every tick. Full is what lands on a
// cell with nothing standing over it, and every number in here is that
// figure with some crowns multiplied into it.
type Sky struct {
W, H int
Full float64
lit []float64
}
// NewSky lays a light field over a grid of this size. It opens in full
// daylight, which is what it goes back to at the top of every tick.
func NewSky(w, h int, full float64) *Sky {
s := &Sky{W: w, H: h, Full: full, lit: make([]float64, w*h)}
s.Clear()
return s
}
// index is one cell's place in the flat slice; In is whether a
// coordinate names a cell of this sky at all. Every reader and writer
// below goes through the pair, so a crown hanging over the rim asks
// about cells that are not there and gets told so.
func (s *Sky) index(c sim.Coord) int { return c.Y*s.W + c.X }
func (s *Sky) In(c sim.Coord) bool {
return c.X >= 0 && c.X < s.W && c.Y >= 0 && c.Y < s.H
}
// Clear puts every cell back into full daylight. It runs once at the
// top of every tick, because shade is not a store: it is recomputed
// from what is standing right now.
func (s *Sky) Clear() {
for i := range s.lit {
s.lit[i] = s.Full
}
}
// At is the light reaching one cell, or nothing at all off the grid.
func (s *Sky) At(c sim.Coord) float64 {
if !s.In(c) {
return 0
}
return s.lit[s.index(c)]
}
// Dim multiplies one cell's light by the share a crown lets through.
// Multiplying is what makes two crowns over one cell stack: half of a
// half is a quarter, and no order of shading can take a cell below
// zero or above the sky.
func (s *Sky) Dim(c sim.Coord, pass float64) {
if !s.In(c) {
return
}
s.lit[s.index(c)] *= pass
}
// Under is the geometry of a crown: every cell of the grid whose centre
// lies within r cells of c, walked in row order. It is the same disc
// arithmetic a root reaches its cells with, applied upward.
func (s *Sky) Under(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 s.In(p) {
out = append(out, p)
}
}
}
return out
}
Dim multiplies and never subtracts, and that one operator is doing more work
than it looks. Subtracting covers would let enough crowns take a cell past zero into
negative light, which is the same class of mistake as a store handing out water it did
not have. Multiplying passes cannot: every factor sits between 0 and 1, so the product
does too, and a cell under nine crowns holds a very small positive number instead of an
impossible one. The cap belongs on pass, at the one place a cover is turned
into a fraction, and the field itself needs no floor at all.
// internal/terra/sky.go
// Canopy is the part of a plant that stands between the sky and the
// ground. All three numbers turn grams of tissue into something about
// the crown, and all three belong to a kind of plant rather than to an
// individual.
type Canopy struct {
Crown float64 // how far from its own cell the crown reaches, in cells
Leafy float64 // cells of leaf area carried per gram of standing tissue
Tall float64 // cells of height per gram of standing tissue
}
// Height is how far off the ground the top of this crown is. Height
// decides nothing about how much light a plant catches and everything
// about who catches it first.
func (c Canopy) Height(mass float64) float64 { return c.Tall * mass }
// Leaf is the leaf area this much tissue is carrying, measured in cells
// of ground it could cover if it were laid out flat.
func (c Canopy) Leaf(mass float64) float64 { return c.Leafy * mass }
// Spread is how many cells a crown of this radius covers when the whole
// disc is on the grid. The leaf is divided by this figure wherever the
// plant stands, so a crown hanging over the rim loses the leaf that
// hung over it instead of packing it into the cells that are left.
func (c Canopy) Spread() int {
n, out := int(c.Crown), 0
for dy := -n; dy <= n; dy++ {
for dx := -n; dx <= n; dx++ {
if float64(dx*dx+dy*dy) <= c.Crown*c.Crown {
out++
}
}
}
return out
}
// Pass is the share of the light arriving above this crown that gets
// through it to one cell underneath. The leaf is spread evenly over the
// disc, a cell covered by more leaf than it has room for is covered
// once, and what is not covered goes straight through.
func (c Canopy) Pass(mass float64) float64 {
cover := c.Leaf(mass) / float64(c.Spread())
if cover > 1 {
cover = 1
}
return 1 - cover
}
// Stand is one plant standing somewhere: the budget of the last two
// chapters, the crown it carries, the ground it is rooted in, and the
// light that reached the top of that crown this tick.
type Stand struct {
At sim.Coord
Plant Plant
Crown Canopy
Water float64 // water units the ground under it can give up in a tick
Lit float64 // light reaching the top of its crown, set by Cast
}
// Height is the top of this stand's crown right now: the crown's height
// per gram against the grams this particular plant has managed to stand
// up. It is the only number the caster sorts on.
func (st *Stand) Height() float64 { return st.Crown.Height(st.Plant.Mass) }
Spread counts the disc from the radius alone and not from where the plant
happens to be. A crown at the rim of the map has part of its disc off the grid, and
dividing its leaf by the cells that remain would make it darker for standing near an
edge, which is an artefact of the map and not a fact about plants. The leaf that hangs
over the edge is lost. Height hangs off the stand rather than the
canopy because it is a question about an individual and not about a kind: the crown
knows how tall a gram stands and only the stand knows how many grams there are.
Lit is the one field on a Stand
that neither the plant nor the crown owns: it is this tick's reading, written by the
caster, read by the harvest, and meaningless in between.
Which leaves the tick itself, and it has two halves for a reason this book has already paid for twice. The walkers work every force out from the state the tick opened in and step afterwards, so a body updated early cannot change the answer a body updated late gets. The automaton copies its whole grid before a sweep, so no cell reads a neighbour that has already moved on. Deciding and applying are kept apart both times, and the argument here is the same one a third time: if a plant harvested the instant it was cast, it would grow, and its new mass would deepen the shade over a neighbour not yet reached, which would then be paying this tick for tissue that did not exist when the tick began.
// internal/terra/sky.go
// Cast is the first half of a tick. The sky is wiped back to full
// daylight and every crown is written into it, tallest first, and each
// stand takes its reading at the moment before its own leaves go in.
// The number a stand keeps is therefore the light that got past
// everything taller than it, and past nothing else. No mass moves here.
func Cast(s *Sky, stands []*Stand) {
s.Clear()
order := make([]int, len(stands))
for i := range order {
order[i] = i
}
slices.SortStableFunc(order, func(a, b int) int {
ha, hb := stands[a].Height(), stands[b].Height()
switch {
case ha > hb:
return -1
case ha < hb:
return 1
}
return a - b // exactly equal heights: the older stand reads first
})
for _, i := range order {
st := stands[i]
st.Lit = s.At(st.At)
pass := st.Crown.Pass(st.Plant.Mass)
for _, c := range s.Under(st.At, st.Crown.Crown) {
s.Dim(c, pass)
}
}
}
// Harvest is the second half. Every stand spends the reading Cast left
// it, in the order the world stores them, and this is the only half
// that moves a gram. Nothing in here touches the sky, so a plant that
// grew this tick casts the shadow of the plant it grew into on the next
// tick and not on this one.
func Harvest(stands []*Stand) []Ledger {
out := make([]Ledger, len(stands))
for i, st := range stands {
out[i] = st.Plant.Tick(Ground{Light: st.Lit, Water: st.Water})
}
return out
}
Two lines in Cast carry the whole chapter. Sorting by height descending is
what makes the competition one-sided: by the time a stand takes its reading, every plant
above it has already dimmed the cell and every plant below it has not been reached, so
the number is the light that got past the taller ones and nothing else. And taking the
reading before writing its own shade is how a plant avoids standing in its own shadow,
which would otherwise charge every plant in the world for the leaves it is holding up.
Ties fall to the older stand by index, so two plants of exactly identical height are
settled by one arbitrary line, and the exercises are going to make that line earn its
place.
$ go run ./cmd/canopy -mode one -ticks 600 -every 100
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 1.5 covering 9 cells, 0.03 of leaf a gram, 0.04 of height a gram
one plant at 4,1 and nothing else standing
tick mass lit height ceiling
1 3.9800 12.0000 0.1592 150.0000
2 6.9004 12.0000 0.2760 150.0000
3 9.7624 12.0000 0.3905 150.0000
4 12.5671 12.0000 0.5027 150.0000
100 130.2397 12.0000 5.2096 150.0000
200 147.3794 12.0000 5.8952 150.0000
300 149.6525 12.0000 5.9861 150.0000
400 149.9539 12.0000 5.9982 150.0000
500 149.9939 12.0000 5.9998 150.0000
600 149.9992 12.0000 6.0000 150.0000
it settles at 149.9992 grams, 6.0000 cells tall, spreading 4.5000 cells of leaf
3.9800, 6.9004, 9.7624, 12.5671. A grid, a sort, a disc walk and a multiplication have been added underneath, and the plant on its own has not moved by a digit: it reads 12.0000 on every tick because there is nothing above it, so the reading and the constant are the same number and the ledger does what it always did. That is the first thing to check after replacing a constant with a computation, and it costs one run.
$ go run ./cmd/canopy -mode under -ticks 600 -every 100 -gap 200 -maps
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 1.5 covering 9 cells, 0.03 of leaf a gram, 0.04 of height a gram
one plant at 4,1 from tick 0, a seedling at 5,1 from tick 200
tick tall mass seed mass seed lit seed income seed grown its ceiling
200 147.3794 2.5067 6.1070 1.5267 1.5067 76.3370
201 147.4318 3.9828 6.1048 1.5262 1.4761 76.3103
300 149.6525 65.7406 6.0142 1.5035 0.1926 75.1773
400 149.9539 73.8191 6.0019 1.5005 0.0246 75.0235
500 149.9939 74.8496 6.0002 1.5001 0.0031 75.0031
600 149.9992 74.9809 6.0000 1.5000 0.0004 75.0004
the tall plant ends at 149.9992 grams and the seedling at 74.9809
light along row 1: 12.00 12.00 12.00 6.00 4.50 4.50 9.00 12.00 12.00 12.00 12.00 12.00
The seedling germinates into 6.1070 units of light and a ceiling of 76.3370, and both of those figures were set two hundred ticks before it existed, by a plant it will never interact with in any other way. The tall plant's column is the flat one. It reads 12.0000 throughout, its own arithmetic untouched by the arrival of a neighbour, and it finishes on the same 149.9992 grams it reached in the run where it stood alone. Nothing was taken from it. The seedling never had the other half.
The row of light at the bottom is the field itself, printed cell by cell. One cell reads 6.00 where only the tall crown reaches, one reads 9.00 where only the seedling's does, and the two between them, under both, read 4.50, which is 12.00 × 0.50 × 0.75. Compare 4.50 with the 6.0000 the seedling harvested. The ground beneath a plant is darker than the reading the plant took, and the difference is precisely its own leaves.
The 40-tick head start
Nothing so far distinguishes the two plants. Same crown radius, same leaf per gram, same height per gram, same thirst, same upkeep, same water under both cells. They are one plant written down twice, and the only fact that differs is which tick each was sown on. Sow them forty ticks apart and run it both ways round.
$ go run ./cmd/canopy -mode pair -ticks 600 -gap 40 -maps
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 1.5 covering 9 cells, 0.03 of leaf a gram, 0.04 of height a gram
the same two seeds 40 ticks apart, 600 ticks of growing
sown first west east world
west 149.9992 75.0038 22a2d3097cf49c29
light along row 1: 12.00 12.00 12.00 6.00 4.50 4.50 9.00 12.00 12.00 12.00 12.00 12.00
east 75.0038 149.9992 0df6c1dedc228d56
light along row 1: 12.00 12.00 12.00 9.00 4.50 4.50 6.00 12.00 12.00 12.00 12.00 12.00
Two numbers, 149.9992 and 75.0038, and they change places. The world digest, taken over every cell of the light field and every mass on the grid in row order, differs between the runs, which it must: these are two different valleys, one with the big plant on the west cell and one with it on the east. The rows of light are mirror images of each other about the pair. Forty ticks of a head start, in a model with no rule in it that mentions competition, is the difference between a plant that reaches its cell's ceiling and one that reaches half of it.
Forty ticks turns out to be forty more than the effect needs.
$ go run ./cmd/canopy -mode same -ticks 600 -every 150
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 1.5 covering 9 cells, 0.03 of leaf a gram, 0.04 of height a gram
both seeds sown on tick 0, 600 ticks of growing
tick west east west lit east lit
1 3.9800 3.9700 12.0000 11.9600
2 6.9004 6.8508 12.0000 11.8408
3 9.7624 9.6448 12.0000 11.7240
150 142.8039 82.4405 12.0000 6.2937
300 149.6525 75.8913 12.0000 6.0142
450 149.9832 75.0687 12.0000 6.0007
600 149.9992 75.0046 12.0000 6.0000
world 68eaeb8944908828
On tick 1 the two are tied at a gram each, the tie-break hands the reading to the west plant, and the east plant loses four hundredths of a unit of light to a seedling's worth of leaf: 3.9800 against 3.9700. That hundredth of a gram is the entire advantage, and by tick 150 it has become 142.8039 against 82.4405. The mechanism is a loop with a positive sign in it. Being slightly bigger makes you slightly taller, which takes slightly more light from your neighbour, which makes your neighbour grow slightly less, which leaves slightly less shade on you. Two identical seeds on identical ground finish 600 ticks twice apart, and one arbitrary line in the caster decided which was which.
None of that is a defect of the model, and none of it is luck. A wood assembles itself out of level ground by exactly this rule. Whatever reaches the light first keeps reaching it, everything arriving later grows up in the remainder, and the two layers a person walking through a forest names as the canopy and the understorey are this rule left running for two hundred years. The same rule says how one kind of plant holds ground it has taken: once that kind is overhead everywhere, a seed of anything else germinates into a ceiling of 75 grams, or 37.50, or less, whatever its own physiology would otherwise have earned it.
Keep the number 37.50 somewhere. When this volume runs a whole valley of these plants for a few thousand ticks and the census falls off a cliff, the first place to look will be here: a rule under which the winners take the light off the losers permanently, in a world where nothing yet dies to make room.
The two halves of the tick are two loops over the same list, and merging them is the obvious tidy-up. One loop, one pass, each plant reads the sky, writes its own shade and spends the reading before the next plant is touched. It is shorter, it allocates nothing, and it visits every stand exactly once.
// cmd/canopy/main.go
// tickOnePass is the tick with its two halves run together: one stand
// at a time reads the sky, writes its own shade into it and spends the
// reading, in the order the world stores them.
func tickOnePass(s *terra.Sky, stands []*terra.Stand) []terra.Ledger {
s.Clear()
book := make([]terra.Ledger, len(stands))
for i, st := range stands {
st.Lit = s.At(st.At)
pass := st.Crown.Pass(st.Plant.Mass)
for _, c := range s.Under(st.At, st.Crown.Crown) {
s.Dim(c, pass)
}
book[i] = st.Plant.Tick(terra.Ground{Light: st.Lit, Water: st.Water})
}
return book
}
Run the pair through it and it looks right, which is the problem. So run it twice: the same world, the same sowing, the same six hundred ticks, with the two stands stored in the slice in the two possible orders. The order a program keeps its plants in is book-keeping and can never be physics, so both storages have to produce the same valley.
$ go run ./cmd/canopy -mode order -ticks 600 -gap 40
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 1.5 covering 9 cells, 0.03 of leaf a gram, 0.04 of height a gram
one plant at 4,1 from tick 0, a seedling at 5,1 from tick 40
the same world stored both ways round, through both ticks
tick stored west east west lit east lit world
two phases west, east 149.9992 75.0038 12.0000 6.0000 22a2d3097cf49c29
two phases east, west 149.9992 75.0038 12.0000 6.0000 22a2d3097cf49c29
one pass west, east 149.9992 75.0038 12.0000 6.0000 22a2d3097cf49c29
one pass east, west 75.0103 149.9982 6.0001 12.0000 7c173d698723290d
Three rows agree to the last digit and the fourth is a different world. The two-phase tick gives one answer whichever way the stands are stored, as it has to. The one-pass tick agrees with it in one storage order and inverts the entire outcome in the other: the seed that went into the ground forty ticks late finishes at 149.9982 grams, the plant that had the head start finishes at 75.0103, and the tallest thing in the valley is reading 6.0001 units of light. Nothing crashed and no number is impossible. The run is about a different valley from the one it was asked about.
$ go run ./cmd/canopy -mode why -ticks 600 -gap 40
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 1.5 covering 9 cells, 0.03 of leaf a gram, 0.04 of height a gram
the same world stored both ways, through the one-pass tick
the numbers first differed on tick 40
entering tick 40, the plant at 4,1 weighs 82.2353 and stands 3.2894 cells tall
the seed at 5,1 weighs 1.0000 and stands 0.0400 cells tall
one pass, west first: the plant reads 12.0000 and earns 3.0000
the seedling reads 8.7106 and earns 2.1776
one pass, east first: the plant reads 11.9600 and earns 2.9900
the seedling reads 12.0000 and earns 3.0000
two phases, either: the plant reads 12.0000 and earns 3.0000
the seedling reads 8.7106 and earns 2.1776
Tick 40 is the tick the seed germinates on, and the disagreement starts there. A plant of 82.2353 grams stands 3.2894 cells tall and carries 2.4671 cells of leaf; a seed of one gram stands 0.0400 cells tall and carries 0.03. When the seed is visited first it reads the untouched sky, 12.0000, and writes its 0.03 of leaf into the cells around it, and the plant eighty times its size then reads 11.9600 and earns 2.9900 instead of 3.0000. Four hundredths of a unit. On tick 40 that is nothing at all, and it is exactly the hundredth of a gram the tie-break was worth two runs ago, fed into the same amplifier for five hundred and sixty ticks.
Merging the loops is not itself the fault. What the merge did was quietly redefine the number being read. In the two-phase tick, a stand's reading means the light that got past the crowns taller than its own, which is a claim about the valley. In the one-pass tick it means the light that got past the crowns of whichever plants sit earlier in a slice, which is a claim about a data structure. Only one of those two sentences has any shade in it.
Carry the general form: when readers and writers of a shared field are interleaved, the order of the loop becomes part of the physics, so either the order is the physics or the loop has to be split. Here the order is part of the physics, and that is exactly why it may not be left to the slice: the caster sorts by height and states its tie-break out loud, because height is the thing that decides who eats first, and index is not. The check that catches it is cheap and belongs in the tree: run the world with its inhabitants stored in a different order and hash both.
The service order
Three ideas came together on this page and each of them turns up again without plants attached.
The first is a field with writers in it. The wind was a field that only ever answered questions, and the soil is a field that gets taken from and holds what is left. The light is a third kind: it is thrown away and recomputed from scratch every tick, from the current state of everything standing in it. That distinction decides how you store it. A store needs conserving, flooring, and a fixed order of service, because a mistake in it accumulates. A recomputed field needs none of the three and can be wiped with impunity, because nothing in it survives a tick. What it needs instead is that every writer has finished before any reader starts.
The second is one-sided competition, which is a different animal from sharing. Two roots in one cell both come away with less than they wanted, and their loss is roughly even. A crown and a seedling do not share anything: the crown takes what it takes at full price and the seedling gets the remainder, and the tall plant's ledger never records that a transaction occurred. Its column of numbers is the same whether the seedling is there or not. Any quantity handed out in that manner amplifies whatever difference it finds, because the reward for being ahead is a larger share of the next round. The runs above fed a hundredth of a gram into that amplifier and took a factor of two out of the far end.
The third is the check. Two things on this page had to be verified rather than believed: that adding a light field did not move the answer for a plant standing alone, and that the order the world stores its plants in does not reach the numbers. Both are identities, both are one run each, and both caught something a correctness test would have passed. The first showed that 3.9800 was still 3.9800 after a grid was slid underneath it. The second showed a valley where a gram of seedling shaded a tree.
The ordering itself generalises past light. Anything a world hands out in a sequence has this structure: whoever is served first sets the conditions for whoever is served second, and if the rule for who is served first is arbitrary then the outcome is arbitrary too. In the light field the rule is height and it is defensible, because being taller is what getting there first physically means. When creatures start feeding, the same question arrives wearing different clothes, and it will want the same answer: state the ordering, justify it, and hash two runs to prove nothing else is deciding.
The understorey run
- Given a crown's leaf per gram, its radius and a plant's mass, you can work out the cover on one cell and the share of light that gets through, and check both against the header line the bench prints.
- compute a seedling's ceiling under one, two and three grown crowns, say why each one halves it, and explain why the plant still reaches its lower ceiling in the same number of ticks.
- say why shade stacks by multiplying passes instead of adding covers, and what goes wrong at four crowns if it is written the other way.
- explain what the caster's height sort buys, why a stand reads before its own leaves are written in, and what the tie-break decides when two plants are exactly the same size.
- Handed two runs of the same sowing with the stands stored in opposite orders, you know which numbers are required to be identical and what it means when they are not.
- Shown a light row of 12.00, 6.00, 4.50 and 9.00 across four cells, you can name which crowns reach which cell and which plant read which number.
Exercise 1 — spread the same leaf wider. A crown of radius 2.5
covers 21 cells instead of 9. Before running anything, work out what 4.50 cells of leaf
over 21 cells lets through, then predict whether the late-sown plant does better or
worse under it. Check with -crown 2.5.
4.50 ÷ 21 = 0.2143 of cover, so 0.7857 of the light gets past one crown instead of 0.50. The late plant should do considerably better: its ceiling under one grown neighbour is 12.00 × 0.7857 × 0.25 ÷ 0.02, about 118 grams.
$ go run ./cmd/canopy -mode pair -ticks 600 -gap 40 -crown 2.5
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 2.5 covering 21 cells, 0.03 of leaf a gram, 0.04 of height a gram
the same two seeds 40 ticks apart, 600 ticks of growing
sown first west east world
west 149.9992 117.8577 8a171b4a6fd394d3
east 117.8577 149.9992 2b7332252e6b69da
117.8577 against 75.0038. The wider crown reaches more cells and takes less out of each, so a valley of broad thin canopies is a gentler place to be second than a valley of narrow dense ones. Note what did not change: the winner is still the plant sown first, and it still finishes at exactly its cell's ceiling. Crown width sets how much the loser loses, never who loses.
Exercise 2 — read the number at the ground instead. Change one
line so every plant harvests the light left in its own cell after the whole cast, its
own leaves included, rather than the reading taken at its crown. Predict what happens
to the two identical seeds, then run it with -level.
Reading at ground level makes the competition even-handed: each plant is dimmed by its neighbour whether that neighbour is taller or shorter, and by its own leaves as well. The head start should stop mattering.
$ go run ./cmd/canopy -mode same -ticks 600 -every 150 -level
canopy: 12x8 valley, 12.00 of daylight on an open cell, 2.50 of water a cell
crown radius 1.5 covering 9 cells, 0.03 of leaf a gram, 0.04 of height a gram
both seeds sown on tick 0, 600 ticks of growing
tick west east west lit east lit
1 3.9600 3.9600 11.9201 11.9201
2 6.8022 6.8022 11.6853 11.6853
3 9.5316 9.5316 11.4620 11.4620
150 80.0131 80.0131 6.4533 6.4533
300 80.3829 80.3829 6.4309 6.4309
450 80.3847 80.3847 6.4308 6.4308
600 80.3848 80.3848 6.4308 6.4308
world a8b2eff37c402130
$ go run ./cmd/canopy -mode pair -ticks 600 -gap 40 -level | tail -3
sown first west east world
west 80.3852 80.3843 5dae554fe6d93272
east 80.3843 80.3852 3b9ab687a92ea0bf
Identical, to four places, and the forty-tick head start is worth nine ten-thousandths of a gram. Both plants settle at 80.3848 instead of 149.9992 and 75.0046, because both are now paying for their own leaves and neither can get above the other. One line about where the reading is taken is the difference between a valley with a canopy in it and a valley of equals, and the line is the model, not an implementation detail.
Exercise 3 — find the shade a seed cannot start under. A seed
germinates at 1.00 gram. Work out how many grown crowns overhead put its ceiling below
that, so it begins shrinking on its first tick, then check with
-crowns 8.
The ceiling reaches 1.00 gram when income reaches 0.02, so the light has to fall to 0.02 ÷ 0.25 = 0.08 units. That is a drop by a factor of 12.00 ÷ 0.08 = 150, and halving gets there in eight steps and not seven: seven halvings divide the light by 128, which leaves more than a hundred and fiftieth of it, and eight divide it by 256, which is past the mark.
$ go run ./cmd/canopy -mode shade -crowns 8 | tail -5
4 0.7500 0.1875 9.3750 8.2643
5 0.3750 0.0938 4.6875 4.1985
6 0.1875 0.0469 2.3438 2.1655
7 0.0938 0.0234 1.1719 1.1491
8 0.0469 0.0117 0.5859 0.6409
Under seven crowns the ceiling is 1.1719 and the seed creeps up to 1.1491 by tick 100: alive, and never going to be anything. Under eight the ceiling is 0.5859, below the gram it germinated with, so it burns tissue from its first tick and reads 0.6409 at tick 100, lighter than the day it started. Nothing on this page kills it. The ledger keeps subtracting, and the seed spends its whole existence getting smaller on ground it never had a say in landing on.
Every number that decided those runs was the same for both contestants. Same crown radius, same leaf per gram, same height per gram, same thirst, same upkeep, and the winner was chosen by a sowing date and a tie-break. A real valley does not work that way: the plant in the shade is a different plant from the one casting it, built to live on 6.00 units where the other needs 12.00, and paying for that with something else. Those numbers are scattered through three chapters now, some in a grammar and some in a ledger and some in a crown, and they belong in one block that a single species owns.