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

A Plant Grown From a String

Plant as rewritten text

Everything this volume has put on the screen so far has been something moving. A mote crossing the soil, leaves carried on a draught, planks riding the pool, a herd closing ranks. The Hollow still has nothing in it that stands where it is and gets bigger, and a valley wants a great many such things: bracken along the southern rim, scrub on the rock, a tree by the water with a hundred branches on it, none of them arranged quite like the branches of the tree beside it.

Growth is rewriting: a plant begins as a string of one or two characters, and each generation every character in it is replaced at the same instant by a longer piece of text. Drawing the plant is a separate job afterwards, and it is nothing more than reading that sentence once, left to right. Drawing them is the obvious answer, and it fails twice over. The tileset chapter already spent an afternoon on the first failure: the eye finds a repeat faster than it reads a texture, so one picture of a tree placed forty times across a valley stops being forty trees the moment somebody looks at two of them together. The second failure is the one this book cannot live with. A picture of a tree cannot be a seedling first. It cannot put out one more branch because the year was wet, it cannot be shorter because it grew in shade, and it cannot be the offspring of two other trees with one number nudged. A drawing has no parts that a world can reach into and change.

What is wanted instead is a description a plant can be built from, small enough to store thousands of, and made of pieces a program can vary.

Everything below is worked in characters and plain numbers before any of it is drawn. One axiom, one rule, three generations written out by hand and counted. Then a reader for the finished string with five instructions in it, built on the angle vocabulary from chapter 28, and a plant standing on the soil of The Hollow with its frame hashed. The last section is the one this book came for: every number that decided how that plant turned out gets pulled out of the code and put in a struct where something else could choose it.

The F production

Start with a string of a single character: F. That string is called the axiom, and it is the plant at generation 0. Now write down one rule saying what F turns into whenever a generation passes:

F → F[+F]F[-F]F

Five of the eleven characters on the right are F again, which is what makes this a rule that never finishes. The other six are [, +, ], [, - and ], and none of them means anything yet. They are marks that get carried along, because the rule says nothing about them and a character with no rule stands for itself.

Apply the rule by hand. Generation 0 is F, one character. Generation 1 replaces that single F with the eleven characters of the production, giving F[+F]F[-F]F. Generation 2 takes each of the five Fs in that string and replaces each one with the same eleven characters, while the six other characters stay exactly where they are, so the count is 5 × 11 = 55 for the rewritten letters plus 6 for the carried marks: 61 characters. Generation 3 has 25 Fs to replace, so it comes to 25 × 11 = 275 plus the 36 characters of generation 2 that were not F: 311. The arithmetic never gets harder than that, and it never needs the string.

▣ Build · stage 1 — the rewriter, and a census that never builds a string
// internal/grow/lsys.go
// Package grow keeps the two halves of a growing thing apart: a grammar
// that turns a short string into a long one by rewriting it, and a
// turtle that reads the long one back as a list of straight segments.
// Nothing in here knows what a pixel is.
package grow

import "math/rand/v2"

// Rules says what each character becomes when one generation passes. A
// character with a single production is rewritten the same way every
// time. A character with several is a choice, taken from the stream the
// caller hands over, so the same seed grows the same plant. A character
// with no entry at all stands for itself, which is how +, -, [ and ]
// survive every generation untouched.
type Rules map[byte][]string

// Grow rewrites every character of s once. Every character is read out
// of the old string and written into a new one, so nothing this pass
// produces is ever rewritten by the same pass.
func Grow(s string, r Rules, rng *rand.Rand) string {
	out := make([]byte, 0, 2*len(s))
	for i := 0; i < len(s); i++ {
		p := r[s[i]]
		switch {
		case len(p) == 0:
			out = append(out, s[i])
		case len(p) == 1:
			out = append(out, p[0]...)
		default:
			out = append(out, p[rng.IntN(len(p))]...)
		}
	}
	return string(out)
}
// internal/grow/lsys.go — counting without growing

// Census is how many of each character a string holds: enough to work
// out how long the next generation will be without building it.
type Census map[byte]int

// Take counts the characters of one string.
func Take(s string) Census {
	c := Census{}
	for i := 0; i < len(s); i++ {
		c[s[i]]++
	}
	return c
}

// Next is the census of the generation after this one, computed from
// counts alone. A character with a choice of productions makes the
// answer a guess instead of a count, and Next says so rather than
// picking one.
func (c Census) Next(r Rules) (Census, bool) {
	n := Census{}
	for ch, k := range c {
		p := r[ch]
		if len(p) > 1 {
			return nil, false
		}
		if len(p) == 0 {
			n[ch] += k
			continue
		}
		for i := 0; i < len(p[0]); i++ {
			n[p[0][i]] += k
		}
	}
	return n, true
}
$ go run ./cmd/grow -mode gens
axiom F, F -> F[+F]F[-F]F
  gen 0     1  F
  gen 1    11  F[+F]F[-F]F
  gen 2    61  F[+F]F[-F]F[+F[+F]F[-F]F]F[+F]F[-F]F[-F[+F]F[-F]F]F[+F]F[-F]F

   gen    characters        F's   brackets   straight run  ink / height
     0             1          1          0              1          1.00
     1            11          5          2              3          1.67
     2            61         25         12              9          2.78
     3           311        125         62             27          4.63
     4          1561        625        312             81          7.72
     5          7811       3125       1562            243         12.86
     6         39061      15625       7812            729         21.43
     7        195311      78125      39062           2187         35.72
     8        976561     390625     195312           6561         59.54
     9       4882811    1953125     976562          19683         99.23
    10      24414061    9765625    4882812          59049        165.38
    11     122070311   48828125   24414062         177147        275.64
    12     610351561  244140625  122070312         531441        459.39

The first three lines are the strings, and the second one can be checked against the paragraph above character for character. The table underneath was never built. Only generations 0, 1 and 2 exist as text anywhere in that run; every row from 3 down is a census being multiplied out, which is why a run that finishes in milliseconds can tell you that generation 12 would hold 610,351,561 characters. Building it would need 610 megabytes of string and a good deal of patience.

Two columns carry the growth. The F count multiplies by 5 every generation, since every F becomes five of them, so it is 5 multiplied by itself as many times as there are generations. The straight run multiplies by 3, because three of those five Fs sit outside every bracket and those three are the ones that carry the main stalk upward. Five against three is the whole story of the last column.

∑ Math Interlude — the notation, and why generation 8 is a wall

Everything above was counting. Here is the shorthand, introduced now that there is something for it to be shorthand for. The axiom, the string a plant is generation 0 of, is written with the Greek letter omega, ω. A rule is written with an arrow: F → F[+F]F[-F]F says that wherever an F appears, those eleven characters take its place. The generation number is n, and how many characters a string s holds is written |s|, in the same upright bars chapter 24 put round a vector, doing the same job: how much of it there is.

One more piece of shorthand, and it is the piece the wall is built out of. Writing 5n means n fives multiplied together, so 53 = 125 and 58 = 390,625. With that, both counted columns fit on two lines. The number of Fs at generation n is 5n. The number of characters has a closed form too, and it can be checked against any row of the table above:

|s| = (5n+1 − 3) ÷ 2

Try it at n = 3: 54 = 625, less 3 is 622, halved is 311. Now put the two growth rates beside each other. The ink multiplies by 5 a generation and the height multiplies by 3, so the ink laid down per pixel of height multiplies by 5 ÷ 3 every time, which is the last column of the table: 7.72 at generation 4, 59.54 at generation 8. The plant is not running out of memory at generation 8. A megabyte of text is nothing. It is running out of picture: 390,625 segments are being laid into a silhouette only 6,561 segments tall, so nearly sixty strokes of green land on every pixel of height and the branches fill in solid. Past a certain generation the drawing stops answering questions about the string, which makes the generation count a knob with a useful range and a hard stop, not a dial that goes to infinity.

ω"omega": the axiom, the string a plant is generation 0 of
a → ba rewrite rule: every a in the string becomes b in the next generation
nthe generation: how many times the rules have been applied to the axiom
|s|how many characters the string s holds; |F[+F]F[-F]F| = 11
5nn fives multiplied together: 5² = 25, 5³ = 125
F's at n5n, since every F is replaced by a production holding five
straight run3n: the F's outside every bracket, which is the plant's height in segments

The turtle stack

Now the characters get meanings, and there are only five of them. F means draw a straight line forward, one segment long, and stand at the far end of it. + means turn one way; - means turn the other. [ and ] are the pair that make the difference between a stalk and a plant.

A reader of this kind is called a turtle, after the drawing robots that first ran instructions like these across a sheet of paper. Ours needs two numbers to know where it is in the world: a position, which is a vector, and a heading, which is a place on the circle from chapter 28 measured as rim walked. Setting off up the page is a quarter turn of rim, 1.5708, and + is nothing but an addition to that number while - is a subtraction. There is no separate idea of "turning left" anywhere in the code, and nothing anywhere counts degrees.

The bracket pair is a memory. When the turtle meets [ it writes down both of its numbers, where it stands and which way it faces, and carries on. When it meets ] it takes the last pair it wrote down, teleports back to that place with that heading, and throws the note away. Everything between the two brackets is a branch, and the note is the branch's memory of the joint it grew out of. The notes have to come back in reverse order, newest first, because a branch growing off a branch has to finish before the branch it interrupted can carry on. A list that gives back its newest item first is called a stack, and pushing and popping one is the entire mechanism.

▣ Build · stage 2 — the reader, in exact numbers, touching no pixel
// internal/grow/turtle.go

// pose is everything a branch has to remember about the turtle that
// left it: where it stood, and which way it faced. Both, or the branch
// does not come back to the same place pointing the same way.
type pose struct {
	pos     vec.Vec2
	heading float64 // rim, measured the way chapter 28 measures a turn
}

// Segment is one straight piece of a drawing: two ends in world pixels,
// and how many brackets deep the turtle was when it drew it. The turtle
// produces these and never touches a pixel.
type Segment struct {
	A, B  vec.Vec2
	Depth int
}

// Step is the movement a heading of h and a length of n come to, in
// world pixels. The heading names a place on chapter 28's circle, whose
// height counts upward, and a framebuffer counts rows downward: the one
// minus sign is that disagreement, and it lives here because here is
// where an angle stops being an angle and becomes a movement.
func Step(h, n float64) vec.Vec2 {
	return vec.Vec2{X: math.Cos(h), Y: -math.Sin(h)}.Scale(n)
}
// internal/grow/turtle.go — the five instructions

// Read walks the string once and hands back every segment an F drew.
// seg is how many world pixels one F carries the turtle; turn is how
// much rim one + or - adds to its heading. Any character the turtle has
// no meaning for is stepped over, which is what lets a grammar carry
// letters that exist only to be rewritten.
func Read(s string, start vec.Vec2, heading, seg, turn float64) []Segment {
	t := pose{pos: start, heading: heading}
	var stack []pose
	var out []Segment
	for i := 0; i < len(s); i++ {
		switch s[i] {
		case 'F':
			out = append(out, Segment{A: t.pos, B: t.pos.Add(Step(t.heading, seg)), Depth: len(stack)})
			t.pos = out[len(out)-1].B
		case '+':
			t.heading += turn
		case '-':
			t.heading -= turn
		case '[':
			stack = append(stack, t)
		case ']':
			if len(stack) == 0 {
				continue // Balanced catches this before Read ever runs
			}
			t = stack[len(stack)-1]
			stack = stack[:len(stack)-1]
		}
	}
	return out
}
$ go run ./cmd/grow -mode read
F[+F]F[-F]F: segment 10 pixels, turn 1/8 of a turn (0.7854 rim), setting off at 1.5708 rim
  [ remembers the place and heading
    i  char what it does                        x        y   heading  stack
    0     F draw (+0.0000,-10.0000)        0.0000 -10.0000    1.5708      0
    1     [ remember here, and this way    0.0000 -10.0000    1.5708      1
    2     + turn, heading gains            0.0000 -10.0000    2.3562      1
    3     F draw (-7.0711,-7.0711)        -7.0711 -17.0711    2.3562      1
    4     ] back to where the [ was        0.0000 -10.0000    1.5708      0
    5     F draw (+0.0000,-10.0000)        0.0000 -20.0000    1.5708      0
    6     [ remember here, and this way    0.0000 -20.0000    1.5708      1
    7     - turn, heading loses            0.0000 -20.0000    0.7854      1
    8     F draw (+7.0711,-7.0711)         7.0711 -27.0711    0.7854      1
    9     ] back to where the [ was        0.0000 -20.0000    1.5708      0
   10     F draw (+0.0000,-10.0000)        0.0000 -30.0000    1.5708      0

Every number in that table was predictable before the run. The turn is an eighth of a turn, which the interlude in chapter 28 wrote as 0.7854 of rim, and a heading an eighth of a turn either side of straight up gives a movement of 7.0711 across and 7.0711 up: the 0.7071 from that chapter's very first table, multiplied by the ten-pixel segment. The turtle drew five lines and the string holds five Fs.

Read the last two columns together and the brackets tell their own story. The stack goes to 1 at index 1 and back to 0 at index 4, and the position and heading at index 4 are digit for digit what they were at index 1. Nothing between them left a trace on the turtle. That is the property the whole picture rests on: a branch may wander as far as it likes and the stalk it grew from does not notice, because the note taken at [ is restored whole at ].

Eleven characters, and the stack that reads them On the left, the little figure the turtle draws from the string F[+F]F[-F]F: a straight stalk of three segments going up, with one branch leaning left off the first joint and one leaning right off the second. Each drawn segment is numbered with its position in the string: 0, 5 and 10 on the stalk, 3 on the left branch, 8 on the right branch. On the right, the eleven characters laid out in order with their positions above them, the five F characters highlighted, and below them a staircase showing how deep the stack is after each character: zero, then one across the opening bracket, the plus and the branch, back to zero at the closing bracket, and the same again for the second branch. Underneath, what each of the five characters means. ELEVEN CHARACTERS, AND THE STACK THAT READS THEM WHAT THE TURTLE DRAWS 0 3 5 8 10 starts here, pointing up THE STRING, AND THE STACK UNDER IT 0 1 2 3 4 5 6 7 8 9 10 F [ + F ] F [ - F ] F 1 0 HOW DEEP THE STACK IS AFTER EACH CHARACTER F draw one segment forward + turn the heading one way - turn it the other way [ remember the place and the heading ] go back to the last remembered pair

Figure 33.1 — the same eleven characters twice. On the left, the line each F drew, numbered by where that F sits in the string; on the right, the string with the stack's depth under it. Only the two bracket characters move that depth, and every segment drawn at depth 1 hangs off the joint the matching [ remembered.

The 41 by 81 stalk

The client can draw a line between two whole pixels and hash a finished frame, which is all a plant needs from it. The turtle works in exact world pixels and the framebuffer holds whole ones, so the rounding waits until the last possible moment, the same discipline the mote was carried across the valley under. Depth gets used for colour: the trunk takes the darkest of the palette's three greens, and every branch off it is lighter than its parent until the ramp runs out.

▣ Build · stage 3 — segments become pixels, and the frame is pinned
// cmd/worldc/plant.go

// The three greens a stalk is drawn in, palette entries 10, 11 and 12.
// Depth is the count of brackets the turtle was inside, so the trunk
// takes the darkest and every branch off it is lighter than its parent
// until the ramp runs out.
var stalk = [3]render.Color{0xFF254C39, 0xFF4C7841, 0xFF90A858}

// twig picks the colour for one segment from how deep it was drawn.
func twig(depth int) render.Color {
	if depth >= len(stalk) {
		return stalk[len(stalk)-1]
	}
	return stalk[depth]
}

// draw paints one segment into the framebuffer. The turtle works in
// exact world pixels and the buffer holds whole ones, so the rounding
// happens here and nowhere earlier.
func draw(b *render.Buffer, cam render.Camera, s grow.Segment) {
	x0, y0 := cam.ToScreen(int(math.Round(s.A.X)), int(math.Round(s.A.Y)))
	x1, y1 := cam.ToScreen(int(math.Round(s.B.X)), int(math.Round(s.B.Y)))
	b.Line(x0, y0, x1, y1, twig(s.Depth))
}
// cmd/worldc/plant.go — once per plant, inside the loop over -at

		s, segs := p.Sprout(root)
		if forget {
			segs = readForgetting(s, root, p.Up, p.Seg, p.Turn)
		}
		deep, balanced := grow.Balanced(s)
		if !balanced {
			die(fmt.Errorf("%s generation %d: the brackets do not close", name, gen))
		}
		box := grow.Bounds(segs)
		for _, sg := range segs {
			draw(b, cam, sg)
		}
$ go run ./cmd/worldc -plant
plant: sprig at generation 3 on the soil of seed 5, segment 3.00 pixels, out of row 110
       axiom F, F -> F[+F]F[-F]F
    at x     turn        rim     chars   segments   deep     w by h
     152      1/8   0.785398       311        125      3    41 by 81
  frame sha256 a4f65b143c3d33782d387fb68cdb9dd88acd774ed85ff7c161e754ff4f19b510

p is one plant's parameters, the five fields this chapter takes apart, and readForgetting behind the -forget flag is an earlier draft of the turtle kept so the failure below can be run instead of described. The ground is generated once from seed 5, tiled once, and never touched again, and no random number is drawn anywhere in this run, so every machine that types that command gets that hash. Three of the printed numbers were known before the program ran. 311 characters and 125 segments are rows 3 of the census table. The height of 81 is the straight run, 33 = 27 segments, at three pixels each.

What stands on the soil east of the pond is a bracken stalk: one dead-straight stem twenty-seven segments long, with side branches leaving it at regular intervals and smaller branches leaving those. It reads as a plant and it reads as a stiff one, and every stiff thing about it is a number somebody typed. The stem is straight because the rule puts three Fs outside its brackets and never a turn between them. The branches leave at exactly an eighth of a turn because a flag said 8. That the picture is a plant at all is the grammar's doing; that it is this plant is four numbers' doing.

⚠ Worked failure — the branch that ate the stalk

The first draft of the turtle pushed one thing onto the stack. A branch is a detour, the reasoning went, so [ notes down where you are and ] puts you back there. The heading never came into it, because at the moment [ is read the heading has not been changed yet: the + comes afterwards. Here is what that reasoning drew.

$ go run ./cmd/worldc -plant -forget
plant: sprig at generation 3 on the soil of seed 5, segment 3.00 pixels, out of row 110
       axiom F, F -> F[+F]F[-F]F
    at x     turn        rim     chars   segments   deep     w by h
     152      1/8   0.785398       311        125      3    46 by 47
  frame sha256 f431fa63a2f360d31dd6e766a6e367f6154280fa9e8f566b9da92ca6b9cec4b0

On screen it is not a plant and not obviously anything: a single ragged line staggering up and to the left across the pond, with no trunk and no branches. The measurement line convicts it before the picture does. The same 311 characters and the same 125 segments came out 46 wide by 47 tall, where the working version is 41 by 81. Twenty-seven segments of three pixels have to make 81 pixels of height if they point the same way, so the number 47 says the stalk stopped pointing the same way, and the string is not where that could have happened: it holds no turn between the three stem segments.

The bench prints the same eleven characters under the broken rule, and the last two columns settle it:

$ go run ./cmd/grow -mode read -forget
F[+F]F[-F]F: segment 10 pixels, turn 1/8 of a turn (0.7854 rim), setting off at 1.5708 rim
  [ remembers the place only (this chapter's worked failure)
    i  char what it does                        x        y   heading  stack
    0     F draw (+0.0000,-10.0000)        0.0000 -10.0000    1.5708      0
    1     [ remember here                  0.0000 -10.0000    1.5708      1
    2     + turn, heading gains            0.0000 -10.0000    2.3562      1
    3     F draw (-7.0711,-7.0711)        -7.0711 -17.0711    2.3562      1
    4     ] back to where the [ was        0.0000 -10.0000    2.3562      0
    5     F draw (-7.0711,-7.0711)        -7.0711 -17.0711    2.3562      0
    6     [ remember here                 -7.0711 -17.0711    2.3562      1
    7     - turn, heading loses           -7.0711 -17.0711    1.5708      1
    8     F draw (+0.0000,-10.0000)       -7.0711 -27.0711    1.5708      1
    9     ] back to where the [ was       -7.0711 -17.0711    1.5708      0
   10     F draw (+0.0000,-10.0000)       -7.0711 -27.0711    1.5708      0

Index 4 is the whole bug in one line. The turtle went back to the joint, and its heading stayed at 2.3562, the branch's heading. So index 5, which is the second segment of the stem, redraws the branch: compare its movement with index 3's and they are the same two numbers. The stem has been captured by the first branch it put out, and from there everything compounds, because the second bracket now saves a heading that was already wrong.

The reasoning that produced the draft was right about one thing and wrong about the general case. At [ the heading really has not changed yet, which is exactly why the mistake survives a small string with one bracket in it. It dies on the second bracket, and generation 3 has sixty-two of them. A branch is not a detour in position; it is a detour in the turtle's entire state, and the fix is to make the note hold everything the drawing depends on. That is why pose is a struct with two fields and the stack is a slice of it: adding a third piece of turtle state, a pen width or a colour, means adding a field, and every branch starts saving and restoring it without one line changing in Read.

Plant parameters

Look back at what decided the bracken. Which rules were applied, how many times they were applied, how far one F carries the turtle, how much rim one + adds, and which way the first segment set off. Five things, none of them a picture, and four of them plain numbers. Gathering them into one struct costs nothing and changes what the plant is: a description that something other than a person could fill in.

▣ Build · stage 4 — everything about a plant that is a number
// internal/grow/plant.go

// Plant is everything about one plant that is a number or a name: which
// grammar it grows by, how many times that grammar is applied, how far
// one F carries the turtle, how much rim one + or - adds, which way the
// first stalk sets off, and which seed a rule with a choice in it draws
// from. There is no picture anywhere in it, and no two of these
// fields have to be changed together.
type Plant struct {
	Axiom string
	Rules Rules
	Gen   int
	Seg   float64
	Turn  float64
	Up    float64
	Seed  uint64
}

// Sprout grows the string and reads it, in that order, and hands back
// both so a caller can print the one and draw the other.
func (p Plant) Sprout(at vec.Vec2) (string, []Segment) {
	rng := rand.New(rand.NewPCG(p.Seed, 33))
	s := p.Axiom
	for i := 0; i < p.Gen; i++ {
		s = Grow(s, p.Rules, rng)
	}
	return s, Read(s, at, p.Up, p.Seg, p.Turn)
}
$ go run ./cmd/worldc -plant -bare -at 48,96,144 -part 24,12,6 -base 120
plant: sprig at generation 3 on the bare frame, segment 3.00 pixels, out of row 120
       axiom F, F -> F[+F]F[-F]F
    at x     turn        rim     chars   segments   deep     w by h
      48     1/24   0.261799       311        125      3    17 by 81
      96     1/12   0.523599       311        125      3    31 by 81
     144      1/6   1.047198       311        125      3    47 by 81
  frame sha256 54bdff9a59a0034f8e3e2a4477b80e322cfbab4c186e894e112644a5ed86d720
go run ./cmd/worldc -plant -bare -at 48,96,144 -part 24,12,6 -base 120 -shot assets/frames/plant-three-turns.png

One field changed between those three plants and the ground was left out of the frame entirely, so the hash is a statement about the plants and nothing else. All three grew from the same 311 characters and drew the same 125 segments; what differs is one number in Turn, and the widths in the last column are 17, 31 and 47. The height is 81 in all three rows, and that is the grammar showing through: the main stalk never turns, so the turn size can only decide how far a branch reaches sideways, never how tall the plant gets. Height belongs to Gen and Seg; spread belongs to Turn; the three knobs do not fight.

Three green plants side by side on a near-black background. All three have the same straight stem and the same number of branches. The left one is a narrow spike with branches held close in; the middle one spreads moderately; the right one throws its branches out almost sideways.

Figure 33.2assets/frames/plant-three-turns.png: the same grammar, the same generation and the same segment length, drawn with a twenty-fourth, a twelfth and a sixth of a turn. One number apart, and no two of them would be taken for the same species.

The rule set is a field too, and swapping it is a bigger lever than any of the numbers. A second grammar in the library uses two rules and a letter the turtle has no meaning for: X → F[+X]F[-X]+X grows the tips, and F → FF lengthens every piece of stem that already exists, so old wood stretches while new wood branches. The trailing +X leaves a turn outside every bracket, which the bracken never had, and the whole stalk leans as it climbs.

▣ Build · save the second grammar on the valley
$ go run ./cmd/worldc -plant -rules weed -gen 4 -seg 3 -part 10 -at 144
plant: weed at generation 4 on the soil of seed 5, segment 3.00 pixels, out of row 110
       axiom X, X -> F[+X]F[-X]+X, F -> FF
    at x     turn        rim     chars   segments   deep     w by h
     144     1/10   0.628319       491        130      4    62 by 84
  frame sha256 796fbead6be5d51533991dfad3226efa5237363d773b56d9405debfe6f7a963c
go run ./cmd/worldc -plant -rules weed -gen 4 -seg 3 -part 10 -at 144 -shot assets/frames/plant-valley.png
A top-down valley of brown speckled soil with a large blue pond in the middle. On the soil to the east of the pond stands a bare tree in three greens: a dark trunk that forks about a third of the way up, then lighter limbs forking again and again into pale outer twigs.

Figure 33.3assets/frames/plant-valley.png: 491 characters read into 130 segments, drawn by the same turtle, the same colour ramp and the same client as the bracken. Only the two lines of the grammar are different.

That is a tree, and no code anywhere in the drawing knows the word. Every plant this section has produced came out of one turtle, one line routine and a struct of five fields. Which leaves one honest gap, and it is the reason the last field exists. Every plant of a given species has come out identical so far, because a rule with one production is a rule with no choice in it. Give a character two productions and the rewriter reaches for the plant's own generator, the one Seed builds, to pick between them once per occurrence.

$ go run ./cmd/worldc -plant -rules wild -gen 4 -seg 3 -part 10 -at 144 -pseed 1
plant: wild at generation 4 on the soil of seed 5, segment 3.00 pixels, out of row 110
       axiom X, X -> F[+X]F[-X]+X or F[-X]F[+X]-X, F -> FF
    at x     turn        rim     chars   segments   deep     w by h
     144     1/10   0.628319       491        130      4    62 by 84
  frame sha256 303d381046072782326b1f9283839be7c30075f24d890a5258d53ec492e8363c
$ go run ./cmd/worldc -plant -rules wild -gen 4 -seg 3 -part 10 -at 144 -pseed 7
plant: wild at generation 4 on the soil of seed 5, segment 3.00 pixels, out of row 110
       axiom X, X -> F[+X]F[-X]+X or F[-X]F[+X]-X, F -> FF
    at x     turn        rim     chars   segments   deep     w by h
     144     1/10   0.628319       491        130      4    62 by 84
  frame sha256 05868b2fd40d70697c4dc4cb75276a61da991574dbd83900fb537cd1011af9b1

Two different trees, and running either command again prints its own hash back. The second production is the first one mirrored, so both are 12 characters long and both hold three Xs; that is why the character count, the segment count and even the bounding box come out identical while the pictures do not. A stand of these is a stand of individuals of one species, which is the thing a repeated sprite could never be, and it costs one uint64 per plant.

Nothing has chosen any of those numbers yet. A person typed 3, and 8, and 4. The rewriter still rewrites, the turtle still reads, and the struct is still five fields; the question of who fills them in is outside this chapter. What this chapter calls a parameter set is what a living thing would call a genome.

Description and rendering

The idea doing the work here is a separation, stated in a form that has nothing to do with plants. There is a description, which is text, and there is a rendering of it, which is pixels, and the only thing joining them is a function that reads the first and produces the second. Everything a world wants to do to a plant, store it, copy it, mutate it, breed it, compare two of them, send one over a wire, is a thing you do to text and numbers. Everything a screen wants is on the other side of that function and knows nothing about grammars. Neither side has to be careful about the other.

Rewriting the whole string at once is the second load-bearing decision, and it is easy to get wrong by accident. Grow reads from the old string and appends to a new one, so a production that puts an F into the output cannot have that F rewritten again by the same pass. Rewrite in place instead and a single generation runs away forever, since every F the rule writes is another F waiting to be replaced. Everything alive in this world grows in steps like that: one whole state read, one whole next state written, and never a mixture of the two.

The stack is the third, and it generalizes past drawing entirely. Any reader that has to go off and do something and then arrive back exactly as it was needs somewhere to leave a copy of itself, and it needs the copies to come back newest first. The worked failure above is what happens when the copy is incomplete, and its symptom is the useful part to remember: the first branch was perfect, and everything after it was wrong. That signature, one good case followed by compounding damage, means a saved state that did not save enough, and it reads the same whether the state is a turtle, a parser or a camera.

✓ Checkpoint — a grammar, a reader, and the knobs between them
  • Given an axiom and one rule, I can write out three generations by hand and get the character count of the fourth without writing it, from the count of rewritten letters plus the count of carried ones.
  • I can say why generation 8 of this grammar is a wall in terms of the two growth rates, 5 for the ink and 3 for the height, and not in terms of memory.
  • I can trace F[+F]F[-F]F character by character, filling in the position, the heading in rim and the stack depth after each one.
  • Shown a branching drawing where the first branch is right and everything after it is displaced, I look at what the push saves before I look at anything else.
  • I can name which of generation, segment length and turn size changes a plant's height and which changes only its spread, and check my answer against the measured bounding box.
  • I can grow the same stochastic grammar twice from one seed and show by frame hash that it produced the same plant, then change the seed and get a different one.
⚡ Exercises — try first, then reveal
Exercise 1 — a generation taller, a segment shorter. Run -plant -gen 4 -seg 1. Predict the four counted columns before you do, and say whether the hash can possibly match the generation 3 run.

Generation 4 is row 4 of the census: 1561 characters and 625 segments. The straight run is 34 = 81 segments, at one pixel each, so the height is 81 again, the same as 27 segments of three pixels. The width stays 41 for the same reason: dividing the segment by three and multiplying the count by three leaves every proportion alone.

$ go run ./cmd/worldc -plant -gen 4 -seg 1
plant: sprig at generation 4 on the soil of seed 5, segment 1.00 pixels, out of row 110
       axiom F, F -> F[+F]F[-F]F
    at x     turn        rim     chars   segments   deep     w by h
     152      1/8   0.785398      1561        625      4    41 by 81
  frame sha256 f4bff0bc93ec1d044b15acd74ad14a6c5246b8fef75c026f3794fe4bcbdb7553

Same box, different hash, and on screen the difference is easy to see: every twig of the generation 3 plant has sprouted twigs of its own, inside the same outline. The envelope was fixed by the arithmetic; the detail inside it is what a generation buys.

Exercise 2 — a grammar of your own. Add "fan": {"F", Rules{'F': {"F[+F][-F]F"}}} to Named: two branches off one joint instead of two off different joints. Work out its character count and its straight run at generation 3 before running -mode gens on it.

The production is 10 characters holding four Fs, so the count multiplies by 4 a generation, and only two of those four sit outside the brackets, so the height multiplies by 2. Characters: generation 1 is 10; generation 2 is 4 × 10 = 40 plus the 6 carried marks, 46; generation 3 is 16 × 10 = 160 plus 30, which is 190.

$ go run ./cmd/grow -mode gens -rules fan -count 8
axiom F, F -> F[+F][-F]F
  gen 0     1  F
  gen 1    10  F[+F][-F]F
  gen 2    46  F[+F][-F]F[+F[+F][-F]F][-F[+F][-F]F]F[+F][-F]F

   gen    characters        F's   brackets   straight run  ink / height
     0             1          1          0              1          1.00
     1            10          4          2              2          2.00
     2            46         16         10              4          4.00
     3           190         64         42              8          8.00
     4           766        256        170             16         16.00
     5          3070       1024        682             32         32.00
     6         12286       4096       2730             64         64.00
     7         49150      16384      10922            128        128.00
     8        196606      65536      43690            256        256.00

The last column is a cleaner disaster than the bracken's: 4 divided by 2 is exactly 2, so the ink per pixel of height doubles every generation and generation 8 is already at 256. Then draw it with -plant -rules fan -gen 4 -seg 2 -at 150 and look at what the paired brackets did. Both branches leave the same joint, so the plant is symmetrical about every fork, and it reads as something closer to coral than to bracken.

Exercise 3 — walk into the wall. Grow generation 8 and keep the plant the same size on screen by shrinking the segment to 0.0123 pixels. Predict what the picture will look like, then run -plant -gen 8 -seg 0.0123.
$ go run ./cmd/worldc -plant -gen 8 -seg 0.0123
plant: sprig at generation 8 on the soil of seed 5, segment 0.01 pixels, out of row 110
       axiom F, F -> F[+F]F[-F]F
    at x     turn        rim     chars   segments   deep     w by h
     152      1/8   0.785398    976561     390625      8    41 by 81
  frame sha256 d69f148b90691c1a3092a80c5aa6c7ca6899d95a98b89ae1680c7f0112299bc6

Nearly a million characters and 390,625 segments, and the frame comes back in about a tenth of a second because a segment an eightieth of a pixel long is one call to the line routine that writes one pixel. The outline is the outline of the generation 3 plant to the pixel, 41 by 81, and inside it the twigs have merged into solid pale lumps. Five sixteenths of a million strokes of green went onto a picture that has room for a few thousand.

Now do the honest version of the experiment: run generations 3, 4, 5 and 6 with the segment divided by three each time, and put the four frames side by side. Somewhere around generation 6 the plant stops looking more detailed and starts looking heavier. That is where the useful range of this grammar ends at this size. Find that range for any grammar before letting a world grow one unattended.