The World Vol 5 · Bodies and Brains
ch 48 / 105
Chapter 48

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.

▣ Build · stage 1 — the four numbers this chapter borrows
// 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.

∑ Math Interlude — the difference across a cell

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.

f(c)the reading taken on cell c; the bed's moisture here, 0.178061 on cell 8,2
E W N Sthe four cells one step east, west, north and south of the cell being read
gx(f(E) − f(W)) ÷ 2: how much the reading rises for each cell walked east
gy(f(S) − f(N)) ÷ 2: the same walking south, since rows count downward on this grid
Fa full cell of that reading, stated before the run; 1.2500 for moisture
F ÷ 2the steepest per-cell slope four samples of that reading could produce; 0.6250
gx ÷ (F ÷ 2)the slope as a share of that steepest one, which is what goes into the row

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.

▣ Build · stage 2 — the four samples, and the divisor that follows from them
// 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.

▣ Build · stage 3 — the divisor, and where the food one comes from
// 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.

⚠ Worked failure — a divisor taken off the reading it was dividing
// 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.

▣ Build · stage 4 — the layout, the lookup, and the reading itself
// 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.

Four neighbour samples becoming two numbers, and the row those two numbers sit in The upper panel shows five cells of the valley arranged in a plus. The centre cell, 8,2, is highlighted and holds 0.178061 of moisture. West of it 7,2 holds 0.178852, east of it 9,2 holds 0.197858, north 8,1 holds 0.178362 and south 8,3 holds 0.177444. Beside the plus, four lines of arithmetic: east minus west is 0.019006, halved to 0.009503 of moisture for each cell east; south minus north is minus 0.000918, halved to minus 0.000459; both divided by 0.6250, half a full cell, giving 0.015204 into slot 18 and minus 0.000734 into slot 19. The lower panel shows the whole row as twenty-four numbered boxes in a strip. A bracket over boxes 0 to 17 is labelled nine rays, two numbers each; a bracket over the highlighted boxes 18 to 21 is labelled gradients; a bracket over boxes 22 and 23 is labelled body. Three lines beneath say what each group holds. FOUR SAMPLES AROUND CELL 8,2 AND THE TWO DIFFERENCES 8,1 0.178362 7,2 0.178852 8,2 0.178061 9,2 0.197858 8,3 0.177444 east − west = 0.197858 − 0.178852 = 0.019006 halved: 0.009503 of moisture for each cell east south − north = 0.177444 − 0.178362 = −0.000918 halved: −0.000459, so it falls going south ÷ 0.6250, half a full cell of moisture: slot 18 = 0.015204 slot 19 = −0.000734 THE ROW: 24 NUMBERS IN ONE ORDER, EVERY ONE BETWEEN −1 AND 1 nine rays, two numbers each gradients body 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 slots 0 to 17 how far each ray ran, and what it met slots 18 to 21 moisture and tissue, rising east and rising south slots 22 to 23 the store as a share of full, the speed of a sprint
Figure 48.1 — the whole chapter. Four lookups and two subtractions at the top; the twenty-four slots those two numbers land in at the bottom. The four highlighted boxes are the only ones the upper panel produces, and the four hundred-odd lookups the fan costs fill the eighteen to their left.

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

✓ Checkpoint — what the row can now be asked
  • 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 Full and Convert, 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.
⚡ Exercises — try first, then reveal
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.