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
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.
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.
// 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.
$ 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.
// 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
// 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.
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.
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.
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.
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.
$ 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.
$ 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.
// 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
- 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.
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.