A Bench That Keeps Score
Fitness needs an arena
Nineteen animals founded in The Hollow were mutated copies of one founder, and every one of them starved before the second year was out. Some ran faster, some turned harder, and some burned through their stores before they found food. The arena draws its ground, its food and its trials once, then draws nothing while it scores.
A genome's number is a fact about that genome. It does not depend on which genomes were scored before it, how many goroutines the work went out on, or what time of the world's own weather cycle happened to arrive.
The open valley is the wrong measuring tool. Weather changes the ground under the same animal. Plants grow back between attempts. A birth or a death changes the roster and the next creature's surroundings. Those are world facts, but they make a bad ruler.
The arena removes those moving parts and keeps the one question this volume needs: how much standing tissue does this genome's body and controller collect in six hundred ticks when every genome gets the same start. A score that cannot answer that question cannot select parents without selecting noise.
The chapter builds a fixed plate, runs sixty-four genomes through it in population order, and prints one number per genome. The later selection code can read the cards without knowing how the arena made them.
The arena ground
The ground first, because everything else is laid out on it. The arena is twenty-four cells on a side. The outer ring of it is open water and the four hundred and eighty-four cells inside that are soil.
The ring is doing real work and it is not scenery. The step check the last page repaired asks whether the ground a creature's step commits it to is ground the valley will carry, and it counts open water as ground it will not. A creature that tries to run at the ring is refused the action and spends its tick standing there. So the plate is closed by the same rule that stops an animal walking into the pond, and no wall had to be invented for it.
// internal/gene/arena.go
// The two streams the arena spends, and the only random numbers
// anything in this file ever asks for.
const (
// StreamDrawn is the arena's opening genomes: one generation of
// them, genome by genome in population order, a fixed count of
// draws each.
StreamDrawn = 15
// StreamPlate is the arena's trial layouts, drawn once when the
// arena is built and never drawn again. Scoring a genome takes no
// numbers off it and no numbers off anything else, which is what
// makes a score a fact about a genome instead of a fact about when
// it was scored.
StreamPlate = 21
)
// The size of the bench, written down in one place.
//
// Plate is the cells on a side of the ground, Crowd is the genomes in
// one generation, Trials is the runs each of them gets, and Ticks is
// how long one of those runs lasts. Larder is the stands of fodder a
// trial is laid out with and Stock is the grams standing in each of
// them, so the food in a trial is fixed before the first creature
// arrives and no creature can eat enough of it to matter to the next
// one.
const (
Plate = 24
Crowd = 64
Trials = 4
Ticks = 600
Larder = 24
Stock = 20.0
Fodder = 2 // the row of terra.Flora the larder is laid out in
)
// NewPlate is the arena's ground: a square of soil with a ring of open
// water round it. The ring is the whole of what makes the plate closed.
// A creature may not choose a step whose ground it cannot stop on, and
// open water is ground no step is allowed to cover, so the ring is a
// rule the creature runs into rather than a wall somebody built.
func NewPlate(n int) *sim.Grid {
g := sim.NewGrid(n, n)
for y := 0; y < n; y++ {
for x := 0; x < n; x++ {
c := sim.Coord{X: x, Y: y}
if x == 0 || y == 0 || x == n-1 || y == n-1 {
g.Set(c, sim.Water)
} else {
g.Set(c, sim.Soil)
}
}
}
return g
}
Six hundred ticks is a minute of world time at ten ticks a second, and a creature stamped out of the founding row could cross the plate a dozen times in that minute at a full sprint. Four trials and not one, because a single trial is a single starting cell and a single heading, and a genome that happens to open facing a stand of scrub would beat a better one that opened facing the water. Four is a small number, and it is enough to make the luck of one placement cost a quarter of the score instead of all of it.
Now the trials themselves. A trial is where the fodder stands, where the creature is stood up, and which way it is facing when the first tick opens. All three are drawn off stream 21, once, when the arena is built, and then they stop being random numbers and start being data. That is the difference the whole page turns on: a layout that is redrawn per genome is a different exam for every candidate, and a layout that is written down is the same exam for all of them.
// internal/gene/arena.go, continued
// Layout is one trial written down: the cells the fodder stands on, the
// cell the creature is stood up on and the heading it opens facing. It
// is drawn once and then it is data, so re-founding a trial is reading
// this struct back and never asking the generator anything.
type Layout struct {
Fodder []sim.Coord
At sim.Coord
Face float64
}
// Arena is the bench a genome is scored on: one plate of ground and the
// trials every genome runs, in the order every genome runs them.
//
// Crew is how many goroutines the generation is scored across, on the
// same interleaved stripes the creature phase casts its fans on. It
// changes how long a generation takes and it cannot change what a
// generation comes to: scoring a genome reads the layouts and writes
// one card, touches nothing any other genome touches, and draws no
// random number at all.
type Arena struct {
Ground *sim.Grid
Trials []Layout
Crew int
}
// NewArena builds the bench and draws every trial it will ever run.
//
// The draws are all here, and there are exactly Larder plus two of them
// a trial: one per stand of fodder, one for where the creature is stood
// up and one for the heading it opens facing. A draw that lands on a
// cell something is already standing on is spent and plants nothing,
// the way the valley's own founding spends one, so the count does not
// depend on where the cells fell.
func NewArena(seed uint64, trials int) *Arena {
g := NewPlate(Plate)
a := &Arena{Ground: g, Trials: make([]Layout, trials)}
// ... the soil cells and the walkable cells, in row order ...
s := rand.New(rand.NewPCG(seed, StreamPlate))
for i := range a.Trials {
var l Layout
taken := map[sim.Coord]bool{}
for j := 0; j < Larder; j++ {
c := soil[s.IntN(len(soil))]
if taken[c] {
continue
}
taken[c] = true
l.Fodder = append(l.Fodder, c)
}
l.At = ground[s.IntN(len(ground))]
l.Face = s.Float64() * 2 * math.Pi
a.Trials[i] = l
}
return a
}
$ go run ./cmd/score -mode plate
score: the arena, 24x24 cells, 64 genomes, 4 trials of 600 ticks
the ground cells
the plate 576
soil a stand of fodder can root in 484
open water round the rim 92
what is switched off setting
rain offered on an average day 0.0000
what the sun lifts off a wet cell 0.0000
the share of a litter pile that rots 0.0000
daylight landing on an open cell 0.0000
seeds waiting in the bank 0
the valley's own tick is never called: nothing here grows, seeds or rots
trial stands grams stood up on facing
1 23 460.0000 20,13 4.6396
2 24 480.0000 19,7 4.5498
3 23 460.0000 16,1 4.5176
4 24 480.0000 7,9 6.0547
1880.0000 grams of fodder a genome, and the same 1880.0000 grams for every one of the 64
26 draws a trial off stream 21, 104 for the whole arena, and none ever again
395 draws a genome off stream 15, 25280 for the generation
and not one number off any stream while a genome is being scored
Trials one and three hold twenty-three stands and trials two and four hold twenty-four, out of twenty-four draws apiece. The missing one in each is a draw that landed on a cell another stand was already on: the draw is spent and nothing is planted, exactly as the valley's founding generation does it, so a trial costs twenty-six numbers whatever those numbers say. All four layouts together cost a hundred and four, and the arena never touches stream 21 again for as long as it runs.
The block of zeroes in the middle of that listing is the whole of what makes this a bench. Rain is nothing, so no cell gets wetter. Evaporation is nothing, so no cell dries out. Decay is nothing, so a pile of litter stays a pile of litter. There is no daylight at all, and no seed is waiting in the ground. Underneath all five of those there is a stronger statement, and it is the last line of the block: the valley's own tick is never called. The arena runs the creature phase and nothing else. So the fodder does not grow while it is being eaten, the fodder does not seed, no plant dies of bad luck, and the water in the ground sits at exactly full in every cell of the plate for all six hundred ticks. Every reading a creature takes of the moisture slope is a flat zero, because the moisture is flat.
The larder is the one thing on the plate that can change, and it changes in exactly one direction. Four hundred and sixty or four hundred and eighty grams of scrub go into a trial, laid out twenty grams to a stand, and the only way a gram leaves is into a mouth. Add the four trials up and every genome in the generation is offered the same 1,880.0000 grams. That number is the reason two scores can be compared at all.
One genome stepped six hundred times
A trial is one genome stamped into a body, wired to its own controller, and stepped six hundred times. Nothing about that is new: stamping a row out of a genome is the previous two pages, and the creature phase has been building a view, casting every fan, stepping the roster in order and burying the dead since the valley learned to hold animals. What the arena adds is the counting.
// internal/gene/arena.go, continued
// Trial is one genome on one layout: the ground stood back up, one
// creature stamped and wired, and Ticks creature phases run against it.
//
// The valley's own tick is never called. Rain, sun and decay are set to
// nothing, the seed bank is left empty and no light is cast, so nothing
// in here grows, rots, seeds or dies of the weather. The larder is the
// grams it was laid out with less the grams this creature has taken out
// of it, and that is the only thing on the plate besides the creature
// that ever changes.
func (a *Arena) Trial(g *Genome, l Layout) Card {
v := terra.NewValley(a.Ground, 0,
terra.Cell{Moisture: 1.25, Nutrient: 12.0}, 0,
terra.NewAir(a.air, field.Drag{B: 0.25}, 0, 0))
v.Fall, v.Sun, v.Decay = 0, 0, 0
v.Wear, v.Brake, v.Soak = 0, 0, 0
for _, c := range l.Fodder {
st := v.Plant(Fodder, c)
v.Opened += Stock - st.Plant.Mass
st.Plant.Mass = Stock
}
b := g.Stamp(beast.Fauna[0]).Spawn(beast.Centre(l.At), 0)
b.Face = l.Face
w := beast.NewWits(g.Net(), b)
r := beast.NewRoster()
r.Add(b)
r.Seen = func(view *beast.View) { beast.Watch(r.Live, view) }
sc := beast.Scale{Wet: v.Full, Food: beast.Fauna[0].Bellyful()}
var c Card
was := b.Cell()
stood := map[sim.Coord]bool{was: true}
for t := 0; t < Ticks && len(r.Live) > 0; t++ {
r.Phase(v, sc)
v.Now++
at := b.Cell()
if at != was {
c.Cells++
stood[at] = true
was = at
}
// ... and a count of the ticks spent over water or off the plate ...
}
c.Grams = r.Ate
c.Left, _ = v.Standing()
c.Steps = r.Steps
c.Stood = len(stood)
c.Took = w.Took
c.Bites = w.Took[beast.Bite]
if b.Dead {
c.Died = 1
}
return c
}
Two lines in there are load-bearing and easy to miss. Roster.Seen is the
hook the phase hands its one reading of the world to before it steps anybody, and
passing it to beast.Watch is what gives a network-driven creature the same
world its eyes got; without it the legality check has nothing to ask about and hands
back whatever the top score wanted. And the loop stops the moment the roster empties. A
creature whose store runs out is buried and struck off, and a plate with nobody on it
has nothing left to measure, so a genome that starves on tick ninety costs the bench
ninety ticks and not six hundred.
Now the number. Every one of those trials produces a small pile of counts, and the
temptation with a pile of counts is to make a score out of several of them. Grams eaten,
plus a little for how long it survived, minus a little for how far it wandered, plus
something for the bites it landed. Every one of those extra terms is somebody's opinion
about what a good creature is, written into the ruler. So the card carries nine numbers
and a tally of the six actions, and the score reads exactly one of them. The single branch in Score is a flag the
last section of this page turns on to show what happens when it reads a different one, and
it is off in every arena anything else here runs.
// internal/gene/arena.go, continued
// Card is everything one genome's run through the arena came to.
//
// Only the first number is the score. The rest are measurements: they
// are printed, they are folded into the digest, and nothing anywhere
// adds any of them to Grams or lets any of them break a tie. A number
// that decides who breeds is a term of the fitness function whether or
// not the code calls it one.
type Card struct {
Grams float64 // grams taken off standing fodder, over every trial
Left float64 // grams still standing when the trials closed
Bites int // bites that actually moved grams
Steps int // creature-ticks it was stepped
Cells int // times it stepped into a cell it was not on last tick
Stood int // cells it stood on at least once
Died int // trials whose store ran out before the ticks did
Wet int // creature-ticks spent over the ring of water
Off int // creature-ticks spent past the last cell of the plate
Took [beast.Acts]int
}
// Score is the number the arena hands back for a card, and it is grams
// eaten and nothing else.
func (a *Arena) Score(c Card) float64 {
if a.Restless {
return float64(c.Cells)
}
return c.Grams
}
$ go run ./cmd/score -mode trial
score: genome 43 of the generation, trial by trial
the row it is stamped out of: 65.7271 grams of body, a store of 1160.6651,
a bite of 0.2393 grams reaching 3.1559 cells, 0.1823 cells a tick at a sprint
it costs 0.4599 a tick to stand there and a full store covers 2524 ticks
trial ticks bites grams cells stood starved
1 600 84 20.0000 0 1 no
2 600 120 28.6154 10 11 no
3 600 252 60.0000 2 3 no
4 600 296 70.8376 1 2 no
all 2400 752 179.4530 13 17 0
the score is the grams column and nothing else: 179.4530
the arena's own books, in grams
fodder laid out 1880.000000
fodder still standing 1700.547009
taken off the fodder 179.452991
counted onto the score 179.452991
the two differ by -6.821e-13, which is the last bits of the adding
Read the trial-one line and then read the body above it. Eighty-four bites and exactly 20.0000 grams: this creature stripped one stand of scrub to nothing and then had nothing left inside its reach, because the mouth check refuses a bite when no cell within reach is holding any tissue. Trials three and four went better, and the total is 179.4530 grams out of a larder of 1,880. The last block is the arena closing its own books: the fodder that left the ground and the grams counted onto the score are the same number to within the last bits of the adding, and they have to be, since nothing on this plate manufactures a gram or destroys one.
The interesting thing about genome 43 is what it is. It has a body of sixty-six grams, a store nearly three times the founding row's, a bite of a quarter of a gram, and a top speed of 0.1823 cells a tick, which is under half the row's own sprint. It is slow. It won this generation by standing more or less still with a reach of three and a bit cells and eating everything it could touch.
Two pieces of arithmetic bound every number on that table, and both of them can be worked out before any code runs.
The first is the ceiling. A bite occupies two ticks: the tick it is taken on and one more before the creature may choose again. So six hundred ticks hold at most three hundred bites, and a bite takes at most one bite's worth of grams off the plant. For genome 43 a bite is 0.2393 grams, so its ceiling for one trial is
most = (T ÷ tb) × B = (600 ÷ 2) × 0.2393 = 71.79 grams
and 287.16 over the four. It scored 179.4530, which is 62 percent of a life spent with its mouth actually on something. For the founding row's own bite of 0.25 grams the ceiling is 75.00 grams a trial and 300 over the four, and the largest bite this world's ranges allow, a factor of 4.00, puts it at 300 a trial. So no genome anywhere in this arena can score above 1,200, and the best of the sixty-four managed 179.4530.
The second is the clock. A standing body is charged its bulk times its basal rate every tick whatever it does, so a creature that never eats has
ticks = F ÷ (Bulk × Basal)
before its store hits nothing. Genome 43's store over its bill is the 2,524 ticks the run prints, four times as long as a trial lasts, so it was never in any danger of starving and the table says so: nought trials of four. That is an upper bound and not a promise. Moving costs on top of standing, and the charge for moving goes up with the square of the speed, so a creature that sprints spends its clock much faster than this number says. Exercise 1 at the end of the page follows one whose clock reads 930 ticks and which lasted 154 of its first trial's six hundred.
Put the two together and the arena has a comfortable middle. A trial is long enough that a creature has to keep finding food, and short enough that a genome cannot win by sitting perfectly still on a full store for a year. Both of those are properties of the numbers 600, 2 and 24, and both were checked with this arithmetic before the bench was ever run.
The scored population
One genome measured against a fixed bench is a number. Sixty-four of them measured against the same bench is a population, and a population is the thing the rest of this volume acts on. The sixty-four come off stream 15, which nothing else in this world reads, and every gene of every one of them is drawn evenly over that gene's own range.
// internal/gene/arena.go, continued
// Drawn is one opening generation: n genomes off stream 15, every gene
// of every one of them drawn evenly over its own range.
//
// A genome costs Len() draws and always Len(), for the same reason a
// mutation pass costs twice that: the sixty-fourth genome of a
// generation has to be the sixty-fourth genome whatever the first
// sixty-three came out as.
func Drawn(seed uint64, n int) []*Genome {
s := rand.New(rand.NewPCG(seed, StreamDrawn))
pop := make([]*Genome, n)
for i := range pop {
g := Identity()
for j, m := 0, g.Len(); j < m; j++ {
sp := g.Span(j)
g.Put(j, sp.Lo+s.Float64()*sp.Wide())
}
pop[i] = g
}
return pop
}
These are not the founders The Hollow opens with. A founder there has all ten body factors at 1.00 and only its brain drawn, and that is what keeps the valley's opening invariant standing. An arena genome has all three hundred and ninety-five of its numbers drawn, bodies included, so the generation holds creatures with ten-gram bodies and creatures with a hundred and sixty, creatures that see six cells and creatures that see twenty-four. That is on purpose. A bench whose candidates all have the same body can only ever compare controllers, and the question this volume asks is what a whole animal is worth.
$ go run ./cmd/score -mode spread | head -15
score: 64 genomes, 4 trials of 600 ticks each, scored on grams eaten
the ten best of a generation nothing has selected
rank genome grams bites ticks cells starved
1 43 179.4530 752 2400 13 0
2 27 140.0000 273 844 12 4
3 46 140.0000 175 1742 13 4
4 12 101.9936 111 1947 0 1
5 61 100.0000 405 1584 27 3
6 19 97.9590 145 987 0 3
7 62 78.6864 113 1005 2 3
8 29 60.0000 369 1701 9 2
9 0 56.0099 70 1889 23 1
10 4 41.5353 110 354 70 4
The cells column of that table is the surprise. It is how many times the
creature stepped into a cell it was not standing on the tick before, added over all four
trials, and the whole top ten is in single or low double figures. Genome 12 and genome 19
never changed cells at all, in any of their four trials, and between them they ate two
hundred grams. The best creature in a generation nothing has selected is a creature
that stands where it was put and eats what it can reach.
That is a fact about this arena and not a law of biology, and the reason is in the
Reach gene. A reach of one cell is the cell underfoot and the eight touching
it; a reach of four cells, which the body range allows, is eighty-one cells. Genome 43 has
a reach of 3.1559, so its mouth covers forty-nine cells of ground without the legs doing
anything, and at roughly one stand of fodder in twenty cells that is two or three stands
it can strip without moving. Standing still is cheap and moving is charged for. Nothing in
this arena had to be told that.
$ go run ./cmd/score -mode spread | tail -19
the spread
the best of them 179.4530
the genome a quarter of the way down 40.0000
the middle one 4.8405
three quarters of the way down 0.0000
the worst of them 0.0000
the average 24.5807
genomes that ate anything at all 38
genomes that scored nothing whatever 26
grams the generation ate between them 1573.1642
grams of fodder it was offered 120320.0000
the share of the larder it found 1.31%
trials that ended in a store at nothing 129
creature-ticks over the ring of water 1885
creature-ticks past the last cell 0
digest of the scored generation e3cd4c94f2fa1612
Twenty-six of the sixty-four scored nothing whatever. Half the generation is at 4.8405 grams or below. The best of them is seven times the average and infinitely better than half its cohort, and between the sixty-four of them they found 1.31 percent of the food they were offered. That is what a population looks like before anything has selected anything: mostly useless, with a long thin tail of things that happen to work. A hundred and twenty-nine of the two hundred and fifty-six trials ended with a store at nothing.
The last two lines before the digest are the plate reporting on itself. One thousand eight hundred and eighty-five creature-ticks were spent standing over the ring of water, which is momentum finishing a step the check had already refused: an animal cannot choose to run at the ring, and one already moving fast enough carries over the edge of the soil anyway. Nought creature-ticks were spent past the last cell of the plate, across all sixty-four genomes and all two hundred and fifty-six trials. The ring holds.
Splitting the work without moving the answer
Sixty-four genomes times four trials is two hundred and fifty-six runs of six hundred ticks, and there is no reason for a machine with eight cores to do them one after another. The creature phase already splits its fan casting across goroutines on the argument that nothing one fan writes is anything another fan reads. The arena has that same argument available and a much stronger version of it: a trial builds its own valley, its own bed, its own roster and its own creature, reads a layout nobody writes to, and draws no random number at all. Two genomes being scored at the same time have literally nothing in common except the layouts they are both reading.
// internal/gene/arena.go, continued
// Generation scores a whole population and hands back one card per
// genome, in population order, whatever order the work was done in.
func (a *Arena) Generation(pop []*Genome) []Card {
cards := make([]Card, len(pop))
if a.Crew <= 1 || len(pop) < 2*a.Crew {
for i, g := range pop {
cards[i] = a.Run(g)
}
return cards
}
var wg sync.WaitGroup
for k := 0; k < a.Crew; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
for i := k; i < len(pop); i += a.Crew {
cards[i] = a.Run(pop[i])
}
}(k)
}
wg.Wait()
return cards
}
// Fold is the sixteen hex digits one scored generation closes on: every
// number of every card, in population order.
//
// The order is the point. The cards may be filled in by eight
// goroutines in whatever order the machine felt like finishing them,
// and this walks them by index afterwards, so the digest is a fact
// about the generation and not about the scheduling.
func Fold(cards []Card) string {
h := sha256.New()
for _, c := range cards {
put(h, math.Float64bits(c.Grams))
// ... and every other number on the card ...
}
return fmt.Sprintf("%x", h.Sum(nil)[:8])
}
$ go run ./cmd/score -mode split
score: one generation of 64 scored on 1, 2, 4, 8 and 16 goroutines
goroutines genomes a stripe digest holds
1 64 e3cd4c94f2fa1612 the first
2 32 e3cd4c94f2fa1612 yes
4 16 e3cd4c94f2fa1612 yes
8 8 e3cd4c94f2fa1612 yes
16 4 e3cd4c94f2fa1612 yes
the work went out in five different ways and came back one number
Two decisions in that listing are what make the five lines agree. The first is that a
goroutine writes cards[i] and touches nothing else on the slice, so the
answer for genome 17 is written into slot 17 whether it was finished first or last. The
second is that Fold walks the cards by index, after every goroutine has
finished, so the digest is built in population order even though the work was not done
in it. Get either one wrong and the arena becomes a bench whose answer depends on the
machine it ran on, which is not a bench.
The stripes are interleaved, genome k and k plus the crew size and so on, and not cut into blocks of eight. The cost of scoring a genome varies enormously here: genome 43 was stepped 2,400 times and genome 4 was stepped 354, so one genome can be nearly seven times the work of another. Interleaving spreads the cheap and the dear over every goroutine, which is the same reasoning the fan casting was cut up on. On the machine this was written on, an eight-core Ryzen 7 3700X running Go 1.26 on Linux, a generation takes about 280 milliseconds on one goroutine and about 70 on eight; yours will differ, and the sixteen characters will not.
Five properties of the arena are the kind that break silently, so they are tests instead of paragraphs. The first is the one everything else rests on.
$ go test ./internal/gene/ -run 'ScoringAGenome|EveryGenomeMeets|SplittingTheArena|TheLarderAccounts|TheScoreIsGrams' -v
=== RUN TestScoringAGenomeDrawsNoRandomNumber
--- PASS: TestScoringAGenomeDrawsNoRandomNumber (0.03s)
=== RUN TestEveryGenomeMeetsTheSameFourTrials
--- PASS: TestEveryGenomeMeetsTheSameFourTrials (0.04s)
=== RUN TestSplittingTheArenaDoesNotMoveItsDigest
--- PASS: TestSplittingTheArenaDoesNotMoveItsDigest (0.09s)
=== RUN TestTheLarderAccountsForEveryGramEaten
--- PASS: TestTheLarderAccountsForEveryGramEaten (0.02s)
=== RUN TestTheScoreIsGramsAndNothingElse
--- PASS: TestTheScoreIsGramsAndNothingElse (0.00s)
PASS
ok theworld/internal/gene 0.190s
The first one scores one genome on its own, scores the whole generation, and then scores that same genome on its own again, demanding all three cards be identical. If any part of the arena were quietly reading a stream, the third card would be taken from further along that stream than the first and would differ. The second checks that a whole generation leaves the four layouts exactly where it found them, and that two arenas built from one seed agree about all four. The fifth is the smallest and the one the volume's argument needs most: a card with fewer grams and far more of everything else has to score lower, and two cards with the same grams and nothing else in common have to score the same.
What the score measures
Everything above rests on one sentence that nobody has defended yet: grams eaten is the score. Here is what happens when a different sentence is chosen, and it is a run and not an argument.
The alternative is a reasonable one. A creature that covers ground is exploring, and an animal that explores will find food that an animal sitting still never sees, so the cells a creature stepped into looks like a proxy for something anybody would want. It has the advantage of being easier to measure than eating: the counter for it is already on the card. The arena keeps it behind a flag, so what it buys can be run.
// internal/gene/arena.go — inside type Arena struct
// Restless is the score as it is tempting to write it: the cells a
// creature stepped into, counted instead of the grams it ate. It is
// kept behind a flag so what a badly chosen score buys can be run
// instead of described, and it is false in every arena this book
// ships.
Restless bool
$ go run ./cmd/score -mode hack
score: the same 64 genomes, scored on cells stepped into
rank genome cells stood grams bites on grams
1 4 70 74 41.5353 110 10
2 25 62 56 20.0000 212 23
3 41 53 53 0.0000 0 55
4 59 50 53 0.0000 0 63
5 48 38 42 40.0000 358 12
6 51 31 31 0.0000 0 57
7 16 29 33 0.0000 0 46
8 31 28 32 14.8106 22 29
9 61 27 29 100.0000 405 5
10 0 23 26 56.0099 70 9
4 of those ten never took a bite
the genome this score puts first is 4, which starved in 4 of its 4 trials
and comes 10 of 64 on grams eaten
and here is what the score is really paying for: genome 51
its place on this score 6
its place on grams eaten 57
grams it ate in 2400 ticks 0.0000
bites it took 0
turns left 1147
turns right 1167
walks and sprints between them 86
cells it stood on, of 484 a trial 31
cells it stepped into 31
The same sixty-four genomes, the same four trials, the same two hundred and fifty-six
runs. Only the number at the end changed, and the board is a different board. The
creature this score puts first, genome 4, ran its store down to nothing in every one of
its four trials, and it ate 41.5353 grams, which puts it tenth on the score that was
replaced. Put it through -mode trial -genome 4 and the reason is a sprint
of 1.5883 cells a tick and four trials that lasted 154, 128, 24 and 48 ticks of six
hundred. Four of the ten best under this score never took a single bite in two thousand
four hundred ticks.
The block at the bottom is the one to sit with. Genome 51 is sixth of sixty-four here and fifty-seventh on grams, and look at how it earned that. One thousand one hundred and forty-seven turns to the left, one thousand one hundred and sixty-seven to the right, eighty-six walks and sprints between them, and no bites at all. It turned on the spot for two thousand four hundred ticks and drifted across thirty-one cell boundaries doing it, out of the four hundred and eighty-four cells a trial offers. It went nowhere. The score paid it anyway, because the score does not know the difference between covering ground and crossing a line.
The last thing to notice is that genome 51 could have eaten very well. Run it through the arena and look at the body it was drawn.
$ go run ./cmd/score -mode trial -genome 51
score: genome 51 of the generation, trial by trial
the row it is stamped out of: 25.4443 grams of body, a store of 1396.0012,
a bite of 0.7824 grams reaching 1.1537 cells, 0.5480 cells a tick at a sprint
it costs 0.3141 a tick to stand there and a full store covers 4444 ticks
trial ticks bites grams cells stood starved
1 600 0 0.0000 0 1 no
2 600 0 0.0000 21 19 no
3 600 0 0.0000 9 9 no
4 600 0 0.0000 1 2 no
all 2400 0 0.0000 31 31 0
the score is the grams column and nothing else: 0.0000
the arena's own books, in grams
fodder laid out 1880.000000
fodder still standing 1880.000000
taken off the fodder 0.000000
counted onto the score 0.000000
the two are the same number to the bit, which is what a larder nothing grows does
Twenty-five grams of body against a store of nearly fourteen hundred, a bite of 0.7824 grams, which is three times the founding row's, and a full store that covers four thousand four hundred ticks. It is a cheap body carrying a large store and a wide mouth, and it survived all four trials without difficulty. Its controller happened to come out wired so that the two turn entries beat the other four on almost every tick, and under a score that counts boundaries that made it the sixth best creature in the world. The fodder ledger at the bottom closes on exactly the number it opened on: not one gram left the larder in four minutes of world time.
The reasoning from symptom to cause is short and it does not involve a bug. The arena did exactly what it was told. It was told that a good creature is one that steps into cells, and it found the cheapest creature in the population that steps into cells, which is one that turns round on the same few of them. The mistake was made when the sentence was written, and no amount of careful implementation underneath a wrong sentence produces a right answer. That is what makes a score dangerous in a way a broken sensor is not: a broken sensor gives an answer nobody believes, and a wrong score gives an answer everybody does.
Why stable arenas isolate genomes
Strip the creatures out and there are two separate ideas on this page, and they fail in two different ways.
The first is the bench. A benchmark is a copy of the world with everything held still except the thing being measured, and the whole craft of it is deciding what "held still" means. Here it meant the ground, the water, the light, the calendar, the food, the starting cell and the starting heading, all of them fixed in advance and written down. The test of whether that worked is a run: score the same genome twice, in two different places in the work, and get the same number both times. The test at the top of the run above is that property written as code.
The mechanism that buys it is narrower than it looks: every random number the bench needs is drawn before any measuring starts, and turned into data. That single move is what lets the work go out across sixteen goroutines without the answer moving, because two things that draw from a shared sequence are two things whose answers depend on which of them got there first. It is also what makes the arena replayable, comparable and debuggable at the same time, and none of those three had to be arranged separately.
The second idea is the score, and it is the one with teeth. A score is a sentence about what you want, written in a language that cannot express most of what you want. Every term in it is a claim, and the search does not read the claim charitably. It reads it exactly. Ask for cells stepped into and you get a creature that spins in place, because spinning is the cheapest way to satisfy the letter of what was asked. This is not a fact about creatures or about networks. It is what happens whenever the number being optimised is a stand-in for something that could not be measured directly, and the gap between the stand-in and the real thing is where everything goes wrong.
Grams eaten is a much better sentence and it is a stand-in all the same. What anybody actually wants out of this valley is a creature that keeps a lineage going, and grams eaten is a guess that eating is how that is done. The guess is a good one and it is still a guess, made by a person, written into a bench, and imposed on every creature scored against it. Nothing in the arena can tell you whether it was the right guess, because the arena cannot see past its own score.
- You can name the things the arena switches off and say why the valley's own tick is never called at all, instead of called with the weather set to nothing.
- You can say how many numbers one trial layout costs off stream 21, why that count does not depend on where the cells landed, and what would break if a layout were redrawn for each genome.
- Handed a creature's bite size, You can work out the most it could possibly score in one trial, and say what part of the six hundred ticks that ceiling assumes.
- You can explain why the same generation folds to the same sixteen characters on one goroutine and on sixteen, and name the two lines of code the claim rests on.
- You can point at the ring of open water and say which existing rule makes it a wall, and why creature-ticks over it are not zero while creature-ticks past it are.
- Shown a score that counts cells stepped into, You can predict what kind of creature tops it and explain the prediction without running anything.
Exercise 1 — the creature that ran out of clock. Genome 4 is
tenth on grams and first on cells. Work out from its body how long its store would have lasted it with nothing coming in, then say why it did not last anything like
that. Check with go run ./cmd/score -mode trial -genome 4.
The run prints a body of 61.8175 grams costing 0.5793 a tick, against a store of 538.4960, so the standing clock is 538.4960 ÷ 0.5793 = 930 ticks. It had enough store to finish a six-hundred-tick trial doing nothing. It finished none of them: 154 ticks, 128, 24 and 48, for 354 out of a possible 2,400.
The gap is the legs. It sprints at 1.5883 cells a tick, and the travel charge goes up with the square of the speed, so a tick spent at full sprint costs it many times the tick a standing body pays for. The standing clock is an upper bound on a creature that never moves, and this one moved constantly, which is exactly why it topped a score that counts movement and starved in every trial.
Exercise 2 — a bench that only asks one question. Run the
generation with a single trial, go run ./cmd/score -mode spread -trials 1,
and compare the top ten with the four-trial board. Say what changed and why the arena
runs four.
The board falls apart. Eight of the ten best are tied at exactly 20.0000 grams, which is one stand of scrub stripped and no more, and the winner is genome 37, which does not appear anywhere in the four-trial top ten. Genome 43, which wins the four-trial board by a distance, comes tenth on a single trial with 20.0000 grams, because its one trial happened to be the trial where a single stand was all it could get to. One trial is one starting cell and one heading, so it ranks placement as much as it ranks the genome.
Now go the other way with -trials 8. The top ten holds nine of the
same genomes in a slightly different order, and the scores are close to double the
four-trial ones because the larder was double. Eight trials cost twice the machine
time and moved the answer hardly at all, which is what steadiness looks like when it
has already arrived. Repeated trials buy their steadiness slowly: the wobble in an
average of n runs falls with the square root of n and not with n, so the fourth
trial is worth much more than the eighth.
Exercise 3 — write a score with an opinion in it. Add a term
to Arena.Score that pays a genome one gram for every trial it survived,
rescore the generation, and say which genome the change promotes and what you have
just told the arena to want.
One line does it: return c.Grams + float64(len(a.Trials)-c.Died). The
effect is small at the top, because four grams against 179.4530 is noise, and large
below the middle, where half the board is packed into the 4.8405 grams between the
median and nothing. Genome 12, which ate 101.9936 and starved in one trial, gains
three; genome 27, which ate 140.0000 and starved in all four, gains nothing.
The promotion to look at is at the bottom. Genome 51, which ate nothing whatever
and turned on the spot for all four trials, now scores 4.00 and beats genome 14,
which found 2.3110 grams and starved in every trial; check both with
-mode trial. The term was added to reward survival and what it actually
rewards is doing nothing carefully. It is four grams of somebody's opinion, and the
search will take it as literally as it took cells stepped into. Put the line back.