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.
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.
// 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.
// 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.
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.
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.
// 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.
$ 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.
// 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.
$ 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.
$ 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.
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
- 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.
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.