Nine Lines of Sight
The ray result
Every velocity a creature has had so far was typed into a bench. Take the bench away and a forty-gram body can pay for a crossing, but it has no evidence for choosing one.
A ray hands back two numbers and nothing else: how far it went, and a code for what stopped it. It never returns the plant, a pointer to the plant, or any value with a method on it.
The price has to stay fixed as the valley fills. Asking every plant where it stands gets slower every year the valley grows; nine fixed directions cost the same in a bare valley and in a crowded one.
Sight is a line walked one step at a time. The ray leaves the eye at a fixed angle off the heading, steps through cells, and stops at the first thing solid enough to matter or at the sight cap. Nine lines over a hundred and twenty degrees make the fan.
The restriction is what makes the sense useful later. The controller reads numbers by multiplying them by weights and adding them. A sense designed against that limit from its first line still works when the network replaces the hand-written rule.
One ray on paper
The bench founds a valley the way the terrarium's own two-century run does, out of world seed 5, three seedlings of every row, and stops it at the midsummer of its first year: twelve plants standing, 1,517.5 grams between them, a pond through the middle and a rim of rock all round. Nothing in this chapter changes a gram of it. Every run below is a reading taken of ground that is holding still.
Stand a creature on cell 8,2, facing due east, and walk one of its nine rays with a calculator before writing a line of Go. Ray 1 is a good one to pick, because the arithmetic along it is the tidiest of the nine: at forty-five degrees a ray moves exactly as far across as it does up.
The fan opens a hundred and twenty degrees and holds nine rays, so there are eight gaps between them and each gap is 120 ÷ 8 = 15 degrees. Number the rays 0 to 8. Ray 4 is the middle one and runs straight along the heading, so ray i leaves at the heading plus (i − 4) × 15 degrees. For ray 1 that is 0 + (1 − 4) × 15 = −45 degrees. Rows count downward on this grid, so a minus angle points up the page: ray 1 is the creature's second ray from the left, aimed north-east.
An angle becomes a direction through the two functions that turn a corner of the unit circle into a pair of lengths. One cell along a ray at −45 degrees is cos(−45) = 0.707107 of a cell east and sin(−45) = −0.707107 of a cell south, and a minus south is a north.
The eye sits in the middle of the cell the creature is on. Cell 8,2 runs from 8 to 9 across and 2 to 3 down, so its middle is 8.5 and 2.5. Samples are taken every quarter of a cell out along the ray, so sample n sits at distance n × 0.25, and its position is the eye plus that distance times the direction:
x = 8.5 + d × 0.707107
y = 2.5 + d × (−0.707107)
At d = 0.25 that is x = 8.676777 and y = 2.323223. Throw away the fraction of each and what is left is the cell: 8,2. That is the cell the creature is standing on, so the sample is dropped and the walk carries on. At d = 0.50 it is 8.853553 and 2.146447, still 8,2, dropped again. At d = 0.75 it is 9.030330 and 1.969670, which is cell 9,1, and 9,1 is bare soil with nothing on it. Five more samples land in the same cell. Then at d = 2.25 the position is 10.090990 and 0.909010, cell 10,0, and row 0 is the rim of rock the valley is cut out of. The walk stops there. Nine samples, and the answer is two numbers: 2.25 cells, rock.
Only two of those numbers are choices. The stride is one of them, and a quarter of a cell
is short for a reason: a ray at 45 degrees moves 0.707107 across and 0.707107 down for
every cell it travels, so a walk that took whole cells at a time would jump the corner
between two cells and never look at either of the two it passed through. Anything shorter
than half a cell catches them, and a quarter leaves room to spare. The sight limit is the
other, and it is not a choice this file makes: it is Sight off the creature's
own row, twelve cells for a browser, so a kind with better eyes gets them by changing a
number in a table.
// 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
// Fan is how many rays one creature looks along, Arc is how wide the
// whole fan opens in radians, and Gap is the angle between one ray and
// the next. Nine rays over a hundred and twenty degrees leaves eight
// gaps of fifteen.
const (
Fan = 9
Arc = 120 * math.Pi / 180
Gap = Arc / (Fan - 1)
)
Hit is five constants and the order they are written in is a fact other
files will index by, so it is fixed here once and never re-sorted for tidiness.
Ray is the whole of what a walked line reports. Arc is the only
angle typed anywhere and Gap is divided out of it, so a fan of eleven rays
over the same arc is one edited digit and no arithmetic anywhere else. And the fan has an
odd number of rays on purpose: an even one has no middle, and a creature whose heading is
not one of the directions it can see is a creature that cannot look where it is going.
$ go run ./cmd/eyes -mode step -at 8,2 -eye 1
eyes: 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
the one looking is stood on 8,2 by hand, eye at 8.5000,2.5000 in cells
facing 0 degrees clockwise of east, sight 12 cells, stride 0.25 of a cell
ray 1 leaves at 0 + (1 - 4) x 15 = -45.0 degrees, so one cell along it
is 0.707107 east and -0.707107 south
n d x y cell what the cell answers
1 0.25 8.676777 2.323223 8,2 the cell it is standing on, skipped
2 0.50 8.853553 2.146447 8,2 the cell it is standing on, skipped
3 0.75 9.030330 1.969670 9,1 nothing
4 1.00 9.207107 1.792893 9,1 nothing
5 1.25 9.383883 1.616117 9,1 nothing
6 1.50 9.560660 1.439340 9,1 nothing
7 1.75 9.737437 1.262563 9,1 nothing
8 2.00 9.914214 1.085786 9,1 nothing
9 2.25 10.090990 0.909010 10,0 rock
the caster hands back ray 1 as 2.25 cells, met rock
The table is the Interlude, digit for digit, and the last line is the caster agreeing with it. Two of the nine samples were spent inside the cell the creature was standing on and both were thrown away. Six landed in 9,1, which is the price of a stride short enough to be safe: most samples are re-asking a question the previous sample already answered. And the ray stopped after nine, having walked 2.25 cells of a possible twelve, which is the number that makes this sense affordable. A ray that met nothing would take forty-eight samples, and nine of those is 432 lookups for one creature for one tick. Almost no ray ever gets there.
The eye point and the cell answer
A caster cannot start without settling two things the earlier chapters left open. Where does the ray leave from, and what does a cell say when it is asked.
The first one is a real conflict and it has to be closed before anything walks. A creature arrived here carrying two positions. One was a cell, set when it was stamped onto the ground. The other is the position inside its moving body, in world pixels, which is what steering pushes and what the movement charge is priced off. Both were true, and they went on being true for exactly as long as nothing moved. The moment a body takes a step, the pixels change and the cell does not, and from then on the world holds two answers to one question and no rule about which to believe.
The cell goes. Position lives in the body, because the body is the thing that changes it,
and the cell of ground a creature is on is worked out from that position every time it is
asked. One of the two constructors goes with it, for the same reason: it stamped a
creature onto a named cell and left the moving body unmade, mass and all, so a creature
made that way divided by a mass of zero the first time anything pushed it. The survivor is
the one that stands the body up, and Centre is the single bridge from the name
of a cell to a place in the world.
// internal/beast/body.go
// Beast is one creature standing somewhere: the row it was stamped
// from, the moving body it stands in, and the store, which is the only
// number a quiet tick changes. Everything else on it is a running total
// or a fact about when it started.
//
// There is exactly one answer on here to "where is it". Position and
// velocity live in the embedded field.Body, in world pixels, because
// that is where everything else in this world that moves keeps them,
// and Cell divides that position down to the cell of ground it lands
// on. A second field naming a cell would be a second answer, and the
// two would agree until the first thing walked.
//
// The body is embedded rather than named so that both spellings read:
// the body as a whole is b.Body, and the two vectors on it are b.Pos
// and b.Vel.
type Beast struct {
Name string
Kind *Kind
Store float64 // energy units in the store right now
Born int // the tick it was born on
Ate float64 // grams it has taken off the valley since it was born
Dead bool // its store reached nothing and it stopped
Last Ledger
field.Body // where it is and how fast, in world pixels
Face float64 // the heading its eyes look along, radians clockwise of east
// Greedy is the bite as it was first written: a whole mouthful
// taken whether or not the plant has one to give. Blind is the
// caster without the line that lets a ray leave the cell it starts
// on. Both are kept behind flags so the failures can be run instead
// of described, and both are false in every creature this book
// ships.
Greedy bool
Blind bool
// Unpaid is the bite with the repayment switched off: the grams
// still leave the plant, but the minerals they were holding go
// nowhere instead of landing on the cell that lent them. It is
// named for the ground's books and not for the animal, because a
// creature carrying it eats exactly as it always did and the whole
// of the loss is the bed's. It is kept behind a flag so what that
// missing line costs can be run instead of described, and it is
// false in every creature this book ships.
Unpaid bool
}
// Centre is the middle of a cell in world pixels: the position a
// creature put on a cell by name is stood up at, and the only bridge
// this package has from the name of a cell to a place in the world.
func Centre(c sim.Coord) field.Vec2 {
return field.Vec2{X: (float64(c.X) + 0.5) * terra.Tile, Y: (float64(c.Y) + 0.5) * terra.Tile}
}
// Cell is the cell of ground the creature is standing on: its position
// in world pixels, divided down by the pixels a cell is wide. It is
// worked out on every call and never stored, so it cannot be stale.
func (b *Beast) Cell() sim.Coord {
return sim.Coord{
X: int(math.Floor(b.Pos.X / terra.Tile)),
Y: int(math.Floor(b.Pos.Y / terra.Tile)),
}
}
// internal/beast/beast.go
// Spawn turns a row of numbers into one creature standing somewhere,
// with a full store and a body of the row's own mass. Every trait is
// copied into the individual and the row is never consulted for a rate
// again, so nothing one creature does can reach back and edit the kind
// it came from.
func (k Kind) Spawn(at field.Vec2, t int) *Beast {
kind := k
return &Beast{Name: k.Name, Kind: &kind, Store: k.Full,
Body: field.NewBody(at, k.Bulk), Born: t}
}
The body is embedded and not named, which is what lets both spellings the earlier
chapters used go on reading. Written as a whole it is b.Body, and the two
vectors on it come through as b.Pos and b.Vel without the
middle word. Face is new here and it is the only thing on a creature the fan
is measured from: an angle in radians, counted clockwise of east, which is the direction
the eyes point and not necessarily the direction the legs are carrying the body.
Now the second question. A ray lands on a cell and needs one answer out of five, which means somebody has to decide what a cell holding a plant, a creature and a puddle answers. The decision is an order, and the order has to be written down because it is a claim about what blocks a line of sight.
// internal/beast/look.go, continued
// View is the valley as it stood when a phase opened: the ground
// itself, and an index of what is on which cell, built once so that
// three thousand creatures walking four hundred samples each do not
// walk the stand list four hundred times over.
type View struct {
Valley *terra.Valley
stand map[sim.Coord]*terra.Stand
herd map[sim.Coord][]*Beast
}
// NewView indexes one valley and one roster of creatures by cell.
func NewView(v *terra.Valley, herd []*Beast) *View {
w := &View{Valley: v,
stand: make(map[sim.Coord]*terra.Stand, len(v.Stands)),
herd: make(map[sim.Coord][]*Beast, len(herd))}
for _, st := range v.Stands {
w.stand[st.At] = st
}
for _, b := range herd {
c := b.Cell()
w.herd[c] = append(w.herd[c], b)
}
return w
}
// 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
}
// Stand is the plant on one cell, and whether there is one.
func (v *View) Stand(c sim.Coord) (*terra.Stand, bool) {
st, ok := v.stand[c]
return st, ok
}
// At is what a ray meets on one cell, with one creature left out of the
// answer because a creature cannot see itself. A cell off the grid is
// the rock the valley is cut out of.
func (v *View) At(c sim.Coord, self *Beast) Hit {
if !v.Valley.Bed.In(c) {
return Rock
}
for _, b := range v.herd[c] {
if b != self && !b.Dead {
return Creature
}
}
switch v.Valley.Bed.Kind(c) {
case sim.Water:
return Water
case sim.Rock:
return Rock
}
if _, ok := v.stand[c]; ok {
return Plant
}
return Nothing
}
View is built once and read many times, which is the entire reason it
exists. A creature walking 432 samples through a list of stands would walk that list 432
times; walking it through a map costs one hash per sample and one pass over the stands
per phase, however many creatures there are. The two maps are unexported because nothing
outside this file has any business editing what is standing where in the middle of a
reading.
At asks its questions in a fixed order. Off the grid first, and off the grid
answers rock, because the valley is cut out of rock and a creature looking over the rim
is looking at the inside of a wall. Then the creatures, before the ground, because a
living animal standing on a cell is what a ray runs into and the soil under its feet is
not. Then the ground itself. Then, last, whatever is rooted there. And the creature doing
the reading is left out of every one of those answers, which is the whole of what
self is for.
$ go run ./cmd/eyes -mode codes -at 8,2
eyes: 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
the one looking is stood on 8,2 by hand, eye at 8.5000,2.5000 in cells
facing 0 degrees clockwise of east, sight 12 cells, stride 0.25 of a cell
what one cell answers, asked of every cell of the grid and four off it
on grid creature ground standing answers
here true yes soil 0.00 g nothing
a plant true - soil 383.56 g plant
a creature true yes soil 0.00 g creature
the pond true - water 0.00 g water
the rim true - rock 0.00 g rock
bare soil true - soil 0.00 g nothing
off the grid false - - - rock
the creature reading is left out of its own answer: cell 8,2 holds it
and comes back nothing to it and creature to anybody else
over the whole 12x8 grid: 29 nothing 12 plant 4 creature 16 water 35 rock
Read the first and third rows against each other. Both cells have a creature standing on
them and they answer differently: the one the reader is standing on comes back
nothing, and the other comes back creature. Ask the same cell on
behalf of somebody else and it answers creature to them. A cell has no single
truth about what is on it, because the question a ray asks is not "what is here" but "what
is here that is not me".
The nine-ray fan
One ray is a loop. Nine of them is the same loop with an angle that changes, and the whole caster comes to about twenty lines.
// internal/beast/look.go, continued
// Look walks the whole fan and fills the caller's slice, one Ray per
// eye. Ray 0 is the leftmost of the fan and ray Fan-1 the rightmost,
// so the middle one looks straight along the heading: ray i leaves at
// Face plus (i minus four) gaps.
//
// The eye is the body's own position, which for a creature stood on a
// cell by name is the middle of that cell. A ray steps Stride cells at
// a time and stops at the first cell that is not empty, ignoring the
// cell the creature is standing on: a creature is not blocked by the
// ground under its own feet. A ray that reaches the row's Sight without
// meeting anything comes back at that distance with nothing on it.
func (b *Beast) Look(v *View, eyes []Ray) {
from := b.Pos.Scale(1.0 / terra.Tile)
self := b.Cell()
for i := range eyes {
a := b.Face + (float64(i)-float64(Fan-1)/2)*Gap
dx, dy := math.Cos(a), math.Sin(a)
r := Ray{Dist: b.Kind.Sight, What: Nothing}
for d := Stride; d <= b.Kind.Sight; d += Stride {
c := sim.Coord{
X: int(math.Floor(from.X + dx*d)),
Y: int(math.Floor(from.Y + dy*d)),
}
if c == self && !b.Blind {
continue
}
if w := v.At(c, b); w != Nothing {
r = Ray{Dist: d, What: w}
break
}
}
eyes[i] = r
}
}
// Eyes is the slice one creature's fan is read into. A caller that
// keeps one for the life of a run allocates nothing on a tick.
func Eyes() []Ray { return make([]Ray, Fan) }
The signature is the thing to look at twice. Look is handed the slice it
fills and allocates nothing, so a caller that keeps one slice for the life of a run pays
for it once instead of three thousand times a tick. The ray is set to its full sight
before the walk starts, so the case where a ray meets nothing at all needs no branch at
the bottom: it is what the loop already holds when it runs out of distance. And a ray
that stops carries the exact distance the sample sat at, not the distance of the cell
boundary it crossed, because the sample is the only place the walk actually looked.
$ go run ./cmd/eyes -mode fan -at 8,2
eyes: 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
the one looking is stood on 8,2 by hand, eye at 8.5000,2.5000 in cells
facing 0 degrees clockwise of east, sight 12 cells, stride 0.25 of a cell
ray degrees east south samples cells and met
0 -60.0 0.500000 -0.866025 3 0.75 plant
1 -45.0 0.707107 -0.707107 9 2.25 rock
2 -30.0 0.866025 -0.500000 3 0.75 plant
3 -15.0 0.965926 -0.258819 3 0.75 plant
4 0.0 1.000000 0.000000 2 0.50 plant
5 15.0 0.965926 0.258819 3 0.75 plant
6 30.0 0.866025 0.500000 3 0.75 plant
7 45.0 0.707107 0.707107 15 3.75 rock
8 60.0 0.500000 0.866025 17 4.25 plant
nearest 0.50 cells on plant, furthest 4.25 on plant, and the fan cost
58 lookups against the 432 a fan that met nothing would
Six of the nine rays stop inside a single cell, which says the creature has plants pressed up against it on almost every side; ray 1 gets 2.25 cells north-east before the rim stops it, and ray 8 gets 4.25 south-east before it finds something to eat. The whole fan cost 58 lookups against the 432 a fan staring across an empty valley would have spent, and that ratio is not a coincidence: a ray stops at the first thing it meets, so the more crowded the ground is, the less the sense costs. It is the opposite of how the price of asking every plant behaves.
Distances and codes are exact and hard to picture. The same fan laid over the ground is neither.
$ go run ./cmd/eyes -mode map -at 8,2
eyes: 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
the one looking is stood on 8,2 by hand, eye at 8.5000,2.5000 in cells
facing 0 degrees clockwise of east, sight 12 cells, stride 0.25 of a cell
@ the one looking : a cell some ray crossed P C W R what a ray stopped on
. soil ~ water # rock o a plant and c a creature no ray reached
##########R#
#.o..oo.P:o#
#....co.@Pc#
#.~~~~~.::.#
#.~~~~~~o::#
c...~~~~.c:R
#...~o..ooP#
############
the fan crossed 11 cells of the 96 on the grid, and stopped on 9 of them
Eleven cells out of ninety-six, and the creature can say nothing at all about the other eighty-five. The pond starts a little over two cells to the south-west and the fan is pointed the other way. All four of the other creatures are invisible. None of that is a defect being confessed: a fan is a narrow, cheap, directional sense, and a creature that wants to know what is behind it has to turn round and spend the ticks.
There is one line in the caster that looks like a special case and is load-bearing. Take out the test that lets a ray leave the cell it started on and the sense stops working, in a way that produces no error and no crash.
// internal/beast/look.go — the caster with the own-cell test taken out
for d := Stride; d <= b.Kind.Sight; d += Stride {
c := sim.Coord{
X: int(math.Floor(from.X + dx*d)),
Y: int(math.Floor(from.Y + dy*d)),
}
if w := v.At(c, b); w != Nothing {
r = Ray{Dist: d, What: w}
break
}
}
$ go run ./cmd/eyes -mode blind -at 9,2
eyes: 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
the one looking is stood on 9,2 by hand, eye at 9.5000,2.5000 in cells
facing 0 degrees clockwise of east, sight 12 cells, stride 0.25 of a cell
standing on 26.46 grams of plant, on cell 9,2
ray cells and met cells and met
shipped blind
0 1.00 plant 0.25 plant
1 0.75 plant 0.25 plant
2 0.75 creature 0.25 plant
3 0.75 creature 0.25 plant
4 0.50 creature 0.25 plant
5 0.75 creature 0.25 plant
6 0.75 creature 0.25 plant
7 2.25 rock 0.25 plant
8 3.00 rock 0.25 plant
the blind fan walked 9 samples against 42, and every ray of it
came back the same two numbers, 0.25 and plant
Every ray returns 0.25 cells and the word plant. Nine directions, nine identical answers, and the creature is standing on a plant of its own so it will get those same two numbers wherever it turns and however far it walks, for as long as it stays on a cell with anything on it. It is not blind in the sense of seeing nothing. It sees one thing, enormous, in every direction, and it is the ground it is already standing on.
The shipped column beside it is what those nine directions actually hold: a plant one cell out, another a bit nearer, five rays that stop on a neighbouring creature, and two that run 2.25 and 3.00 cells to the rim. The failure did not lose a little accuracy. It replaced the entire contents of the sense with a constant, and the total cost of the broken fan is nine lookups against forty-two, so the bug also looks like an optimisation if the only thing being watched is the clock.
The rule underneath is about where a measurement starts. An instrument sitting inside the thing it is measuring will report the thing it is sitting in. The eye is at the middle of a cell that is occupied by definition, since the creature is in it, so the first sample of every ray is guaranteed to land on an occupied cell. Any sense that begins inside its own subject has to say so and step past it, and the alternative fixes are worse: starting the ray at the cell boundary makes the distance depend on which way the creature happens to face, and skipping the first two samples by count breaks the day a creature stands anywhere but the middle of a cell.
Why two numbers are enough
The claim at the top of the chapter was that a ray hands back a code and never the thing it met. The cheapest way to test whether that is enough is to try to draw the fan with nothing but what the fan returned, into the same buffer the client already paints the valley into.
// cmd/eyes/main.go — the fan, drawn from what the fan returned
var (
wants = render.Color(0xFFE8D8A0)
warns = render.Color(0xFFC05A4A)
empty = render.Color(0xFF8A9096)
)
// ink is the colour one finished ray is drawn in.
func ink(h beast.Hit) render.Color {
switch h {
case beast.Plant, beast.Creature:
return wants
case beast.Water, beast.Rock:
return warns
}
return empty
}
// draw paints the valley the client already knows how to paint and puts
// the fan on top of it: one line per ray, from the eye to the pixel the
// ray stopped at, in a colour that says what stopped it.
func draw(v *terra.Valley, at sim.Coord, face float64, seed uint64, n int, path string) {
b, w, herd := looker(v, at, face, seed, n)
who(b, herd, face)
eyes := beast.Eyes()
b.Look(w, eyes)
buf := scene.Paint(v)
for _, h := range herd {
dot(buf, h.Pos, empty)
}
for i, r := range eyes {
a := angle(b, i)
end := field.Vec2{
X: b.Pos.X + math.Cos(a)*r.Dist*terra.Tile,
Y: b.Pos.Y + math.Sin(a)*r.Dist*terra.Tile,
}
buf.Line(int(math.Round(b.Pos.X)), int(math.Round(b.Pos.Y)),
int(math.Round(end.X)), int(math.Round(end.Y)), ink(r.What))
}
dot(buf, b.Pos, wants)
fmt.Printf(" the valley %d by %d pixels, %s\n", buf.W, buf.H, buf.Hash())
if path == "" {
return
}
big := scene.Scale(buf, 3)
if err := big.SavePNG(path); err != nil {
fmt.Fprintf(os.Stderr, "eyes: %v\n", err)
os.Exit(1)
}
fmt.Printf(" wrote %s at 3 times, %d by %d\n", path, big.W, big.H)
}
// dot marks one body with a two-pixel square, which is as small as a
// mark can be and still be found on a printed page.
Nothing in that loop can reach any object in the valley. It has an angle it worked out from the ray's index, a distance the caster returned, and a code it turns into a colour through a table with five entries. The endpoint is the eye plus the direction times the distance, in pixels, and the line routine is the one the client has been drawing plant stems with since the plants learned to branch.
$ go run ./cmd/eyes -mode draw -at 1,1 -face 30 -shot assets/frames/fan-nine-rays.png
eyes: 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
the one looking is stood on 1,1 by hand, eye at 1.5000,1.5000 in cells
facing 30 degrees clockwise of east, sight 12 cells, stride 0.25 of a cell
the valley 192 by 128 pixels, 36fd69da93c2ec40422b97d6a66b8b6712995c523c5b7b175b6e9fcb14b3efc9
wrote assets/frames/fan-nine-rays.png at 3 times, 576 by 384
assets/frames/fan-nine-rays.png: the
same nine rays the table printed, drawn into the client's own buffer at three times size.
Gold is a ray that stopped on something a browser eats, red is one that stopped on
something it cannot. Every pixel of it was drawn from a distance and a small whole
number.
That picture is the argument finished. A drawing is about as demanding a consumer as a number can have, and it never needed the plant. It needed to know that something was 0.75 of a cell away in a particular direction and that the something was of a kind the table calls to inspect. Any other consumer of the fan is strictly easier to satisfy than the renderer, and one of them is coming: a row of numbers that gets multiplied by weights and added up, which cannot follow a pointer, cannot call a method, and would not know what to do with a plant if it were handed one.
The pattern generalises past creatures and past this book. A measurement crossing a boundary should be reduced, at the instrument, to the smallest thing the far side can act on. The far side then cannot accidentally depend on anything else: not on the plant's mass, not on its species, not on whether it is still alive by the time the reading is used. Every one of those would have come free with a pointer, and every one of them would have been a way for a reading taken at the top of a phase to change underneath the code reading it. Distance and code cannot go stale, because they are copies of nothing.
The cost of the discipline is real and it is paid in the same place every time. Five codes cannot tell a moss from a canopy tree, and a creature that walks up to a plant on the strength of a ray may find a gram and a half where it hoped for four hundred. That information is dropped on purpose: one slot per ray, one number, and the number the fan chose to spend it on is which of five kinds of thing is there. What a ray is bad at is quantity, and a creature that needs quantity is going to have to buy a second sense that answers a different question entirely.
Checkpoint
- Given a heading and a ray number, produce the angle it leaves at, and say why ray 4 is the only one that agrees with the heading.
- Handed a stride and an angle, work out where sample n lands in cells, turn that into a cell of the grid, and say what a stride of a whole cell would step over on a diagonal.
- Name the five things
Atasks about a cell in the order it asks, and say why a creature's own cell answers differently to it than to anybody else. - Shown a fan whose nine rays all report the same distance and the same code, look at where the ray starts rather than at the angles.
- Say what a ray costs when it meets something at half a cell against what it costs when it meets nothing, and why a crowding valley makes this sense cheaper.
- Draw a creature's whole field of view into a frame holding nothing but a distance and a code per ray, and say what the drawing never had access to.
Exercise 1 — the long way round. Ray 8 of the fan at 8,2
runs 4.25 cells, the furthest of the nine. Work out its angle and its first sample by
hand, then walk the whole thing with
go run ./cmd/eyes -mode step -at 8,2 -eye 8.
Ray 8 leaves at 0 + (8 − 4) × 15 = 60 degrees, so one cell along it is cos(60) = 0.5 east and sin(60) = 0.866025 south. The first sample sits at 8.5 + 0.25 × 0.5 = 8.625 and 2.5 + 0.25 × 0.866025 = 2.716506, which is still cell 8,2 and thrown away. Seventeen samples in all, and the interesting part is the middle: five in a row land in 10,5 without anything changing. A ray at sixty degrees needs two whole cells of travel to move one column east, so it lingers, and the further a ray leans off the axis the more of its samples re-ask a question the sample before it already answered.
$ go run ./cmd/eyes -mode step -at 8,2 -eye 8 | tail -8
12 3.00 10.000000 5.098076 10,5 nothing
13 3.25 10.125000 5.314583 10,5 nothing
14 3.50 10.250000 5.531089 10,5 nothing
15 3.75 10.375000 5.747595 10,5 nothing
16 4.00 10.500000 5.964102 10,5 nothing
17 4.25 10.625000 6.180608 10,6 plant
the caster hands back ray 8 as 4.25 cells, met plant
Exercise 2 — a creature at the water's edge. Cell 1,4 is
on the west shore of the pond. Run
go run ./cmd/eyes -mode fan -at 1,4 and say what the fan can tell the
creature about how big the pond is.
Nothing whatever. Seven of the nine rays stop on water between half a cell and one cell out, and the two that miss the pond run 3.75 and 3.00 cells to rock. The whole reading is "water, close, in seven directions". The pond covers sixteen cells of this valley and a browser standing at the edge of it cannot tell that from a puddle one cell wide, because a ray stops at the first thing it meets and never reports how much of that thing there was. This is the exact case where the fan is spending nine expensive walks to say one cheap thing.
$ go run ./cmd/eyes -mode fan -at 1,4 | tail -13
ray degrees east south samples cells and met
0 -60.0 0.500000 -0.866025 4 1.00 water
1 -45.0 0.707107 -0.707107 3 0.75 water
2 -30.0 0.866025 -0.500000 3 0.75 water
3 -15.0 0.965926 -0.258819 3 0.75 water
4 0.0 1.000000 0.000000 2 0.50 water
5 15.0 0.965926 0.258819 3 0.75 water
6 30.0 0.866025 0.500000 3 0.75 water
7 45.0 0.707107 0.707107 15 3.75 rock
8 60.0 0.500000 0.866025 12 3.00 rock
nearest 0.50 cells on water, furthest 3.75 on rock, and the fan cost
48 lookups against the 432 a fan that met nothing would
Exercise 3 — take the others away. The fan cast from 9,2
stops on a neighbouring creature five times out of nine. Predict what those five rays
do when the population is founded empty, then run
go run ./cmd/eyes -mode fan -at 9,2 -herd 0.
Four of them carry on to the rim behind where the neighbour was standing, at 1.75, 1.50, 1.75 and 1.75 cells instead of stopping at half a cell or three quarters of one. The fifth, ray 2, finds a plant at 1.25 that the neighbour had been hiding. Two things fall out of that. A creature blocks a line of sight like anything else, so a herd standing between an animal and its dinner makes the dinner invisible; and a fan is a reading of one moment, so the index it walks is built at the top of a phase and thrown away at the bottom of it.
$ go run ./cmd/eyes -mode fan -at 9,2 -herd 0 | tail -13
ray degrees east south samples cells and met
0 -60.0 0.500000 -0.866025 4 1.00 plant
1 -45.0 0.707107 -0.707107 3 0.75 plant
2 -30.0 0.866025 -0.500000 5 1.25 plant
3 -15.0 0.965926 -0.258819 7 1.75 rock
4 0.0 1.000000 0.000000 6 1.50 rock
5 15.0 0.965926 0.258819 7 1.75 rock
6 30.0 0.866025 0.500000 7 1.75 rock
7 45.0 0.707107 0.707107 9 2.25 rock
8 60.0 0.500000 0.866025 12 3.00 rock
nearest 0.75 cells on plant, furthest 3.00 on rock, and the fan cost
60 lookups against the 432 a fan that met nothing would
Eighteen numbers now, two per ray, and every one of them is about something a straight line ran into. The water in the ground is not something a straight line runs into, and neither is the difference between a moss and a scrub standing on the same cell. Those are the questions the fan is worst at, and answering them turns out to cost four lookups instead of four hundred.