The World Vol 1 · A World That Ticks
ch 02 / 105
Chapter 02

A Place on the Grid

The Coord rule

worldd announces itself and exits because it has no way to say where anything is. The first server rule is small and permanent: a place is one value: the column and the row travel together, or not at all.

The world is a grid: columns running east, rows running south, and every future inhabitant of The Hollow standing on exactly one square of it. A place on that grid is a column number and a row number.

Keeping the numbers loose makes every grid function grow a pair of parameters: x int, y int. A function that moves something takes four integers, two for where it is and two for where it goes.

Nothing stops you from passing the row where the column belongs, and the compiler cannot object, because an int is an int. The call move(7, 12, 12, 7) compiles as happily backwards as forwards.

In a simulation that runs for days unattended, a swapped pair does not crash anything. It quietly puts a creature on the wrong square and lets the world carry on being subtly wrong.

Go's tool for binding several values into one is the struct: a type you define by listing named fields, each with its own type. Once a place is a single value, a function that moves something takes two arguments, a from and a to, and passing them backwards is a mistake you can see.

◆ Note — the types the fields are made of

Struct fields are built from Go's basic types, and this book leans on four of them. int is a whole number, positive or negative; grid columns and rows are ints. float64 is a number with a fractional part, like a height of 2.5 meters.

bool is true or false. string is text.

Every type also has a zero value, the value a variable holds before you assign anything: 0 for numbers, false for bool, "" for strings. Go never leaves a variable undefined, a guarantee that matters in a program meant to run unattended.

The type also needs a home. The layout from last chapter reserved internal/sim for the simulation itself, and this is the moment it earns its keep.

cmd/worldd is the command: it starts things and prints things. The world's own vocabulary, starting today with what a place is, belongs to the sim package, where the command can use it but does not own it.

The Coord struct

▣ Build · stage 1 — define Coord and hold one in your hand
// internal/sim/coord.go
// Package sim holds the simulation's own types. Nothing in here
// prints, listens, or knows that a command called worldd exists.
package sim

// Coord is a place on the world grid: column X, row Y.
type Coord struct {
	X int
	Y int
}
// cmd/worldd/main.go
package main

import (
	"fmt"

	"theworld/internal/sim"
)

func main() {
	var origin sim.Coord
	spring := sim.Coord{X: 12, Y: 7}
	fmt.Println("origin:", origin)
	fmt.Println("spring:", spring)
	fmt.Println("spring column:", spring.X)
}
$ go run ./cmd/worldd
origin: {0 0}
spring: {12 7}
spring column: 12

type Coord struct { ... } teaches Go a new noun. From that line on, Coord is as real to the compiler as int: you can declare variables of it, pass it, return it.

The import path theworld/internal/sim is the module name from last chapter plus the directory. Inside main, the package's names arrive with a sim. prefix.

Capitalized names like Coord and X are the ones a package makes visible outside itself.

var origin sim.Coord got the zero value with no assignment anywhere. A struct's zero value is every field at its own zero, so an unset place is {0 0}, the top-left corner, never garbage.

The struct literal sim.Coord{X: 12, Y: 7} builds a value with the fields named, so the reader of the call site knows which 12 is which. The dot in spring.X reaches into the value for one field.

A Coord that only sits there is still a pair of boxes. The server will constantly step from one place to a neighbor and measure how far apart two places are.

Those questions should live on the type, so that every part of the program asks the same code the same question. That is what a method is: a function with a home.

▣ Build · stage 2 — methods: behavior beside the data
// internal/sim/coord.go — add below the type

// Offset returns the coordinate dx columns and dy rows away.
func (c Coord) Offset(dx, dy int) Coord {
	return Coord{X: c.X + dx, Y: c.Y + dy}
}

// Dist returns the number of grid steps between two coordinates,
// counting only moves along a row or a column.
func (c Coord) Dist(o Coord) int {
	dx := c.X - o.X
	if dx < 0 {
		dx = -dx
	}
	dy := c.Y - o.Y
	if dy < 0 {
		dy = -dy
	}
	return dx + dy
}
// cmd/worldd/main.go — the new body of main
	var origin sim.Coord
	spring := sim.Coord{X: 12, Y: 7}
	east := spring.Offset(1, 0)
	fmt.Println("east of spring:", east)
	fmt.Println("origin to spring:", origin.Dist(spring), "steps")
	fmt.Println("spring to origin:", spring.Dist(origin), "steps")
$ go run ./cmd/worldd
east of spring: {13 7}
origin to spring: 19 steps
spring to origin: 19 steps

The part in parentheses before the name, (c Coord), is the receiver. It declares that Offset belongs to Coord, and inside the method c is the coordinate the call happened on.

You call it with the same dot that reads a field. spring.Offset(1, 0) asks the spring for the square one column east.

Dist counts steps the way something walking the grid would, no diagonals: from the origin to the spring is 12 columns plus 7 rows, 19 steps. The two if blocks flip negative differences positive, so the answer comes out the same from either end, as the run confirms.

Offset never touches c. It builds and returns a new Coord, and spring is still {12 7} afterwards. That choice turns into this chapter's worked failure.

The Cell struct

A coordinate names a square. A Cell is what the square holds.

For now, before terrain proper arrives, a cell needs three things: where it is, how high its ground sits, and how deep the water standing on it runs. One of those fields is a struct, and Go does not blink: a field's type can be any type, including one you defined a page ago.

▣ Build · stage 3 — a struct inside a struct, and two questions it can answer
// internal/sim/cell.go
package sim

// Cell is one square of ground: where it sits, how high the ground
// is, and how deep the water standing on it runs. Heights and depths
// are in meters.
type Cell struct {
	At        Coord
	Elevation float64
	Water     float64
}

// Wet reports whether any water stands on the cell.
func (c Cell) Wet() bool {
	return c.Water > 0
}

// Surface returns the height of the cell's top: ground plus water.
func (c Cell) Surface() float64 {
	return c.Elevation + c.Water
}
// cmd/worldd/main.go — the new body of main
	spring := sim.Coord{X: 12, Y: 7}
	pool := sim.Cell{At: spring, Elevation: 1.0, Water: 0.4}
	bank := sim.Cell{At: spring.Offset(1, 0), Elevation: 2.5}
	fmt.Println("pool:", pool.At, "wet:", pool.Wet(), "surface:", pool.Surface())
	fmt.Println("bank:", bank.At, "wet:", bank.Wet(), "surface:", bank.Surface())
$ go run ./cmd/worldd
pool: {12 7} wet: true surface: 1.4
bank: {13 7} wet: false surface: 2.5

The bank literal names only two of the three fields, and Water quietly takes its zero: dry, exactly as a field left unsaid should be.

Both methods answer questions any part of the server might ask, and both answer them in one place. If the definition of wet ever grows a threshold, the code that changes is one line in sim, not a comparison copied through the codebase.

That is the case for methods in one sentence: the type carries its own vocabulary, so the rest of the program asks instead of assuming.

A Cell value on the grid A small grid with the pool cell highlighted at column 12, row 7, and beside it a card listing the Cell struct's fields: At, a Coord holding 12 and 7; Elevation 1.0; Water 0.4. Arrows connect the highlighted square to the card. one square, one value X = 12 Y = 7 sim.Cell At {12 7} Elevation 1.0 Water 0.4 travels as one value

Figure 2.1 — the pool at column 12, row 7, and the single struct value that carries everything the server knows about that square.

Pointer receivers

Wet and Surface read a cell. A server also needs to change one: rain falls, and the water on a square deepens.

Following the pattern of every method so far gives an obvious next method. It is wrong in a way no error message reports.

⚠ Worked failure — the flood that evaporated on return
// internal/sim/cell.go — the obvious attempt
// Flood adds depth meters of standing water to the cell.
func (c Cell) Flood(depth float64) {
	c.Water += depth
}
// cmd/worldd/main.go — the new body of main
	spring := sim.Coord{X: 12, Y: 7}
	bank := sim.Cell{At: spring.Offset(1, 0), Elevation: 2.5}
	fmt.Println("before:", "wet:", bank.Wet(), "water:", bank.Water)
	bank.Flood(0.3)
	fmt.Println("after: ", "wet:", bank.Wet(), "water:", bank.Water)
$ go run ./cmd/worldd
before: wet: false water: 0
after:  wet: false water: 0

It compiles. go vet ./... passes. The line c.Water += depth runs, and adds 0.3 to something.

Reason from the symptom: the addition definitely happened, so there must have been a second Cell for it to happen to. There was.

Go passes copies. Calling a function with a struct hands the function a duplicate of it, and a receiver written (c Cell) is a parameter like any other.

c was a copy of bank, the flood soaked the copy, and the copy was thrown away when the method returned.

Stage 2's Offset had the same receiver and never noticed, because it never tried to change c. It returned a new value instead. A reading method on a copy is correct. A writing method on a copy is a no-op with good intentions.

▣ Build · stage 4 — a pointer receiver, so the change lands
// internal/sim/cell.go — the fix: *Cell, not Cell
// Flood adds depth meters of standing water to the cell.
func (c *Cell) Flood(depth float64) {
	c.Water += depth
}
$ go run ./cmd/worldd
before: wet: false water: 0
after:  wet: true water: 0.3

One character changed. *Cell means the method receives not a copy of the cell but its address: a pointer, a value that says where the original lives in memory.

Through the pointer, c.Water += depth reaches the actual bank, and the run shows the water arriving and staying.

The call site did not change at all. bank.Flood(0.3) still reads the same, because Go sees a pointer receiver and passes the address for you.

The receiver declaration is where the choice lives, and it is a one-word answer to one question: does this method change the thing it belongs to? No: value receiver. Yes: pointer receiver.

Why copies are the default

Everything in this chapter rests on one property of Go: assignment and argument passing copy the value, whatever the value is. An int copies, and so does a struct of two ints, and so does a struct holding another struct.

Copies make code easy to reason about. No function can change your variables behind your back unless you handed it their address on purpose.

The cost is the trap you walked out of: a method that wants to mutate must say so, with a pointer receiver. The * in the declaration makes that statement in public. When you read func (c *Cell) Flood six volumes from now, it will still tell you at a glance that flooding changes the cell.

The same property explains why Coord can stay copy-friendly forever. A coordinate is a fact, not a thing with a life: moving something should produce the new place, not edit the old one.

Offset returning a fresh value means the spring's coordinates cannot be corrupted by whoever borrowed them.

Small immutable facts get value receivers. Stateful residents of the world, of which Cell is the first and creatures will be louder, get pointer receivers on anything that changes them. The whole server stays readable through that split.

That is what behavior beside data buys in practice. The two types you wrote are not passive records with helper functions scattered around them; they are the beginnings of a vocabulary.

Code elsewhere in the server will say cell.Wet() and place.Dist(target), sentences with the noun and the verb attached. The definitions of those verbs sit in one file, next to the fields they read, where a change is one edit.

Checkpoint

✓ Checkpoint — what you can now do
  • Define a struct type, build one with a named-field literal, and predict its zero value field by field, including {{0 0} 0 0} for a struct inside a struct.
  • Explain what the two loose ints design costs, and what binding them into Coord makes impossible.
  • Write a method, name its receiver, and say precisely what c is inside Offset when spring.Offset(1, 0) runs.
  • Compute Dist by hand for any two coordinates: 12 columns plus 7 rows made the run print 19 steps.
  • Handed a method that compiles, runs, and changes nothing, check the receiver before anything else, and narrate where the lost mutation went.
  • Choose between (c Cell) and (c *Cell) for a new method and defend the choice in one sentence.
⚡ Exercises — try first, then reveal
Exercise 1 — a coordinate that knows its limits. Give Coord a method In(w, h int) bool that reports whether the coordinate lies on a grid w columns wide and h rows tall. Print spring.In(64, 64) and sim.Coord{X: 64, Y: 7}.In(64, 64), and decide before running which is false.

Four comparisons joined with &&: c.X >= 0 && c.X < w && c.Y >= 0 && c.Y < h. The prints come out true then false: on a 64-wide grid the columns run 0 through 63, so X: 64 is the first column that does not exist. Off-by-one boundaries like this are exactly where a method earns its keep, because the <-versus-<= decision gets made once.

Exercise 2 — drain, but never below dry. Write Drain(depth float64) on Cell: it removes up to depth meters of standing water and refuses to go negative. Start a cell at Water: 0.4, drain 1.0, and print the result. Which receiver does it need?

Pointer receiver, because draining changes the cell: subtract, then clamp with if c.Water < 0 { c.Water = 0 }. The print reads pool water: 0, not -0.6. Write it with (c Cell) first if you want the failure box again on your own terms: the print stays 0.4, and this time you can name the missing character.

Exercise 3 — predict the zero world. Declare var blank sim.Cell and write down, before running, exactly what fmt.Println(blank) prints, field by field. Then run it.

{{0 0} 0 0}: the inner braces are the zero Coord inside At, then Elevation and Water at 0. A blank cell is a dry square at the origin on the valley floor: every field defined, none of them meaningful yet. Distinguishing "zero" from "never set" is a real problem this server will meet again, and the first defense is knowing the zero values cold.