Game Engine Zero Vol 2 · The Math of Motion
ch 16 / 24
Chapter 16

Seeded Randomness

Drawing repeatable random numbers

This chapter adds a seeded random source and a dots lab. The lab draws three panels from one seed: a flat scatter, a weighted set of bars and a normal cloud.

Press R and the lab draws seed 1 again. The picture and printed line match because the seed names the same sequence of draws. Press Right and the seed becomes 2. That gives a different sequence, and that sequence is repeatable too.

Use one generator, seed it once from a number a person can type and pass it by hand to code that draws random values. Choose the distribution for the quantity being made. The worked failure uses a generator nobody seeded and nobody passes.

Random distributions

∑ Math Interlude — three recipes on a draw from [0, 1)

A generator hands out one thing: a draw, a number from 0 up to but not including 1, each as likely as any other. Everything else is arithmetic on draws. A number spread flat across a range is one draw lerped into the range, as chapter 15's Lerp does it: a draw of 0.55 across 0..96 is 52.8. Three hundred such numbers have a mean near the middle, 48, and a spread, the standard deviation, near the range's width divided by the square root of twelve, 96 / 3.4641 = 27.7128; that is what a flat distribution's spread is, and the lab prints its sample's beside it.

A weighted pick from a table is one draw and one walk. Weights 1, 2, 3 and 4 total 10, so a draw of 0.55 scaled by the total is 5.5; walk the table subtracting each weight until the number is smaller than the weight in hand: 5.5 is not under 1, so subtract, 4.5; not under 2, subtract, 2.5; under 3, so the third outcome. The fourth outcome is four times as likely as the first, and over three hundred picks the counts come out near 30, 60, 90 and 120, with a scatter of a few either side, because three hundred is not many: the first outcome's count has a spread of about 5, so a count of 19 or 41 turns up once in twenty seeds or so.

A number bunched round a middle is the normal distribution, and the recipe that makes one from flat draws is Box–Muller: two draws u1 and u2, a radius r = √(−2 ln(1 − u1)) and an angle θ = 2πu2, and the point at that radius and angle has two coordinates, r cos θ and r sin θ, each of which is a normal number with mean 0 and spread 1. With u1 = 0.5 and u2 = 0.25 the radius is √(−2 ln 0.5) = √1.3863 = 1.1774 and the angle is a quarter turn, so the two normals are 0 and 1.1774; scaled to a mean of 50 and a spread of 15 they are 50 and 67.66. Two draws make two normals, so the recipe keeps the second for the next call and a normal costs one draw on average; that is the reason the lab's three hundred pairs of normals take six hundred draws and not twelve hundred. The logarithm takes 1 − u1 and not u1 because a draw can be exactly 0 and can never be exactly 1, and the logarithm of 0 is not a number.

uone draw, a number in [0, 1), every value as likely as any other; the seed fixes the sequence of them
uniform(lo, hi)lerp(lo, hi, u): a draw of 0.55 across 0..96 is 52.8; mean (lo + hi)/2, spread (hi − lo)/√12 = 27.7128
weighted picku × total, then walk the weights: 5.5 through 1, 2, 3, 4 lands on the third; counts near 30, 60, 90, 120 in 300
r, θ√(−2 ln(1 − u₁)) and 2πu₂: 1.1774 and a quarter turn for draws of 0.5 and 0.25
normal(m, s)m + s · r cos θ, and m + s · r sin θ kept for the next call: 50 and 67.66 at mean 50, spread 15
mean, spreadthe average of a sample, and the square root of the mean squared distance from it
Box–Muller: two flat draws become a radius and an angle, and the point's two coordinates are two normals On the left, two short bars labelled u1 = 0.5 and u2 = 0.25. Arrows lead to a circle diagram in the middle: a radius of length 1.1774 drawn from the centre straight up, labelled r from the first draw, and an arc labelled theta = 2 pi u2, a quarter turn. The point at the tip has a dashed drop to the x-axis labelled r cos theta = 0 and a dashed run to the y-axis labelled r sin theta = 1.1774. On the right a note says that each coordinate is a normal of mean 0 and spread 1, that the second is kept for the next call, and that one normal therefore costs one draw on average. TWO FLAT DRAWS, ONE POINT, TWO NORMALS u1 = 0.5 u2 = 0.25 r = √(−2 ln(1 − u1)) = 1.1774 θ = 2π · u2 = a quarter turn θ r cos θ = 0 r sin θ = 1.1774 each coordinate is a normal of mean 0 and spread 1 the first is returned now, the second kept for next time: one normal costs one draw at mean 50 and spread 15: 50.0000 and 67.6612
Figure 16.1 — the Box–Muller recipe on the interlude's pair of draws: a radius from the first, an angle from the second, and the point's two coordinates as two normals, one returned and one kept.
▣ Build · stage 1 — a Source passed by hand, and three recipes on its draws
// internal/vec/random.go — create
package vec

import (
	"math"
	"math/rand/v2"
)

// Source is one random number generator, seeded once and passed by hand
// to everything that draws from it, so that the same seed gives the same
// numbers in the same order however many things share it. It counts its
// draws, and it keeps the spare half of a Box–Muller pair.
type Source struct {
	rng      *rand.Rand
	Draws    int     // uniform numbers taken from the generator so far
	spare    float64 // the second normal of the last pair, if unused
	hasSpare bool
}

// NewSource seeds a PCG generator, the same kind Pong serves and Snake
// places food with.
func NewSource(seed uint64) *Source {
	return &Source{rng: rand.New(rand.NewPCG(seed, 0))}
}

// Float is one draw: a number in [0, 1), counted.
func (s *Source) Float() float64 {
	s.Draws++
	return s.rng.Float64()
}

// Uniform is a number spread evenly across [lo, hi): one draw, lerped.
func (s *Source) Uniform(lo, hi float64) float64 {
	return Lerp(lo, hi, s.Float())
}

// Weighted is a table of outcomes with weights, summed once, so that a
// pick is one draw scaled by the total and one walk down the table.
type Weighted struct {
	weights []float64
	total   float64
}

// NewWeighted takes the weights in outcome order; a weight is any
// non-negative number, and the chance of an outcome is its weight over
// the total.
func NewWeighted(weights ...float64) Weighted {
	w := Weighted{weights: append([]float64(nil), weights...)}
	for _, x := range weights {
		w.total += x
	}
	return w
}

// Pick returns the index of one outcome: a draw scaled to the total falls
// somewhere along the weights laid end to end, and the weight it falls in
// is the answer.
func (w Weighted) Pick(s *Source) int {
	r := s.Float() * w.total
	for i, x := range w.weights {
		if r < x {
			return i
		}
		r -= x
	}
	return len(w.weights) - 1
}

// Normal is a number bunched round mean with a spread of sd, by
// Box–Muller: two uniform draws make two independent normals, a radius
// from the first and an angle from the second; one is returned and the
// other is kept for the next call, so that a sample costs one draw on
// average.
func (s *Source) Normal(mean, sd float64) float64 {
	if s.hasSpare {
		s.hasSpare = false
		return mean + sd*s.spare
	}
	u1, u2 := s.Float(), s.Float()
	r := math.Sqrt(-2 * math.Log(1-u1)) // 1 − u1 is never 0, so the log is finite
	th := 2 * math.Pi * u2
	s.spare, s.hasSpare = r*math.Sin(th), true
	return mean + sd*r*math.Cos(th)
}

// MeanSpread is the mean of a sample and its standard deviation: the
// square root of the mean squared distance from the mean.
func MeanSpread(xs []float64) (mean, sd float64) {
	if len(xs) == 0 {
		return 0, 0
	}
	for _, x := range xs {
		mean += x
	}
	mean /= float64(len(xs))
	for _, x := range xs {
		sd += (x - mean) * (x - mean)
	}
	return mean, math.Sqrt(sd / float64(len(xs)))
}
go vet ./...

Source is a pointer type because a generator has state. Every draw advances it. Copying the generator would copy its place in the sequence and risk drawing the same values twice.

Pass *Source by hand. A function signature that takes one shows that the function draws random values and shows which sequence it uses. Draws counts the flat draws so the lab can check its total.

Weighted stores its total when it is built. A pick then costs one draw, one multiplication and one walk through the table. The last return gives an answer even if rounding carries the draw to the end.

Drawing three panels

Extend cmd/motion with the dots lab. The lab draws three panels, each 96 pixels wide and 120 pixels tall. The first panel uses uniform positions. The second counts weighted picks with weights 1, 2, 3 and 4. The third uses normal values for a cloud around the middle.

Each burst makes a fresh Source from the current seed and draws the panels in a fixed order. The -seed flag sets the first seed. R repeats the same seed, Right moves to the next seed and Left moves back.

▣ Build · stage 2 — the dots lab, and a seed on the command line, extend main.go
// cmd/motion/dots.go — create
package main

import (
	"fmt"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/vector"

	"gez/internal/vec"
)

// The dots lab bursts three hundred samples from each of three
// distributions into three panels, all from one seeded source: positions
// spread flat across a panel, a pick from four weighted outcomes shown as
// four bars, and positions bunched round the panel's middle. R bursts
// again from the same seed; Right and Left burst from the next seed and
// the last.
const (
	dotsN     = 300  // samples a panel
	panelW    = 96.0 // a panel's width and height in pixels
	panelH    = 120.0
	panelTop  = 28.0
	panelStep = 104.0 // from one panel's left edge to the next
)

// dotsWeights are the four outcomes' weights: the fourth is four times as
// likely as the first.
var dotsWeights = vec.NewWeighted(1, 2, 3, 4)

// dotsLab keeps the seed it last burst from, what the burst produced, and
// the keys of the last tick, so that a held arrow bursts once.
type dotsLab struct {
	seed    uint64
	uniform []vec.Vec2
	counts  [4]int
	normal  []vec.Vec2
	draws   int
	was     keys
}

func newDotsLab(seed uint64) *dotsLab {
	l := &dotsLab{seed: seed}
	l.burst()
	return l
}

// burst draws every dot again from a fresh source seeded with the lab's
// seed, and prints what the three panels hold.
func (l *dotsLab) burst() {
	s := vec.NewSource(l.seed)
	l.uniform, l.normal, l.counts = l.uniform[:0], l.normal[:0], [4]int{}
	for i := 0; i < dotsN; i++ {
		l.uniform = append(l.uniform, vec.Vec2{X: s.Uniform(0, panelW), Y: s.Uniform(0, panelH)})
	}
	for i := 0; i < dotsN; i++ {
		l.counts[dotsWeights.Pick(s)]++
	}
	for i := 0; i < dotsN; i++ {
		l.normal = append(l.normal, vec.Vec2{X: s.Normal(48, 12), Y: s.Normal(60, 15)})
	}
	l.draws = s.Draws
	um, us, nm, ns := l.stats()
	fmt.Printf("seed %d: uniform x mean %.4f spread %.4f (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts %v (30, 60, 90 and 120 expected); normal x mean %.4f spread %.4f (48 and 12 asked for); %d draws\n",
		l.seed, um, us, l.counts, nm, ns, l.draws)
}

// stats reads the three panels: the mean and spread of the flat panel's
// x, and of the bunched panel's x.
func (l *dotsLab) stats() (um, us, nm, ns float64) {
	xs := make([]float64, dotsN)
	for i, p := range l.uniform {
		xs[i] = p.X
	}
	um, us = vec.MeanSpread(xs)
	for i, p := range l.normal {
		xs[i] = p.X
	}
	nm, ns = vec.MeanSpread(xs)
	return um, us, nm, ns
}

// step bursts on R, and on the tick Right or Left goes down.
func (l *dotsLab) step(k keys) {
	switch {
	case k.r:
		l.burst()
	case k.right && !l.was.right:
		l.seed++
		l.burst()
	case k.left && !l.was.left && l.seed > 0:
		l.seed--
		l.burst()
	}
	l.was = k
}

func (l *dotsLab) draw(screen *ebiten.Image, h *hud) {
	for i, name := range []string{"uniform", "weighted 1:2:3:4", "normal"} {
		x0 := 8 + float64(i)*panelStep
		vector.StrokeRect(screen, float32(x0), panelTop, panelW, panelH, 1, dimColor, false)
		h.text(screen, name, x0, panelTop+panelH+2)
	}
	for _, p := range l.uniform {
		vector.FillRect(screen, float32(8+p.X), float32(panelTop+p.Y), 1, 1, lineColor, false)
	}
	for i, c := range l.counts {
		hgt := float32(c)
		vector.FillRect(screen, float32(8+panelStep+float64(i)*24+4), float32(panelTop+panelH)-hgt, 16, hgt, yColor, false)
	}
	for _, p := range l.normal {
		vector.FillRect(screen, float32(8+2*panelStep+p.X), float32(panelTop+p.Y), 1, 1, lineColor, false)
	}
}

func (l *dotsLab) lines() (top, bottom string) {
	um, us, nm, ns := l.stats()
	return fmt.Sprintf("seed %d  draws %d  counts %v", l.seed, l.draws, l.counts),
		fmt.Sprintf("uniform x %.4f +- %.4f  normal x %.4f +- %.4f", um, us, nm, ns)
}
// cmd/motion/main.go — extend
var (
	labFlag  = flag.String("lab", "circle", "the lab to open")
	seedFlag = flag.Uint64("seed", 1, "the seed a lab that draws random numbers starts from")
)

// newLab builds the lab named, from the seed: the one place a name
// becomes a lab. Labs that draw no random numbers ignore the seed.
func newLab(name string, seed uint64) (lab, error) {
	switch name {
	case "circle":
		return newCircleLab(), nil
	case "arrows":
		return newArrowsLab(), nil
	case "ship":
		return newShipLab(), nil
	case "ball":
		return newBallLab(), nil
	case "ease":
		return newEaseLab(), nil
	case "dots":
		return newDotsLab(seed), nil
	}
	return nil, fmt.Errorf("no lab called %q", name)
}

func main() {
	flag.Parse()
	l, err := newLab(*labFlag, *seedFlag)
	if err != nil {
		log.Fatal(err)
	}
	h, err := newHUD()
	if err != nil {
		log.Fatal(err)
	}
	ebiten.SetWindowSize(960, 540)
	ebiten.SetWindowTitle("Motion: " + *labFlag)
	ebiten.SetTPS(60)
	if err := ebiten.RunGame(&Game{lab: l, hud: h}); err != nil {
		log.Fatal(err)
	}
}
go vet ./...
go run ./cmd/motion -lab dots
Three outlined panels side by side, labelled uniform, weighted 1:2:3:4 and normal beneath. The first is filled evenly with small white dots. The second holds four gold bars rising from the bottom, each taller than the last, the first much the shortest. The third has a cloud of white dots dense in the middle and thinning toward the edges. The top line reads seed 1 draws 1500 counts [19 66 98 117], the bottom uniform x 46.8191 +- 28.6623 normal x 48.1587 +- 11.4405.
The window when the lab opens, seed 1: a flat square, four bars in the ratio the weights asked for, give or take, and a cloud with its middle where the mean was put.

Press R, then Right, then Left:

$ go run ./cmd/motion -lab dots
seed 1: uniform x mean 46.8191 spread 28.6623 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [19 66 98 117] (30, 60, 90 and 120 expected); normal x mean 48.1587 spread 11.4405 (48 and 12 asked for); 1500 draws
seed 1: uniform x mean 46.8191 spread 28.6623 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [19 66 98 117] (30, 60, 90 and 120 expected); normal x mean 48.1587 spread 11.4405 (48 and 12 asked for); 1500 draws
seed 2: uniform x mean 47.3415 spread 28.7626 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [32 66 82 120] (30, 60, 90 and 120 expected); normal x mean 48.1740 spread 12.0402 (48 and 12 asked for); 1500 draws
seed 1: uniform x mean 46.8191 spread 28.6623 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [19 66 98 117] (30, 60, 90 and 120 expected); normal x mean 48.1587 spread 11.4405 (48 and 12 asked for); 1500 draws
The same three panels from seed 2: a different scatter of dots in the flat square, four gold bars closer to the ratio 1:2:3:4 than before, and a different cloud. The top line reads seed 2 draws 1500 counts [32 66 82 120], the bottom uniform x 47.3415 +- 28.7626 normal x 48.1740 +- 12.0402, and the tick count reads 11.
Seed 2, after Right: a new list of fifteen hundred draws, and the numbers move by about what three hundred samples' worth of scatter allows.

The first and second lines match, and the first and fourth match. R repeats seed 1. Left after Right returns to seed 1. Both make a fresh Source and draw the same fifteen hundred numbers in the same order.

The count is fifteen hundred, not eighteen hundred, because Normal keeps a spare value. The lab uses six hundred draws for the flat positions, three hundred for the weighted picks and six hundred for the normal positions.

The printed numbers sit beside the expected values, and they don't equal them. The flat panel's mean is 46.82 against 48. Its spread is 28.66 against 27.71. The weighted counts are 19, 66, 98 and 117 against 30, 60, 90 and 120.

Those gaps are normal for three hundred samples. Seed 2 gives counts of 32, 66, 82 and 120. It is not a better seed; its gaps landed elsewhere.

The panels draw from stored dots and counts, not from the generator. Ebitengine may call Draw more or fewer times than Update. Drawing from the generator would change the picture between ticks.

Right and Left burst only on the tick the key goes down. A held arrow would otherwise burst once per tick and skip past the seed the player meant to choose.

⚙ Tool — the generator underneath is PCG, and it is the games'

math/rand/v2 supplies rand.NewPCG(seed, 0) and Float64. PCG is a generator whose sequence is fixed by its seed words. The lab wraps it in Source to count draws and keep the spare normal.

The package also has top-level functions such as rand.Float64() and rand.Uint64(). They draw from a generator the runtime seeded from the operating system. The worked failure uses that generator.

Avoiding unseeded randomness

Add a flag for the wrong design: seeding each burst from the package-level generator. That generator gets its seed from the operating system when the program starts, not from the -seed flag.

▣ Build · stage 3 — the unseeded generator, behind a flag, extend cmd/motion/dots.go
// cmd/motion/dots.go — extend
import (
	"flag"
	"fmt"
	"math/rand/v2"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/vector"

	"gez/internal/vec"
)

// burst draws every dot again from a fresh source seeded with the lab's
// seed, and prints what the three panels hold. Under -global the source
// is seeded from the package's own generator, which nobody seeded, so the
// seed is whatever the machine had: the mistake this chapter shows, kept
// so it can be seen.
func (l *dotsLab) burst() {
	seed := l.seed
	if *global {
		seed = rand.Uint64()
	}
	s := vec.NewSource(seed)
	l.uniform, l.normal, l.counts = l.uniform[:0], l.normal[:0], [4]int{}
	for i := 0; i < dotsN; i++ {
		l.uniform = append(l.uniform, vec.Vec2{X: s.Uniform(0, panelW), Y: s.Uniform(0, panelH)})
	}
	for i := 0; i < dotsN; i++ {
		l.counts[dotsWeights.Pick(s)]++
	}
	for i := 0; i < dotsN; i++ {
		l.normal = append(l.normal, vec.Vec2{X: s.Normal(48, 12), Y: s.Normal(60, 15)})
	}
	l.draws = s.Draws
	um, us, nm, ns := l.stats()
	fmt.Printf("seed %d: uniform x mean %.4f spread %.4f (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts %v (30, 60, 90 and 120 expected); normal x mean %.4f spread %.4f (48 and 12 asked for); %d draws\n",
		l.seed, um, us, l.counts, nm, ns, l.draws)
}

var global = flag.Bool("global", false, "seed each burst from the package's own unseeded generator instead of the lab's seed: the mistake, kept so it can be seen")
go vet ./...
go run ./cmd/motion -lab dots

Without the flag, the lab still prints the same lines for the same keys. Under the flag, one line changes: seed = rand.Uint64(). The program hands that untyped seed to NewSource.

The printed line still says seed 1 because it prints the lab's field. That is part of the bug: the program reports a seed it didn't use.

⚠ Worked failure — the same seed, two different bursts

Run go run ./cmd/motion -lab dots -global and press R. Then close it and run it again. These lines were measured once, on the machine that made this page's pictures, and yours will differ, which is the failure. The first run printed:

$ go run ./cmd/motion -lab dots -global
seed 1: uniform x mean 45.9478 spread 28.0429 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [34 59 83 124] (30, 60, 90 and 120 expected); normal x mean 48.2034 spread 11.8154 (48 and 12 asked for); 1500 draws
seed 1: uniform x mean 49.9628 spread 28.6668 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [33 65 86 116] (30, 60, 90 and 120 expected); normal x mean 48.2717 spread 11.9758 (48 and 12 asked for); 1500 draws

and the second:

$ go run ./cmd/motion -lab dots -global
seed 1: uniform x mean 48.3520 spread 27.6041 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [31 59 100 110] (30, 60, 90 and 120 expected); normal x mean 49.2267 spread 11.9585 (48 and 12 asked for); 1500 draws
seed 1: uniform x mean 51.7157 spread 28.2045 (48 and 27.7128 for a flat 0..96); weighted 1:2:3:4 counts [22 53 96 129] (30, 60, 90 and 120 expected); normal x mean 48.2606 spread 11.5068 (48 and 12 asked for); 1500 draws

Four lines print four different pictures, and every line says seed 1. The seed on the line is the lab's field. The seed that made the numbers came from rand.Uint64().

Nothing else changed: the recipes, panels and draw count are the same. Put the code back to the default. Make one generator from the typed seed, then pass that generator to every function that draws.

Seeds as sequences

A seeded generator acts like a long sequence of numbers and a position in that sequence. The seed selects the sequence. Two runs that start with the same seed and take the same draws in the same order read the same values.

Two mistakes break that. A second generator uses a different sequence. A different draw order reads different positions in the same sequence. Passing one Source by hand prevents the first mistake. Drawing the panels in a fixed order prevents the second.

The distributions add no extra randomness. Uniform, weighted and normal values are arithmetic on draws from the same sequence. That is why the printed line can compare each sample with the expected mean, spread or count.

Checkpoint

✓ Checkpoint — what you can now do
  • Turn one draw into a number in any range with a lerp, and say what mean and spread three hundred of them have.
  • Pick from a table of weights with one draw and one walk, by hand for a draw of 0.55 through 1, 2, 3 and 4.
  • Work Box–Muller for draws of 0.5 and 0.25, and say why two draws give two normals and why a normal costs one draw on average.
  • Read the lab's printed line and say which gaps from the expected numbers are what three hundred samples allow.
  • Pass a generator by hand and draw in a fixed order, and say which of the two makes the same seed produce the same result.
  • Take two runs that both say seed 1 and print different numbers back to a call of rand.Uint64() on the unseeded generator.
⚡ Exercises — try first, then reveal
Exercise 1 — three thousand. Change dotsN to 3000 and read the printed gaps against the expected numbers. Then 30.

At three thousand the flat panel's mean lands within a fraction of a pixel of 48 and the counts within a few per cent of 300, 600, 900 and 1200: the scatter of a count grows with the square root of the sample and the sample grows faster, so the gaps shrink. At thirty the bars can come out in the wrong order altogether, and the "expected" numbers are 3, 6, 9 and 12, which a single seed can miss by half. The picture at three thousand is also a solid block: every pixel of the flat panel is hit.

Exercise 2 — a fourth panel. Draw a cloud whose distance from the panel's centre is a normal of mean 0 and spread 20, at a uniform angle, and compare it with the third panel.

vec.FromAngle(s.Uniform(0, 2*math.Pi)).Scale(s.Normal(0, 20)) added to the centre. The cloud is round like the third panel's but denser at the centre, because a radius that is normal in one dimension puts more points in the small inner rings than two independent normals do; the two are different distributions that both deserve the word "bunched", and a game that scatters particles picks one on purpose. Draw it after the third panel so the first three keep their numbers.

Exercise 3 — a loot table. Make the weights 60, 30, 9 and 1, name the four outcomes common, uncommon, rare and legendary, and count how many bursts of three hundred it takes to see a legendary.

vec.NewWeighted(60, 30, 9, 1) and four names in the label. A legendary is one pick in a hundred, so a burst of three hundred usually has two or three and sometimes none; walk the seeds with Right and watch the fourth bar. The printed counts are the table a designer reads to decide whether one in a hundred is the rate they meant, which is a decision the seed lets them make from the same three hundred picks every time.