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

Winter Comes on Schedule

The yearly angle

A plant that earns 3.00 units on tick 1 and tick 100,000 lives in a valley with no year. The terrarium needs daylight, warmth and rain to move on the same calendar. One angle names the tick's place in a 3,600-tick year; daylight, warmth and rain are three readings of that angle at fixed offsets, and plant rates read those seasonal numbers instead of constants.

The angle is read from the tick number, not accumulated from the previous tick. A run that opens at year 40 reads the same season a run from year 0 reaches there, because the calendar is arithmetic on the tick.

A hard winter changes the meaning of a species row. An annual can finish inside one growing window. A tree can shut down, pay less, and wait while the light is poor.

Four calendar ticks

A year is 3,600 ticks and a season is 900 of them. One trip round a circle of radius 1 is 6.283185307 of rim, so the angle gains 6.283185307 ÷ 3600 = 0.001745329 a tick, and the four quarter-days are the ticks where the angle stands on an axis: tick 0 at the start, tick 900 a quarter of the way round, tick 1800 halfway, tick 2700 three quarters. The heights at those four places need no arithmetic at all, because chapter 28 read them off a drawing: 0, 1, 0 and −1.

Daylight is a multiplier on the light landing on an open cell, sitting at 1.00 with a swing of 0.60 either side. Multiply the four heights by 0.60 and add 1.00 and the year's four corners are 1.0000, 1.6000, 1.0000 and 0.4000. On a cell that takes 12.00 units on an average day that is 12.00 at the spring equinox, 19.20 at the summer solstice, 12.00 again at the autumn equinox, and 4.80 at midwinter. The longest day is four times the shortest, and the two equinoxes are the two ticks in the year where the reading is exactly average.

Halve the gaps and the eighth-turn number comes back. A point an eighth of the way round stands 0.7071 high, so 1.00 + 0.60 × 0.7071 = 1.4243, which is the daylight at tick 450 and again at tick 1350. Four numbers a year, plus the four between them, and every one of them came off the same table.

Warmth is where the year stops being a copy of itself. Ground and water take time to heat up, so the warmest weeks of a real year arrive after the longest day, not on it. That delay costs nothing to build: read the height at a point standing a little behind the year's own position and the whole warmth curve arrives late. Set the lag at an eighth of a turn, sit warmth at 0.50 with a swing of 0.50, and the four quarter-days read 0.5 + 0.5 × (−0.7071) = 0.1464 at the spring equinox, 0.8536 at the solstice, 0.8536 again at the autumn equinox, and 0.1464 at midwinter. Warmth is highest at tick 1350, which is no quarter-day and no solstice: it is 450 ticks after the longest day. A real valley's lag is nearer three or four weeks than six, and an eighth of a turn overstates it; the reason to use an eighth anyway is that it lands every quarter-day reading on the one number you already computed by hand.

Rain gets the third place on the rim, half a turn round from warmth, which makes it warmth turned upside down: wet when the valley is cold, dry when it is hot. Sitting at 1.00 with a swing of 0.90 that gives 1.6364 at the spring equinox, 0.3636 at the solstice, 0.3636 at the autumn equinox and 1.6364 at midwinter, with the wettest tick of the year at 3150 and the driest at 1350. Summers here are bright, hot and dry, and winters are dark, cold and wet. That is a choice about this valley and not a law, and moving it is one number.

∑ Math Interlude — one angle, three heights

Every number above is one line of arithmetic repeated nine times, so here is the line. Call the year's angle at tick t the letter θ, measured in rim the way chapter 28 measured it. The year is Y ticks long and one whole turn is 2π of rim, so:

θ = t × 2π ÷ Y

Check it: t = 900 and Y = 3600 gives 900 × 6.283185307 ÷ 3600 = 1.570796, a quarter turn. A reading is then three numbers and one call: a middle m it sits at, a swing s either side of that middle, and a lag ℓ saying how far behind the year's own position it takes its height.

reading(θ) = m + s × sin(θ + ℓ)

The lag is written as an addition and given a negative value for a reading that runs late, because adding a negative angle is stepping backwards round the rim. Daylight uses m = 1.00, s = 0.60, ℓ = 0. Warmth uses m = 0.50, s = 0.50, ℓ = −2π/8. Rain uses warmth's numbers for m and s scaled up, and warmth's lag plus half a turn.

One consequence is useful before the run. Every one of these readings is highest a quarter turn after its swing crosses its middle going upward, so the peak of a reading with lag ℓ lands at θ = 2π/4 − ℓ. For warmth that is a quarter turn plus an eighth, or three eighths of the year: 0.375 × 3600 = 1350. The peak is computed, never hunted for in a run.

tthe tick, counted from the world's first spring
Yticks in one year; 3,600 here, so a season is 900
θ"theta": where the year stands on the rim, 0 at the spring equinox
one whole turn of rim, 6.283185307, measured in chapter 28
sin θthe height of the point standing θ round the rim, between −1 and 1
mthe middle a reading sits at when its height is zero
sthe swing: how far either side of m the reading reaches
the lag, in rim; negative for a reading whose peak arrives late
One year angle, and the three readings taken off it On the left, a circle marked with the year's four quarter-days at ticks 0, 900, 1800 and 2700. The year is drawn standing at tick 600, and three points are marked on the rim: daylight at the year's own angle, warmth an eighth of a turn behind it, and rain half a turn round from warmth. On the right, the same three readings unrolled across one year, stacked: daylight swinging between 1.60 and 0.40 with its peak at tick 900, warmth between 1.00 and 0.00 with its peak at tick 1350, and rain between 1.90 and 0.10 with its peak at tick 3150. ONE ANGLE, THREE READINGS TAKEN OFF IT 0 900 1800 2700 daylight warmth rain the year at tick 600 three fixed offsets DAYLIGHT 1.00 ± 0.60, ON THE ANGLE 1.60 at tick 900 WARMTH 0.50 ± 0.50, AN EIGHTH LATE 1.00 at tick 1350 RAIN 1.00 ± 0.90, HALF A TURN FROM WARMTH 1.90 at tick 3150 0 900 1800 2700 3600 three curves, one state variable: the offsets do all the rest
Figure 40.1 — the three curves on the right are not three oscillators. They are one angle read at three places on the circle on the left, which is why their peaks land on three different ticks and their spacing never drifts.
▣ Build · stage 1 — the year, and three readings of it
// internal/terra/year.go

// Year is one trip round the world's calendar, in ticks. It is the
// period of a single oscillator, and every seasonal number in the
// valley is a reading taken off that one angle.
const Year = 3600

// Quarter is the length of one season: four of them make the year.
const Quarter = Year / 4

// Seasons are the four quarters of the year, in the order they open.
var Seasons = [4]string{"spring", "summer", "autumn", "winter"}

// Season is the quarter tick t falls in.
func Season(t int) string { return Seasons[(((t%Year)+Year)%Year)/Quarter] }

// Angle is where on the rim the year stands at tick t. The angle is
// taken from the tick's place inside the year rather than accumulated,
// so a run that starts at year 40 opens on exactly the angle a run that
// started at year 0 reaches there.
func Angle(t int) float64 {
	return float64(((t%Year)+Year)%Year) * field.Turn / Year
}

// Reading is one weather number taken off the year's angle: a middle it
// sits at when the swing is nowhere, how far it swings either side, and
// how far round the rim its swing is shifted from the angle itself. A
// negative Lag is a reading whose peak arrives after the angle's does.
type Reading struct {
	Mid   float64
	Swing float64
	Lag   float64
}

// At is this reading at that angle: the height of a point standing Lag
// behind the year's own position, stretched to the swing and lifted to
// the middle.
func (r Reading) At(theta float64) float64 {
	return r.Mid + r.Swing*math.Sin(theta+r.Lag)
}

// Weather is the three readings the valley keeps its year by.
type Weather struct {
	Daylight Reading
	Warmth   Reading
	Rain     Reading
}

// Read takes all three readings at one angle, so no caller can take two
// of them at different places on the rim.
func (w Weather) Read(theta float64) (day, warm, wet float64) {
	return w.Daylight.At(theta), w.Warmth.At(theta), w.Rain.At(theta)
}

// Day is the valley's climate: the numbers this book runs with. The
// daylight swing is six tenths, so the longest day is 1.60 of an
// average one and the shortest is 0.40. Warmth lags an eighth of a turn
// behind the light. Rain is warmth's opposite, half a turn round from
// it, so the wet part of the year is the cold part.
var Day = Weather{
	Daylight: Reading{Mid: 1.00, Swing: 0.60, Lag: 0},
	Warmth:   Reading{Mid: 0.50, Swing: 0.50, Lag: -field.Turn / 8},
	Rain:     Reading{Mid: 1.00, Swing: 0.90, Lag: -field.Turn/8 + field.Turn/2},
}

Season is the line that turns a tick into a word. Divide the tick's place in the year by the length of a season and the answer is 0, 1, 2 or 3, which picks one of the four names, and that is where every spring and winter printed anywhere in this chapter comes from. The remainder is taken twice there, and in Angle above it, for a reason that only shows itself before tick 0. Go's % keeps the sign of the number on its left, so tick −1000 stays −1000, divides to −1, and indexes off the front of a four-name array, which Go answers by stopping the program. Add a whole year and take the remainder again and any tick, however far back, lands inside 0 to 3599.

Weather exists so that the three readings cannot be taken apart. Each of them would work perfectly well as a loose variable, and a valley holding three loose readings will one day take daylight at one angle and warmth at another, and run half a tick of summer against half a tick of autumn with nothing crashing and nothing to see. Read takes one angle and hands back all three, so there is no way to write that tick.

$ go run ./cmd/season -mode year
the year is 3600 ticks, and one angle gains 0.001745329 of rim a tick
    tick      angle       sin   daylight    warmth      rain  quarter opens
       0   0.000000    0.0000     1.0000    0.1464    1.6364  spring
     900   1.570796    1.0000     1.6000    0.8536    0.3636  summer
    1800   3.141593    0.0000     1.0000    0.8536    0.3636  autumn
    2700   4.712389   -1.0000     0.4000    0.1464    1.6364  winter
    3600   6.283185   -0.0000     1.0000    0.1464    1.6364
  warmth is highest at tick 1350 and rain at tick 3150
  species       cue    wakes    opens    shuts  ticks up
    herb    warmth     0.50      450     2251      1801
    tree  daylight     1.00        0     1801      1801

Every number in the daylight column was worked out above with a calculator, and so was every number in the two beside it. The sin column is there to make the claim checkable: it holds 0, 1, 0, −1 at the four quarter-days, which is chapter 28's table of eighths with every other entry left out. The last row is the same lesson that chapter closed on. π is not exact in a float64, so a whole turn is a hair under a whole turn, and the height at tick 3600 prints as -0.0000 where tick 0 prints 0.0000. The daylight, warmth and rain columns agree anyway, to four places.

$ go run ./cmd/season -mode year -eighths
the year is 3600 ticks, and one angle gains 0.001745329 of rim a tick
    tick      angle       sin   daylight    warmth      rain  quarter opens
       0   0.000000    0.0000     1.0000    0.1464    1.6364  spring
     450   0.785398    0.7071     1.4243    0.5000    1.0000
     900   1.570796    1.0000     1.6000    0.8536    0.3636  summer
    1350   2.356194    0.7071     1.4243    1.0000    0.1000
    1800   3.141593    0.0000     1.0000    0.8536    0.3636  autumn
    2250   3.926991   -0.7071     0.5757    0.5000    1.0000
    2700   4.712389   -1.0000     0.4000    0.1464    1.6364  winter
    3150   5.497787   -0.7071     0.5757    0.0000    1.9000
    3600   6.283185   -0.0000     1.0000    0.1464    1.6364
  warmth is highest at tick 1350 and rain at tick 3150
  species       cue    wakes    opens    shuts  ticks up
    herb    warmth     0.50      450     2251      1801
    tree  daylight     1.00        0     1801      1801

The eighths are where the lag shows itself. Warmth and rain both hit their extremes on the eighth-turn ticks and never on a quarter-day: warmth is 1.0000 at tick 1350 and 0.0000 at tick 3150, and rain is exactly the reverse of it, 0.1000 against 1.9000. That is what an eighth of a lag buys, and it is the whole reason the valley's hottest weeks are not its brightest ones. Daylight, taking its height on the angle itself, is symmetric about the solstice and reads 1.4243 twice.

Daylight warmth and rain readings

A reading is only weather once it multiplies something. Daylight multiplies the light landing on an open cell, so the light field opens each tick at 12.00 × daylight and every crown shades a brighter or darker sky without changing a line of the caster. Rain multiplies what the sky offers each cell of soil. Warmth multiplies every rate in the valley that is a chemistry and not a geometry, and there are three of those: the energy a plant can fix, the water the sun lifts off wet ground, and the speed anything dead rots at. Warmth is the valley's throttle.

The plant's ledger does not change to accept this. The three resources are still priced in energy, still compared, and the smallest still wins; the year touches the answer and not the comparison.

▣ Build · stage 2 — the tick with the year in it, and the tick with the leaves down
// internal/terra/terra.go

// Grow is one tick of the ledger with the year in it. The three
// resources are priced and compared exactly as before; what the year
// changes is how fast the chemistry runs, so the income the smallest of
// them paid for is multiplied by the warmth and everything downstream
// follows from the smaller figure.
func (p *Plant) Grow(g Ground, warmth float64) Ledger {
	l := Ledger{}
	e, name := p.Limit(g)
	l.Caught, l.Limit = e*warmth, name
	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
}

// Sleep is the tick a plant takes with its leaves down. It fixes
// nothing, draws nothing, and pays share of its usual upkeep out of the
// tissue it is standing up: spending less and earning none.
func (p *Plant) Sleep(share float64) Ledger {
	l := Ledger{Limit: "asleep"}
	l.Paid = p.Mass * p.Upkeep * share
	l.Grown = -l.Paid
	p.Mass += l.Grown
	return l
}

Two methods, and between them they are the entire annual-versus-perennial argument in arithmetic. Grow still charges upkeep at the full rate whether the plant is earning or not, so a plant holding its leaves up in cold weather pays the bill and collects almost nothing against it. Sleep collects nothing at all and cuts the bill by whatever share is. Dormancy is a slower burn with the income switched off instead of a pause, and whether that trade is useful depends entirely on how long the dark half lasts.

▣ Build · stage 3 — the flag, the method, and the one line inside the caster

The tick below reads three things off a Stand that the last chapter left it no way to ask for. Here they are, smallest first.

// internal/terra/sky.go — one more line inside type Stand

	Sleep bool    // its leaves are down this tick

The flag is set once, at the top of every tick, out of the row's own calendar, and read twice after that. A stand with it up pays only the doze share of its upkeep and casts no shade at all. It is worked out fresh every tick and never carried across one, which is the only thing keeping it from being half a season out of date at the moment it matters.

// internal/terra/sky.go — and one more method on it

// Holds is the nutrient locked up in this stand's standing tissue.
func (st *Stand) Holds() float64 { return st.Plant.Mass * st.Plant.Hunger }

One multiplication, and the reason it is a method instead of that multiplication written out wherever it is wanted is that it is wanted in several places and they must never disagree with each other. A plant is standing up nutrient in proportion to the tissue it is standing up. The tick below asks for the figure on the line that buries a plant, the longer runs two chapters on ask for it on that same line and again whenever they add up what the valley is holding, and the last section of this page is about where the nutrient goes once the plant has let go of it.

// internal/terra/sky.go — the loop inside Cast, one line longer

	for _, i := range order {
		st := stands[i]
		st.Lit = s.At(st.At)
		if st.Sleep {
			continue // bare branches shade nothing
		}
		pass := st.Crown.Pass(st.Plant.Mass)
		for _, c := range s.Under(st.At, st.Crown.Crown) {
			s.Dim(c, pass)
		}
	}

Where that guard sits is the entire content of the change. It is after st.Lit = s.At(st.At), so a sleeping stand still takes its reading and skips only the writing, and both halves of that are deliberate. The light over a bare tree in the dark half of the year is still there, and the tree is still standing in it, so refusing it the reading would be inventing a darkness nothing casts. What it no longer does is put its own crown into the sky, because bare branches shade nothing, and the ground under a tree goes back to full daylight on the tick the leaves come down. Move the guard one line up and a dormant plant spends half the year on a number nobody wrote; move it one line down and the sky over the valley never notices that winter happened.

▣ Build · stage 4 — the question a row answers, and the order a seasonal tick runs in

One line of the tick below asks every stand a question the row has to answer for itself: are this plant's leaves up at this place in the year? Two short functions answer it.

// internal/terra/species.go — the question the tick asks every row

// Awake reports whether this row has its leaves up at that place in the
// year, and the value of the reading it decided by.
func (s Species) Awake(w Weather, theta float64) (bool, float64) {
	v := w.Cue(s.Cue).At(theta)
	return v >= s.Wake, v
}

Two return values, and the tick keeps the first one. The boolean is the decision. The float beside it is the reading the decision was made on, the actual warmth or daylight at that angle, and it is there because a run that prints asleep and nothing else hands you nothing to check. Beside the threshold it was compared against, that number is a claim you can hold up to the curve you worked out with a calculator at the top of this page. The tick throws it away with up, _ :=. The tables keep it, and that is why the tables are believable.

// internal/terra/year.go — one more method on Weather

// Cue is the reading a species keeps its calendar by, named.
func (w Weather) Cue(name string) Reading {
	if name == "warmth" {
		return w.Warmth
	}
	return w.Daylight
}

Both halves of w.Cue(s.Cue) are spelled Cue and they are two different things. The one on the row is a string somebody typed when they wrote the species down; the one on the weather turns that string into a reading with a middle, a swing and a lag on it. The method is dull on purpose: two names, no validation, and anything that is not warmth gets daylight. A row with a typo in its cue therefore keeps a daylight calendar and runs a plausible-looking year. The cheapest guard against that is the cue column in the tables above, where every run prints the dial each row actually read.

// internal/terra/valley.go

// Tick advances the valley by one. The order is the whole design: read
// the calendar, settle who has leaves up, cast the light, let
// everything eat in a fixed order, bury whatever died, and only then
// let the weather work on the ground.
func (v *Valley) Tick() (day, warm, wet float64) {
	theta := Angle(v.Now)
	day, warm, wet = v.Climate.Read(theta)

	v.Sky.Full = v.Light * day
	for _, st := range v.Stands {
		up, _ := st.Kind.Awake(v.Climate, theta)
		st.Sleep = !up
	}
	Cast(v.Sky, v.Stands)

	for _, st := range v.Stands {
		var l Ledger
		if st.Sleep {
			l = st.Plant.Sleep(st.Kind.Doze)
		} else {
			g := v.Bed.Offer(st.At, st.Plant.Root, st.Lit)
			l = st.Plant.Grow(g, warm)
			v.Bed.Draw(st.At, st.Plant.Root, Cell{Moisture: l.Drawn, Nutrient: l.Ate})
			v.Built += l.Caught
		}
		// The tissue upkeep burned is gone as tissue, and the nutrient
		// that was locked in it is not: it falls where the plant stands.
		v.Burned += l.Paid
		v.Bed.Fall(st.At, Pile{Nutrient: l.Paid * st.Plant.Hunger})
		v.Fell += l.Paid * st.Plant.Hunger
		st.Last = l
	}

	live := v.Stands[:0]
	for _, st := range v.Stands {
		up, _ := st.Kind.Awake(v.Climate, theta)
		dead := st.Plant.Mass < st.Kind.Least || (st.Kind.Annual && !up)
		if !dead {
			live = append(live, st)
			continue
		}
		v.Bed.Fall(st.At, Pile{Mass: st.Plant.Mass, Nutrient: st.Holds()})
		v.Fell += st.Holds()
	}
	v.Stands = live

	v.Bed.Shower(v.Fall*wet, v.Full)
	v.Bed.Evaporate(v.Sun * warm)
	burned, back := v.Bed.Rot(v.Decay * warm)
	v.Rotted += burned
	v.Back += back

	v.Now++
	return day, warm, wet
}

Three placements in that function are decisions and not habits. The sleep flags are all settled before the caster runs, because a bare tree has to pass its light through on the same tick it drops its leaves and not on the one after. The death sweep runs after everything has eaten, so a plant that died this tick still paid this tick's bill and its tissue lands in the litter with the correct mass on it. And the weather comes last: rain, evaporation and rotting all work on the ground the plants have already finished with, so no plant drinks water that fell after it stopped drinking.

$ go run ./cmd/season -mode life -every 450
season: 12x8 valley from seed 5, 44 soil cells, 12.00 light on an average day
        rain 0.02 a cell an average day, sun lifts 0.01 at full warmth, litter rots 0.01000

    tick   season daylight  warmth    rain      tree  held by     herb    wet
       0   spring   1.0000  0.1464  1.6364     1.172    light    0.000   1.00
     450   spring   1.4243  0.5000  1.0000   120.569    light    2.842   1.00  the herb comes up
     900   summer   1.6000  0.8536  0.3636   309.870    light   57.527   0.99
    1350   summer   1.4243  1.0000  0.1000   418.494    light   51.947   0.93
    1800   autumn   1.0000  0.8536  0.3636   340.231    light   35.442   0.87
    1801   autumn   0.9990  0.8529  0.3647   340.197   asleep   36.226   0.87  the tree drops its leaves
    2250   autumn   0.5757  0.5000  1.0000   325.259   asleep   18.310   0.91
    2251   autumn   0.5750  0.4991  1.0016   325.227   asleep    0.000   0.91  the herb dies, 18.310 grams into the litter
    2700   winter   0.4000  0.1464  1.6364   310.946   asleep    0.000   0.97
    3150   winter   0.5757  0.0000  1.9000   297.263   asleep    0.000   1.00
    3599   winter   0.9990  0.1458  1.6375   284.211   asleep    0.000   1.00

One year, one seed of each, and the calendar has taken over the whole run. The tree climbs to 418.494 grams by tick 1350 and then loses mass through the back half of summer while its light is still falling, because its ceiling is income divided by upkeep and the income is now a moving number. The wet column is the average soil cell as a share of full: 1.00 through the wet spring, down to 0.87 as the rain fails and the sun works, back to 1.00 by midwinter. Nothing in that column ever limits anybody here, and the held by column says so by reading light on every tick the tree is awake.

The herb and the tree

The row of numbers the last chapter wrote grows a third block on the end of it. Five fields, all of them about time: which reading the plant keeps its calendar by, the value of that reading at which its leaves go up, the share of its upkeep it still pays with them down, the mass below which there is no longer enough of it to be alive, and a plain flag which is the branch this page is about. An annual dies on the tick its window shuts. A perennial sleeps through.

The herb keeps its calendar by warmth, at 0.50, which is the middle of the warmth curve. The tree keeps its by daylight, at 1.00, which is the middle of the daylight curve. Both thresholds are the average of their own reading, so both windows are exactly half the year: the run's table says 1801 ticks each. They are not the same half. The tree's leaves are up from tick 0 to 1800, opening at the spring equinox and closing at the autumn one. The herb's are up from tick 450 to 2250, opening a whole season later and closing a season later still, because warmth is a reading that arrives late. Two plants, one valley, two different years, and the entire difference is which place on the rim each of them reads.

▣ Build · stage 5 — the two rows, and the cue each keeps
// internal/terra/species.go — the third block of the row

	// the year
	Cue    string  // "daylight" or "warmth": the reading its calendar keeps
	Wake   float64 // the cue at or above which its leaves are up
	Doze   float64 // the share of its upkeep it still pays with them down
	Least  float64 // the mass below which there is not enough of it to live
	Annual bool    // true: the tick its window shuts is the tick it dies
// cmd/season/main.go

// The two species this chapter runs. Everything above the Cue line is
// the row the last chapter wrote; everything below it is what the plant
// does with a year.
var herb = terra.Species{
	Name: "herb",
	Seed: 1, Catch: 0.25, Thirst: 0.03, Hunger: 0.05, Upkeep: 0.05, Root: 1.5,
	Reach: 1.5, Leafy: 0.03, Tall: 0.01,
	Cue: "warmth", Wake: 0.50, Doze: 0, Least: 0.25, Annual: true,
}

var tree = terra.Species{
	Name: "tree",
	Seed: 1, Catch: 0.10, Thirst: 0.02, Hunger: 0.02, Upkeep: 0.004, Root: 2.5,
	Reach: 2.5, Leafy: 0.02, Tall: 0.02,
	Cue: "daylight", Wake: 1.00, Doze: 0.025, Least: 0.50, Annual: false,
}

Read the two Upkeep figures against each other and the rest follows. The herb charges itself 0.05 an hour on every gram, twelve times the tree's rate, so it climbs to its ceiling twelve times as fast and that ceiling is a twelfth as high. It is a plant built to convert a good season into as much tissue as possible and to be finished before the season is. The tree is built the opposite way round: cheap tissue, a low ceiling per unit of light, and a mass that takes hundreds of ticks to move. Expensive tissue makes a small plant quickly and cheap tissue makes a large one slowly, and a year with a hard winter in it is what turns that trade into two kinds of plant instead of one preference.

The herb's ending is the honest part. Its window shuts on tick 2251 with 18.310 grams of tissue standing, and there is no negotiation in the code: the flag says annual, the window says shut, the stand leaves the world and every gram of it lands on the cell it grew on. Death here is a routing decision and not an event. Nothing is deleted, because deleting mass in a world that keeps books is the one thing the books exist to catch.

The tree's ending is arithmetic you can do before running anything. It sleeps from tick 1801 to the end of the year, which is 1799 ticks. Its upkeep is 0.004 a gram and its dormant share is 0.025, so a sleeping tree pays 0.004 × 0.025 = 0.0001 of itself every tick. The rough figure comes first: 1799 × 0.0001 = 0.1799, so about eighteen per cent of the tree is gone by spring if the bill were charged on the mass it started with. It is not, because the bill shrinks as the tree does, so the true answer is 0.9999 multiplied by itself 1799 times, and that is 0.835346. The tree comes out of the dark half holding a shade over five sixths of what it went in with, and to be the same size next autumn it has to earn back one part in five.

▣ Build · stage 6 — the dormant half-year, against the paper figure
$ go run ./cmd/season -mode winter
a dormant tree pays 0.004 x 0.025 = 0.000100 of itself a tick
    tick   season     asleep         mass of what went in
       0   spring      false   398.575736     1.000000
     450   spring      false   186.022351     1.000000
     900   summer      false   320.650535     1.000000
    1350   summer      false   420.269752     1.000000
    1800   autumn      false   340.523443     1.000000
    2250   autumn       true   325.538821     0.955995
    2700   winter       true   311.213593     0.913927
    3150   winter       true   297.518742     0.873710
    3599   winter       true   284.454973     0.835346
  it slept from tick 1801, so 1799 ticks of paying 0.000100 of itself
  flat estimate: 1799 x 0.000100 = 0.179900 of it gone
  compounded:    0.999900 multiplied by itself 1799 times = 0.835346

The last column and the last line are the same six digits, 0.835346, one measured across 1799 ticks of a simulation and one worked out from two constants before the simulation started. That agreement is the point of doing the arithmetic first: a dormancy discount is not a feel, it is a number with a known effect over a known number of ticks, and any run that disagrees with it has a bug in it somewhere between the two.

The spring rows are the other half of the story. This tree was stood up at 400 grams, which is more than the cold months can carry, so it loses more than half of itself by tick 450 before the strengthening light turns it round. A perennial does not settle at one mass. It settles into a loop, and the loop's low point is the one that decides whether it is alive.

Dead mass returns

There are now two ways for tissue to leave a plant and neither of them is tidy. Upkeep burns tissue every tick of every plant's life, and death moves all of it at once. Both go to the same place: a pile on the cell, holding two numbers that leave separately. The grams go as the rotting burns them, the way a fire's fuel goes. The nutrient goes downward, into the cell, and it is the only thing in this valley that is neither created nor destroyed.

That asymmetry is the sentence to keep. Light arrives from outside and heat leaves, so energy is not conserved here and never can be. Rain arrives from outside and vapour leaves, so water is not either. The nutrient has nowhere to go: every unit of it is standing in a body, lying in a pile, or dissolved in the soil, and no line of code anywhere can make a new one. Energy passes through a valley. Matter goes round it.

▣ Build · stage 7 — the pile, and the fraction of it that goes back
// internal/terra/bed.go

// Pile is what has died on one cell and not yet gone back into it. The
// two numbers are counted separately because they leave separately: the
// grams go off as the rotting burns them, and the nutrient goes down
// into the cell underneath.
type Pile struct {
	Mass     float64 // grams of dead tissue lying on the cell
	Nutrient float64 // nutrient units still locked up in it
}

// Rot works the same fraction off every pile in the valley and hands
// back what left each column. The nutrient goes straight into the cell
// the pile was lying on, with no cap on it: a cap would be a hole in
// the books, since the units it refused would be units nothing in the
// world is holding any more.
//
// The rate is a share of a pile per tick and is passed in already
// multiplied by the warmth, because rotting is something alive doing
// the work and cold weather slows it the way it slows every other
// chemistry in the valley.
func (b *Bed) Rot(rate float64) (burned, returned float64) {
	if rate <= 0 {
		return 0, 0
	}
	if rate > 1 {
		rate = 1
	}
	for i := range b.litter {
		p := &b.litter[i]
		m, n := p.Mass*rate, p.Nutrient*rate
		p.Mass -= m
		p.Nutrient -= n
		b.cells[i].Nutrient += n
		burned += m
		returned += n
	}
	return burned, returned
}

The comment about the cap is the whole reason this function is short. A soil cell caps its moisture, and it must, because rain that falls on saturated ground runs off and goes somewhere else in the world. Nutrient arriving from a pile is already in the world and already accounted for, so refusing it would delete it, and a ledger that balances is worth more than a cell that never exceeds a tidy maximum.

One line above says why a gram of tissue holds any nutrient at all. A gram was built out of one unit of fixed energy, and fixing one unit of energy cost Hunger units of nutrient, so a body of mass m is holding m × Hunger units and nothing has to accumulate that figure. When upkeep burns a gram, that gram's share falls onto the cell; when the plant dies, all of it does.

▣ Build · stage 8 — six years, and both sets of books
$ go run ./cmd/season -mode years -years 6
season: 12x8 valley from seed 5, 44 soil cells, 12.00 light on an average day
        rain 0.02 a cell an average day, sun lifts 0.01 at full warmth, litter rots 0.01000

   year tree spring tree autumn   winter  herb best      fell      back    litter      soil     books
      1       1.000     340.569   16.55%     58.140  243.3683  240.9471    2.4212  123.8946  0.000000
      2     284.211     340.777   16.55%     57.682  247.9215  247.9212    2.4215  123.8908 -0.000000
      3     284.384     340.778   16.55%     57.682  247.9239  247.9239    2.4215  123.8908 -0.000000
      4     284.384     340.778   16.55%     57.682  247.9239  247.9239    2.4215  123.8908  0.000000
      5     284.384     340.778   16.55%     57.682  247.9239  247.9239    2.4215  123.8908  0.000000
      6     284.384     340.778   16.55%     57.682  247.9239  247.9239    2.4215  123.8908  0.000000

  the books in grams              the books in nutrient units
    standing          284.3842      in living tissue      5.687684
    lying dead          2.8535      lying dead            2.421542
    burned          37330.0685      in the soil         123.890774
    rotted            107.0075      the valley opened   132.000000
    ever built      37724.3137      difference            0.000000
    difference        0.000000

Both differences are zero to six places, every year, and they are zero for different reasons. In grams, every gram ever manufactured is standing in a plant, lying in a pile, burned as upkeep, or rotted away, and 37724.3137 is exactly the four of them added up. In nutrient, the 132.000000 units the valley opened with (44 soil cells at 3.0 each) are still 132.000000, distributed across bodies, piles and soil. The second identity is the stronger one, because it is checkable against a number nobody computed from a run: the opening stock is a multiplication.

The fell and back columns are the ones to stare at. 247.9239 units go from bodies into the litter every year, and 247.9239 come back out of it, on a valley that owns 132.000000 units in total. The whole nutrient stock makes nearly two complete circuits a year. That is the number that decides how much can be alive here at once, and it is a rate and not a quantity: 132 units moving twice a year support a 340-gram tree and a 58-gram herb, where the same 132 units sitting still would support nothing at all.

▣ Build · stage 9 — the same valley, four times
$ go run ./cmd/season -mode frames -year 3
season: 12x8 valley from seed 5, 44 soil cells, 12.00 light on an average day
        rain 0.02 a cell an average day, sun lifts 0.01 at full warmth, litter rots 0.01000

    tick   season  daylight   warmth     rain      tree      herb  frame
   11250   spring    1.4243   0.5000   1.0000   167.056     2.747  e98e052b8edba852
   12150   summer    1.4243   1.0000   0.1000   419.755    51.834  2352314866533cfb
   13050   autumn    0.5757   0.5000   1.0000   325.458    18.310  fdba48ce1880cdd8
   13950   winter    0.5757   0.0000   1.9000   297.445     0.000  f54f16648b487429

Four frames from the middle of each season of the fourth year. A frame here is not a picture but everything a picture would have to be drawn from, folded down the way volume 3 folded a run of moving bodies: every cell's moisture and nutrient as raw float64 bits, big-endian, in row order, then the two totals lying dead on top of them, then each stand's cell and its mass in the order the valley holds them. The first eight bytes of that are what gets printed. The spring and summer frames share a daylight reading of 1.4243 and are nothing like the same valley: one has a 167-gram tree in leaf over a seedling, the other a 419-gram tree over a herb at 51.834 grams. The autumn and winter frames share the other 0.5757 and differ by a whole herb. A single number never identifies a moment in a year; the position on the rim does, and these four digests are what pins that to something a machine can check.

Add -art to any of those and the sky prints under each frame, one character a cell, as the share of the open daylight that reached it out of nine. Midsummer has a patch five cells across in it, deepest in the three the herb is standing in as well. In the autumn frame the tree's part of that patch is gone and the herb's three cells are all that is left of it, which is a bare tree turning from a claim about a boolean into something you can look at.

⚠ Worked failure — a valley that lost four fifths of its life without losing a unit

The rotting rate is the one number on this page with no arithmetic behind it. A share of a pile per tick, at full warmth: it has to be small, since a pile does not vanish overnight, and 0.0002 is a defensible kind of small. Run a year on it and there is a valley: the herb comes up in spring, reaches 45.405 grams, and dies on schedule; the tree puts on 72 grams of wood. Nothing on the page says anything is wrong, and nothing would, because there is no earlier run to compare it with.

$ go run ./cmd/season -mode years -years 6 -decay 0.0002
season: 12x8 valley from seed 5, 44 soil cells, 12.00 light on an average day
        rain 0.02 a cell an average day, sun lifts 0.01 at full warmth, litter rots 0.00020

   year tree spring tree autumn   winter  herb best      fell      back    litter      soil     books
      1       1.000      72.254   16.52%     45.405   34.5880    8.4937   26.0943  104.6994 -0.000000
      2      60.319      66.676   16.49%      0.112   10.2324   10.1497   26.1770  104.7094  0.000000
      3      55.679      66.675   16.49%      0.113   10.1506   10.1506   26.1770  104.7094  0.000000
      4      55.678      66.675   16.49%      0.113   10.1506   10.1506   26.1770  104.7094  0.000000
      5      55.678      66.675   16.49%      0.113   10.1506   10.1506   26.1770  104.7094  0.000000
      6      55.678      66.675   16.49%      0.113   10.1506   10.1506   26.1770  104.7094 -0.000000

  the books in grams              the books in nutrient units
    standing           55.6780      in living tissue      1.113561
    lying dead          0.2436      lying dead           26.177048
    burned           3806.1394      in the soil         104.709391
    rotted              0.5407      the valley opened   132.000000
    ever built       3862.6017      difference           -0.000000
    difference       -0.000000

Year 2 is where it becomes visible. The herb, which reached 45.405 grams in year 1, comes up at 0.112 grams and never gets anywhere, and it does that again in year 3 and in every year after. The tree finishes at a fifth of the size the fixed run reaches. And the books balance perfectly. Both differences are zero to six places, the valley still holds exactly 132.000000 units of nutrient, and no rule anywhere has been broken.

Which is the most useful thing a conservation ledger ever does. It cannot tell you the world is healthy, because a world is allowed to be a corpse and still balance. What it can tell you is where everything is, and the columns say it plainly: 26.177048 units are lying in a pile of dead tissue against 2.421542 in the working run, and the soil is 19.18 units poorer for it. A fifth of the valley's entire matter is sitting in the litter, and while it sits there nothing can spend it.

The fell column dates the whole thing. In year 1 the plants put 34.5880 units into the litter and the rotting handed only 8.4937 back, so the pile grew all year; by year 2 the circuit has throttled itself down to 10.1506 units, a twenty-fourth of the healthy run's 247.9239. What a rate means in ticks is the question that was never asked, so ask it:

$ go run ./cmd/season -mode rot
a litter pile losing a share of itself every tick, and how long it takes to halve
        rate   at full warmth   at half warmth       in years
     0.02000            35 ticks            69 ticks           0.02
     0.01000            69 ticks           139 ticks           0.04
     0.00200           347 ticks           693 ticks           0.19
     0.00080           867 ticks          1733 ticks           0.48
     0.00020          3466 ticks          6932 ticks           1.93
  a season is 900 ticks and a year is 3600

At 0.0002 a pile takes nearly two years to lose half of itself, so litter dropped this autumn is still mostly there the autumn after next, with two more years of litter on top of it. At 0.01 the same pile is half gone inside 139 ticks and the circuit turns twice a year. The rate was never small or large in itself; it was a half-life, and a half-life only means anything held against the length of the year it is running in.

The general form of the mistake outlives the plants. A rate that moves a quantity in proportion to itself has a doubling or halving time, and that time is the only honest way to compare it with anything. Cold ground does exactly this in the real world, which is why peat exists: the litter arrives faster than the cold lets it rot, and the difference piles up for ten thousand years. A tropical forest running the same arithmetic at a warmer rate keeps almost nothing on the ground and almost everything in the trees.

The seasonal state variable

What makes this design cheap is that the whole weather system holds exactly one number that changes, and that number is the tick count the world was already keeping. Daylight, warmth and rain are computed from it every time they are wanted and stored nowhere, so there is no way for them to disagree with each other, no way for one of them to be stepped twice in a tick, and nothing to save when the world is written to disk. Three curves that must keep a fixed relationship for the world to make sense are guaranteed to keep it, because their relationship is three constants and not three update rules.

The offsets are where the modelling actually lives. Give warmth the same lag as daylight and the valley stops having seasons and starts having a brightness setting: everything peaks together, nothing arrives before or after anything else, and no plant has any reason to keep a different calendar from its neighbour. Give warmth a lag and there is suddenly a right answer to what a plant should read: the tree reads daylight because daylight is exact and never lies, and the herb reads warmth because warmth is what its chemistry actually runs on. Both are defensible, they produce windows a season apart, and the cost of choosing wrong is a run away in the exercises.

The two ledgers generalise past plants, and they generalise differently. The grams ledger is a flow: something is manufactured, and every unit of it must end up in one of a fixed list of sinks. The nutrient ledger is a stock: a fixed quantity exists, and every unit must be in one of a fixed list of places. Any simulation with either property should print the identity every so often, because the failure mode is silent by construction. A number leaking into nowhere looks exactly like a number that is small, right up until the world has none of it left.

And the last thing this page builds is the first thing in the valley that ends. A plant can now stop being: the annual because its window shut, anything at all because its mass fell under what a body needs to be one. Everything before this could only grow or shrink. A world where things can end is a different kind of world, because it has room in it.

Calendar checkpoint

✓ Checkpoint — what you can now do
  • Given a year of 3,600 ticks, you can name the tick of each solstice and equinox, state the height of the year's angle at each, and turn those four heights into daylight multipliers of 1.0000, 1.6000, 1.0000 and 0.4000 with a calculator.
  • say why warmth peaks at tick 1350 and not at tick 900, compute the peak of any reading from its lag alone, and explain what happens to a valley whose three readings all share one lag.
  • Given an upkeep of 0.004, a dormant share of 0.025 and 1,799 sleeping ticks, you can produce both the flat 0.1799 estimate and the compounded 0.835346, and say which one the run has to agree with.
  • trace one gram of tissue from the light that fixed it, through upkeep or death, into a pile, and out of the pile as either grams burned or nutrient returned, naming which of the two ledgers each step belongs to.
  • Handed a rotting rate, you can convert it into a half-life in ticks and compare that with the length of a season before deciding whether it is fast or slow.
  • Shown a run whose books balance while a fifth of the world's matter sits in the litter, you can say what a conservation ledger does and does not prove.
⚡ Exercises — try first, then reveal
Exercise 1 — the tree that read the wrong dial. Give the tree the herb's calendar with -cue warmth -wake 0.5. Its window is still 1,801 ticks long, so predict whether it ends up bigger or smaller, and by roughly how much.

The window is the same length and sits 450 ticks later, so the tree gives up early spring and keeps its leaves through the first half of autumn. Autumn light is falling and the tree pays full upkeep on every gram while it holds leaves, so those 450 extra awake ticks cost more than they earn.

$ go run ./cmd/season -mode years -years 6 -cue warmth -wake 0.5   (the first three years of the table)
   year tree spring tree autumn   winter  herb best      fell      back    litter      soil     books
      1       1.000     340.036   55.70%     59.401  240.3710  238.1797    2.1913  126.7957  0.000000
      2     150.651     340.675   55.75%     57.904  240.8606  240.8606    2.1914  126.7938  0.000000
      3     150.743     340.676   55.75%     57.903  240.8592  240.8592    2.1914  126.7938  0.000000

The tree still reaches 340 grams every autumn and comes out of the dark half at 150.743 instead of 284.384, so the winter column reads 55.75% against 16.55%. The lost mass is not paid in dormancy at all: it is paid in the 450 ticks of full upkeep the tree spends in leaf after the light has already gone. A calendar cue is not a cosmetic choice, and reading a signal that runs late means running late.

Exercise 2 — ten times more expensive to sleep. Set -doze 0.25. Work out the compounded winter factor on paper first, then predict what the tree weighs in spring.

The dormant rate becomes 0.004 × 0.25 = 0.001 a tick. Flat, that is 1799 × 0.001 = 1.799, which is already more than the whole tree and tells you the flat estimate has stopped being useful. Compounded, 0.999 multiplied by itself 1799 times is 0.1652, so the tree keeps a sixth of itself.

$ go run ./cmd/season -mode years -years 6 -doze 0.25   (the first three years of the table)
   year tree spring tree autumn   winter  herb best      fell      back    litter      soil     books
      1       1.000     340.569   83.48%     58.140  247.9276  244.0676    3.8600  127.0151  0.000000
      2      56.245     340.610   83.48%     58.049  249.3116  249.3114    3.8603  127.0147  0.000000
      3      56.252     340.610   83.48%     58.049  249.3117  249.3117    3.8603  127.0147  0.000000

83.48% gone, and 56.252 grams standing in spring against 284.384. The tree survives, because it still reaches 340.610 by autumn: what a tenfold dormancy bill costs is not the tree but its spring, and a spring that starts at a sixth of the mass is a spring spent rebuilding instead of growing. The valley's nutrient circuit barely notices, at 249.3117 units a year against 247.9239.

Exercise 3 — turn the sun up. Multiply the evaporation constant by five with -sun 0.05. The wet column never limited anything in the working run; predict which column changes first and which plant feels it.

Evaporation is scaled by warmth, so five times the constant means the drying happens in high summer and hardly at all in winter. The soil should still refill every winter and run out somewhere in late summer, and the plant that feels it is the one still trying to earn then.

$ go run ./cmd/season -mode years -years 6 -sun 0.05   (the first three years of the table)
   year tree spring tree autumn   winter  herb best      fell      back    litter      soil     books
      1       1.000     340.569   16.55%     58.140  189.1771  188.5794    0.5976  125.7181 -0.000000
      2     284.211     115.150   16.80%     57.682  151.0799  151.4780    0.1995  129.8844  0.000000
      3      95.805     115.011   16.80%     57.984  148.2657  148.2659    0.1993  129.8869  0.000000

Year 1 is untouched, at 340.569, because the soil starts full and takes a season to draw down. From year 2 the tree tops out at 115.011 instead of 340.778, a third of the size, while the herb is barely affected at 57.984: the herb finishes its whole life before the ground is dry, and the tree is the one still standing there in late summer with nothing to drink. The nutrient circuit falls with the tree, to 148.2634 units a year, because a smaller tree turns over less of everything.

Look again at what the bench does on tick 450 of every year. It stands a herb on cell 5,1, because there is a hard-coded line in the run loop that says so. The herb that died on tick 2251 left 18.310 grams of tissue on the ground and left nothing else at all: no seed, no claim on that cell, nothing that would put a plant there next spring or anywhere else ever. An annual that cannot make another annual is a one-year experiment being restarted by hand. What the herb spent its whole window building was supposed to buy something, and buying it means paying out of the same mass budget everything else comes out of, then putting the result somewhere the wind decides.