A Copy With One Number Changed
Fixed-draw mutation
A genome can already be copied: three hundred and ninety-five numbers in the child match three hundred and ninety-five numbers in the parent, and the MIND block gets its own slice. Every gene of a copy takes two numbers off the stream: one to decide whether it moves, and one to decide where it moves to.
The second number is spent whether or not anything uses it. A pass over a genome of 395 numbers is 790 draws every single time, and nothing anywhere in the run can work out how many genes happened to mutate by counting the stream.
Mutation needs that rule because this world compares long runs. If a copy that changes one gene spends fewer draws than a copy that changes ten, the next child in line starts from a different stream position. The difference then leaks into every body, brain and birth after it, so the run has changed more than the gene under discussion.
The operator has two ways to get a gene wrong. A nudge keeps the parent's number and tries next door; most improvements a lineage finds are small. A reset throws the old value away and draws anywhere inside the gene's allowed span, because a lineage also needs a road out of a bad part of the range.
This page keeps both operations behind one roll, then proves the arithmetic. One hundred copied founders carry measurable differences in body factors, controller weights, temperaments and look genes, while the unchanged parts still point back to the same founder row.
Mutation rates from one roll
There are two useful ways for a copy to get a gene wrong, and a mutation operator wants both. The first is a nudge: keep the number the parent had and try just next door. Most of the improvements a lineage ever finds are small ones, because a body that is nearly right is surrounded by bodies that are nearly right, and a small step lands in that neighbourhood on purpose. The second is a fresh start: throw the gene away and write a new one anywhere in the range. A line of copies that only ever nudges can take a very long time to cross a range from one end to the other, and if a lineage has painted itself into a corner, the only way out of the corner in one step is to stop caring where it was.
Both are read off one number. A single draw between 0 and 1 is split into three bands: the bottom sliver replaces the gene, the band above it nudges the gene, and everything above that leaves the gene alone. One number, three answers, no second coin to flip. That matters for the same reason everything on this page matters: a second coin is a third draw, and this pass gets two.
// internal/gene/copy.go
// StreamMutate is the stream every mutation in this world draws from,
// and the one number this file is allowed to spend. It is its own
// stream and not a corner of an existing one, because a stream drawn a
// different number of times is a different world: the founding takes
// its cells off stream 12 and its weights off stream 14, and both of
// them have to read the same numbers in the same order whether anything
// in this valley ever makes a child or not.
const StreamMutate = 16
// Mutations opens the generator every mutation of one run draws from.
func Mutations(seed uint64) *rand.Rand {
return rand.New(rand.NewPCG(seed, StreamMutate))
}
// Rates is how wrong a copy is allowed to be: the two chances a gene
// moves at all, and how far one step may carry it when it does.
//
// Replace and Perturb are read off one draw as bands of the same
// number, not as two separate coins. A draw under Replace throws the
// gene away and writes a new one anywhere in its range. A draw under
// Perturb and not under Replace nudges the gene it already has.
// Everything else leaves the gene alone. So Perturb is the whole of
// what moves, Replace is the part of that which starts over, and the
// two rates are stated the way they are read.
//
// Step is a share of the gene's own range and not a share of the gene's
// own value, which is the difference between a nudge that means the
// same thing everywhere and a nudge that gets timid near zero. Five
// percent of a body factor's range is 0.1875 either way; five percent
// of a weight's is 0.4000; five percent of a temperament gene's is
// 0.0500.
type Rates struct {
Replace float64
Perturb float64
Step float64
}
// Usual is what every child in this valley is copied at.
var Usual = Rates{Replace: 0.01, Perturb: 0.09, Step: 0.05}
$ go run ./cmd/copy -mode rates
copy: one pass over a genome of 395 numbers, two draws a gene
the first draw decides, and it is one number read as three bands
first draw the gene the second draw becomes
under 0.01 starts over anywhere in the whole range
0.01 to under 0.09 is nudged a step of at most 5 percent of the range
0.09 and over is left alone nothing, and is drawn anyway
block genes low high range one nudge
BODY 10 0.2500 4.0000 3.7500 0.1875
MIND 378 -4.0000 4.0000 8.0000 0.4000
TEMPER 3 0.0000 1.0000 1.0000 0.0500
LOOK 4 0.0000 1.0000 1.0000 0.0500
Sight 1 0.5000 2.0000 1.5000 0.0750
395 genes x 2 draws = 790 numbers off stream 16, every copy, whatever moves
genes expected to move: 395 x 0.09 = 35.55, of which 395 x 0.01 = 3.95 start over
Look at the one nudge column, because it is doing something the code never
says out loud. A nudge is five percent of the gene's range, so the operator
hands out a different-sized step to each block without knowing what any block is for. A
body factor runs over 3.7500 and gets a step of up to 0.1875. A weight runs over 8.0000
and gets 0.4000. A temperament gene runs over 1.0000 and gets 0.0500.
Sight, which carries half the leash the other nine body genes carry because
a wider line of sight is arithmetic the phase has to pay for every tick, gets 0.0750:
half the step, for free, out of the same line of code. Had the nudge been five percent of
the gene's value instead, a gene sitting near zero would barely move at all and
a gene sitting near four would move in strides, and a temperament gene that ever reached
0.0000 would be stuck there for ever.
The third row of that figure is the whole trick and it looks like waste. Nine genes in ten come through a pass untouched, and for every one of them the second number is drawn, read by nobody and dropped: about three hundred and sixty numbers a pass, pulled out of the generator and thrown away. What they buy is that the position in the sequence after the pass is a function of the length of the genome and of nothing else at all.
The mutation walk
The operator itself is one loop with a switch in it. It walks the flat run of genes the genome chapter laid down, in block order, BODY then MIND then TEMPER then LOOK, and asks the flat walk for the range of every gene it touches so it never has to know which block it is in.
// internal/gene/copy.go, continued
// Moves is what one pass over one genome came to: the numbers it took
// off the stream, and the genes that ended up somewhere else. Draws is
// there to be checked rather than to be read, and the thing to check is
// that it is twice the length of the genome every single time.
type Moves struct {
Draws int
Replaced int
Perturbed int
}
// Moved is the genes this pass put somewhere else.
func (m Moves) Moved() int { return m.Replaced + m.Perturbed }
// Mutate walks a genome gene by gene in block order and moves the ones
// the stream says to move.
//
// Two draws a gene, always. The first decides what happens to the gene
// and the second says where it goes, and the second is taken off the
// stream whether or not anything is done with it. That is the whole of
// the discipline, and it is worth being plain about why a pass cannot
// be written the obvious way instead: draw the first number, and only
// reach for a second when the first says the gene is moving. That
// version is shorter, it draws a little over half as many numbers,
// and it makes a run that cannot be replayed, because how many it
// spends on a genome depends on what the numbers said. Every draw after
// the first gene that moves lands at a different place in the sequence.
//
// The clamp is not here. Genome.Put holds every number it is given
// inside that gene's own range, so a perturbation that would walk a
// body factor past four stops at four and a replacement can only ever
// name a number the range already contains.
func (r Rates) Mutate(g *Genome, s *rand.Rand) Moves {
var m Moves
for i, n := 0, g.Len(); i < n; i++ {
what, where := s.Float64(), s.Float64()
m.Draws += 2
sp := g.Span(i)
switch {
case what < r.Replace:
g.Put(i, sp.Lo+where*sp.Wide())
m.Replaced++
case what < r.Perturb:
g.Put(i, g.At(i)+(where*2-1)*r.Step*sp.Wide())
m.Perturbed++
}
}
return m
}
// Child is one parent's genome copied and then copied wrong: a deep
// copy the parent shares nothing with, put through one mutation pass.
// The parent is not touched, which is the whole reason the copy happens
// before the pass and not after it.
func (r Rates) Child(parent *Genome, s *rand.Rand) (*Genome, Moves) {
g := parent.Copy()
return g, r.Mutate(g, s)
}
Two things this listing does not do are as load-bearing as the loop. It does not
clamp, because Genome.Put holds every number it is handed inside that gene's
own range before it writes it, so a nudge that would carry a body factor past four stops
at four and there is no road into a genome that skips the check. And Child
copies before it mutates, never after, so the parent that made the child is exactly the
parent it was before the child existed. Getting that backwards produces a lineage where
every ancestor quietly becomes its own descendant, and nothing about it looks wrong until
somebody prints a family tree.
// internal/gene/copy.go, continued
// Apart is how far two genomes have got from each other, gene by gene:
// the largest gap anywhere in the flat walk, and which gene it is at.
// It is a plain largest-difference and not a distance with weights in
// it, because at this point in the valley nothing has any business
// deciding that one gene matters more than another.
func Apart(a, b *Genome) (at int, gap float64) {
at = -1
for i, n := 0, a.Len(); i < n; i++ {
d := a.At(i) - b.At(i)
if d < 0 {
d = -d
}
if d > gap {
at, gap = i, d
}
}
return at, gap
}
$ go run ./cmd/copy -mode once
copy: one pass over the identity genome, seed 5, stream 16
genes walked 395
numbers taken off the stream 790
genes replaced outright 3
genes nudged 34
genes left where they were 358
block genes moved share
BODY 10 1 10.0%
MIND 378 35 9.3%
TEMPER 3 1 33.3%
LOOK 4 0 0.0%
every gene outside the MIND block that ended up somewhere else:
i block gene was is moved by
0 BODY Bulk 1.0000 1.0747 0.0747
390 TEMPER tameness 0.5000 0.4997 -0.0003
the gene that moved furthest is 71, weight 61 in MIND, by 3.9256
the row this child is stamped out of:
i gene factor the row stamped
0 Bulk 1.0747 40.0000 42.9896
1 Full 1.0000 400.0000 400.0000
2 Basal 1.0000 0.0040 0.0040
3 Work 1.0000 0.6000 0.6000
4 Bite 1.0000 0.2500 0.2500
5 Convert 1.0000 4.0000 4.0000
6 Reach 1.0000 1.0000 1.0000
7 Sight 1.0000 12.0000 12.0000
8 Top 1.0000 0.4500 0.4500
9 Swing 1.0000 0.0833 0.0833
Thirty-seven genes of three hundred and ninety-five ended up somewhere else, which is
nine and a bit percent, and the block shares are 10.0, 9.3, 33.3 and 0.0 percent. The last two are what a rate of nine
percent looks like when it is only rolled three or four times: one gene of three is a
third, zero genes of four is nothing, and neither says anything about the operator. The one body gene that moved is Bulk,
nudged from 1.0000 to 1.0747, which stamps a body of 42.9896 grams instead of 40.0000.
One temperament gene moved by three ten-thousandths. Nothing in this valley reads a
temperament gene, and it moved anyway, because the operator has no idea which genes
anything reads.
The gene that moved furthest is number 71, the sixty-second weight of the controller, by 3.9256. It is a weight for two reasons that will hold on every run of this thing. The MIND block is 378 of the 395 numbers, so nearly every gene is a weight. And a weight's range is 8.0000 wide against a body factor's 3.7500, so a replacement that lands at one end of a weight's range starts further from where it began than any body factor could.
Now push the operator past where it is ever asked to work, so the edges show. Turn the
nudge rate up to certainty and the step up to half a range, point it at
Sight, and run forty copies in a chain.
$ go run ./cmd/copy -mode clamp -step 0.5 -rungs 40
copy: gene 7, Sight, nudged every copy for 40 copies
the range is 0.5000 to 2.0000 and one nudge is at most 0.7500
rung factor cells at an end
1 0.8882 10.6583 no
2 1.2057 14.4680 no
3 1.3124 15.7485 no
4 1.4959 17.9506 no
5 1.6618 19.9421 no
6 1.3930 16.7154 no
7 0.8977 10.7723 no
8 1.4060 16.8724 no
9 0.7768 9.3220 no
10 0.5000 6.0000 yes, the bottom
15 0.5000 6.0000 yes, the bottom
20 2.0000 24.0000 yes, the top
25 1.2563 15.0753 no
30 1.6544 19.8524 no
35 1.6021 19.2249 no
40 1.2797 15.3559 no
5 of 40 copies ended sitting on the top of the range and 6 on the bottom
the factor never left 0.5000 to 2.0000, so the cells never left 6.0000 to 24.0000
Rung 10 asks for a factor below half and gets 0.5000. Rung 20 asks for one above two and gets 2.0000. Eleven of the forty copies finished sitting exactly on an end of the range, and the stamped sight cap never once left the band from six cells to twenty-four. The clamp lives inside the write itself, which is what keeps it from being a validation step some caller can forget. The number that leaves the operator and the number that lands in the genome are allowed to differ, and the one in the genome is always a number this world has prices for. A genome that stored what the operator asked for and clamped on the way out would print a factor above two — the run says one nudge at this step is at most 0.7500, so as high as 2.7500 — in the record of an animal that could only ever see twenty-four cells.
Sitting on an end is a perfectly good place for a gene to be, and the copy that put it there did no less work than any other. A gene pinned at the top of its range is a gene the operator is still pushing at, and one nudge in the other direction moves it off again. What the clamp buys is that no arithmetic anywhere in this world, however many copies deep, can produce a creature with a negative sight cap or a basal rate a hundred times the row's.
The thrifty mutation pass
Here is the operator as almost everybody writes it the first time. Draw the number that decides. If it says the gene is moving, draw a second one to say where. If it does not, move on. It is shorter, it is obviously equivalent, and it spends a little over half as much of the generator.
// cmd/copy/main.go
// thrifty is the operator as it is tempting to write it: the second
// draw taken only when the first one says a gene is moving. It is here
// to be run and not to be shipped.
func thrifty(r gene.Rates, g *gene.Genome, s *rand.Rand) gene.Moves {
var m gene.Moves
for i, n := 0, g.Len(); i < n; i++ {
what := s.Float64()
m.Draws++
sp := g.Span(i)
switch {
case what < r.Replace:
g.Put(i, sp.Lo+s.Float64()*sp.Wide())
m.Draws++
m.Replaced++
case what < r.Perturb:
g.Put(i, g.At(i)+(s.Float64()*2-1)*r.Step*sp.Wide())
m.Draws++
m.Perturbed++
}
}
return m
}
$ go run ./cmd/copy -mode count
copy: what a pass costs the stream, at four settings of the same operator
rates draws moved 2 x genes
replace 0.00, perturb 0.00 790 0 790
replace 0.01, perturb 0.09 790 37 790
replace 0.00, perturb 1.00 790 395 790
replace 1.00, perturb 1.00 790 395 790
three children in a row off one stream, made two ways
child fixed thrifty and the two of them
1 790 441 differ most at gene 71, by 3.9256
2 790 421 differ most at gene 71, by 4.0381
3 790 441 differ most at gene 71, by 4.0381
total 2370 1303
the fixed rule spent 2370 numbers, the thrifty one 1303
every child after the first is a different child, on the same seed
The top table is the promise. Rates that move nothing at all, the rates this valley actually uses, rates that nudge every gene, and rates that replace every gene: four operators with nothing in common except the length of the genome, and every one of them takes 790 numbers. A run that ships a change to the mutation rates does not have to think about what that does to the stream, because it does nothing to the stream.
The bottom table is what the thrift costs. Three children in a row, made from one seed by each operator. The fixed rule spends 790, 790 and 790. The thrifty one spends 441, then 421, then 441: three different numbers, and the reason they differ is that a different number of genes happened to move each time. By the third child the two operators have spent 2,370 numbers and 1,303 numbers of the same sequence, so they are reading in two different places and will be for the rest of the run.
The line to sit with is the first one. Child 1 already differs, most at gene 71, by 3.9256. Both operators read the same first number for gene 0 and agreed about what to do with it. They part company at the first gene neither of them moved: the fixed rule burned its second number there and the thrifty one did not, so from the next gene on the two passes were reading the sequence one step out of step. Nothing about that shows up as an error. Both children are valid genomes. Both runs are perfectly deterministic on their own. They are just two different worlds, and if the one you are trying to reproduce is the other one, no amount of holding the seed fixed will get you back.
This is the rule that makes replay possible at all, and it generalises past mutation without changing a word: the number of draws an operation takes must be a function of its inputs' sizes and never of its inputs' values. A pass over a genome of G numbers costs 2G draws. Not "about 2G". Not "2G on average". Exactly 2G, on a genome of all zeroes, on a genome that has been copied ten thousand times, at any rates anyone ever sets.
One hundred founder copies
One pass moves a genome hardly at all. Ninety-one percent of it is untouched, and of the nine percent that moved, eight parts in nine only shuffled a little. The interesting question is what a hundred of those in a row come to, because that is what a lineage is: a chain of copies, each one made from the copy before it, with nothing anywhere keeping a record of the original.
$ go run ./cmd/copy -mode ladder
copy: 100 copies in a chain from the identity genome, seed 5
rung draws moved Bulk Sight Top furthest
1 790 37 1.0747 1.0000 1.0000 3.9256
10 7900 367 1.0747 1.0107 1.0000 4.0000
25 19750 888 1.0787 0.9832 0.9019 3.9641
50 39500 1770 0.9782 1.7826 0.8043 4.0000
75 59250 2675 1.0770 1.7826 0.8829 4.0000
100 79000 3574 1.1402 1.9591 0.6117 4.0000
79000 numbers off stream 16 for 100 copies, and 3574 genes moved
the gene that wandered furthest in each block:
block i gene started ended moved by
BODY 1 Full 1.0000 2.4010 1.4010
MIND 10 weight 0 0.0000 4.0000 4.0000
TEMPER 390 tameness 0.5000 0.2512 0.2488
LOOK 391 coat hue 0.5000 0.0561 0.4439
furthest of all 395: gene 10, weight 0 in MIND, 4.0000 from where it started
Follow the three body columns down. Bulk starts at 1.0000, is nudged on the
first rung to 1.0747, and is still at 1.0747 at rung 10, because nine more passes went by
without the first draw for gene 0 ever coming in under 0.09. By rung 100 it is 1.1402,
having been touched eight or ten times in a hundred passes and having wandered up and
back. Sight goes the other way: 1.0000 on the first rung, barely off it by
rung 10, and 1.9591 at the end, with most of that gained in one jump between rung 25 and
rung 50 where a draw under 0.01 landed on it and it started over near the top of its
range. Top drifts down from 1.0000
to 0.6117. Three genes, three completely different histories, off one operator with one
setting.
The per-block table at the bottom is the one to keep. In BODY the furthest traveller is
Full, which more than doubled. In TEMPER it is tameness, which nothing reads
and which moved anyway. In LOOK it is the coat hue, which fell from the middle of its range
almost to the floor. And in MIND, weight 0 went from 0.0000 to exactly 4.0000 and stopped
there, which is the clamp again: the operator went on pushing and the range refused.
The same hundred rungs read out as records instead of as a table, six of them, one line of JSON each. It is the same ladder either way, and a line-per-record file is the form something other than a person can read without a parser of its own.
$ go run ./cmd/copy -mode tape -rungs 6
{"rung":1,"draws":790,"moved":37,"bulk":1.0747,"sight":1,"top":1,"gene":71,"gap":3.9256}
{"rung":2,"draws":1580,"moved":70,"bulk":1.0747,"sight":1,"top":1,"gene":71,"gap":3.9256}
{"rung":3,"draws":2370,"moved":104,"bulk":1.0747,"sight":1.0107,"top":1,"gene":71,"gap":3.9256}
{"rung":4,"draws":3160,"moved":145,"bulk":1.0747,"sight":1.0107,"top":1,"gene":71,"gap":4}
{"rung":5,"draws":3950,"moved":196,"bulk":1.0747,"sight":1.0107,"top":1,"gene":71,"gap":4}
{"rung":6,"draws":4740,"moved":237,"bulk":1.0747,"sight":1.0107,"top":1,"gene":71,"gap":4}
gap holds at 3.9256 for three rungs and then goes to exactly 4, which is a
weight arriving at the top of its range and being held there. draws climbs by
790 a line and by nothing else, which is the whole of this chapter's discipline written
down as a column of numbers you can check by subtraction.
Start with how many genes move. Every gene is looked at once a pass, and it moves when the first draw comes in under 0.09. Over 395 genes that is
moved = G × p = 395 × 0.09 = 35.55
and the run above printed 37 on its first pass and 3,574 over a hundred passes, which is 35.74 a pass. Those are the same number with the roughness a real draw has.
Now one gene, over many copies. Being nudged is rare, so ask how long the wait is. A gene is nudged when the draw lands between 0.01 and 0.09, a band 0.08 wide, so on average it waits
1 ÷ 0.08 = 12.5 copies
between one nudge and the next, and it starts over completely once in a hundred copies.
In a hundred rungs a given gene is nudged about eight times and replaced about once, and
that is why Bulk above sat still for whole stretches of the ladder.
Each nudge is a step of a size drawn evenly between minus and plus the nudge size, and the steps are independent, so they do not add up: they partly cancel. A run of n steps of typical size s ends up about
spread = s × √n
away from where it started, not s × n. That square root is why a hundred copies is
not a hundred times as far from the parent as one copy. For a body factor the nudge is at
most 0.1875 and its typical size is about 0.108, so eight nudges carry a gene about
0.108 × √8 = 0.31 away from where it began. Bulk in the run above
ended 0.1402 from 1.0000, which is inside that, and Top ended 0.3883 away,
which is right on it.
The replacement rate is the other half of the story, and it does not care about square
roots at all. One copy in a hundred throws a gene anywhere in its range, so a hundred
rungs of ladder gives each gene about one chance to cross its whole range in a single
step. Sight took that chance: it stood at 0.9832 on rung 25 and at 1.7826 on
rung 50, and nudging cannot plausibly account for that: a nudge moves a sight factor
by at most 0.0750, a gene is nudged about one copy in twelve, and two or three nudges
do not cover eight tenths of a factor. Nudging is how a lineage searches near where it is; replacing is how it gets
anywhere else.
The ground a copied body walked through
A ladder in a text file is a claim about numbers. Put the same copies into The Hollow and they become a claim about animals. The bench founds the herd exactly as it has always been founded, taking its cells off stream 12 and a brain apiece off stream 14, and then puts every founder's genome through twenty-four mutation passes off stream 16 before the row is stamped. Nineteen creatures are founded, which is what that founding has always produced, because nothing this chapter does touches a draw either of those two streams was going to make.
Then the ledger stopped closing.
$ go run ./cmd/copy -mode valley -adrift
copy: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
19 creatures founded on stream 12, a brain apiece off stream 14,
and every genome put through 24 mutation passes off stream 16
the ground rules are the old ones: one cell of step check, and no rim
the five fastest of them, in cells a tick at a sprint:
id Top the row coast, cells
5 1.7613 0.4500 2.6420
16 1.4938 0.4500 2.2407
8 1.1887 0.4500 1.7831
12 0.4977 0.4500 0.7465
3 0.4854 0.4500 0.7281
this run
ticks run 6750
creatures struck off 19
creatures still walking 0
creature-ticks spent past the last cell 1
grams of carcass buried off the grid 40.000000
grams in 44687.958166
grams out 44647.958166
the two ledgers differ by 40.000000 grams, which is not a rounding
Forty grams, exactly. A ledger that closes leaves a residue of a fraction of a nanogram, the last bits of tens of thousands of additions. This is a whole number of grams that went into the valley and cannot be found anywhere in it, and forty is the founding row's own body mass, so it is one entire body gone at once and not a leak spread over thousands of transfers.
The line above it says where. One creature-tick was spent past the last cell of the grid, and 40.000000 grams of carcass were buried off the grid. The ground the valley keeps books on is a rectangle of cells; the litter bed takes a pile and drops it without a word when the cell it is handed is not one of its own. So a body that ran off the edge of the world and starved out there was written into the death list, counted as struck off, counted as grams that had left the living, and then handed to a bed that quietly refused it. Both halves of the roster's books said the carcass had gone somewhere. Nowhere was somewhere.
The cause is four lines further up the same run, in the coast column. Creature 5 sprints
at 1.7613 cells a tick where the founding row sprints at 0.4500, which is a
Top factor of 3.9140 on the row's own number. Creature 16 does 1.4938 and
creature 8 does 1.1887. The step check that decides
whether an animal may put weight somewhere asked one question and had asked it for as
long as there had been animals: is the cell one cell along the heading ground You can walk
on. That question is right if and only if the body cannot get past that cell, and a body
cannot get past that cell only if the legs can stop it inside one. Nothing in this world
stops instantly. A tick of legs can change a velocity by a quarter of the top speed, so a
body running flat out takes four ticks to stop and travels one and a half times its top
speed doing it. At 0.4500 that is 0.6750 of a cell, safely under one, and the discrete
question was the right question by luck of the arithmetic. At 1.7613 it is 2.6420 cells,
and the check is being asked about the first third of a step whose last two thirds nobody
is checking at all.
That hole has been in the ground rules since the first creature stood up, and nothing could reach it while every animal in the world had the same legs. A genome is the first thing in this valley that can.
Two things have to change, and both of them are corrections to what
internal/beast already does rather than new machinery bolted onto it. The
first is the check: it has to cover the step, and the step is however far the legs have
committed the body, never fewer than the one cell it always looked at.
// internal/beast/move.go
// Coast is how far this body still travels after the legs stop pushing,
// in cells. A tick of legs may change a velocity by no more than one
// Ramp's worth of the top speed, so a body asked to stop sheds that
// much a tick and covers whatever speed is left over each time, until
// there is none left to shed. It is worked out at the fastest this
// creature may be asked to go this tick, because that is the speed a
// step it is allowed to take can leave it carrying.
func (b *Beast) Coast() float64 {
d := b.Kind.Top / Ramp
if d <= 0 {
return 0
}
far := 0.0
for v := b.Pace() - d; v > 0; v -= d {
far += v
}
return far
}
// internal/beast/act.go
// ground refuses a step the valley will not carry. It tests the ground
// the step covers and not the cell underfoot, because the cell
// underfoot has already been walked on.
func ground(b *Beast, v *View) error {
if _, no := b.Blocked(v); no {
return ErrNoGround
}
return nil
}
// Along is the cell d cells along the heading from where the body is
// standing.
func (b *Beast) Along(d float64) sim.Coord {
p := b.Pos.Scale(1.0 / terra.Tile)
return sim.Coord{
X: int(math.Floor(p.X + math.Cos(b.Face)*d)),
Y: int(math.Floor(p.Y + math.Sin(b.Face)*d)),
}
}
// Ahead is the first cell of a step: one cell along the heading from
// where the body is standing.
func (b *Beast) Ahead() sim.Coord { return b.Along(1) }
// Blocked is the first cell of this creature's step that the valley
// will not carry, and whether there is one.
//
// A step is not one cell. A tick of legs cannot stop a body dead, so
// the ground a step commits a creature to is everything the body is
// still crossing when the legs have finished with it: b.Coast(), and
// never less than the single cell this rule looked at for as long as
// every animal had the same legs. A body that can stop inside a cell is
// a body the old arithmetic was right about, and every creature stamped
// out of the founding row coasts 0.6750 of a cell.
//
// The walk goes out a cell at a time and stops at the first cell it
// does not like, so a refusal names the cell the creature would have
// run into rather than the far end of a step it never gets to take.
// Nothing here allocates: the ranking walk asks five or six times a
// tick, and the loop is over a count of cells and not a slice of them.
func (b *Beast) Blocked(v *View) (sim.Coord, bool) {
far := 1.0
if !b.Adrift {
if coast := b.Coast(); coast > far {
far = coast
}
}
for d := 1.0; ; d++ {
if d > far {
d = far
}
c := b.Along(d)
if !v.Valley.Bed.In(c) || v.Valley.Bed.Kind(c) == sim.Water {
return c, true
}
if d >= far {
return c, false
}
}
}
Along is the old Ahead with a distance on it, and
Ahead is now that function asked for one cell, which is the same arithmetic
the old line did: multiplying by exactly 1.0 hands a float64 straight back. So for any
creature whose coast is under a cell, Blocked looks at
b.Along(1), finds it is at or past the end of the walk, and returns. One
cell, one comparison, the same answer the old rule gave, for every cell of the valley in
every heading. Every creature stamped out of the founding row coasts 0.6750 of a cell, so
the repaired check and the old one are the same check for every animal this book has
printed a run about, and that is a fact about the arithmetic rather than a hope about the
seeds. The refusal itself got sharper for free: Blocked hands back the cell
it stopped on, so the *Refusal the log carries names the cell the creature
would have run into and not the first cell of a step it never got to take.
errors.Is(err, beast.ErrNoGround) is the same question it always was.
The second change is the rim. A check keeps a creature from choosing to run at ground that is not there; it cannot promise that momentum never carries one over anyway, because a body that is already moving has already spent that decision. The valley is one plate of ground with rock at its edge, and a body over the rock is still a body: the ground under it is the last ground the valley has, and that is the cell a carcass falls on.
// internal/beast/act.go, continued
// Underfoot is the cell of the valley this body is over: the cell it is
// standing on, or, for a body that has been carried past the last cell
// there is, the rim cell it is lying against.
//
// A position is allowed off the grid, because a body carries momentum
// no tick of legs can cancel and the rim is one cell wide. A body is
// not allowed to be nowhere. Everything this world writes onto the
// ground under a creature comes through here, and the ground under a
// creature is always ground the valley has.
func (b *Beast) Underfoot(bed *terra.Bed) sim.Coord {
c := b.Cell()
if b.Adrift {
return c
}
switch {
case c.X < 0:
c.X = 0
case c.X >= bed.W:
c.X = bed.W - 1
}
switch {
case c.Y < 0:
c.Y = 0
case c.Y >= bed.H:
c.Y = bed.H - 1
}
return c
}
// internal/beast/roster.go — inside Roster.Phase
if t.Left <= 0 {
b.Dead = true
c := Carcass{Who: b.ID, Name: b.Name, At: b.Underfoot(v.Bed),
Grams: b.Kind.Bulk, Tick: v.Now}
died = append(died, c)
r.gone = append(r.gone, home)
One expression changed in the phase: the carcass is written at
b.Underfoot(v.Bed)
instead of at b.Cell(). For every body standing on the grid those are the
same cell and the same bits, so this cannot move a run in which nothing ever left the
valley. It can only ever act on a body the step rule had already failed to hold, which is
the narrowest thing a repair can be and still be a repair.
// internal/beast/body.go — inside type Beast struct
// Adrift is the ground rules as they were written while every
// animal in the world had the same legs: the step check looking one
// cell along the heading and no further, and a body allowed to be
// nowhere at all. It is kept behind a flag so what those two cost a
// valley of unequal creatures can be run instead of described, and
// it is false in every creature this book ships.
Adrift bool
$ go run ./cmd/copy -mode valley
copy: 12x8 valley, tick 901, year 1 summer, 12 plants standing at 1517.5 grams
19 creatures founded on stream 12, a brain apiece off stream 14,
and every genome put through 24 mutation passes off stream 16
the five fastest of them, in cells a tick at a sprint:
id Top the row coast, cells
5 1.7613 0.4500 2.6420
16 1.4938 0.4500 2.2407
8 1.1887 0.4500 1.7831
12 0.4977 0.4500 0.7465
3 0.4854 0.4500 0.7281
this run
ticks run 6750
creatures struck off 19
creatures still walking 0
creature-ticks spent past the last cell 0
grams of carcass buried off the grid 0.000000
grams in 44970.601626
grams out 44970.601626
the two ledgers differ by -8.076e-10, which is the last bits of the adding
Same seed, same nineteen founders, same twenty-four passes, same three fast animals with the same coasts. No creature-ticks past the last cell now instead of one, no grams buried off the grid instead of forty, and the two ledgers differ by -8.076e-10, which is what tens of thousands of additions taken in two orders cost. The grams total moved, from 44687.958166 to 44970.601626, and it moved because three creatures out of nineteen are now refused steps they used to be allowed, so they graze different plants and the valley grows a slightly different amount of tissue. That is the repair doing its job. A run whose books do not close has no claim on any of its other numbers either, so there was nothing there to preserve.
Two properties of that repair are the kind that break quietly, so they are tests. The first walks a founding-row creature over every cell of the valley in eight headings, asks both the old check and the new one, and fails if a single refusal names a different cell. The second puts a body past the rim on purpose and asks where it would be buried.
$ go test -count=1 -run 'AStepIsChecked|NoBodyIsBuried' -v ./internal/beast/
=== RUN TestAStepIsCheckedToTheEndOfIt
--- PASS: TestAStepIsCheckedToTheEndOfIt (0.00s)
=== RUN TestNoBodyIsBuriedOffTheValley
--- PASS: TestNoBodyIsBuriedOffTheValley (0.00s)
PASS
ok theworld/internal/beast 0.002s
The first of those has a guard at the top of it that matters more than the loop does: it checks that the founding row's coast really is under a cell before it compares anything. Without that line the test would go on passing if somebody made creatures faster by default, while quietly comparing the new check against itself and proving nothing.
Why fixed draws preserve comparison
The mutation operator and the ground repair are the same idea twice, which is why they share a page. Both are about a quantity that was constant for so long that something started depending on it being constant.
For the stream it is the draw count. Nothing in this world ever asked how many numbers a copy spends, because until this page nothing copied anything. The moment something does, the count becomes part of the interface between the operator and every other user of that sequence, and an interface nobody wrote down is an interface that will be broken. Fixing the count at 2G is what turns "the mutation operator" into a component with a stated cost: it is allowed to change its rates, its bands, its step size and its whole internal design without any of that being visible to anything downstream of it. The generator was always deterministic on its own; what reproducibility actually rests on is the agreement about how much of it each thing eats.
For the valley it is the top speed. The step check treated a move as a hop from one cell to the next, which is exactly true when a body cannot travel or coast further than one cell in the time it takes the legs to stop it. Every creature in every run this book had printed satisfied that, so the check and the world agreed, and nothing on any page distinguished "this rule is right" from "this rule happens to be right for these animals". A genome makes animals nobody wrote the rules for, and the first thing this one did with that freedom was find the rule that had been resting on a number instead of on an argument.
The general form is plain, because it is not a fact about creatures. When a
discrete check stands in for a continuous quantity, the check is only as good as the bound
that made the discretisation safe, and that bound has to be written down next to the check
or it will be lost. Blocked now derives its own bound: it asks the body how far
it is committed and walks that far. It cannot go stale when the numbers change, because it
reads the numbers.
- Given a genome of any length, You can say how many numbers one mutation pass takes off stream 16, and say why that count does not change when the rates do.
- You can read a single draw as the three bands of the operator, say which band replaces and which nudges, and give the size of one nudge for a body gene, a weight and a temperament gene.
- You can explain why a nudge is a share of the gene's range and not of the gene's value, and name the gene that gets a smaller nudge for free because of it.
- Handed the number of times a gene has been nudged, You can estimate how far it has wandered, and say why a hundred copies is not a hundred nudges' worth of distance.
- You can say what a body's coast is, work it out from a top speed, and explain why a check on the next cell alone was correct at 0.4500 and wrong at 1.7613.
- Shown a run whose grams ledger is out by a whole body mass, You can name the two places a carcass can be lost and say which one leaves the death counted and the litter unchanged.
Exercise 1 — a copy that changes nothing. Predict the draws and
the genes moved for go run ./cmd/copy -mode count's first row, where both
rates are zero, then say what the stream position is afterwards and why anybody
needs to care.
No genes move and 790 numbers are drawn, which the run prints. The first draw of every pair is compared against 0.00 and fails both bands; the second is drawn anyway and dropped. Afterwards the generator sits exactly 790 numbers along, which is where it would sit if every gene in the genome had been replaced.
Why care: it means a run can carry a creature that does not mutate at all beside one that mutates hard, in the same valley, off the same stream, without the still one shifting the sequence the moving one reads. A rate is a setting on a creature and not a setting on the world.
Exercise 2 — how many copies to double a body. Work out roughly
how many copies it takes for the Bulk factor to reach 2.00 from 1.00 by
nudging alone, then check the ladder run for whether it did anything like that.
A nudge on a body factor is drawn evenly over plus or minus 0.1875, so its typical size is about 0.108. Wandering 1.00 away needs a spread of 1.00, and spread is s × √n, so √n = 1.00 ÷ 0.108 = 9.26 and n is about 86 nudges. A gene is nudged about once in 12.5 copies, so that is roughly 1,070 copies, and because the walk is as likely to go down as up it is a typical distance and not a deadline.
The ladder run bears that out: after a hundred copies Bulk is at 1.1402.
Full, the furthest-travelled body gene on that run, reached 2.4010, and
it did not walk there. It was replaced, which is the one move that crosses a range
without waiting for the square root.
Exercise 3 — a faster valley, both ways. Run
go run ./cmd/copy -mode valley -passes 40 -adrift and then the same
command without -adrift. Say what the repaired rules changed and what they
did not.
Forty passes make faster animals than twenty-four: the fastest three coast 2.6420, 2.1699 and 1.8050 cells. Under the old rules that valley spends 281 creature-ticks past the last cell of the grid and buries 166.002575 grams of carcass off it, and the ledgers part company by exactly that figure. Under the repaired rules the same valley spends 123 creature-ticks outside and buries none of them outside, and the books close to -2.983e-10.
Read the two numbers that did not go to zero. Creatures still leave the valley, 123 times, because the step check governs what an animal may choose and not what its momentum does with a choice already made. What the rim guarantees is narrower and enough: wherever a body ends up, the ground it is buried on is ground the valley has. A repair that made the first number nought as well would have to stop bodies at the rock, and stopping a body is a change to every run in the book rather than to the runs that were already broken.
Every animal in that valley was copied from one founder, and nothing in the valley decided which of the copies deserved to make the next one: all nineteen starved before the two years were out. Twenty-four passes of drift produced a creature that sprints nearly four times as fast as the row it came from, and the ground it ran over had no opinion about whether that was an improvement. Before a lineage can be said to be getting better at anything, there has to be somewhere to measure it, and it cannot be a valley where the weather, the plants and the neighbours are all moving while you watch.