Speed Has a Price
The movement bill
A creature can cross The Hollow without paying for the crossing. Its store falls by the flat bill for existing, but the push that moves it east, west or back again costs the same as standing still.
Movement is charged by the tick, and what a tick charges is the work it took: the mass being moved times the square of the speed it is moved at. A cell is not the expensive thing. Speed is.
A flat price per cell balances the books and teaches the creature the wrong lesson. One cell costs the same whether the creature spends two ticks or fifty crossing it, so sprinting everywhere wins and the row's top speed becomes the only speed that matters.
The square changes that trade. Half speed costs a quarter as much per tick, which becomes half as much per cell after the longer trip is counted. Crawling still has a floor under it, because the body pays its basal bill for every tick spent crawling.
The cheapest travel speed falls out of the basal bill and the movement price already in the row. The page prices one tick by hand, wires the charge into the body update, caps a sprint at what the store can pay for, and sends four creatures over the same twelve cells to measure what each has left.
One tick of travel
Five numbers off the row, and every one of them is already written down. The body weighs 40 grams. A full store holds 400 energy units. Each gram of body charges 0.004 units a tick just to go on being a gram. One unit of movement work costs 0.60 units of energy. And the legs top out at 0.45 cells of ground a tick, which at ten ticks a second is four and a half cells a second.
Start with the rent, because it is the number that does not care what the creature is doing. 40 grams at 0.004 a gram is 0.160 units a tick, every tick, forever. A full store on its own covers 400 ÷ 0.160 = 2,500 ticks of lying perfectly still: a little over four minutes of wall clock, and about seven tenths of a year on the calendar the plants keep.
Now a sprint. The work is the mass times the square of the speed: 40 × 0.45 × 0.45 = 8.10 units of work. At 0.60 energy units apiece that is 4.86 energy units for one tick of sprinting, thirty times the rent. Halve the speed to 0.225 and the work is 40 × 0.225 × 0.225 = 2.025, costing 1.215 units. Half the speed, a quarter of the charge. That is the square doing its job, and it is the reason a sprint is not two walks.
Per tick, though, is not the question a creature crossing a valley actually has. It wants to know what a cell costs. A tick at 0.45 covers 0.45 of a cell, so the 4.86 units buy 0.45 cells and one whole cell costs 4.86 ÷ 0.45 = 10.80. A tick at 0.225 covers 0.225 cells, so a cell costs 1.215 ÷ 0.225 = 5.40. Exactly half. Dividing the charge by the distance cancels one of the two speeds, and a cost that climbed with the square of the speed per tick climbs in a straight line with the speed per cell.
The rent goes the other way. A cell at 0.45 takes 1 ÷ 0.45 = 2.222 ticks, and those ticks owe 2.222 × 0.160 = 0.356 units of rent. A cell at 0.225 takes 4.444 ticks and owes 0.711. Add the two columns and a cell costs 11.156 units at a sprint and 6.111 at a walk. Keep slowing down and the work per cell keeps falling while the rent per cell keeps climbing, and somewhere below the walk the second one overtakes the first.
Find that point by asking when the two are equal. The work over a cell is 24 × speed, since 0.60 × 40 = 24. The rent over a cell is 0.160 ÷ speed. Set them equal: 24 × speed = 0.160 ÷ speed, so 24 × speed × speed = 0.160, so speed × speed = 0.006667, so the speed is the square root of that, 0.081650 cells a tick. At that speed each side of the bill comes to 1.9596 units and a cell costs 3.9192 all in, against 6.1111 at a walk and 11.1556 at a sprint. A full store of 400 buys 102 cells of ground at that pace and 36 at a sprint, and this valley is twelve cells wide.
All of that was numbers. Here is the shorthand, useful because the same two lines are about to be asked for a hundred creatures at a hundred speeds. Call the body's mass m, the speed it is travelling at v (in cells a tick, always), the price of a unit of work w, and the rent one gram owes each tick b. A dot between two letters means multiply, so m·v·v is 40 × 0.45 × 0.45.
One tick of travel charges the work it took at the price of work:
tick = w·m·v·v
Check it: 0.60 × 40 × 0.45 × 0.45 = 4.86. One tick covers v cells, so dividing by v gives the charge for one whole cell, and one of the two v's cancels. The rent for that cell is what the body owes over the 1 ÷ v ticks the cell takes:
cell = w·m·v + b·m ÷ v
Check it at a sprint: 0.60 × 40 × 0.45 = 10.80, plus 0.004 × 40 ÷ 0.45 = 0.356, giving 11.156. The left term climbs with v and the right term falls with it, so their sum has a lowest point, and the lowest point is where the two terms are equal. Setting them equal and tidying leaves v·v = b ÷ w, so the speed itself is the square root of that. The square root of a number is the number that multiplies by itself to give it back: the square root of 9 is 3, because 3 × 3 is 9. Written with the tick mark √:
best = √(b ÷ w)
√(0.004 ÷ 0.60) = √0.006667 = 0.081650, the mark on the lower panel of Figure 46.1. Look at what is not in that formula: m. The mass cancelled when the two terms were set equal, so the cheapest speed to travel at is the same for a forty-gram creature and a four-hundred-gram one. Bigger animals do not get a better cruising speed out of this law. They get the same one and a bigger bill for using it.
The TravelCharge method
Seven functions, none longer than a line or two, and every one of them answers a question about a kind of creature without needing a creature to exist. They go in a new file beside the row, because the row is a table of numbers and this is what the numbers mean.
One thing has to be settled before the first line: which unit a speed is in. A body keeps its position and its velocity in world pixels, because that is what every moving thing in this world has done since the first one. The row writes distances in cells, because that is what the ground is measured in. Sixteen pixels make a cell, and if the two are ever confused the charge comes out 256 times wrong. So there is exactly one function that converts, and everything downstream of it takes cells.
// internal/beast/move.go
// Speed is how fast a creature is actually travelling, in cells a
// tick. The body moves in world pixels, because everything in this
// world that moves keeps its position in world pixels; the price of
// moving is written in cells, because cells are the unit this valley's
// distances come in. terra.Tile is the only place the two units meet.
func (c *Beast) Speed() float64 { return c.Body.Vel.Len() / terra.Tile }
// Rent is what a body of this kind costs a tick whatever it does: the
// basal rate charged on every gram of it.
func (k Kind) Rent() float64 { return k.Basal * k.Bulk }
// Effort is the movement work one tick at this speed comes to: the
// mass being moved, times the square of the speed it is moved at, with
// the speed in cells a tick. It is work and not yet energy, which is
// what the row's Work is for.
func (k Kind) Effort(speed float64) float64 { return k.Bulk * speed * speed }
// Travel is the energy one tick at this speed charges the store: the
// work that tick took, at the row's price for a unit of it.
func (k Kind) Travel(speed float64) float64 { return k.Work * k.Effort(speed) }
// Cross is what one whole cell of ground costs to get over at this
// speed, with the upkeep left out of it: the tick's charge divided by
// the cells that tick covered. One of the two speeds cancels, so this
// is a straight line up from nothing and not a curve.
func (k Kind) Cross(speed float64) float64 {
if speed <= 0 {
return math.Inf(1)
}
return k.Travel(speed) / speed
}
// Cost is what that same cell costs with the upkeep put back in: the
// work the legs did crossing it, plus the rent the body paid for the
// ticks it spent getting across. Going fast costs work and going slow
// costs time, and this is the one number that adds the two together.
func (k Kind) Cost(speed float64) float64 {
if speed <= 0 {
return math.Inf(1)
}
return k.Cross(speed) + k.Rent()/speed
}
// Cheapest is the speed at which Cost is least: the speed where the
// rent charged over one cell and the work charged over that cell come
// to the same number. Below it a creature pays more to exist than its
// legs are saving; above it the legs cost more than the clock does.
func (k Kind) Cheapest() float64 { return math.Sqrt(k.Rent() / (k.Work * k.Bulk)) }
$ go run ./cmd/pace -mode row
pace: the browser's row, and what it prices before a tick is taken
bulk 40.0000 grams of body
a full store 400.0000 energy units
basal 0.0040 energy units a gram a tick
work 0.6000 energy units a unit of movement work
top 0.4500 cells a tick at a sprint
standing still it is charged 0.1600 a tick, so a full store on its own
covers 2500 ticks
what one tick of travel costs at four speeds, what a cell costs at them,
and how far a full store goes
cells effort a tick a cell all in reach
a crawl 0.020000 0.0160 0.0096 0.4800 8.4800 47.1698
the cheapest 0.081650 0.2667 0.1600 1.9596 3.9192 102.0621
a walk 0.225000 2.0250 1.2150 5.4000 6.1111 65.4545
a sprint 0.450000 8.1000 4.8600 10.8000 11.1556 35.8566
effort is bulk times the square of the speed; a tick is that at 0.60 a unit;
a cell is the tick's charge spread over the cells the tick covered; all in
adds the 0.1600 of rent those ticks also owed; reach is the cells a full
store buys at that speed and nothing to eat on the way
the cheapest speed is 0.081650 cells a tick, where the rent over a cell and the
work over it are both 1.959592, and a cell costs 3.919184 all in
crossing 12 cells at it costs 47.0302 against 73.3333 at a walk and 133.8667 at a sprint
a full store would pay for 4.0817 cells a tick if the legs could deliver it,
which is 9.07 times the 0.45 they can. The store stops paying for a sprint
at 5.0200 units left, 0.0125 of a full one
Every figure in that table was worked out on paper two sections ago, and not one tick
was taken to produce any of it. The reach column is the one to keep an eye
on: a full store crosses 102 cells at the cheapest speed, 65 at a walk and 36 at a
sprint, so a sprinting creature has about three valley widths in it before the store is
empty and a cruising one has eight and a half. The last two lines are the other end of
the same law. A full store would pay for 4.0817 cells a tick if legs existed that could
deliver it, nine times what these legs can, so for almost all of a creature's life the
body is the limit and the store is not.
The paid push
A price is not a charge until something pays it, and paying it needs three things: a body for the creature to have, a force to put in that body's accumulator, and a tick that settles up afterwards.
The body is the same field.Body everything else in this world moves with,
made with the row's own mass, so a creature is forty grams to the physics as well as to
the ledger. Position and velocity live in it, in world pixels, and nothing about the
charge changes that. Beast already exists and already keeps a store, so the
body is one line added to it.
// internal/beast/body.go — the line Beast gains
field.Body // where it is and how fast, in world pixels
That line has no field name in front of it, and the missing name is the point. An
embedded struct lends its own fields to whatever it is embedded in, so a creature's
position reads c.Pos and its velocity reads c.Vel, with no
middle word to remember and no pair of one-line methods forwarding them by hand. The
whole body is still there under its type name whenever something wants to hand the
physics an entire body: c.Body. Name the field instead and only the second
of those two spellings works, and every line that wants a velocity has to name the body
before it can name the velocity.
Both files gain an import along with the line. body.go and
beast.go have needed nothing but their own package until now; a body and
the constructor below both name field, so each file gets
import "theworld/internal/field" at the top. The compiler will
say so if it is forgotten, and it is the only housekeeping this chapter adds.
A creature is now two things that have to agree about how heavy it is, so one call makes
both. The store is filled from the row's Full and the body is built on the
row's Bulk, and from the first tick the physics and the ledger are looking
at the same forty grams.
// 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 force is where a trap is waiting. Steering answers a request for a velocity with a vector: the velocity the body wants, less the velocity it has, cut down to a limit. The walkers that machinery was written for had a mass of one, and on a body of mass one a change in velocity and the force that buys it are the same number, so nobody ever had to say which of the two the limit was. On forty grams they part company at once. Hand a browser the walkers' cap of 0.3 and the acceleration it produces is 0.3 ÷ 40 = 0.0075 pixels a tick per tick, which needs 960 ticks to reach a sprint. The creature would spend a minute and a half getting up to speed.
// internal/beast/move.go
// Ramp is how many ticks a creature standing still takes to reach its
// top speed. It is what turns a top speed into the hardest push the
// legs can give, and it sits here rather than in the row because four
// tenths of a second is the same answer for everything in this valley
// that walks.
const Ramp = 4
// Afford is the fastest a creature could travel this tick without
// asking its store for more than the store holds: whatever is left
// after the rent it owes however it spends the tick, divided by the
// price of a unit of work and by the mass being moved, then square
// rooted, because that is what undoes the square in Effort.
func (c *Beast) Afford() float64 {
left := c.Store - c.Kind.Rent()
if left <= 0 {
return 0
}
return math.Sqrt(left / (c.Kind.Work * c.Kind.Bulk))
}
// Pace is the fastest this creature may be asked to go this tick: the
// top speed its legs have, or the top speed its store will pay for,
// whichever of the two runs out first.
func (c *Beast) Pace() float64 { return math.Min(c.Kind.Top, c.Afford()) }
// Gait is what this creature can do about its own motion this tick, in
// the world pixels field.Gait steers in. It may aim at no velocity
// faster than its Pace, and it may change the velocity it has by no
// more than one Ramp's worth of its top speed in a tick. The second of
// those is a limit on acceleration and not on force, which is the
// whole reason Push exists.
func (c *Beast) Gait() field.Gait {
return field.Gait{
MaxSpeed: c.Pace() * terra.Tile,
MaxForce: c.Kind.Top * terra.Tile / Ramp,
}
}
// Push is the force the legs put into the accumulator to aim at a
// velocity. Three things happen to the velocity asked for. It is cut
// to the Pace this creature may travel at this tick, because
// field.Gait.Steer enforces its force limit and takes the desired
// velocity entirely on trust. The difference between what is left and
// the velocity the body has is cut to what a tick of legs can change.
// And the result is multiplied by the mass that has to be moved,
// because steering was written against bodies of one unit of mass,
// where a change in velocity and the force that buys it are the same
// number; on forty grams they are not, and the mass is the whole of
// the difference. That last line is field.Body.Weight's trick, done
// to a push instead of to a pull.
func (c *Beast) Push(want field.Vec2) field.Vec2 {
g := c.Gait()
if want.Len() > g.MaxSpeed {
want = want.Unit().Scale(g.MaxSpeed)
}
return g.Steer(c.Body, want).Scale(c.Kind.Bulk)
}
Three limits, doing three different jobs. MaxForce here is an acceleration:
a sprint is 0.45 × 16 = 7.2 pixels a tick, and a quarter of that is 1.8, so the
legs may change a velocity by 1.8 pixels a tick in any one tick and a standstill becomes
a sprint in four. MaxSpeed is the velocity a creature is allowed to aim
for, which is where the store gets a vote. And the Scale at the end turns
the whole thing from an acceleration into a force by multiplying by the mass, exactly
the way a body turns a downward acceleration into its own weight. Leave that
multiplication out and a heavy creature accelerates like a leaf.
The cut on want in the first two lines matters more than it looks. Steering
cuts the force it produces and trusts the desired velocity it was handed, so a caller
that asks for a sprint gets pushed toward a sprint whatever MaxSpeed says.
Capping the request is the caller's job, and Push is the caller for every
creature in this book.
// internal/beast/move.go
// Toll is what one tick took out of one creature's store, in the order
// the charges are worked out.
type Toll struct {
Rent float64 // energy the body charged for being a body
Speed float64 // cells a tick the body actually travelled
Effort float64 // the movement work that speed came to
Haul float64 // energy that work charged
Spent float64 // the two charges added together
}
// Step is one tick of one creature's motion: whatever the tick piled
// into the accumulator moves the body, and then the store is charged
// for the two things a body costs, being one and moving one. The
// charge is worked out after the move because the speed being priced
// is the speed that did the moving, not the one the creature had
// before the tick's forces arrived.
func (c *Beast) Step() Toll {
c.Body.Step()
t := Toll{Rent: c.Kind.Rent(), Speed: c.Speed()}
t.Effort = c.Kind.Effort(t.Speed)
t.Haul = c.Kind.Travel(t.Speed)
t.Spent = t.Rent + t.Haul
c.Store -= t.Spent
return t
}
// cmd/pace/main.go — the loop under every table in this chapter: ask
// for a velocity, push, step, print what the tick took
for t := 1; t <= ticks && c.Store > 0; t++ {
pace := c.Pace()
c.Body.ApplyForce(c.Push(want))
l := c.Step()
fmt.Printf(" %5d %9.6f %9.4f %9.6f %9.4f %9.4f %9.4f %11.4f\n",
t, pace, c.Body.Vel.Len(), l.Speed, l.Effort, l.Haul, l.Spent, c.Store)
}
$ go run ./cmd/pace -mode hold -ticks 6
pace: one browser asked to hold 0.4500 cells a tick from a standstill
that is 7.2000 world pixels a tick, and one tick of legs may change a
velocity by 1.8000 pixels, so it is 4 ticks getting there
opening store 400.0000
tick pace pixels cells effort haul spent store
1 0.450000 1.8000 0.112500 0.5062 0.3037 0.4637 399.5362
2 0.450000 3.6000 0.225000 2.0250 1.2150 1.3750 398.1612
3 0.450000 5.4000 0.337500 4.5563 2.7338 2.8938 395.2675
4 0.450000 7.2000 0.450000 8.1000 4.8600 5.0200 390.2475
5 0.450000 7.2000 0.450000 8.1000 4.8600 5.0200 385.2275
6 0.450000 7.2000 0.450000 8.1000 4.8600 5.0200 380.2075
2.0250 cells covered, 19.7925 units spent, 380.2075 left
Four ticks of acceleration, exactly as the header predicted, and then a flat line at
5.0200 a tick. The square is visible in the haul column without any
arithmetic. The second tick travels twice as fast as the first and its haul is four
times as big, 0.3037 becoming 1.2150. The fourth travels four times as fast and its haul
is sixteen times as big, 4.8600 against that same 0.3037, because four squared is
sixteen. Six ticks of it bought 2.0250 cells of ground and cost 19.7925 units, which is
5% of everything the creature had.
The 5.0200 is a number to hold on to. It is the whole cost of one sprinting tick, rent
included, and it is also the store at which a creature stops being able to sprint at
all: below 0.160 + 4.86 units there is not enough left to pay for one tick of it. That
is 1.25% of a full store, which is why Afford has been sitting in the code
doing nothing visible. Ask a creature with three units left for a sprint and it becomes
visible immediately.
$ go run ./cmd/pace -mode hold -store 3 -ticks 10
pace: one browser asked to hold 0.4500 cells a tick from a standstill
that is 7.2000 world pixels a tick, and one tick of legs may change a
velocity by 1.8000 pixels, so it is 4 ticks getting there
opening store 3.0000
tick pace pixels cells effort haul spent store
1 0.343996 1.8000 0.112500 0.5062 0.3037 0.4637 2.5362
2 0.314659 3.6000 0.225000 2.0250 1.2150 1.3750 1.1613
3 0.204252 3.2680 0.204252 1.6688 1.0013 1.1613 0.0000
0.5418 cells covered, 3.0000 units spent, 0.0000 left
The request never changes: 0.45 cells a tick, every tick. What changes is the
pace column, and it changes because the store is draining. On the first two
ticks the legs are still the limit, since the body has not accelerated far enough for
the cap to bite. On the third the cap is 0.204252 and the creature is held there, slower
than it was going a tick earlier, because Push cut the request before
steering ever saw it. And the store lands on 0.0000 to the last digit, which is not
luck: Afford returns the speed whose charge is precisely what the store
holds, so a creature cut down to its affordable speed has, by construction, exactly one
tick left in it.
The body's velocity is right there on the body, already a number, already a length in
the units the physics uses. Charging off it directly is the obvious thing to write and
it type-checks perfectly, because Travel takes a float64 and a
length in pixels is a float64. The bench keeps that version so the two can
be run against each other.
// cmd/pace/main.go
// pixels is the charge as it was first written, with the speed taken
// straight off the body instead of turned into cells first. Every
// other number in here is the shipped one.
func pixels(k beast.Kind, speed, dist float64) {
c := k.Spawn(field.Vec2{}, 0)
want := field.Vec2{X: speed * terra.Tile}
fmt.Printf("pace: the same %.2f-cell sprint, charged on the body's own pixels a tick\n\n", speed)
fmt.Printf(" %5s %9s %9s %9s %11s\n", "tick", "pixels", "effort", "spent", "store")
for t := 1; c.Store > 0 && c.Body.Pos.X < dist*terra.Tile; t++ {
c.Body.ApplyForce(c.Push(want))
c.Body.Step()
px := c.Body.Vel.Len()
spent := k.Rent() + k.Travel(px)
c.Store -= spent
fmt.Printf(" %5d %9.4f %9.4f %9.4f %11.4f\n", t, px, k.Effort(px), spent, c.Store)
}
fmt.Printf("\n a sprint priced this way costs %.4f a tick against the %.4f it should,\n",
k.Travel(k.Top*terra.Tile), k.Travel(k.Top))
fmt.Printf(" which is %.0f times too much, and %.0f is %d squared\n",
k.Travel(k.Top*terra.Tile)/k.Travel(k.Top), math.Pow(terra.Tile, 2), terra.Tile)
}
$ go run ./cmd/pace -mode pixels
pace: the same 0.45-cell sprint, charged on the body's own pixels a tick
tick pixels effort spent store
1 1.8000 129.6000 77.9200 322.0800
2 3.6000 518.4000 311.2000 10.8800
3 5.4000 1166.4000 700.0000 -689.1200
a sprint priced this way costs 1244.1600 a tick against the 4.8600 it should,
which is 256 times too much, and 256 is 16 squared
Three ticks. The creature is still accelerating, has not reached its sprint, has covered 0.675 of a cell, and owes 689 units against a store that holds 400. Nothing crashed and no error was returned. The arithmetic ran perfectly on numbers that meant something other than what the formula thought they meant.
The diagnosis is in the size of the error, not in the code. A wrong constant would be
out by some ordinary factor. This is out by 1244.16 ÷ 4.86, and that division
comes to exactly 256, which is 16 squared, and 16 is the number of pixels in a cell.
When a quantity is wrong by the square of a conversion factor, the conversion was
skipped somewhere upstream of something that squares its argument:
Effort multiplies the speed by itself, so one missing division by 16
arrives as a factor of 256 by the time the charge lands. Finding the fault is then a
matter of asking which of the two units the formula was written in, and the answer is in
the comment on Effort: cells a tick.
The rule is older than this world. A number is not its units. Two quantities
that are both float64 and both called a speed can be measured in different
things, and no compiler will ever mention it. The defence is not more types; it is
having exactly one function that converts, giving it a name, and making every formula
downstream take the converted unit and say so in its comment. Speed is that
function here, and the fault above is precisely that it was not called.
Four speeds over twelve cells
The bench can now send a creature somewhere and add up what it cost. One loop does it: ask for a velocity, push, step, keep the tolls, and stop when the far side arrives or the store runs out.
// cmd/pace/main.go
// cruise runs one creature east at a chosen speed until it has covered
// dist cells of ground, and hands back the ticks it took, the charges
// added up, and the creature itself.
func cruise(k beast.Kind, speed, dist float64) (int, beast.Toll, *beast.Beast) {
c := k.Spawn(field.Vec2{}, 0)
want := field.Vec2{X: speed * terra.Tile}
var sum beast.Toll
n := 0
for c.Body.Pos.X < dist*terra.Tile && c.Store > 0 {
c.Body.ApplyForce(c.Push(want))
l := c.Step()
sum.Rent += l.Rent
sum.Effort += l.Effort
sum.Haul += l.Haul
sum.Spent += l.Spent
n++
}
sum.Speed = c.Body.Pos.X / terra.Tile / float64(n)
return n, sum, c
}
$ go run ./cmd/pace -mode race
pace: four browsers over the same 12 cells, one speed each
every one of them starts still, with a full store of 400
asked ticks mean got rent haul spent left
a crawl 0.020000 601 0.020000 12.0200 96.1600 5.7696 101.9296 298.0704
the cheapest 0.081650 147 0.081650 12.0025 23.5200 23.5200 47.0400 352.9600
a walk 0.225000 54 0.222917 12.0375 8.6400 64.6988 73.3387 326.6612
a sprint 0.450000 29 0.426724 12.3750 4.6400 130.6125 135.2525 264.7475
mean is the cells covered divided by the ticks it took, which is under the
speed asked for because the first ticks are spent getting up to it; got is
how far it actually reached, and a run that stops short of 12 ran out
Four creatures, one valley width, and the fastest of them pays nearly three times what the cheapest pays to reach the same place. The sprinter is there in 29 ticks and hands over 135.2525 units; the cruiser takes 147 ticks, five times as long, and hands over 47.0400. Look at where each one's money went. The sprinter's bill is 96% legs. The cruiser's splits down the middle, 23.5200 of rent against 23.5200 of haul, which is the definition of the cheapest speed turning up in a measured run. And the crawler, slowest of the four, pays 101.9296 by being out in the open for 601 ticks: below the bottom of that curve, going slower costs more.
The paper prediction was 47.0302 at the cheapest, 73.3333 at a walk and 133.8667 at a sprint, and the run says 47.0400, 73.3387 and 135.2525. The gaps are small and they are explainable. A crossing has to end on a whole tick, so the sprinter actually covered 12.3750 cells instead of 12, and every creature spent its first ticks below the speed it asked for while the legs caught up. The prediction is what a creature already at speed pays. The run also pays for getting there.
Widen the valley and the arithmetic stops being about tactics.
$ go run ./cmd/pace -mode race -dist 100
pace: four browsers over the same 100 cells, one speed each
every one of them starts still, with a full store of 400
asked ticks mean got rent haul spent left
a crawl 0.020000 2359 0.019992 47.1600 377.4400 22.6368 400.0768 -0.0768
the cheapest 0.081650 1225 0.081650 100.0208 196.0000 196.0000 392.0000 8.0000
a walk 0.225000 293 0.223799 65.5731 46.8800 353.3377 400.2177 -0.2177
a sprint 0.450000 82 0.440969 36.1595 13.1200 386.8800 400.0000 -0.0000
mean is the cells covered divided by the ticks it took, which is under the
speed asked for because the first ticks are spent getting up to it; got is
how far it actually reached, and a run that stops short of 100 ran out
One of the four arrives. The other three empty their stores on the way and stop where
they stopped: the sprinter at 36.1595 cells, the walker at 65.5731, the crawler at
47.1600. Set those against the reach column from two sections back, which
said 35.8566, 65.4545 and 47.1698 before a tick was taken, and the two lists agree to
within the ticks each creature spent accelerating. A number computed from four constants
said how far a creature could get on one meal, and four runs went out and got that far.
Those three deaths happen off the end of a table. The same thing watched from inside the ticks reads differently, and it is the last run of the chapter: one creature walking away from everything with nothing to eat.
// cmd/pace/main.go — the walk that never eats. It asks for the same
// velocity every tick and records the two ticks on which its store
// stops covering something it used to be able to do.
for c.Store > 0 {
t++
af, pace := c.Afford(), c.Pace()
if sprint == 0 && af < k.Top {
sprint = t
}
if slowed == 0 && af < walk {
slowed = t
}
want := field.Vec2{X: walk * terra.Tile}
c.Body.ApplyForce(c.Push(want))
l := c.Step()
if t%every == 0 || t == sprint || t == slowed || c.Store <= 0 {
fmt.Printf(" %6d %9.4f %9.4f %9.6f %9.4f %11.6f\n",
t, af, pace, l.Speed, l.Spent, c.Store)
}
}
$ go run ./cmd/pace -mode fade
pace: one browser walking at 0.2250 cells a tick with nothing to eat
it is asked for that walk every tick, and gets whatever its store allows
tick afford pace cells spent store
50 3.7270 0.4500 0.225000 1.3750 332.161250
100 3.3206 0.4500 0.225000 1.3750 263.411250
150 2.8568 0.4500 0.225000 1.3750 194.661250
200 2.3015 0.4500 0.225000 1.3750 125.911250
250 1.5596 0.4500 0.225000 1.3750 57.161250
289 0.4449 0.4449 0.225000 1.3750 3.536250
292 0.1615 0.1615 0.161536 0.7862 0.000000
293 0.0000 0.0000 0.049036 0.2177 -0.217708
it could not have sprinted from tick 289, and could not hold its own walk
from tick 292. The store was empty on tick 293, 65.5731 cells from where it
started, having spent 400.2177 of the 400 it opened with
For 288 of its 293 ticks this creature could have sprinted at any moment and never knew
the store had anything to say about it. Then afford crosses 0.45 on tick
289 and the sprint quietly leaves the list of things it can do; three ticks later the
walk goes the same way; on the last tick the store is empty and the creature is still
drifting, still being charged, and 0.2177 units overdrawn. That final negative is not a
rounding error. Momentum is not billed in advance: a body already moving keeps moving
whether or not anything can pay for it, and the tick that catches up with it charges
what it charges.
Why a rate is priced by the tick
Take the creature out and a pattern is left that the rest of this book keeps using. Some quantity is chosen (here, a speed). One charge per tick grows faster than that quantity does. A second charge is paid every tick regardless. Divide both by the quantity to get a price per unit of the thing actually wanted, and the first term rises while the second falls, so a best value exists and can be computed instead of hunted for. Bandwidth against latency does it. A batch size against a fixed per-batch overhead does it. Anything with a standing cost and a running cost does it.
The reason to price per tick and not per cell is the design decision of the chapter, and it is subtler than the arithmetic. Charging by distance would have been simpler and would have balanced the books just as exactly. What it destroys is the trade: with a flat price per cell, every journey costs the same however it is made, so nothing a creature could learn about pacing itself would ever pay off, and a controller choosing between a walk and a sprint would be choosing between two identical options. Pricing the tick makes speed a real decision with a real answer, and puts that answer somewhere a creature can be selected for finding it.
There is a standard in that, and it is the one the plants were already held to. If a number decides how a thing behaves, it should be computable from the constants before the simulation runs and checkable against the run afterwards. 0.081650 cells a tick and 102 cells of reach were both on the first screen of output before a tick was taken, and the runs spent thousands of ticks agreeing with them. A model whose important numbers can only be found by watching it is a model nobody can reason about, and there are a great many more creatures coming.
Three simplifications are in here on purpose. Acceleration is free: only the speed being held is charged, so a creature that slams from a sprint to a standstill pays nothing for the stop, and a real animal pays plenty. Terrain is free: crossing shallow water, climbing a slope and strolling over open soil all cost the same, because the bed carries no gradient a leg could feel. And a body of forty grams that never changes is doing a lot of work in this model, standing in for muscle, bone, gut and whatever the creature is carrying. Real locomotion also does not go up with the square of speed for a running animal: a walking mammal's cost per distance is closer to flat, and a square is what you get for pushing through something. This world charges the square because it wants pacing to matter, and it says so instead of pretending the choice was forced.
- Given a body mass, a price per unit of work and a speed in cells a tick, work
out the charge for one tick with a calculator and check it against the
haulcolumn digit for digit. - Explain why halving the speed quarters the charge per tick but only halves the charge per cell, and point at the cancelled speed that does it.
- Compute the cheapest travelling speed as the square root of the basal rate divided by the price of work, and say why the body's mass is not in that formula.
- Handed a full store and the four constants, predict how many cells a creature crosses before it empties at any given speed, and match the four runs against those predictions.
- Shown a movement charge 256 times too large, look for a missing unit conversion upstream of something that squares its argument, rather than for a wrong constant.
- Say what
MaxForcemeans for a forty-gram body, and why a steering vector has to be multiplied by the mass before it reaches the accumulator. - Name the store below which a creature cannot sprint, derive it from the rent plus one sprinting tick, and explain why the store lands on exactly zero on the tick the affordable speed binds.
Exercise 1 — a crawl as expensive as a sprint. Somewhere below
the cheapest speed there is a crawl whose price per cell equals a sprint's 11.1556.
Work it out, then run go run ./cmd/pace -mode race -crawl with your
answer and set it beside the sprint's line.
A cell costs 24 × speed + 0.160 ÷ speed, and you want that to equal
11.1556. Multiply through by the speed and you have 24 × speed × speed
− 11.1556 × speed + 0.160 = 0, a quadratic whose two roots are the two
speeds that cost the same: 0.45, which you already knew, and 0.014817. Run
go run ./cmd/pace -mode race -crawl 0.0148173 and the crawler spends
133.8681 units over 810 ticks against the sprinter's 135.2525 over 29. Two creatures
thirty times apart in speed, arriving at the same place, having paid the same. The
sprinter bought its speed with work and the crawler bought its idleness with rent,
and the ledger cannot tell the two apart.
Exercise 2 — a creature twice the size. Predict what doubling
Bulk to 80 grams does to the cheapest travelling speed, then check with
go run ./cmd/pace -mode row -bulk 80.
Nothing at all: it stays 0.081650. The rent over a cell is the mass times the basal rate divided by the speed, and the work over a cell is the mass times the price of work times the speed, so the mass stands on both sides of the equation that finds the bottom and cancels out of it. Everything else on the line does change. The rent doubles to 0.3200 a tick, a cell at the cheapest speed costs 7.838367 instead of 3.919184, and a store still holding 400 units reaches half as far. A bigger animal under this law travels at the same best speed as a small one and gets half the distance out of the same meal, which is a good argument for letting the store grow with the body on the day a genome starts writing these rows.
Exercise 3 — flatten the curve. The lower panel of Figure 46.1
is steep on the left because the rent is heavy. Run
go run ./cmd/pace -mode curve, then run it again with the basal rate cut
to a quarter, and say which way the cheapest speed moves and why.
go run ./cmd/pace -mode curve -basal 0.001 moves the bottom of the
curve from 0.081650 to 0.040825, exactly half, because the cheapest speed is the
square root of the basal rate over the price of work and a quarter of a number has
half its square root. The against column tells the rest of it: at the
default rate a sprint costs 2.8464 times the best speed, and on the cheap body it
costs 5.5567 times. A creature that is cheap to keep alive has more to lose by
hurrying, because the clock it is racing costs it so little. That is the trade
an animal which sits out a winter is making, and it will matter to every lineage
this valley raises.
A creature can now be told where to go and made to pay for going there, which leaves one question standing: told by whom, and on what evidence? Every velocity above was typed on a command line. A creature out in the valley has to work its own out, and the only honest way to let it is to give it something to look with.