The World Vol 3 · Forces of Nature
ch 24 / 105
Chapter 24

Numbers With a Direction

The grid cannot carry a diagonal

The valley now draws tiles, a camera, a tick readout and walkers on soil. Its movement is still locked to a cell. A thing stands in one cell; a tick later it stands in a neighbouring cell, one square north, south, east or west.

The first moving thing in this chapter crosses the window on a diagonal, so a cell step is not enough. A vector is two numbers kept together and given arithmetic of its own: add two of them to combine two movements, scale one to change how far it carries, measure one to learn how far that is, and divide one by its own length to keep the direction and throw the distance away. The world needs that pair before it can describe a nudge, a pull, a drift or a ripple.

A cell is a place, and two places do not add up to anything useful. A movement does: so much across, so much down, with fractions allowed. The water's drag on a drifting seed can be a fifth of a pixel to the west. A frightened walker can flee on a heading no grid direction names. Both facts fit in the same pair.

Those four operations live in internal/vec. Each operation starts in plain numbers; the symbols arrive after the arithmetic has something to name. No angle appears in this chapter. By the end, an amber mote crosses The Hollow by adding one vector to its position every tick, and the client hashes each frame so the movement is checked by bytes rather than by eyesight.

The Vec2 type

Do it on paper before doing it in Go. Something stands at the pixel 24 across and 24 down. Cells are sixteen pixels wide, so that is the middle of the cell one in from the northwest corner of the rim, and pixels are the unit because a cell is too coarse to hold a movement this book cares about. Every tick, the thing moves 3 pixels east and 4 pixels south. After one tick it stands at 24 + 3 = 27 across and 24 + 4 = 28 down. After another, 30 and 32. There is no arithmetic there a ten-year-old cannot do, and the only new thing on the page is the decision to treat "3 across and 4 down" as one object with one name instead of two loose numbers that happen to be near each other.

Keep cranking and the across column reads 27, 30, 33, 36, 39, 42, 45, 48, 51, 54 while the down column reads 28, 32, 36, 40, 44, 48, 52, 56, 60, 64. Ten ticks moved the thing 30 across and 40 down in total, and 30 and 40 are 3 and 4 multiplied by ten. Repeating a step ten times and taking one step ten times as long land in the same place. Multiplying both numbers of a pair by the same plain number is called scaling, and the table has just shown that scaling and repeated adding agree.

The decision to bind the two numbers together pays for itself in bugs that never happen. Loose x and y variables have to be updated in pairs by hand, and the family of mistakes that follows is famous: the line that moves x and forgets y, the copy-pasted line that moves y by step.X, the function that returns one of them and drops the other. A pair with a type has one line per operation and no way to do half of it.

▣ Build · stage 1 — the type, and three ways to move it
// internal/vec/vec.go
// Package vec holds the one type everything in this world that moves is
// described with: two numbers kept together, with arithmetic of their own.
// Nothing in here knows what a tick is or what a pixel is.
package vec

// Vec2 is two numbers treated as one thing: X across and Y down,
// measured in whatever unit the caller is already using.
type Vec2 struct {
	X, Y float64
}

// Add is the two steps taken one after the other.
func (v Vec2) Add(w Vec2) Vec2 { return Vec2{v.X + w.X, v.Y + w.Y} }

// Sub is the step that gets from w to v: subtract the firsts, subtract
// the seconds.
func (v Vec2) Sub(w Vec2) Vec2 { return Vec2{v.X - w.X, v.Y - w.Y} }

// Scale multiplies both numbers by k, which changes how far the step
// goes and never which way it points.
func (v Vec2) Scale(k float64) Vec2 { return Vec2{v.X * k, v.Y * k} }
// cmd/drift/main.go — a bench for one vector at a time: no window, no
// world, just the arithmetic printed while it happens.
package main

import (
	"flag"
	"fmt"

	"theworld/internal/vec"
)

// walk prints where a dot stands after each of n ticks, given a start
// and one step it repeats.
func walk(start, step vec.Vec2, n int) {
	fmt.Printf("start (%.2f, %.2f)   step (%.2f, %.2f)\n", start.X, start.Y, step.X, step.Y)
	fmt.Println("  tick        x        y")
	at := start
	for t := 1; t <= n; t++ {
		at = at.Add(step)
		fmt.Printf("  %4d %8.2f %8.2f\n", t, at.X, at.Y)
	}
	jump := start.Add(step.Scale(float64(n)))
	fmt.Printf("  %-33s (%.2f, %.2f)\n",
		fmt.Sprintf("%d steps taken one at a time:", n), at.X, at.Y)
	fmt.Printf("  %-33s (%.2f, %.2f)\n",
		fmt.Sprintf("the step scaled by %d, added once:", n), jump.X, jump.Y)
}

func main() {
	show := flag.String("show", "add", "which demonstration to run")
	flag.Parse()

	start := vec.Vec2{X: 24, Y: 24}
	switch *show {
	case "add":
		walk(start, vec.Vec2{X: 3, Y: 4}, 10)
	}
}
$ go run ./cmd/drift -show add
start (24.00, 24.00)   step (3.00, 4.00)
  tick        x        y
     1    27.00    28.00
     2    30.00    32.00
     3    33.00    36.00
     4    36.00    40.00
     5    39.00    44.00
     6    42.00    48.00
     7    45.00    52.00
     8    48.00    56.00
     9    51.00    60.00
    10    54.00    64.00
  10 steps taken one at a time:     (54.00, 64.00)
  the step scaled by 10, added once: (54.00, 64.00)

Three choices in that file matter. The methods hang off a Vec2 value and not a pointer to one, and each returns a brand new Vec2 instead of editing the receiver, so a.Add(b) can never change a behind somebody's back. Two bodies handed the same velocity are then two independent facts, not one fact with two owners. The fields are float64 because a step of 2.4 pixels has to survive being stored; integers would round the useful part to zero.

Sub reads in the direction its comment states: target.Sub(here) is the movement from here to the target. That order matters because reversing it points the step back toward where the body already stands.

The last two lines are the table's summary and its first real claim. Ten additions of (3, 4) and one addition of (3, 4) scaled by ten produce the same pair of numbers, digit for digit, so an author who wants a body's position ten ticks from now can take either route to it. Both routes stay in whole numbers here, which is what makes the agreement exact.

The 3-by-4 diagonal

The table has a hole in it. The mote moved 3 across and 4 down every tick for ten ticks, and finished 30 across and 40 down from where it started. How far did it actually travel? The answer is not 70, because 70 measures a walk that goes 30 east, turns, and then goes 40 south, and no turn happened. Nor is the answer 40, the longer of the two numbers. What is wanted is the straight-line distance from the start to the finish: the long diagonal of a rectangle 30 across and 40 down, and, one tick at a time, the diagonal of a rectangle 3 across and 4 down.

Nobody can read that diagonal off the two numbers by staring at them, so get at it through areas instead, because an area can be counted. Take four identical copies of the right-angled triangle whose short sides are 3 and 4, and fit them into a square whose side is 3 + 4 = 7, each copy tucked into a corner and turned a quarter turn from the one before it. Figure 24.1 is that arrangement. The four copies leave a tilted hole in the middle, and each of the hole's four sides is one triangle's long side: the diagonal you are trying to measure. Because the copies are identical and each is turned by a quarter turn, the hole's four sides are equal and its four corners are equal, so the hole is a square.

Now count. The big square holds 7 × 7 = 49 units of area. Each triangle is half of a 3-by-4 rectangle, so it holds 12 ÷ 2 = 6 units, and four of them hold 24. Whatever is left is the tilted square: 49 − 24 = 25 units of area. A square of area 25 has a side of 5, because 5 multiplied by itself is 25. The diagonal of a 3-by-4 rectangle is exactly 5, and the recipe that produced it never mentioned 3 and 4 in particular: multiply each of the two numbers by itself, add the two results, and then find the number which multiplied by itself gives that total.

The diagonal of a 3-by-4 step, found by counting area A square of side 7 holds four identical right triangles with short sides 3 and 4, one in each corner, each turned a quarter turn from the last. They leave a tilted square hole in the middle whose every side is the triangles' long side. Beside the drawing, four lines of arithmetic: the whole square is 7 times 7 is 49, the four triangles are 4 times 6 is 24, the tilted square is 49 minus 24 is 25, and its side is 5 because 5 times 5 is 25. THE DIAGONAL, FOUND BY COUNTING AREA 3 4 4 3 area 25 side 5 side 7 the whole square: 7 × 7 = 49 four triangles: 4 × 6 = 24 the tilted square: 49 − 24 = 25 its side: 5 × 5 = 25 each triangle is half a 3-by-4 rectangle: 6 every side of the tilted square is one step of 3 across and 4 down 9 + 16 = 25, and 5 × 5 = 25, so 5

Figure 24.1 — the same area, counted twice. Once as a square of side 7, once as four triangles plus the tilted square, and the difference is the answer.

Try the recipe on a second rectangle to be sure it is a recipe and not a coincidence. Sides 6 and 8: 36 and 64, which add to 100, and 10 multiplied by itself is 100, so the diagonal is 10. Now try one that refuses to be tidy. Sides 3 and 3 give 9 and 9, which add to 18, and no whole number multiplied by itself is 18: 4 gives 16 and 5 gives 25, so the answer is somewhere between them. 4.2 multiplied by itself is 17.64, a little low. 4.25 gives 18.0625, a little high. Squeeze further and the digits keep coming without ever stopping, which is ordinary: most diagonals are numbers like that. The operation being performed here has a name, the square root, and Go's math.Sqrt does the squeezing to fifteen digits in one instruction.

∑ Math Interlude — the notation for a step and its length

Everything above was done in numbers. Here is the usual shorthand for it, introduced only now that there is something for it to be shorthand for. A vector is written as its two numbers in brackets in a fixed order, across first: (3, 4). A single letter can stand for the whole pair, and a bold v is the convention, so that an expression says which of its parts are pairs and which are plain numbers.

Addition and scaling are what the code already does: (3, 4) + (2, 1) = (5, 5), and 10·(3, 4) = (30, 40). Length gets a mark of its own, a pair of upright bars around the vector, and the two pieces of shorthand inside the rule are the ones to slow down for. Writing x² means x multiplied by itself, so 4² = 16 and 0.6² = 0.36. Writing √a means the number which multiplied by itself gives a, so √25 = 5 and √18 = 4.2426…. With those two, the whole of the area-counting fits on one line:

|v| = √(x² + y²)

Check it against the figure: x = 3 and y = 4 give √(9 + 16) = √25 = 5. A direction on its own gets a small hat, and v̂, said "v hat", means v with both of its numbers divided by |v|, so that |v̂| is always 1. The last stage of this chapter aims a mote along (120, 90), whose length is 150, so its hat is (0.8, 0.6): and 0.64 + 0.36 = 1 says the hat came out the right size.

(x, y)a vector: two numbers in a fixed order, across first, down second
vone name for the whole pair
v + wadd across to across and down to down
k·vscaling: multiply both of v's numbers by the plain number k
x multiplied by itself: 4² = 16
√athe square root: the number that, multiplied by itself, gives a
|v|the length of v, which is √(x² + y²) and never negative
v at length 1: both numbers divided by |v|
▣ Build · stage 2 — length, and a table of it
// internal/vec/vec.go — one more method, and the import it needs
import "math"

// Len is how far the step actually travels: the straight-line diagonal
// of a rectangle X across and Y down.
func (v Vec2) Len() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) }
// cmd/drift/main.go — a second demonstration, and its case in main

// lengths prints the length of each vector beside the arithmetic that
// produced it.
func lengths(vs []vec.Vec2) {
	fmt.Printf("  %-15s %9s %9s %9s %9s\n", "vector", "x·x", "y·y", "x·x + y·y", "length")
	for _, v := range vs {
		fmt.Printf("  (%6.2f,%6.2f) %9.4f %9.4f %9.4f %9.4f\n",
			v.X, v.Y, v.X*v.X, v.Y*v.Y, v.X*v.X+v.Y*v.Y, v.Len())
	}
}

//	case "len":
//		lengths([]vec.Vec2{
//			{X: 3, Y: 4}, {X: 6, Y: 8}, {X: 3, Y: 3},
//			{X: 5, Y: 0}, {X: 0.6, Y: 0.8}, {X: 30, Y: 40},
//		})
$ go run ./cmd/drift -show len
  vector                x·x       y·y x·x + y·y    length
  (  3.00,  4.00)    9.0000   16.0000   25.0000    5.0000
  (  6.00,  8.00)   36.0000   64.0000  100.0000   10.0000
  (  3.00,  3.00)    9.0000    9.0000   18.0000    4.2426
  (  5.00,  0.00)   25.0000    0.0000   25.0000    5.0000
  (  0.60,  0.80)    0.3600    0.6400    1.0000    1.0000
  ( 30.00, 40.00)  900.0000 1600.0000 2500.0000   50.0000

Every row of that table was predicted on paper before the machine printed it, including the ugly one: 4.2426 is the squeeze between 4.2 and 4.25 carried four decimal places further. Two rows are doing extra work. The fourth shows what happens when one of the numbers is zero, and the answer is the other number, since a rectangle with no height is just its own side; a length is never negative, and a step of (−5, 0) would print 5.0000 too. The last row closes the hole this section opened. Ten steps of length 5 is 50 pixels of travel, and the straight-line distance from start to finish, |(30, 40)|, is also 50, because the mote never turned. When a body's steps do turn, those two numbers part company: the road it walked gets longer while the gap between its first position and its last one does not.

The diagonal speed bug

Here is the first job the world will actually ask for. A mote is to move at 3 pixels a tick. Not 3 east: 3, in whatever direction it happens to be going. Send it east and the step is plainly (3, 0). Send it southeast and the step that suggests itself is (3, 3), which is wrong in a way no amount of staring will reveal. Run both and measure.

⚠ Worked failure — the diagonal that outran its own speed
// cmd/drift/main.go — two motes, both told to move at 3 a tick

// race walks two dots that were both meant to move at the same speed and
// prints how far each has actually gone.
func race(start, a, b vec.Vec2, n int) {
	fmt.Printf("  step A (%.2f, %.2f) length %.4f     step B (%.2f, %.2f) length %.4f\n",
		a.X, a.Y, a.Len(), b.X, b.Y, b.Len())
	fmt.Printf("  %4s %9s %8s %8s    %9s %8s %8s\n",
		"tick", "A x", "A y", "A gone", "B x", "B y", "B gone")
	pa, pb := start, start
	for t := 1; t <= n; t++ {
		pa, pb = pa.Add(a), pb.Add(b)
		fmt.Printf("  %4d %9.2f %8.2f %8.2f    %9.2f %8.2f %8.2f\n",
			t, pa.X, pa.Y, pa.Sub(start).Len(), pb.X, pb.Y, pb.Sub(start).Len())
	}
}

//	case "race":
//		race(start, vec.Vec2{X: 3, Y: 0}, vec.Vec2{X: 3, Y: 3}, 10)
$ go run ./cmd/drift -show race
  step A (3.00, 0.00) length 3.0000     step B (3.00, 3.00) length 4.2426
  tick       A x      A y   A gone          B x      B y   B gone
     1     27.00    24.00     3.00        27.00    27.00     4.24
     2     30.00    24.00     6.00        30.00    30.00     8.49
     3     33.00    24.00     9.00        33.00    33.00    12.73
     4     36.00    24.00    12.00        36.00    36.00    16.97
     5     39.00    24.00    15.00        39.00    39.00    21.21
     6     42.00    24.00    18.00        42.00    42.00    25.46
     7     45.00    24.00    21.00        45.00    45.00    29.70
     8     48.00    24.00    24.00        48.00    48.00    33.94
     9     51.00    24.00    27.00        51.00    51.00    38.18
    10     54.00    24.00    30.00        54.00    54.00    42.43

Both motes were told 3, and after ten ticks one has gone 30 pixels while the other has gone 42.43. The southeast-bound mote is travelling 41% faster than the mote beside it, and in a game with a keyboard this is the bug where holding two arrow keys at once makes you sprint.

The header line convicts the code before the table does. Step B's length is 4.2426, which is the √18 from the last section: the number 3 written in each slot did not promise a speed of 3, it promised 3 across and 3 down, and the actual travel is the diagonal of that little square. Naming the mistake exactly gives the fix for free. A speed is a property of the whole pair, so it cannot be set by writing it into each of the two numbers separately. Choose the direction first, ask how long that direction came out, divide both numbers by that length to get a step of length exactly 1, and only then multiply by the speed you meant.

▣ Build · stage 3 — length one, then any speed you like
// internal/vec/vec.go — the fourth operation

// Unit is the same direction at a length of exactly one. A step of no
// length points nowhere, so it is handed back unchanged rather than
// divided by zero.
func (v Vec2) Unit() Vec2 {
	n := v.Len()
	if n == 0 {
		return Vec2{}
	}
	return Vec2{v.X / n, v.Y / n}
}
// cmd/drift/main.go — aiming, and its case in main

// aim prints the step that carries a dot from one point toward another
// at a fixed speed, whichever way the target lies.
func aim(from vec.Vec2, targets []vec.Vec2, speed float64) {
	fmt.Printf("  from (%.2f, %.2f) at %.2f pixels a tick\n", from.X, from.Y, speed)
	fmt.Printf("  %-16s %-17s %9s  %-13s %8s %7s\n",
		"target", "offset", "distance", "step", "length", "ticks")
	for _, t := range targets {
		off := t.Sub(from)
		step := off.Unit().Scale(speed)
		fmt.Printf("  (%6.2f,%7.2f) (%7.2f,%7.2f) %9.2f  (%5.2f,%5.2f) %8.4f %7.2f\n",
			t.X, t.Y, off.X, off.Y, off.Len(), step.X, step.Y, step.Len(), off.Len()/speed)
	}
}

//	case "aim":
//		aim(start, []vec.Vec2{
//			{X: 144, Y: 114}, {X: 144, Y: 24}, {X: 24, Y: 114}, {X: 0, Y: 0},
//		}, 3)
$ go run ./cmd/drift -show aim
  from (24.00, 24.00) at 3.00 pixels a tick
  target           offset             distance  step            length   ticks
  (144.00, 114.00) ( 120.00,  90.00)    150.00  ( 2.40, 1.80)   3.0000   50.00
  (144.00,  24.00) ( 120.00,   0.00)    120.00  ( 3.00, 0.00)   3.0000   40.00
  ( 24.00, 114.00) (   0.00,  90.00)     90.00  ( 0.00, 3.00)   3.0000   30.00
  (  0.00,   0.00) ( -24.00, -24.00)     33.94  (-2.12,-2.12)   3.0000   11.31

One column is the point of the whole table: every step, in every direction, has length 3.0000. Follow the first row through by hand. The offset is (120, 90), whose length is √(14400 + 8100) = √22500 = 150. Dividing both numbers by 150 gives (0.8, 0.6), the hat from the interlude. Multiplying by 3 gives (2.40, 1.80), and 5.76 + 3.24 = 9, whose square root is 3 exactly. The last row is the same three operations on an offset that points up and to the left, and negative numbers need no special handling anywhere: a negative multiplied by itself is positive, so the length comes out 33.94 either way, and dividing negatives by a positive length keeps them negative, which is what pointing northwest means.

The zero case in Unit is not defensive clutter. A body sitting exactly where it wants to be produces an offset of (0, 0), whose length is 0, and dividing by 0 in floating-point arithmetic gives NaN, "not a number", which then poisons every sum it touches: a position added to NaN is NaN, and a mote whose position is NaN vanishes from the screen with no error printed anywhere. Returning the zero vector says the honest thing, which is that a step of no length has no direction to preserve.

The drift mode

Now put it on the screen you built. The client already knows how to turn terrain into tiles, how to draw those tiles through a camera, and how to reduce a finished frame to a sixty-four-character hash. All that is missing is something to move across it, so cmd/worldc gains a mode: generate the valley from a seed, tile it once, leave it alone, and carry one four-pixel mote from the northwest interior to a point down near the southeast rim, adding the same vector to its position once per tick.

Freezing the ground is what makes this a proof rather than a picture. If the terrain never changes and no walker is drawn, then the only thing on the frame that can possibly differ between one tick and the next is the mote, so the hash becomes a statement about the mote alone.

▣ Build · stage 4 — one vector a tick, hashed at every tick boundary
// cmd/worldc/main.go — one vector a tick

// Mote is the first thing in this world that moves without the grid's
// permission: a position in world pixels, and the step added to it once
// every tick.
type Mote struct {
	Pos, Step vec.Vec2
}

// mote is the amber the dot is painted in, palette entry 15.
const mote render.Color = 0xFFF0BE35

// dot paints a four-pixel square centred on a position given in world
// pixels. A screen holds whole pixels only, so the position is rounded
// on the way in and the fraction stays in the mote.
func dot(b *render.Buffer, cam render.Camera, p vec.Vec2, c render.Color) {
	sx, sy := cam.ToScreen(int(math.Round(p.X)), int(math.Round(p.Y)))
	b.FillRect(render.Rect{X0: sx - 2, Y0: sy - 2, X1: sx + 2, Y1: sy + 2}, c)
}

// What the run reports: every tenth tick, plus the first and the last,
// and one pair of consecutive frames compared pixel by pixel.
const (
	driftEvery = 10
	driftMark  = 20
)

// drift holds seed's valley perfectly still and carries one mote across
// it, adding the same vector once per tick and hashing the frame at
// every tick boundary. Nothing else on the frame is allowed to move, so
// a hash that changes is the mote and only the mote.
func drift(a *art, cam render.Camera, seed uint64, ticks int, from, to vec.Vec2, speed float64, shot string) {
	g := sim.Generate(MapW, MapH, seed)
	kind := make([]render.Kind, MapW*MapH)
	for i := range kind {
		t, _ := g.At(sim.Coord{X: i % MapW, Y: i / MapW})
		kind[i] = kindOf(t)
	}
	tiles := render.Autotile(MapW, MapH, kind)

	off := to.Sub(from)
	m := Mote{Pos: from, Step: off.Unit().Scale(speed)}
	fmt.Printf("drift: (%.0f,%.0f) to (%.0f,%.0f) is (%.0f,%.0f), %.2f pixels; at %.2f a tick the step is (%.2f,%.2f)\n",
		from.X, from.Y, to.X, to.Y, off.X, off.Y, off.Len(), speed, m.Step.X, m.Step.Y)

	b := render.NewBuffer(cam.W, cam.H)
	var prev *render.Buffer
	fmt.Printf("  %4s %9s %9s %9s  %s\n", "tick", "x", "y", "travelled", "frame")
	for n := 1; n <= ticks; n++ {
		m.Pos = m.Pos.Add(m.Step) // the whole of this chapter, once a tick

		// Nothing on this frame moves but the mote: the ground was
		// tiled once, before the loop, and is drawn from the same tile
		// map every time.
		b.Fill(render.Void)
		render.DrawMapVia(b, tiles, a.tiles, cam)
		dot(b, cam, m.Pos, mote)
		h := b.Hash()

		if n == driftMark {
			prev = clone(b)
		} else if n == driftMark+1 && prev != nil {
			d := render.Diff(prev, b)
			fmt.Printf("  ticks %d and %d differ in %d of %d pixels, the first at %v\n",
				driftMark, n, d.Count, b.W*b.H, d.At)
		}
		if n%driftEvery != 0 && n != 1 && n != ticks {
			continue
		}
		fmt.Printf("  %4d %9.2f %9.2f %9.2f  %s\n",
			n, m.Pos.X, m.Pos.Y, m.Pos.Sub(from).Len(), h[:16])
		if shot != "" {
			if err := b.SavePNG(fmt.Sprintf("%s-t%03d.png", shot, n)); err != nil {
				die(err)
			}
		}
	}
	fmt.Printf("  the target was (%.2f,%.2f); the mote missed it by %e pixels\n",
		to.X, to.Y, m.Pos.Sub(to).Len())
}
// cmd/worldc/main.go — two flags, and the branch that takes them
	drifting := flag.Bool("drift", false, "carry one mote across a still valley, a vector a tick")
	speed := flag.Float64("speed", 3, "how many world pixels the mote covers in a tick")

	// ... after the art is loaded and the camera is placed:
	if *drifting {
		drift(a, cam, *seed, int(*ticks), vec.Vec2{X: 24, Y: 24}, vec.Vec2{X: 144, Y: 114},
			*speed, *shot)
		return
	}
$ go run ./cmd/worldc -drift -ticks 50
drift: (24,24) to (144,114) is (120,90), 150.00 pixels; at 3.00 a tick the step is (2.40,1.80)
  tick         x         y travelled  frame
     1     26.40     25.80      3.00  842c3957b3d8dd5f
    10     48.00     42.00     30.00  09f620c978221335
    20     72.00     60.00     60.00  31234d5faab466aa
  ticks 20 and 21 differ in 24 of 24576 pixels, the first at (70,58)
    30     96.00     78.00     90.00  81c0bb4536f11241
    40    120.00     96.00    120.00  3e7d20b626119ebb
    50    144.00    114.00    150.00  bd95bacaf123b4cc
  the target was (144.00,114.00); the mote missed it by 2.131628e-13 pixels

The amber square sets off from the soil inside the northwest rim, crosses the pond without noticing that it is water, and comes to rest on the rock of the southern rim on tick 50, the arrival the aiming table predicted from 150 pixels at 3 a tick. The travelled column climbs by exactly 30 every ten ticks, so the speed the code promised is the speed the mote kept the whole way. Nothing in this world has an opinion about the mote yet: the ground does not stop it, the water does not slow it, and the run would sail off the edge of the map if it kept going.

The comparison line is the proof the frozen ground bought. Between tick 20 and tick 21, 24 pixels of 24,576 changed, and 24 is exactly what the arithmetic predicts: the mote occupies sixteen pixels, it moved by (2.4, 1.8) which rounds to a shift of two across and two down, the old square and the new square overlap in four pixels, so twelve pixels were vacated and twelve were newly covered. The first disagreement is at (70, 58), the top-left corner of where the square used to be. A hash that changes and a pixel count that matches the prediction are two different kinds of evidence, and together they leave nowhere for a silent drawing bug to hide.

◆ Note — the fraction stays in the mote

dot rounds the position on its way to the screen and the mote keeps its fractional pixels. Round the position itself instead, once a tick, and the 0.4 is thrown away fifty times while the 0.8 is rounded up fifty times: the step quietly becomes two across and two down, so the mote walks an exact diagonal instead of the line it was aimed along and finishes at (124, 124). Rounding at the last possible moment is a habit to keep: hold the exact number in the simulation, and let the display be the only place that has to compromise. It is also why the last line reads what it does. Fifty additions of the step leave the mote 0.0000000000002 pixels from its target, because binary fractions cannot hold 2.4 exactly, and no picture on any screen will ever be able to tell.

Four operations on pairs

Four operations were built here, and not one of them asked what the two numbers meant. A position is a vector from the map's corner to a body. A step is a vector. So is a push from the wind, a pull toward a neighbour, the offset to the nearest water, and the difference between where a herd's centre is and where one animal stands. Adding two pushes and adding two steps are the same three characters of code because addition of pairs never cared which meaning the pair carried.

The methods all return a new Vec2, and that closure lets a whole intention fit on one line. to.Sub(from).Unit().Scale(speed) reads left to right as a sentence: take the offset from here to there, reduce it to a bare direction, then give it the speed the body should have. Aiming by subtracting, normalizing and scaling stays in ordinary arithmetic the whole way, and it never has to worry about which quadrant the target is in.

Length is the operation that carries the most weight for the least code. It is how far a path ran, how fast a body is going, how near two animals are to each other, how strong a pull should be when strength falls off with distance. All of it is one square root over a sum of two products, computed from a triangle you can count the area of on paper, which means that when a body ends up in the wrong place there is always a version of the question you can check by hand.

✓ Checkpoint — the four operations, and what each one is for
  • Given a start and a step, I can fill in a table of positions tick by tick, and I can get the same answer in one move by scaling the step and adding it once.
  • I can show, by counting the area of a 7-by-7 square holding four 3-by-4 triangles, why the diagonal of a 3-by-4 rectangle is exactly 5.
  • I can find a length from two numbers with no calculator, to better than a decimal place, by squeezing in on the number whose square is the total.
  • Shown a body that moves faster diagonally than it does straight, I check the length of its step before I suspect anything else.
  • I can turn any offset into a step of a stated speed with Unit and Scale, and say why Unit must special-case a vector of length zero.
  • I can pin a moving thing's rendering with a per-tick frame hash, and predict from the geometry how many pixels two consecutive frames should differ by.
⚡ Exercises — try first, then reveal
Exercise 1 — aim it somewhere else. Send the mote due south instead: leave the start at (24, 24) and change the target in the branch that calls drift to (24, 114). Predict the step and the arrival tick before running -show aim, then run the client and watch it.

The offset is (0, 90), whose length is 90 because one of the numbers is zero. Divide by 90 and the direction is (0, 1); multiply by 3 and the step is (0.00, 3.00), which is the third row of the aiming table. 90 pixels at 3 a tick is 30 ticks, and the table prints 30.00. Run go run ./cmd/worldc -drift -ticks 30 and the mote falls down the column just inside the western rim, passing west of the pond, and lands on the southern rim on tick 30. Two things are quietly instructive. No case anywhere in the code knows the word "south", and the miss line reads 0.000000e+00 this time, because 3 and 90 are numbers binary can hold exactly.

Exercise 2 — half the speed, twice the ticks. Run the drift with -speed 1.5 -ticks 100. Predict what the table looks like, then compare its hashes with the ones printed above.

The step halves to (1.20, 0.90) and the mote takes 100 ticks to cover the same 150 pixels, arriving at the same target. The interesting part is in the hashes: tick 20 of the slow run prints 09f620c978221335, which is tick 10 of the fast run, and tick 40 prints 31234d5faab466aa, which is tick 20 of the fast run. The two runs draw byte-identical pictures wherever they stand in the same place. One line is being walked at two paces, and the frame is a function of the position and nothing else, which is the property that makes a frame hash a usable test at all.

Exercise 3 — look at the last decimal. The run says the mote missed its target by 2.131628e-13 pixels. Print the position after 50 ticks with %.20f and explain where that number came from.

The step prints as 2.40000000000000035527: 2.4 has no exact form in binary, the way a third has no exact form in decimal, so what is stored is the nearest number that does exist. Add it fifty times and the tiny excesses accumulate into 144.00000000000017053026 across and 113.99999999999987210231 down. Both round to the pixels 144 and 114, so every frame is identical to one drawn from perfect numbers. Notice also what from.Add(step.Scale(50)) gives: 144.00000000000000000000 across and 113.99999999999998578915 down, fifteen times closer to the target than the fifty additions were. One path by two routes is not bit-for-bit one answer, and any code that adds motion up over thousands of ticks has to be built knowing it.