The World Vol 2 · Drawing the World
ch 19 / 105
Chapter 19

The Map Draws Itself

Twenty pictures and 96 cells

The tileset is finished and nothing puts it on a screen. Twenty tiles sit in assets/tiles/, the valley they were drawn for was arranged by hand in an editor, and the ground worldd has been ticking since volume 1 was arranged by nobody: 96 cells of terrain grown from a seed, with one more of them turning to water every forty ticks as the spring seeps.

The picture has to come out of the grid, cell by cell. The ground already knows what it is. Drawing a cell is a lookup and never a decision: the terrain names a tile, and where the terrain is water the four cells around it name which water tile, because the sheet was drawn in the order those four answers produce.

There are two ways to approach the problem. The first treats the renderer as something that works out where the shore is: follow the outline of the pond, mark which stretches face north, lay the right tiles along them. It sounds like what a person does with an editor open, and it needs the pond to exist as an object before it can start, which the grid has no notion of.

The second way never looks at a pond at all. One command reads seed 5's grid, turns all 96 cells into sheet cells, blits them into a framebuffer and leaves the valley on disk as a PNG with a hash under it.

Four things stand in the way: a rectangle of ground has to reach a package that must not know what a world is, the mask arithmetic has to be written down, plain soil needs a second tile picked without a coin toss, and the cells past the edge of the map need an answer the drawing code can use.

One cell, four questions, one number

∑ Interlude: a cell of the pond, from terrain to pixels

Seed 5's valley is twelve cells across and eight down, with a rock rim, soil inside it and a pond of sixteen cells laid down by the generator's walk before the first tick. Take the cell at column 2, row 3. The grid says water, so it is one of the sixteen transition tiles and the only remaining question is which. Ask the four cells sharing an edge with it.

North is (2, 2), and the grid says soil. Soil is land, so the north bit is set: add 1. East is (3, 3), which is water: add nothing. South is (2, 4), water again: add nothing. West is (1, 3), soil: add 8.

1 + 0 + 0 + 8 = 9

Nine is the tile. The sheet holds its transition set in mask order, so no table stands between that number and the picture, and the arrangement it describes is the one the previous chapter walked through when it argued for sixteen drawings: land above, land to the left, open water to the right and below.

Now find cell 9 on the sheet, which is 4 tiles across. Its column is what is left over when 9 is divided by 4, and its row is how many whole 4s fit: 9 % 4 = 1 and 9 / 4 = 2. Column 1, row 2. Multiply each by 16 and the source rectangle runs from (16, 32) to (32, 48).

The destination is the easier half. Map cell (2, 3) starts at world pixel 2 · 16 = 32 across and 3 · 16 = 48 down, so the blit copies (16, 32)–(32, 48) of the sheet to (32, 48) of the framebuffer. Two numbers went in, four rectangles came out, and nothing looked at the pond.

One more, from the far end of the same pond. Cell (4, 6) is the single wet cell hanging off its southern tip. North of it is water: 0. East is soil: 2. South is (4, 7), which is the rock rim, and rock is land: 4. West is soil: 8. Total 14, a cell wet only on its north side, with three banks drawn round it.

cx, cya cell of the map: column and row, counted in cells from the top-left
ia tile index: which of the sheet's twenty cells draws this piece of ground
%the remainder after whole-number division: 9 % 4 is 1
/whole-number division, remainder thrown away: 9 / 4 is 2
|=set these bits of a number and leave every other bit alone

That arithmetic belongs in internal/render, and internal/render has carried one rule since its first file: it owns pixels, and it does not know that a package called sim exists. Importing the simulation to read a terrain value would end that in one line, and would tie every drawing test to a world that has to be generated before a rectangle can be filled. So the ground arrives as data instead: a flat slice of one byte per cell, in the same reading order the framebuffer stores pixels.

▣ Build · stage 1: three kinds, four bits
// internal/render/tilemap.go

// Kind is what the ground is at one cell, as far as this package is
// concerned: three numbers, and no idea what a tick or a seed is.
// Whoever owns a world converts its own terrain into these.
type Kind uint8

const (
	KindRock Kind = iota
	KindSoil
	KindWater
)

// wet reports whether cell (cx,cy) of a cols-by-rows kind rectangle is
// water. A coordinate off the rectangle is not water: there is no cell
// out there to be wet, and nothing can stand, swim or grow in it.
func wet(cols, rows int, kind []Kind, cx, cy int) bool {
	if cx < 0 || cx >= cols || cy < 0 || cy >= rows {
		return false
	}
	return kind[cy*cols+cx] == KindWater
}

// NeighbourMask is the four-bit mask of one water cell: a bit is set
// when that neighbour is land, so the answer is 0 for open water and 15
// for a single wet cell with a bank on every side. The sheet is drawn in
// mask order, so the answer is also the cell that draws it. (The short
// name was taken: chapter 14 gave Mask to the picture that paints a
// pixel diff red.)
func NeighbourMask(cols, rows int, kind []Kind, cx, cy int) int {
	m := 0
	if !wet(cols, rows, kind, cx, cy-1) {
		m |= LandN
	}
	if !wet(cols, rows, kind, cx+1, cy) {
		m |= LandE
	}
	if !wet(cols, rows, kind, cx, cy+1) {
		m |= LandS
	}
	if !wet(cols, rows, kind, cx-1, cy) {
		m |= LandW
	}
	return m
}
// cmd/worldc/main.go

// ground copies a terrain grid into the rectangle of kind bytes the
// renderer draws from. The switch is not decoration: sim.Terrain and
// render.Kind are two iota blocks written in two packages, and nothing
// anywhere says their numbers agree. Naming every case is what keeps a
// renumbering in one of them from silently repainting the other.
func ground(g *sim.Grid) []render.Kind {
	k := make([]render.Kind, g.W*g.H)
	for y := range g.H {
		for x := range g.W {
			t, err := g.At(sim.Coord{X: x, Y: y})
			if err != nil {
				fmt.Fprintln(os.Stderr, "worldc:", err)
				os.Exit(1)
			}
			switch t {
			case sim.Rock:
				k[y*g.W+x] = render.KindRock
			case sim.Soil:
				k[y*g.W+x] = render.KindSoil
			case sim.Water:
				k[y*g.W+x] = render.KindWater
			}
		}
	}
	return k
}

LandN, LandE, LandS and LandW are already in the package: they were written beside the sheet as the terms it was drawn to, and the four if statements above are the first code to read them. Nothing here re-derives what 1, 2, 4 and 8 mean. One file says it and two programs obey it.

The switch in ground looks like a longer way to write Kind(t), and the shorter way happens to work today by coincidence alone. Two packages, written months apart, each numbered their own list of ground from zero; that the lists agree anywhere is luck, and a cast would bake the luck into the program without saying so anywhere a reader would look.

Off the grid is not water

wet answers false for a coordinate off the rectangle, which is a decision to state explicitly, because the mask has no other way to treat the four neighbours of a cell on the boundary. Off the grid is either land or water. There is no third option, and the two produce visibly different maps.

The simulation already answered it. Asking the grid for a coordinate it does not hold returns an error and the terrain Rock, which is the zero value a fresh grid is full of before anything is generated into it. The world refuses to move an entity off the edge with the same firmness it refuses to walk one into stone. Nothing out there can be waded through, drunk, or swum across. Calling it land is the reading that matches what every other part of the program does with it.

The alternative is not neutral either. A pond running to the boundary would draw as open water at the frame's edge, no bank, no waterline, the blue stopping where the picture does. Anyone looking at it reads more water past the edge, and there is none.

▣ Build · stage 2: the same pond, twice
$ go run ./cmd/worldc -edges
a 4x3 pond, every cell water

  off the grid is not water        off the grid is more water
     9   1   1   3                    0   0   0   0
     8   0   0   2                    0   0   0   0
    12   4   4   6                    0   0   0   0

Twelve cells of water and nothing else. On the left, a grid four by three; on the right, the identical twelve cells cut out of the middle of a six-by-five pond, so that every off-grid neighbour on the left is a water neighbour on the right. The left column of numbers is a shoreline: 9, 8 and 12 down the west side, 1 at the top, 4 along the bottom, and 0 for the two interior cells that really are surrounded. The right is twelve tiles of open water, which would tile a pond with no edge anywhere in it.

Both readings come out of one line, and the line reads as the plain fact rather than as a policy: a coordinate off the grid is not water. Nothing special-cases the corners, nothing checks whether the map has a rim, and the top-left cell scores 9 because the two neighbours it does not have are, by that rule, not wet.

The mixed coordinate

Stone and soil ask no questions of their neighbours: the boundary between them was left undrawn on purpose, so a rock cell is cell 18 and that is the end of it. Soil has two tiles, drawn to hold the same amount of ink in different places, and something has to choose between them 44 times on this map and several thousand times on a larger one.

The obvious instrument is a random number generator, and it is the wrong one twice over. A generator has a position, so the variant a cell receives would depend on how many cells were drawn before it, which makes the picture a function of the drawing order instead of the ground. Seed it per frame and the field flickers. Seed it once and store the results and it is no longer random, just a table nobody can rebuild.

What is wanted is a function: same coordinate, same variant, every run and every machine, computed on demand with nothing remembered between calls. A cell can then be asked for its tile in isolation, without the map around it having been visited first, and two people running the same seed on different hardware get the same picture down to the pixel.

The trap is that the obvious functions are patterns. (cx + cy) % 2 is same coordinate, same answer, and it lays the two soils out as a chessboard: every cell's east neighbour is the other variant, always, and the eye finds a regular alternation faster than it finds anything else on a map. The last chapter matched the two tiles mark for mark precisely so a field of them would not read as a grid, and the wrong picker throws that away.

▣ Build · stage 3: a number with no pattern in it
// internal/render/tilemap.go

// mix turns a coordinate into one 32-bit number that looks unrelated to
// both halves of it. Three odd primes, two shifts and two exclusive-ors:
// each step spreads the bits of the inputs further across the output, so
// coordinates one step apart land nowhere near each other. Same
// coordinate, same number, on every machine and every run.
func mix(cx, cy int) uint32 {
	h := uint32(cx)*374761393 + uint32(cy)*668265263
	h = (h ^ (h >> 13)) * 1274126177
	return h ^ (h >> 16)
}

// Variant is which of two tiles a cell uses, 0 or 1, taken from the
// bottom bit of the mixed coordinate.
func Variant(cx, cy int) int { return int(mix(cx, cy) & 1) }
$ go run ./cmd/worldc -field 256
256x256 cells of soil, 65536 in all

  rule                 soil A   soil B   same as east   same as south
  (x+y) % 2             32768    32768           0.0%            0.0%
  mixed coordinate      32832    32704          50.2%           49.7%

Both rules split the field down the middle, and only one of them is usable. The count columns are identical to within a rounding error, so a tally of A against B says nothing about which picker is which. The two columns on the right are the measurement that separates them: how often a cell and the cell beside it landed on the same variant.

Zero percent is the chessboard, stated as a number. Not one of the 65,280 east-west pairs shares a variant, and not one of the north-south pairs either, because the rule guarantees the opposite every time. The mixed coordinate agrees with its neighbour about half the time in both directions, which is what having no rule at all looks like from the outside. Runs of two and three appear, then break; nothing repeats at any spacing the eye can lock onto.

Open water takes the same treatment for the same reason. Mask 0 covers the middle of a pond, where a repeat announces itself sooner than anywhere on the map, and the sheet's twentieth tile was drawn as a second open water for exactly this. So a cell whose mask is 0 and whose variant is 1 draws from cell 19 instead, and every other mask keeps the tile its number names.

Ninety-six cells and one number under them

▣ Build · stage 4: kinds in, tile indices out, pixels after that
// internal/render/tilemap.go

// TileMap is one tile index per cell, held in a flat slice the same way
// the framebuffer holds pixels. By the time ground reaches this type
// every decision about what to draw has already been made: the numbers
// in it are cells of the sheet, not kinds of ground.
type TileMap struct {
	Cols, Rows int
	Cell       []uint8
}

// At is the sheet cell one map cell is drawn from.
func (m *TileMap) At(cx, cy int) int { return int(m.Cell[cy*m.Cols+cx]) }

// TileOf is the sheet cell one cell of ground is drawn from: stone and
// soil by their kind alone, water by its four neighbours.
func TileOf(cols, rows int, kind []Kind, cx, cy int) int {
	switch kind[cy*cols+cx] {
	case KindRock:
		return CellRock
	case KindSoil:
		if Variant(cx, cy) == 1 {
			return CellSoilB
		}
		return CellSoilA
	}
	m := NeighbourMask(cols, rows, kind, cx, cy)
	if m == 0 && Variant(cx, cy) == 1 {
		return CellWaterB // open water gets a second tile too
	}
	return m
}

// Autotile turns a rectangle of ground into a rectangle of tile indices,
// asking TileOf once per cell. A cell's answer depends on itself and its
// four neighbours and on nothing else, so a map is built once and only
// the cells around a change are ever asked again.
func Autotile(cols, rows int, kind []Kind) *TileMap {
	m := &TileMap{Cols: cols, Rows: rows, Cell: make([]uint8, cols*rows)}
	for cy := range rows {
		for cx := range cols {
			m.Cell[cy*cols+cx] = uint8(TileOf(cols, rows, kind, cx, cy))
		}
	}
	return m
}

// DrawMap blits every cell of a tile map onto the buffer, with the map's
// own cell (0,0) landing at screen pixel (ox,oy). It returns how many
// cells it drew. Whatever falls outside the buffer is thrown away by the
// clip rectangle, one blit at a time.
func DrawMap(b *Buffer, m *TileMap, s *Sheet, ox, oy int) int {
	n := 0
	for cy := range m.Rows {
		for cx := range m.Cols {
			i := m.At(cx, cy)
			b.Blit(s, s.Frame(i%TileCols, i/TileCols), ox+cx*TileCell, oy+cy*TileCell)
			n++
		}
	}
	return n
}
$ go run ./cmd/worldc -shot valley.png
worldc 0.0.1 sheet assets/tiles/valley-tileset.png 64x80, 20 cells of 16, 4 across
valley seed 5, 12x8 cells: 36 rock, 44 soil, 16 water

  row  ground        tiles
    0  ############  18 18 18 18 18 18 18 18 18 18 18 18
    1  #..........#  18 16 17 16 16 17 16 16 16 16 16 18
    2  #..........#  18 17 17 16 17 16 17 16 17 16 16 18
    3  #.~~~~~....#  18 16  9  1  1  1  3 17 17 17 17 18
    4  #.~~~~~~...#  18 16 12  4 19  0  0  3 17 17 17 18
    5  #...~~~~...#  18 16 16 17  8  4  4  6 16 17 17 18
    6  #...~......#  18 16 17 17 14 17 17 16 16 17 17 18
    7  ############  18 18 18 18 18 18 18 18 18 18 18 18

  16 water cells draw from 10 tiles:
     cell  0 x2  cell  1 x3  cell  3 x2  cell  4 x3  cell  6 x1
     cell  8 x1  cell  9 x1  cell 12 x1  cell 14 x1  cell 19 x1
  44 soil cells: 21 of cell 16, 23 of cell 17
  36 rock cells, every one of them cell 18

cell (2,3) is water
  N ( 2, 2) soil     land  +1
  E ( 3, 3) water    wet   +0
  S ( 2, 4) water    wet   +0
  W ( 1, 3) soil     land  +8
  mask 9 -> sheet cell 9, column 1 row 2

cell (4,6) is water
  N ( 4, 5) water    wet   +0
  E ( 5, 6) soil     land  +2
  S ( 4, 7) rock     land  +4
  W ( 3, 6) soil     land  +8
  mask 14 -> sheet cell 14, column 2 row 3

frame 192x128, 96 cells blitted at origin (0,0)
sha256 8fdff25f39f196d0bc7781112aa5278a0a5b6af2c7f1162fbba75aea77ebe4b6
wrote valley.png

The two columns in the middle of that table are the check the whole chapter rests on. On the left is what volume 1 has printed since its third chapter, one glyph per cell, straight out of the grid. On the right is what the renderer made of it, and the two can be read against each other by eye: every # is an 18, every . is a 16 or a 17, and every ~ is a number from 0 to 15 or the second open water at 19.

Read row 3 across. Rim, soil, then 9 where the pond's north-west corner starts, three 1s along its northern bank, a 3 where the bank turns the corner, and soil after that. Row 4 has 12 at its western end and three cells in the middle where the water is surrounded on all four sides: two of them drew cell 0 and the third drew 19, because its variant said so. The two hand calculations from the interlude are printed underneath, each with its four questions itemised, and both land where the paper said.

Sixteen water cells drew from nine of the transition tiles plus the second open water, so seven of the sixteen never appeared. That is not a fault in the sheet. A twelve-by-eight valley has room for one pond and a stub, and the masks it does not contain are the ones needing an inlet, a one-cell island, or a corner shaped in a way this pond does not have.

One row of the valley, looked up twice Three strips of twelve cells, stacked. The top strip is row 3 of the terrain grid as glyphs: rock, soil, five water cells, four soil, rock. The middle strip is the tile index each of those became: 18, 16, 9, 1, 1, 1, 3, 17, 17, 17, 17, 18. The bottom strip is the column and row of the sheet each index names. The third cell, the water cell whose mask is 9, is highlighted in all three strips and joined by vertical connectors. ROW 3 OF THE VALLEY, LOOKED UP TWICE THE GROUND (one glyph per cell) # . ~ ~ ~ ~ ~ . . . . # THE TILE MAP (one sheet cell per cell) 18 16 9 1 1 1 3 17 17 17 17 18 WHERE THAT CELL SITS ON THE SHEET (column, row) 2,4 0,4 1,2 1,0 1,0 1,0 3,0 1,4 1,4 1,4 1,4 2,4 sheet cell i sits at column i % 4, row i / 4 map cell (cx, 3) is blitted at pixel (cx · 16, 48)

Figure 19.1: the same twelve cells at three removes. Nothing in the second strip depends on anything but the first, and nothing in the third depends on anything but the second, so a change to the ground can only reach the pixels by changing a number in the middle.

A top-down valley: a grey stone rim around brown speckled soil, with an irregular blue pond in the lower left half. The pond has dark bands along its northern and western edges and pale bands along its southern and eastern ones.

Figure 19.2: valley.png, seed 5 at tick 0, 192 by 128 pixels, shown enlarged. Every pixel came from the tileset and every tile was chosen by the arithmetic above. The pond's banks are dark where the land stands between it and the light and pale where the land does not, all the way round, without the renderer ever being told where the pond is.

That is the finished result, and getting to it took one wrong picture first.

⚠ Worked failure: a pond with its shoreline on the inside

The four if statements were written without the !. It is the natural way to say it while looking at a pond: this cell is water, that neighbour is water too, set the bit. The program ran, the map filled in, and the client's own report gave it away before the file was opened:

$ go run ./cmd/worldc -shot inside-out.png
worldc 0.0.1 sheet assets/tiles/valley-tileset.png 64x80, 20 cells of 16, 4 across
valley seed 5, 12x8 cells: 36 rock, 44 soil, 16 water

  row  ground        tiles
    0  ############  18 18 18 18 18 18 18 18 18 18 18 18
    1  #..........#  18 16 17 16 16 17 16 16 16 16 16 18
    2  #..........#  18 17 17 16 17 16 17 16 17 16 16 18
    3  #.~~~~~....#  18 16  6 14 14 14 12 17 17 17 17 18
    4  #.~~~~~~...#  18 16  3 11 15 15 15 12 17 17 17 18
    5  #...~~~~...#  18 16 16 17  7 11 11  9 16 17 17 18
    6  #...~......#  18 16 17 17  1 17 17 16 16 17 17 18
    7  ############  18 18 18 18 18 18 18 18 18 18 18 18

  16 water cells draw from 9 tiles:
     cell  1 x1  cell  3 x1  cell  6 x1  cell  7 x1  cell  9 x1
     cell 11 x3  cell 12 x2  cell 14 x3  cell 15 x3
  44 soil cells: 21 of cell 16, 23 of cell 17
  36 rock cells, every one of them cell 18

cell (2,3) is water
  N ( 2, 2) soil     land  +1
  E ( 3, 3) water    wet   +0
  S ( 2, 4) water    wet   +0
  W ( 1, 3) soil     land  +8
  mask 6 -> sheet cell 6, column 2 row 1

cell (4,6) is water
  N ( 4, 5) water    wet   +0
  E ( 5, 6) soil     land  +2
  S ( 4, 7) rock     land  +4
  W ( 3, 6) soil     land  +8
  mask 1 -> sheet cell 1, column 1 row 0

frame 192x128, 96 cells blitted at origin (0,0)
sha256 f619ad6c7f925077860e41ba0c0d525a8308edacd95f713465c1d384379abc5f
wrote inside-out.png

The four questions add up to 9 and the line under them says 6. Those two numbers are printed by different code: the itemised list is the client working out for itself what each neighbour is, and the total comes from NeighbourMask. When a program contradicts itself in one paragraph of its own output, the disagreement is the shortest route to the cause.

9 and 6 add to 15. Check another: the second cell reported 14 by hand and 1 from the mask, and 14 plus 1 is 15 again. Every mask in the table is 15 minus the one it should be, and 15 minus a four-bit number is that number with every bit flipped. The bug is not in the order of the bits, which is what the previous chapter's sheet got wrong; it is in the sense of all four at once. The sheet was drawn to land here and the code asked water here.

The picture is the loudest part. Each cell drew banks on the sides facing its fellow water cells and flat open water on the sides facing soil, so the shoreline moved to the inside of the pond and the actual boundary with the land lost its waterline entirely. The result is the pond drawn as separate squares, each outlined, meeting in a lattice. The last chapter drew sixteen tiles to stop water and land meeting as two colours of graph paper, and one missing ! puts the graph paper back, inside the pond, using those same sixteen tiles to do it.

The same valley, but the pond is drawn as a grid of separate blue squares, each with dark and pale edges, and the boundary between the water and the surrounding soil is a plain hard cut with no bank.

Figure 19.3: inside-out.png: the same grid, the same tileset, every bit of every mask inverted. Where the water meets the soil there is now nothing at all, and where water meets water there is a bank.

▣ Build · stage 5: four exclamation marks, and the damage measured
$ go run ./cmd/worldc -diff valley.png inside-out.png
valley.png sha256 8fdff25f39f196d0bc7781112aa5278a0a5b6af2c7f1162fbba75aea77ebe4b6
inside-out.png sha256 f619ad6c7f925077860e41ba0c0d525a8308edacd95f713465c1d384379abc5f
1492 of 24576 pixels differ, first at (32,48): ff243e6b became ff3d7799

1,492 pixels of 24,576, and the first of them is the corner of the cell the interlude worked through: world pixel (32, 48) is map cell (2, 3), the top-left pixel of its sixteen-by-sixteen square. In the correct frame that pixel is the dark band of a northern bank; in the broken one it is plain mid-tone water. 1,458 of the 1,492 sit in the outermost two pixels of a tile, which is the only place the sixteen transition tiles differ from one another at all. The remaining 34 are one cell: (4, 4) drew the second open water in the correct map and cell 15 in the broken one, and those two disagree in their middles as well as at their edges.

The counts in the earlier table also moved in a way that reads as a symptom once you know what to look for. The correct map uses cell 0 twice and cell 15 never; the broken one uses cell 15 three times and cell 0 not at all. Cell 15 draws a lone wet cell with a bank on every side, and every one of this pond's sixteen cells has at least one wet neighbour, so that tile belongs nowhere on this map.

▣ Build · stage 6: the same map through a smaller window
$ go run ./cmd/worldc -window | tail -2
frame 96x64, 96 cells blitted at origin (-48,-32)
sha256 9cd77d1b26198e80aab9402550599d78b83e46c31b71d6bd448ebbe97ae4fc16

DrawMap takes the screen pixel its cell (0, 0) lands on, and moving that corner is the only lever anything outside the renderer needs. Here it is (−48, −32) on a buffer a quarter of the valley's size, and the picture is the middle of the map: the blits at negative coordinates are cut away by the clip rectangle, which has been refusing writes outside itself since it was written, and the ones that survive land where the subtraction puts them. All 96 cells were still offered, which is honest work for a twelve-by-eight map and would stop being honest on a large one.

▣ Build · stage 7: the claims, written down
// cmd/worldc/main_test.go

// valleyHash is the SHA-256 of the pixels seed 5's valley draws to.
const valleyHash = "8fdff25f39f196d0bc7781112aa5278a0a5b6af2c7f1162fbba75aea77ebe4b6"

// TestValleyMatchesItsHash is the whole chapter in one assertion: the
// generator's grid, the tileset on disk, and the picture they make
// together. This test lives here rather than in internal/render because
// this file is the only one in the program that imports both packages.
func TestValleyMatchesItsHash(t *testing.T) {
	s, err := render.LoadSheet(filepath.Join("..", "..", sheetPath), render.TileCell)
	if err != nil {
		t.Fatal(err)
	}
	g := sim.Generate(valleyW, valleyH, valleySed)
	b := render.NewBuffer(g.W*render.TileCell, g.H*render.TileCell)
	render.DrawMap(b, tiles(g), s, 0, 0)
	if got := b.Hash(); got != valleyHash {
		if err := b.SavePNG("valley.got.png"); err != nil {
			t.Fatal(err)
		}
		t.Fatalf("pixels hash %s\n          want %s\n    wrote valley.got.png", got, valleyHash)
	}
}
$ go test -count=1 -v ./internal/render/ ./cmd/worldc/
=== RUN   TestOverKeepsItsEnds
--- PASS: TestOverKeepsItsEnds (0.00s)
=== RUN   TestSheetIsStraight
--- PASS: TestSheetIsStraight (0.00s)
=== RUN   TestSpriteSceneMatchesItsHash
--- PASS: TestSpriteSceneMatchesItsHash (0.00s)
=== RUN   TestMaskReadsFourNeighbours
--- PASS: TestMaskReadsFourNeighbours (0.00s)
=== RUN   TestOffTheGridIsLand
--- PASS: TestOffTheGridIsLand (0.00s)
=== RUN   TestVariantDoesNotCheckerboard
--- PASS: TestVariantDoesNotCheckerboard (0.00s)
PASS
ok  	theworld/internal/render	0.00s
=== RUN   TestValleyMatchesItsHash
--- PASS: TestValleyMatchesItsHash (0.00s)
PASS
ok  	theworld/cmd/worldc	0.00s

Three of the six in internal/render are new and none of them needs a world. TestMaskReadsFourNeighbours builds the five-cell pond from the last chapter's first exercise out of a string of dots and ws and checks the five answers that exercise worked out by hand. TestOffTheGridIsLand pins the boundary decision on a two-by-two grid of pure water, where all four cells are corners. TestVariantDoesNotCheckerboard is the measurement from stage 3 with limits on it: near half the cells on soil B, and near half of the east-west pairs matching. The chessboard rule passes the first and fails the second at every cell on the field.

Five cells can change one tile

A tile index is a property of a cell and its four neighbours, as fixed as the terrain itself, and it can only change when one of those five cells changes. Treating it as a drawing decision, made afresh every frame, gets the same answer at a hundred times the price. That is what makes TileMap a stored rectangle instead of a computation inside the drawing loop. Build it once when the world arrives; when the spring floods a cell, five entries need recomputing, and the other ninety-one are already correct. The drawing loop then does no thinking at all: read a number, split it into a column and a row, blit.

The layering underneath is the part to carry forward. Four separate rectangles describe the same 96 cells: terrain in the simulation, kinds in a byte slice, indices in the tile map, pixels in the framebuffer. Each one is derived from exactly the one before it, so each boundary can be checked on its own. The mask arithmetic is tested with no PNG in sight, the blit was tested with no world in sight, and the single test that needs both is the one that owns the frame's hash. A renderer that reached into the simulation for a terrain value would collapse all four into one, and every question about the picture would become a question about the whole program.

The general pattern is one that keeps returning: when a picture must be derived from data, derive it with a function of the data alone and nothing else. No draw from a generator, no counter incremented as cells are visited, no memory of what was drawn last frame. Then two machines agree, a test can name a hash, and a bug is reproducible on the first attempt instead of the fifth. The soil variant is the smallest possible example: it looks like randomness, it behaves like randomness, and it is a function of two integers that anybody can evaluate by hand.

And the sheet's ordering deserves a second look now that something depends on it. Drawing the transition set in mask order collapsed a lookup table into an equality, so there is no table to keep in step with the art, no place where a tile can be renamed without the code noticing, and no chance for the two to drift. Arranging data so the arithmetic falls out of its layout is older than tilesets and shows up wherever an index can be computed instead of searched for.

Checkpoint

✓ Checkpoint: what you can now do
  • Given a cell of the terrain grid and its four neighbours, name the sheet cell that draws it, and locate that cell's column and row with a division and a remainder.
  • Say what a coordinate off the edge of the grid counts as, and give the reason from what the simulation already does with such a coordinate.
  • Explain why a soil variant is computed from the coordinate and not drawn from a generator, and name the two things a seeded draw would tie the picture to.
  • Measure a variant picker for hidden pattern by counting how often neighbouring cells agree, and say what 0 percent and 50 percent each mean on the map.
  • Given a map whose masks are all inverted, recognise the sum of 15 in the client's own output and name the line of code responsible.
  • Say why a tile map is stored rather than recomputed per frame, and how many entries one flooded cell invalidates.
⚡ Exercises: try first, then reveal
Exercise 1: four masks along the south shore. Row 5 of seed 5 reads #...~~~~...#, and row 4 above it reads #.~~~~~~...#. Work out the masks for all four of row 5's water cells before running anything, then check them against the table the client prints.

8, 4, 4 and 6. Cell (4, 5) has water above it and water to its east, water below at (4, 6), and soil to its west: 8 alone. Cells (5, 5) and (6, 5) each have water above and water on both sides, with soil beneath: 4 alone, twice. Cell (7, 5) has water above at (7, 4), soil east and soil south: 2 + 4 = 6.

Those four are the pond's southern bank, and the run above prints them in row 5 in that order. Look at the picture with the numbers in mind: 8 draws a dark band down the left of its tile, the two 4s draw pale shallows along the bottom, and 6 draws pale along the bottom and the right, turning the corner.

Exercise 2: swap two bits and count the damage. In NeighbourMask, set LandS for the eastern neighbour and LandE for the southern one, leaving north and west alone. Predict which cells change before you render.

Six of the sixteen, and 273 pixels. Swapping two bits changes nothing wherever those two bits are already equal, so every mask in which east and south agree survives untouched: 0, 1, 6, 7, 8, 9, 14 and 15. The valley's 9, its three 1s, its three cells of open water, its 8, its 6 and its 14 all come through. The six that do not are its two 3s, its 12 and its three 4s, which become 5, 10 and 2.

$ go run ./cmd/worldc -diff valley.png swapped.png
valley.png sha256 8fdff25f39f196d0bc7781112aa5278a0a5b6af2c7f1162fbba75aea77ebe4b6
swapped.png sha256 34813743bedab907f524875e184f3b72692fbc509c39f0154f51c0a26b8602c2
273 of 24576 pixels differ, first at (111,49): ff6dbec7 became ff3d7799

The set of survivors is the same eight the previous chapter's seam checker named when the art had this fault instead of the code, for the same arithmetic reason. From the picture alone the two are indistinguishable: a bank on the wrong side is a bank on the wrong side, whichever half of the agreement broke it.

Exercise 3: a different world through the same renderer. Nothing in the code mentions the number 5. Add a -seed flag, render seed 9, and count how many of the sixteen transition tiles that valley needs.
$ go run ./cmd/worldc -seed 9 | head -19
worldc 0.0.1 sheet assets/tiles/valley-tileset.png 64x80, 20 cells of 16, 4 across
valley seed 9, 12x8 cells: 36 rock, 45 soil, 15 water

  row  ground        tiles
    0  ############  18 18 18 18 18 18 18 18 18 18 18 18
    1  #..~~......#  18 16 17  9  3 17 16 16 16 16 16 18
    2  #..~~.~....#  18 17 17  8  2 16 11 16 17 16 16 18
    3  #.~~~~~....#  18 16  9 19  0  1  6 17 17 17 17 18
    4  #.~~~~.....#  18 16 12  0  4  6 16 16 17 17 17 18
    5  #..~.......#  18 16 16 14 16 16 17 16 16 17 17 18
    6  #..........#  18 16 17 17 17 17 17 16 16 17 17 18
    7  ############  18 18 18 18 18 18 18 18 18 18 18 18

  15 water cells draw from 12 tiles:
     cell  0 x2  cell  1 x1  cell  2 x1  cell  3 x1  cell  4 x1
     cell  6 x2  cell  8 x1  cell  9 x2  cell 11 x1  cell 12 x1
     cell 14 x1  cell 19 x1
  45 soil cells: 23 of cell 16, 22 of cell 17
  36 rock cells, every one of them cell 18

Twelve tiles for fifteen water cells, against seed 5's ten for sixteen. Seed 9's pond is more ragged, so it needs masks the other never asked for: a 2 at (4, 2), a stray 11 at (6, 2) where a single wet cell sits open only to the south, and two 9s instead of one. The renderer changed by not one line. That is what it means for the picture to be a function of the ground.