The World Vol 10 · Creature-Speakers
ch 104 / 105
Chapter 104

Twenty Browsers Beside the Village

Twenty controllers beside twenty-four people

The first speaker roll is 222617 bytes, and the contract compares every one of them. It holds twenty controllers, not twenty bodies, because the body is still the browser row: Bulk 40, Sight 12, Reach 1, a store of 100 grams at founding, and no new action a six-output controller can name.

A speaker is an ordinary browser whose controller came from a pinned arena, whose goal stands only for its span, and whose fallback is the same evolved animal when no goal stands. The model is absent on this page. The database is absent too. A bench presses the goal off a flag so the seam can be measured before any prompt exists.

One champion on the bare plate proves the painter; a village needs twenty roots. Chapter 60 measured one-seed lineages collapsing early, so this roll takes one champion from each of twenty arena seeds, 5 through 24, instead of the top twenty from one board. The body block is pinned after every generation and the mutation draw count still runs across it; what survives is a spread of controllers and temperaments on the same browser body.

$ go run ./cmd/speakers -mode roll -write configs/speakers.json
speakers: 20 pinned arenas, 10 generations each, tournaments of 4, 1 champion kept, first seed 5, off the flags

  the roll       configs/speakers.json, rebuilt here and compared by the contract
  the body       browser row pinned to 1.00 over all ten body factors
  the driver     beast.NewWits over each champion's 378 weights, wrapped by speaker.Paint

  seed      best     board digest      weights digest    aggression    wariness    tameness
     5  267.7500  fd9219ac238c82ab  b67d97ce187d6963       0.2982     0.0353     0.8700
     6  270.0000  92f8841713182102  8078a8e294a1b928       0.3405     0.1264     0.4637
     7  282.5000  1202d947c4f3578f  1e7fe725a98a84a1       0.8573     0.3860     0.2253
     8  275.2500  c1d2fa29c7237062  2cd078e80a9b23fa       0.3371     0.0663     0.5973
     9  265.7500  cb4c72264b092577  a26d6dd664d31b7f       0.3542     0.5841     0.9268
    10  253.7500  89ed48aad3b2ddbe  be7f0ba846826ad2       0.4351     0.4512     0.1303
    11  277.7500  6ec503beb7cdff9e  34d953e8134ef0a3       0.1495     0.5411     0.2667
    12  268.0000  9c4f5517d8ffe5df  909e389cc7fb223f       0.4934     0.6852     0.4947
    13  283.7500  4debc02eb53a45d3  90ba01d2ab0f2f9e       1.0000     0.3874     0.1780
    14  277.0000  f36595bbaf85b59b  66fe39e641ef9758       0.1684     0.7455     0.0311
    15  261.7500  c280ea75e0ef21e8  04c61c4302e2257c       0.1636     0.9790     0.6895
    16  274.0000  08f5db86ee993c25  6bb57d779036df38       0.5959     0.2942     0.4864
    17  277.0000  8915b0cf60acdeda  b6dab1280a31f2ed       0.0938     0.1162     0.3613
    18  274.7500  da8a1e069fdbdf6c  9260bf1538708511       0.4115     0.8753     0.3509
    19  281.2500  a2329f176d4da8f8  97c18037cd253be4       0.4099     0.4771     0.0735
    20  280.0000  15077b9b6bdc564d  3a9e9ac988a6dd12       0.3936     0.8162     0.5275
    21  279.2500  5608b0deadc0a4e2  024b2b7ccd261483       0.2358     0.2429     0.2395
    22  266.0000  23c7deb583804cdb  fd2966765542f532       0.2620     0.0635     0.9341
    23  278.0000  f45ebcc54bbad971  17d4ca4914cc4785       0.4287     0.9701     0.1119
    24  263.2500  f23748db8570b07f  c859a47a51fb978d       0.5352     0.8556     0.1044

  seeds recorded                                        20
  controllers recorded                                  20
  bytes written                                     222617
  roll sha256                           fa9583936e568f48
configs/speakers.json: byte for byte the roll the bench wrote

Ten generations is the chapter's settled count. It is long enough for every champion in the table to outscore the opening browser reflex on the pinned plate, and short enough that the contract can rebuild all twenty controllers rather than treating the file as hand-written data. The best scores run from 253.7500 to 283.7500 grams over four trials. That range is the roll's floor, not a claim about one chosen animal.

The configs/speakers.json reader

The file is embedded beside the village roll because later benches read it by name. Generated data is rebuilt by cmd/speakers, and a byte change is a red contract.

▣ Build · stage 1 — the speaker roll, as data the contract rebuilds
// configs/configs.go
//go:embed genesis.json models.json village.json importance.json thought.json reflect-questions.tmpl reflect-statement.tmpl plan.tmpl taboo.json line.tmpl village-24.json speakers.json
var Files embed.FS

// Speakers is the roll of twenty browser controllers chapter 104
// writes from the pinned-body arena. It is data, not an exemplar: a
// contract run makes it again and compares the bytes.
const Speakers = "speakers.json"
// internal/speaker/roll.go
type Controller struct {
	Seed       uint64    `json:"seed"`
	Gen        int       `json:"gen"`
	Score      float64   `json:"score"`
	Board      string    `json:"board"`
	Weights    string    `json:"weights"`
	Body       []float64 `json:"body"`
	Mind       []float64 `json:"mind"`
	Temper     []float64 `json:"temper"`
	Aggression float64   `json:"aggression"`
	Wariness   float64   `json:"wariness"`
	Tameness   float64   `json:"tameness"`
}

type Roll struct {
	Seeds    []uint64     `json:"seeds"`
	Speakers []Controller `json:"speakers"`
}
// internal/speaker/roll.go, continued
func DecodeRoll(b []byte) (Roll, error) {
	var r Roll
	if err := json.Unmarshal(b, &r); err != nil {
		return Roll{}, fmt.Errorf("speaker: speakers.json: %w", err)
	}
	if len(r.Speakers) != 20 {
		return Roll{}, fmt.Errorf("speaker: speakers.json holds %d controllers, want 20", len(r.Speakers))
	}
	if len(r.Seeds) != len(r.Speakers) {
		return Roll{}, fmt.Errorf("speaker: %d seeds for %d controllers", len(r.Seeds), len(r.Speakers))
	}
	seen := map[uint64]bool{}
	for i, s := range r.Seeds {
		if seen[s] {
			return Roll{}, fmt.Errorf("speaker: seed %d appears twice", s)
		}
		seen[s] = true
		c := r.Speakers[i]
		if c.Seed != s {
			return Roll{}, fmt.Errorf("speaker: row %d says seed %d under seed %d", i, c.Seed, s)
		}
		if len(c.Body) != gene.Bodies {
			return Roll{}, fmt.Errorf("speaker: seed %d body has %d numbers", s, len(c.Body))
		}
		for j, v := range c.Body {
			if v != 1 {
				return Roll{}, fmt.Errorf("speaker: seed %d body factor %d is %.4f, not pinned", s, j, v)
			}
		}
		if len(c.Mind) != gene.Weights() {
			return Roll{}, fmt.Errorf("speaker: seed %d mind has %d weights", s, len(c.Mind))
		}
		if len(c.Temper) != gene.Tempers {
			return Roll{}, fmt.Errorf("speaker: seed %d temper has %d numbers", s, len(c.Temper))
		}
	}
	return r, nil
}

The reader checks the part a later run cannot repair. It refuses the wrong count, a repeated root, a seed mismatch, a body factor other than 1.00, a mind whose 378 weights are not present, and a temperament row of the wrong width. The file also carries the board and weight digests; those name the controller, while the checks keep the body from sliding away from the browser.

$ go test ./internal/speaker/ -run 'TestTheSpeakerRollHasTwentyPinnedControllers' -v
=== RUN   TestTheSpeakerRollHasTwentyPinnedControllers
    roll_test.go:29: 20 controllers read; 7560 mind weights and 200 body factors pinned to 1.00
--- PASS: TestTheSpeakerRollHasTwentyPinnedControllers (0.00s)
PASS
ok  	theworld/internal/speaker	0.00s

The test reads the embedded file and counts the two numbers that matter before a creature is founded from it: 7560 weights, which is twenty times 378, and 200 body factors, which is twenty times ten. It is a file test rather than a generation test. The generation test is the contract's byte comparison.

Creature 25 through creature 44

Founding does not ask the arena again. It reads the roll, stamps each controller through the browser row, wraps the wits with the painter, and scatters those twenty bodies on stream 12, the same founding-creatures stream earlier volumes used for browsers. The twenty-four people already own identities 1 through 24, so the first browser in the roll becomes creature 25 and the last becomes 44.

▣ Build · stage 2 — founding the roll beside the people
// cmd/speakers/main.go
func foundSpeakers(v *terra.Valley, r *beast.Roster, sr speaker.Roll, set setup) []*beast.Beast {
	rng := rand.New(rand.NewPCG(set.first, streamHerd))
	ground := beast.Ground(v)
	taken := map[sim.Coord]bool{}
	var out []*beast.Beast
	for _, row := range sr.Speakers {
		c := ground[rng.IntN(len(ground))]
		for taken[c] {
			c = ground[rng.IntN(len(ground))]
		}
		taken[c] = true
		g := row.Genome()
		b := g.Stamp(beast.Fauna[0]).Spawn(beast.Centre(c), v.Now)
		b.Mind = beast.NewWits(g.Net(), b)
		p := speaker.Wrap(b)
		if set.goal != (sim.Coord{}) {
			p.Goal = speaker.Goal{At: set.goal, Sign: 1, Gain: 1}
		}
		r.Add(b)
		out = append(out, b)
	}
	r.Seen = func(view *beast.View) { speaker.Watch(r.Live, view) }
	return out
}

The scatter has one new file and no new stream. The seed is the roll's first seed, printed off the flag, and the stream number is 12 because this is still founding creatures. The roster's Seen hook is the one chapter 103 needed: a wits behind a painter still gets the view before it reads its row.

$ go run ./cmd/speakers -mode found
speakers: twenty browsers founded beside the village, off configs/speakers.json

  the ground     24x16 cells, tick 4450, 119 plants standing after 4000 settle ticks
  the village    24 people, off configs/village-24.json
  the speakers   20 browsers, scattered on stream 12 in roll order
  the next id     45; no new stream is claimed

  row   seed  creature   cell   carrying   weights digest    tameness
    0      5        25   15,5   100.0000  b67d97ce187d6963      0.8700
    1      6        26   3,11   100.0000  8078a8e294a1b928      0.4637
    2      7        27   13,6   100.0000  1e7fe725a98a84a1      0.2253
    3      8        28   3,10   100.0000  2cd078e80a9b23fa      0.5973
    4      9        29   1,12   100.0000  a26d6dd664d31b7f      0.9268
    5     10        30   7,13   100.0000  be7f0ba846826ad2      0.1303
   17     22        42    5,3   100.0000  fd2966765542f532      0.9341
   18     23        43   13,3   100.0000  17d4ca4914cc4785      0.1119
   19     24        44  12,10   100.0000  c859a47a51fb978d      0.1044
  ... 11 rows between omitted from the listing

The listing shows nine rows because the page needs the identity boundary, not every weight. The omitted rows are in configs/speakers.json and in the contract transcript. The useful number is the next identity, 45: twenty-four people and twenty speakers are one roster, and a transfer search sees bodies, not classes.

1600 speaker-ticks with a goal standing

A goal in this chapter is still not a model answer. The bench presses cell 16,8 through the same values the seam later writes, with gain 1.0000 and a span of eighty ticks. While the span stands, the painter paints. At the boundary after tick 80 the bench clears the goal, and from then on each browser is the evolved animal the roll founded.

$ go run ./cmd/speakers -mode lapse -span 80 -ticks 200
speakers: a goal pressed through the seam off a flag, then lapsed

  the goal       cell 16,8, toward, gain 1.0000, span 80 ticks, off the flags
  the lapse      at tick 4530, then the browser is its evolved animal again
  no model       nothing here asks anything; the goal is pressed by this bench
  no database    nothing here opens anything

  ticks run                                             200
  speakers                                              20
  speaker-ticks with a goal standing                  1600
  speaker-ticks with a ray painted                    1211
  living speakers at the close                          20
  first speaker closed on                             22,5
  first speaker store                              93.2137

Twenty speakers times eighty ticks gives 1600 speaker-ticks with a goal standing. Only 1211 of those paint a ray, because the goal cell is past Sight for some bodies on some ticks. The other 389 leave the row as the world read it. That is the same limit chapter 103 printed on a bare plate: a goal about what the eyes cannot reach is not painted.

The run does not save a goal anywhere. If the process dies while one stands, the value is lost with the process and the speaker is an animal on restart. That is a limit of this chapter, and it is kept because the fallback is the design, not an error path.

Twenty more mouths leave twenty-two bodies living

The console ground fed twenty-four people badly in chapter 102, and the roll adds twenty more mouths to that same ground. The page does not tune the ground to hide the cost. It runs an hour of the world's time with the people alone and then with the speakers founded too.

$ go run ./cmd/speakers -mode mouths
speakers: what twenty more mouths do to the console ground

  run                                     people only   people and speakers
  people founded                                  24                    24
  speakers founded                                 0                    20
  bodies dead after 36000 ticks                   15                    22
  living bodies                                    9                    22
  plants standing                                277                   286
  standing plant grams                       12178.7               12135.5

With people alone, fifteen of twenty-four bodies die and nine live. With the speakers added, twenty-two of forty-four bodies die and twenty-two live. The ground has slightly more plants standing and slightly fewer grams in them, because dead bodies stop eating and the plants keep growing. The added mouths are real, but the hour is not a clean hunger proof: the population also changes who dies soon enough to leave food behind. The negative result stays on the page.

The tick measurement is the other cost, and the only one here that belongs to the machine. The contract flattens the duration fields, but the page records the run that produced the figures.

$ go run ./cmd/speakers -mode pace -ticks 200
  twenty-four people, no speakers                200 ticks in 113ms,     1770 ticks a second (measured here; yours will differ); living 22
  twenty speakers, no goal standing              200 ticks in 150ms,     1332 ticks a second (measured here; yours will differ); painted 0 of 4000 speaker-ticks
  twenty speakers, goals standing                200 ticks in 134ms,     1497 ticks a second (measured here; yours will differ); painted 3131 of 4000 speaker-ticks

The rate moves with the host and the contract says so, but the counts beside it are the engine's: 4000 speaker-ticks in each speaker run, no painted ray when no goal stands, and 3131 painted rays when every speaker opens with the goal. The painter adds arithmetic inside the tick; it does not add a transaction, a goroutine or a model call.

⚠ Worked failure — a roll whose body is not pinned

The first broken roll is small: one body factor in the first row is changed from 1.00 to 1.25. That is enough to make the file describe a different animal. The reader refuses it before any body is founded.

$ go run ./cmd/speakers -mode broken
broken roll refused: speaker: seed 5 body factor 0 is 1.2500, not pinned
exit=1

The symptom is the refusal, and the cause is the first body factor. A roll reader that accepted it would let the body drift while every paragraph still called the row a browser. The fix is not to clamp the value at founding, because that would turn a bad file into a different file silently. The fix is the reader's check: every body factor in the shipped roll must already be 1.00, and a file that says otherwise stops.

What the speaker roll can now do

✓ Checkpoint — twenty controllers, one browser body, and a lapsed goal
  • Say why the roll takes one champion from each of twenty seeds instead of twenty champions from one seed, and point to the number in the run that proves there are twenty roots.
  • Given one row of configs/speakers.json, name the fields that identify the controller and the fields that prove the body stayed pinned.
  • Explain why creature 25 is the first speaker founded beside the village, and what the next identity 45 says about the people and speakers sharing one roster.
  • Work out why eighty ticks over twenty speakers gives 1600 speaker-ticks with a goal standing, then explain why only 1211 painted a ray.
  • State the limit this chapter keeps about a goal in memory when the process dies.
⚡ Exercises — run the roll on another count
Exercise 1 — five roots. Run go run ./cmd/speakers -mode roll -count 5 and compare the byte count and digest with the page's twenty-root roll.

The command prints five controllers and does not write the shipped file unless you pass -write. The byte count and roll sha256 differ because the file has a different number of rows; the body check still expects twenty when the embedded reader opens the shipped file.

Exercise 2 — a longer span. Run go run ./cmd/speakers -mode lapse -span 120 -ticks 200 and predict the speaker-ticks with a goal standing before reading the output.

Twenty speakers times 120 ticks gives 2400 speaker-ticks with a goal standing. The painted count is lower when bodies spend part of that span with the goal past Sight.

Exercise 3 — the broken file. Change a copy of the roll so one mind has 377 weights and write down which check should refuse it.

DecodeRoll refuses the row with mind has 377 weights. The browser body can be right and the controller still be unusable, because beast.NewWits reads exactly 378 weights.