Which Way the Numbers Rise
The four-sample gradient
Nine rays report what lies along nine straight lines. They do not report moisture underfoot, and they do not report how many grams stand on a nearby cell.
A gradient is four samples and two differences, and it can see one step in each direction; what it buys for that price is a bearing on a quantity no ray can perceive. East minus west gives the x direction. South minus north gives the y direction.
The fan is the expensive sense. One ray steps a quarter of a cell at a time and gives up at twelve cells, so it can spend forty-eight lookups. Nine rays can spend four hundred and thirty-two. A gradient spends four lookups.
Two per-cell numbers matter to a body that eats plants: the moisture the bed holds, because it affects where plants grow, and the grams of tissue standing on a cell, because that is food. Both become gradients, then join the ray distances, ray codes and body readings in one fixed row of twenty-four numbers.
Two listings are read here and left unchanged: the creature row, and the value a walked ray returns.
// internal/beast/beast.go
// Kind is one kind of creature written down as numbers, laid out the
// way terra.Species is: one flat row with nothing nested inside it, so
// two creatures of different kinds run the same code and differ only
// here. The first block is the body, the second is what a tick costs,
// the third is what it eats, and the fourth is what it is physically
// able to do.
type Kind struct {
Name string
// the body
Bulk float64 // grams of body
Full float64 // energy units the store holds when it is full
// what a tick costs
Basal float64 // energy units one gram of body costs a tick
Work float64 // energy units one unit of movement work costs
// what it eats
Bite float64 // grams one bite takes off a stand
Convert float64 // energy units one gram of eaten tissue is worth
Reach float64 // cells a bite reaches
// what it is able to do
Sight float64 // cells one line of sight runs
Top float64 // cells a tick at a sprint
Swing float64 // the part of a whole turn one turn action covers
}
// Fauna is the valley's creature list, opened here with one row. A
// browser is an animal that takes small mouthfuls off standing plants
// and keeps taking them: it is the cheapest kind of eater to price,
// because nothing it does to its food is sudden.
var Fauna = []Kind{
{
Name: "browser",
Bulk: 40, Full: 400,
Basal: 0.004, Work: 0.60,
Bite: 0.25, Convert: 4.0, Reach: 1.0,
Sight: 12, Top: 0.45, Swing: 1.0 / 12,
},
}
// Ground is every cell of a valley a creature can stand on, walked in
// the row order stream 11 already scatters plants over: anything that
// is not open water. A creature is not a plant and does not need ground
// it could put a root into, so the rocky rim is somewhere it can walk
// and somewhere a plant can never be.
func Ground(v *terra.Valley) []sim.Coord {
var out []sim.Coord
for y := 0; y < v.Grid.H; y++ {
for x := 0; x < v.Grid.W; x++ {
c := sim.Coord{X: x, Y: y}
if v.Bed.Kind(c) != sim.Water {
out = append(out, c)
}
}
}
return out
}
// internal/beast/look.go
// Hit is what one ray met.
type Hit uint8
// The five things a ray can come back with, in the order the codes are
// written down.
const (
Nothing Hit = iota
Plant
Creature
Water
Rock
)
// Ray is one line of sight after it has been walked: how far it went in
// cells, and what stopped it.
type Ray struct {
Dist float64
What Hit
}
// Stride is how far along a ray one sample sits, in cells. It is short
// enough that no cell the ray passes through is stepped over.
const Stride = 0.25
// Mass is the grams standing on one cell, and nothing at all where
// nothing is standing.
func (v *View) Mass(c sim.Coord) float64 {
if st, ok := v.stand[c]; ok {
return st.Plant.Mass
}
return 0
}
Four of the ten numbers in the row do any work below. Sight is twelve cells,
and it is what a ray distance gets divided by. Full is the four hundred
energy units the store holds, and it is what the store reading gets divided by.
Top is nine twentieths of a cell in a tick, the fastest this creature moves,
and it is what the speed reading gets divided by. Convert is four energy
units for every gram eaten, and it turns out to answer a question about the tissue
gradient that looked like it needed a judgement call. The other six belong to chapters
that charge and spend rather than read.
The moisture slope
The bench for this chapter founds a valley the way the two-century run does, from world seed 5, three seedlings of every row scattered on stream 11, and stops it at the midsummer of its first year: twelve plants standing, 1,517.5 grams between them, a pond in the middle and a rim of rock all round. Nothing below moves a gram or spends an energy unit. Every run in this chapter is a reading taken of a valley that is not being changed.
Pick a cell in it and do the arithmetic by hand before writing any. Cell 8,2 sits east of the pond with soil on all four sides, and the numbers underneath it are all different, which makes it a good place to see what the subtraction is actually doing.
The bed is holding 0.178061 units of moisture on cell 8,2. One cell east, on 9,2, it is holding 0.197858. One cell west, on 7,2, it is holding 0.178852. So the ground is wetter to the east by 0.197858 − 0.178852 = 0.019006.
That figure covers two cells of walking, because 7,2 and 9,2 are two apart with 8,2 between them. Halve it and the answer is per cell of ground: 0.009503. The same going down. South is 8,3 at 0.177444 and north is 8,1 at 0.178362, so south minus north is −0.000918, halved to −0.000459. A minus sign is not a mistake; it says the reading falls in that direction instead of rising.
gx = (east − west) ÷ 2
gy = (south − north) ÷ 2
The cell the creature is standing on does not appear anywhere in that. It is deliberate. The question a gradient answers is which way to go from here, and the answer has to be the same whichever direction the creature happens to be facing. A version that read its own cell and one neighbour would give a different answer for every neighbour it picked.
Now the scaling, because 0.009503 means nothing until you know what a large one would be. A cell of this valley's soil holds 1.2500 of moisture when it is full and 0 when it is bone dry, so the widest gap two samples of it can ever show is 1.2500, which halves to 0.6250 per cell. Divide by that and the answer stops being a quantity of water and becomes a share of the steepest slope there could be: 0.009503 ÷ 0.6250 = 0.015204.
Four lines of Go, and the useful decision in them is that the reading arrives as a function.
Slope knows how to take four samples and subtract; it has no idea whether it is
sampling water or plants, so the same three lines serve both and will serve the third and
fourth quantity without being touched.
// internal/beast/sense.go
// Grad is how fast a per-cell reading rises walking east and walking
// south, in units of that reading per cell of ground.
type Grad struct{ X, Y float64 }
// Slope is a field's gradient at one cell, taken as the difference
// across it: the cell east minus the cell west, halved because those
// two samples sit two cells apart, and the same going down. The reading
// arrives as a function, so the same four lookups serve the water in
// the ground and the tissue standing on it.
func Slope(c sim.Coord, at func(sim.Coord) float64) Grad {
return Grad{
X: (at(c.Offset(1, 0)) - at(c.Offset(-1, 0))) / 2,
Y: (at(c.Offset(0, 1)) - at(c.Offset(0, -1))) / 2,
}
}
// Over scales a slope into -1 to 1 against a stated full cell. A
// reading running from nothing to full changes by at most full across
// the two cells the samples straddle, so half of full is the steepest
// slope this arithmetic can produce and dividing by it is the whole
// scaling.
func (g Grad) Over(full float64) Grad {
if full <= 0 {
return Grad{}
}
return Grad{X: Clamp(g.X / (full / 2)), Y: Clamp(g.Y / (full / 2))}
}
// Clamp holds a number inside -1 to 1. Every entry of the row is inside
// that span, so a reading past a stated full cell stops at the edge
// instead of shouting over the rest of them.
func Clamp(v float64) float64 {
if v > 1 {
return 1
}
if v < -1 {
return -1
}
return v
}
$ go run ./cmd/senses -mode hand -at 8,2
senses: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
the four cells around 8,2, and the two readings taken at each
cell moisture grams
west 7,2 0.178852 0.000000
east 9,2 0.197858 26.461034
north 8,1 0.178362 21.613108
south 8,3 0.177444 0.000000
here 8,2 0.178061 0.000000
east south
moisture per cell 0.009503 -0.000459
grams per cell 13.230517 -10.806554
moisture scaled 0.015204 -0.000734
grams scaled 0.264610 -0.216131
a full cell of moisture is 1.2500, so half of one is 0.6250
a full cell of tissue is 100.0 grams, so half of one is 50.0
The moisture column is the hand arithmetic, digit for digit. The grams column beside it is the same four lookups pointed at what is standing on those cells instead: 26.461034 grams on 9,2 to the east, 21.613108 on 8,1 to the north, nothing at all on the other two. So the tissue rises east at (26.461034 − 0) ÷ 2 = 13.230517 grams a cell, and falls south at (0 − 21.613108) ÷ 2 = −10.806554. A creature standing here is being told that the food is that way and the water is barely anywhere, and both of those statements cost four lookups.
One cell is a number. Every cell at once is a picture, and it says something about the moisture gradient that is easy to miss when you only look at one.
$ go run ./cmd/senses -mode flat
senses: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
the two slopes on every cell a creature can stand on, 80 of them
~ open water . a slope under 0.0005 of a full cell
> < v ^ which way the reading rises from there
moisture
.vvvvvvvvvv.
>>vv<vvvv>v<
>>v<vvv<>><<
>>~~~~~<>>^<
>>~~~~~~<^^<
>>^>~~~~<><<
>>^^~^^^<^<<
.^^^^^^^^^^.
tissue
..v..vv.v.v.
.>.<>>v<.>.<
..^..>^<>.^.
..~~~~~.v^..
..~~~~~~.<..
....~~~~vvv.
....~.<>><<<
.....^..^^^.
the moisture arrow points at open water on 15 of the 80 cells, and straight
away from bare rock or off the grid on 58 of them
the steepest tissue slope anywhere is 191.7780 grams a cell, against a full cell of
100.0 grams: 20 cells are pinned at the edge of the span, and on 45 others
the tissue slope says nothing whatever
Read the moisture map from the outside in. Along both walls of rock the arrows point inward; along the pond they point at the water. Of the eighty cells a creature can stand on, fifteen have their arrow aimed straight at open water and fifty-eight have it aimed straight away from bare rock or off the edge of the grid. That is the gradient telling the exact truth and the truth being unhelpful: the two wettest facts in this valley are a pond a browser cannot eat and a rim that holds no water because nothing can hold water in rock. A sense reports what it measures. What that is good for is a separate question, and one this chapter is careful not to answer on the creature's behalf.
The tissue map has the opposite problem. On forty-five of the eighty cells it says nothing whatever, because no plant is standing on any of the four cells around them, and four lookups that all come back zero produce a slope of zero. A gradient is blind past one step. Twelve plants scattered over a valley leave most of it out of reach of that, and the fan of rays is the sense that covers the gap: four hundred lookups that can see twelve cells, four lookups that can see one.
The full food cell
Moisture had its divisor handed to it: a cell of soil is stood up holding 1.2500 and there is no arithmetic in the valley that puts more in one. Tissue has no such ceiling. A scrub on good ground settles near 180 grams and a tree near 600, a cell can carry whichever of them germinated there, and dividing by 600 would squash every ordinary reading into the first hundredth of the span. So what number is a cell of food?
The row answers it without anybody choosing. A browser's store holds 400 energy units, and a gram of eaten tissue is worth 4 of them, so 100 grams standing on a cell is exactly the amount that would take this creature from empty to full. More than that on one cell is more than the creature can carry away, and a sense that distinguished 400 grams from 800 would be reporting a difference the body cannot act on.
// internal/beast/sense.go
// Scale is what the row divides by: one stated full cell per field.
// Every number in here is a constant of the world, written down before
// the run. It is never a figure off the reading being scaled, which can
// be zero, and never one measured off the run, which cannot be written
// down in advance.
type Scale struct {
Wet float64 // the moisture a full cell of ground holds
Food float64 // the grams of standing tissue counted as a full cell
}
// internal/beast/beast.go
// Bellyful is the grams of standing tissue that would fill this
// creature's store from empty: what the store holds, at what one gram
// of eaten tissue is worth. It is the row's own answer to how much food
// on one cell counts as a lot, so nothing about the scaling has to be
// chosen by hand.
func (k Kind) Bellyful() float64 { return k.Full / k.Convert }
// cmd/senses/main.go — the bench, standing one valley up for every mode
// stand founds a valley the way chapter 42's does and ticks it to the
// midsummer of a named year, so every run in this chapter reads the
// same ground.
func stand(seed uint64, light float64, years, found int) *terra.Valley {
g := sim.Generate(Cols, Rows, seed)
a := terra.NewAir(scene.Wind(g), air, g.Count(sim.Soil)*90, seed)
v := terra.NewValley(g, seed, soil, light, a)
v.Fall, v.Sun, v.Decay = 0.60, 0.05, 0.01
v.Wear = 1
v.Now = terra.Quarter / 2
v.Found(found)
for y := 1; y <= years; y++ {
for {
v.Tick()
if v.Now%terra.Year == terra.Quarter+1 {
break
}
}
}
return v
}
sc := beast.Scale{Wet: v.Full, Food: beast.Fauna[0].Bellyful()}
Two numbers, both taken off things that already exist. Wet is the valley's own
Full, the figure its bed was stood up with, so a valley founded wetter scales
its gradients differently without a line changing. Food is
Bellyful, which is two fields of the creature row divided into each other. A
second kind of creature with a bigger stomach or a worse gut gets a different number for
free, and no page anywhere holds a table of scaling constants that somebody has to
remember to update.
There is an obvious-looking alternative to all of this, and running it is better than arguing about. Instead of dividing the slope by a full cell, divide it by the reading on the cell the creature is standing on. It sounds better than a stated constant: the answer comes out as "half as much again as this cell holds", which is a relative quantity, needs no table, and adapts by itself to a valley that is wetter or drier than this one.
// cmd/senses/main.go — the slope over the moisture underfoot, instead of over a full cell
for _, c := range ground {
here := v.Bed.Moisture(c)
g := beast.Slope(c, v.Bed.Moisture)
rel := beast.Grad{X: g.X / here, Y: g.Y / here}
s[beast.WetX] = g.X / v.Bed.Moisture(at)
s[beast.WetY] = g.Y / v.Bed.Moisture(at)
$ go run ./cmd/senses -mode share -at 0,5 | tail -27
1,1 1.202318 0.601159 0.961854 0.500000
5,1 0.196722 0.001730 0.002767 0.008792
9,1 0.194736 0.514480 0.823169 2.641933
11,1 0.000000 -0.603661 -0.965858 -Inf
0,3 0.000000 0.601159 0.961854 +Inf
1,3 1.202318 0.625000 1.000000 0.519829
9,3 0.197858 0.514939 0.823903 2.602577
11,3 0.000000 -0.603661 -0.965858 -Inf
0,5 0.000000 0.603661 0.965858 +Inf
1,5 1.207322 0.603661 0.965858 0.500000
9,5 0.175864 0.001123 0.001797 0.006386
11,5 0.000000 -0.089055 -0.142488 -Inf
0,7 0.000000 0.000000 0.000000 NaN
1,7 0.000000 0.000000 0.000000 NaN
5,7 0.000000 0.000000 0.000000 NaN
9,7 0.000000 0.000000 0.000000 NaN
11,7 0.000000 0.000000 0.000000 NaN
of 80 cells, 24 come back not a number, 12 endless, and 10 merely past 1
the reading at 0,5 with those two numbers in it, and what it is worth
the moisture underfoot is 0
slot 18 holds +Inf and slot 19 holds NaN
the row added up, every weight at one: NaN
is that total above zero? false. below? false. equal to it? false
the largest entry, found the usual way: slot 18
can slot 19 win that scan against slot 0? false
row 58f5cb5a87bcd151
Start with the rows that look fine. On 1,1 the ground is holding 1.202318 and the slope east is 0.601159, so the relative figure is 0.500000: half again as much water one cell over. On 1,3 it is 0.519829. Both are inside the span the row promises and both are easier to explain in a sentence than 0.961854 is. If the run had stopped there, this would look like the better arithmetic.
Then 9,1, where the ground is holding 0.194736 and the slope is 0.514480. The relative
figure is 2.641933. The row promised every entry between −1 and 1 and this one is
past twice that, because the divisor got small. On 11,1 it goes further: the cell is bare
rock holding nothing, dividing by zero, and the answer is -Inf. On 0,7 both
samples are zero as well as the divisor, zero over zero, and the answer is
NaN. Of the eighty cells, twenty-four come back not a number, twelve come
back endless, and ten more are merely outside the span. Forty-six of eighty.
The bottom half of the run is why that matters more than a bad number usually does. Stand
a creature on 0,5, which is a perfectly ordinary cell of the rim it is allowed to walk on,
and its row now holds +Inf in slot 18 and NaN in slot 19. Add
the row up and the total is NaN. Ask whether that total is above zero: false.
Below zero: false. Equal to zero: false. All three at once, which no ordinary number can
manage. And the last line is the quiet one: a scan that walks the row keeping whatever
beats what it has cannot ever pick slot 19, because NaN > anything is
false and so is anything > NaN. One bad division has produced an entry
that is permanently invisible to every comparison anything will ever make against it.
The rule the mistake teaches is small and it applies well past creatures. A scaling divisor must be a constant of the world, not a figure from the reading being scaled. A quantity that can be zero cannot be a denominator, and every reading in this valley that has a floor of zero is a quantity that will reach it eventually, on the rim if nowhere else. 1.2500 is a number somebody wrote down before the run started and can check by reading one line of the founding code. The moisture underfoot is a number the run produces, which is exactly why it cannot be trusted to be there.
The twenty-four input slots
Everything a creature perceives now has to be written down somewhere, and the somewhere is a plain array of twenty-four float64 values indexed the way the terrain grid is indexed: position carries the meaning, nothing carries a label. Slots 0 to 17 are the fan, two to a ray, the distance then the code. Slots 18 to 21 are the two gradients. Slots 22 and 23 are the only two readings a creature takes of itself, how full its store is and how fast it is going.
Eighteen of those slots hold a ray code, and that is where the interesting decision is. There are five things a ray can meet and the ray brings back one of five constants. Writing those constants into the row as 0, 1, 2, 3 and 4 is one line of code and it says something false: it claims a rock is four times a plant and twice a creature, an ordering the world has no opinion about. One slot per ray leaves room for exactly one number, so the ordering has to be one a browser's body can actually defend, and the only one available is how much it wants to go that way.
// internal/beast/sense.go
// Inputs is how many numbers one reading of the world comes to: two for
// every ray of the fan, four for the two gradients, and two the
// creature takes off itself.
const Inputs = 2*Fan + 4 + 2
// Where every reading sits in the row. The fan takes the first
// eighteen slots, two to a ray; the six after them are named because
// nothing else can tell them apart.
const (
WetX = 2*Fan + iota // the moisture slope, going east
WetY // and going south
FoodX // the standing-tissue slope, going east
FoodY // and going south
StoreAt // what is in the store, as a share of Full
SpeedAt // how fast it is going, as a share of Top
)
// Eye is where ray i's two numbers sit: how far it went at Eye(i), and
// what it met at Eye(i)+1.
func Eye(i int) int { return 2 * i }
// Worth is the number one ray's hit takes in the row. A code is not a
// magnitude: nothing about a rock is four times a plant, so numbering
// the five hits 0 to 4 would hand whatever reads this row an ordering
// the world does not have. One slot per ray leaves room for exactly one
// number, so that number is the only ordering a body can defend: how
// much a browser wants to go that way.
var Worth = [...]float64{
Nothing: 0.00,
Plant: 1.00,
Creature: 0.25,
Water: -0.50,
Rock: -1.00,
}
// Senses is one creature's reading of the world: Inputs numbers in one
// fixed order. The order is the whole contract between the valley and
// whatever ends up driving the creature.
type Senses [Inputs]float64
// Read fills the row in place. It writes nothing but the row, so every
// creature in a phase can take its reading of the world as the world
// stood when the phase opened, and a tick of three thousand of them
// allocates nothing.
func (b *Beast) Read(row *Senses, eyes []Ray, v *View, s Scale) {
k := b.Kind
for i, r := range eyes {
row[Eye(i)] = Clamp(r.Dist / k.Sight)
row[Eye(i)+1] = Worth[r.What]
}
c := b.Cell()
wet := Slope(c, v.Valley.Bed.Moisture).Over(s.Wet)
food := Slope(c, v.Mass).Over(s.Food)
row[WetX], row[WetY] = wet.X, wet.Y
row[FoodX], row[FoodY] = food.X, food.Y
row[StoreAt] = Clamp(b.Store / k.Full)
row[SpeedAt] = Clamp(b.Vel.Len() / (k.Top * terra.Tile))
}
Read takes a pointer to a row and fills it, which is the difference between a
valley of a few dozen creatures and a valley of a few thousand: the row is handed in, so a
caller can keep one and hand it back every tick for the whole life of the run. It also
writes nothing outside that row. No bed, no stand, no other creature is touched, so every
creature in a phase can be handed the world as it stood when the phase opened and none of
them can move it under the others.
Stand a creature on 8,2, facing east, store full, and read it.
$ go run ./cmd/senses -mode row -at 8,2
senses: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
4 creatures founded on stream 12, at 5,2 9,5 10,2 0,5
reading taken by one stood on 8,2 by hand, which the others can see
facing 0 degrees clockwise of east, store 400.0 of 400.0, sight 12 cells
a fan of 9 rays over 120 degrees, 15 degrees apart
slot value what the number is
0 0.0625 ray 0 ran 0.75 cells of 12
1 1.0000 and met plant
2 0.1875 ray 1 ran 2.25 cells of 12
3 -1.0000 and met rock
4 0.0625 ray 2 ran 0.75 cells of 12
5 1.0000 and met plant
6 0.0625 ray 3 ran 0.75 cells of 12
7 1.0000 and met plant
8 0.0417 ray 4 ran 0.50 cells of 12
9 1.0000 and met plant
10 0.0625 ray 5 ran 0.75 cells of 12
11 1.0000 and met plant
12 0.0625 ray 6 ran 0.75 cells of 12
13 1.0000 and met plant
14 0.3125 ray 7 ran 3.75 cells of 12
15 -1.0000 and met rock
16 0.3542 ray 8 ran 4.25 cells of 12
17 1.0000 and met plant
18 0.0152 moisture rising east at 0.009503 a cell, of 0.6250
19 -0.0007 moisture rising south at -0.000459 a cell
20 0.2646 tissue rising east at 13.2305 grams a cell, of 50.0
21 -0.2161 tissue rising south at -10.8066 grams a cell
22 1.0000 400.0 in the store, of 400 full
23 0.0000 0.0000 pixels a tick, of 7.20 at a sprint
24 numbers, none outside -1 to 1: the smallest is -1.0000 and the largest 1.0000
row b0dbe170ffa6e2c7
Slots 18 through 21 are the Interlude's four numbers arriving in the row, which is the check that the two halves of this chapter are describing one thing. The fan needs reading across: ray 1 got 2.25 cells before it hit rock and ray 8 got 4.25 before it found a plant, while six of the nine stopped within a cell, and the distance column turns all of that into numbers between 0.0417 and 0.3542 by dividing by a sight of twelve. Not one of those nine distances is anywhere near the edge of the span. The entries that are pinned there are all codes: the seven plant codes at 1.0000, the two rock codes at −1.0000, and slot 22 at 1.0000 because the store is full. A code is pinned because the table says so, and a distance is small because the valley is crowded.
The last two entries are the ones people leave out. A creature that reports its store and its speed can be sensitive to how hungry it is and how hard it is already working, and one that does not is a creature whose reaction to a plant is the same on a full store as an empty one. Two slots, and both of them are the body looking at itself instead of at The Hollow.
Now take the scaling out and put the same twenty-four readings in raw: cells, codes numbered 0 to 4, moisture per cell, grams per cell, energy units, pixels a tick. Six different units in one array.
// cmd/senses/main.go — the same reading with nothing divided
// The same twenty-four readings with nothing divided and nothing
// held inside -1 to 1: cells, codes numbered 0 to 4, moisture per
// cell, grams per cell, energy units and pixels a tick.
var bare beast.Senses
for i, r := range eyes {
bare[beast.Eye(i)] = r.Dist
bare[beast.Eye(i)+1] = float64(r.What)
}
c := b.Cell()
wet := beast.Slope(c, v.Bed.Moisture)
fd := beast.Slope(c, w.Mass)
bare[beast.WetX], bare[beast.WetY] = wet.X, wet.Y
bare[beast.FoodX], bare[beast.FoodY] = fd.X, fd.Y
bare[beast.StoreAt] = b.Store
bare[beast.SpeedAt] = b.Vel.Len()
$ go run ./cmd/senses -mode raw -at 8,2 | tail -6
scaled unscaled
the largest number in the row 1.0000 400.0000
the smallest -1.0000 -10.8066
the length of the row 3.2244 400.4636
the largest entry's share of it 0.3101 0.9988
the row added up, weights all one 7.2713 431.9330
The bottom line is the argument. Treat the twenty-four numbers as one vector and measure its length, and the unscaled row is 400.4636 long, of which one entry accounts for 0.9988. That entry is the store: a number about the creature's own belly, in units nobody chose for this purpose, drowning every fact about the world it is standing in. The two rock codes and the seven plant codes together move the total by less than the rounding on the store does. Anything that reads the row and adds it up with weights is reading slot 22 and a faint hiss. Scaled, the longest the row can get is the square root of twenty-four and the largest single entry accounts for 0.3101 of the 3.2244 this one measures. Every sense is audible.
The codes are the exception that proves the discipline: they are not divided by anything. There is no unit to divide out of "this ray met rock", so the scaling for that slot is a lookup table with five entries in it, chosen so the numbers sit in the same span as everything else. Scaling and dividing are not the same job. The job is to make every slot mean the same amount of loudness, and division only happens to be how five of the six kinds of reading get there.
Why the span is fixed before the run
Strip the creature out and one idea is left. A sense is a measurement plus a span, and the span has to be a fact about the world that somebody wrote down in advance. Every number in the row got there by being divided by such a span: a ray distance by a sight of twelve cells, a moisture slope by half of a full cell, a tissue slope by half of what fills a stomach, a store by what the store holds, a speed by what the legs can do. Take the span away and the number is just a quantity in whatever unit it happened to be measured in, and quantities in different units cannot be compared, added, or weighed against each other by anything.
The reason to state the span in advance instead of measuring it is the one the worked
failure made concrete. A span read off the run is a span that changes with the run: it can
be zero, it can be tiny, and it makes the same creature on the same cell produce different
numbers on a different day. A span written down is checkable, is the same in every valley
this book will ever found, and turns the row into something two runs can be compared on. The
twenty-four numbers hash to b0dbe170ffa6e2c7 on this cell in this valley and
they will do so on any machine, because there is not a single figure in them that came from
anywhere except arithmetic and constants.
The order is the other half, and it is a contract rather than a convenience. Nothing that
reads this row will ever be told that slot 20 is about plants. It will be handed twenty-four
numbers and a set of weights, and the only reason the weight on slot 20 does anything useful
is that slot 20 held the same kind of reading last tick and will hold it next tick. Move one
entry and every weight in the world that was tuned against the old order is now wrong, in a
way that produces no error and no crash and only slightly stranger behaviour. That is why
the slot numbers are named constants derived from Fan instead of literals: the
layout is written down once and the arithmetic that produces 24 is on the page.
There is a last thing the two senses say together that neither says alone. The fan is expensive, long-sighted and blind to quantity. The gradient is cheap, quantity-aware and blind past one cell. A creature carrying only the fan can find a plant across the valley and cannot tell a moss from a tree. A creature carrying only the gradient knows exactly which neighbour is richer and cannot see anything it is not already next to. Neither is the better sense. They fail in opposite directions, which is what makes it reasonable to pay for both.
Checkpoint
- Given the four moisture readings around cell 8,2, produce 0.009503 and −0.000459 with a calculator, and say why each difference is halved and why the cell underfoot appears in neither.
- From a creature row alone, work out that a full cell of food is 100 grams, using
nothing but
FullandConvert, and defend it as the amount the body could actually carry away. - Handed a slot number between 0 and 23, name what is in it, and say which slots change when a creature turns on the spot and which cannot.
- Shown a row whose longest entry accounts for 0.9988 of its length, name the entry, name the unit that did it, and say what any weighted sum over that row is actually reading.
- Say what goes wrong when a slope is divided by the reading underfoot, in the exact order it goes wrong: past the span, then endless, then not a number, then a slot no comparison can ever pick.
- Add a sixth kind of thing a ray might meet without touching
Read, and say what the one number chosen for it means.
Exercise 1 — turn it round. Run
go run ./cmd/senses -mode row -at 8,2 -face 180, which faces the same
creature west on the same cell. Predict which of the twenty-four slots change before
you look.
Slots 0 to 17 all change, because the fan is measured from the heading and a creature looking west casts nine different rays. Slots 18 to 23 do not move a digit: a gradient is a fact about the ground under the creature and has nothing to do with which way it is pointing, and the store and the speed are facts about the body.
$ go run ./cmd/senses -mode row -at 8,2 -face 180 | tail -9
18 0.0152 moisture rising east at 0.009503 a cell, of 0.6250
19 -0.0007 moisture rising south at -0.000459 a cell
20 0.2646 tissue rising east at 13.2305 grams a cell, of 50.0
21 -0.2161 tissue rising south at -10.8066 grams a cell
22 1.0000 400.0 in the store, of 400 full
23 0.0000 0.0000 pixels a tick, of 7.20 at a sprint
24 numbers, none outside -1 to 1: the smallest is -1.0000 and the largest 1.0000
row 8ee8a6c55ef552ef
The row digest is 8ee8a6c55ef552ef against the east-facing
b0dbe170ffa6e2c7, so the two readings are different rows, and the eighteen
slots that did the differing are all in the fan.
Exercise 2 — stand it on top of dinner. Cell 9,2 has a
plant on it and another creature next door. Run
go run ./cmd/senses -mode row -at 9,2 and predict what slots 20 and 21
say.
Both come back 0.0000, which catches people out: the creature is standing on 26.46 grams of plant and the food gradient reports nothing at all. It is correct. The gradient never reads the cell underfoot, so a creature can be standing in the middle of the only food for four cells and have a slope of exactly zero, and the thing that tells it about the plant it is on is the bite check, not this row.
$ go run ./cmd/senses -mode row -at 9,2 | tail -12
15 -1.0000 and met rock
16 0.2500 ray 8 ran 3.00 cells of 12
17 -1.0000 and met rock
18 0.4857 moisture rising east at 0.303560 a cell, of 0.6250
19 0.0025 moisture rising south at 0.001561 a cell
20 0.0000 tissue rising east at 0.0000 grams a cell, of 50.0
21 0.0000 tissue rising south at 0.0000 grams a cell
22 1.0000 400.0 in the store, of 400 full
23 0.0000 0.0000 pixels a tick, of 7.20 at a sprint
24 numbers, none outside -1 to 1: the smallest is -1.0000 and the largest 1.0000
row 6c70e6198ab390c3
Five rays come back 0.2500, which is the one entry of the lookup table the other runs in this chapter never exercised: another creature, worth a quarter, nearer nothing than food.
Exercise 3 — let the valley fill up. Run
go run ./cmd/senses -mode row -at 8,2 -years 3, which reads the same cell
two years later with thirty-five plants standing instead of twelve. Predict what
happens to the fan and what happens to the gradients.
The fan collapses. All nine rays stop at a plant inside three quarters of a cell, so the eighteen slots that carried the most information in the first run now carry almost none: eight of the nine distances are identical and all nine codes read 1.0000. Meanwhile the gradients wake up. The tissue slope climbs from 13.2305 grams a cell to 48.9562, which is 0.9791 of a full cell and nearly pinned, and the moisture slope from 0.0152 to 0.3438.
$ go run ./cmd/senses -mode row -at 8,2 -years 3 | tail -11
16 0.0625 ray 8 ran 0.75 cells of 12
17 1.0000 and met plant
18 0.3438 moisture rising east at 0.214894 a cell, of 0.6250
19 0.0087 moisture rising south at 0.005427 a cell
20 0.9791 tissue rising east at 48.9562 grams a cell, of 50.0
21 0.0700 tissue rising south at 3.5012 grams a cell
22 1.0000 400.0 in the store, of 400 full
23 0.0000 0.0000 pixels a tick, of 7.20 at a sprint
24 numbers, none outside -1 to 1: the smallest is 0.0000 and the largest 1.0000
row 933a3544036b32aa
A crowded valley is exactly the case where the expensive sense goes quiet and the cheap one does the work, which is the argument for the four lookups made by the run instead of by the page.
Twenty-four numbers, filled every tick, and so far nothing anywhere reads one of them. A creature can perceive the valley and has no way to answer it: no walking, no turning, no biting, and no rule that says which of those it is allowed to attempt on the cell it happens to be standing on. What the row needs next is the other end of the loop, a list of the things a body may do with a tick and what each of them costs before it is permitted to happen.