Part III · Randomness & implementation
Randomness has a history
Shared streams connect the result of one action to events elsewhere on the lawn.
3 min read
A random generator carries state. Taking a value advances that state; seeding it replaces the state. When unrelated systems share a generator, their histories meet at the next draw.
There is more than one stream
The game-owned global generator, the library shuffle generator, and object-owned generators have separate state. The process also initializes another engine from random_. Treating all of them as one sequence gives the wrong dependencies.
The global game generator starts with seed 4357. Its raw result has 31 bits. Bounded integers use modulo reduction, while the float helper scales a value and rounds it to float32.11The shared game generatorThe game-owned MT19937 starts at 4357, returns its low 31 bits, uses modulo for bounded integers, and divides floating draws by 2147483647.0 before float32 conversion. A branch census finds 38 calls to the global seed wrapper, including 30 that pass time(NULL). Separate object-owned generators and a random_device-seeded process engine also exist. These game reseeds do not reach libc++’s shuffle engine.0x1034ff9440x103608ff40x1036090b40x10360921c
Up to build 4.2.4.399, Evolution's result selection instead calls std:. Its library engine is shared with other list shuffles, including Modern portal queues. Resetting the game's global generator does not reset that library engine.22The separate libc++ shuffle streamThe imported __rs_get and __rs_default symbols identify two-argument std::random_shuffle. Its forward permutation uses uniform_int_distribution with rejection. Live PlayCover captures record raw outputs matching mt19937 seed 5489, complete permutations and selections; the host-library probe is separate evidence. Recorded iPad outcomes corroborate specified routes without identifying the loaded iPad library. Evolution and Modern portals share this library stream.0x1009a92800x1001210440x10001bf38 Build 4.2.6.401 chooses Evolution results with a generator of the artifact's own, as Evolution's choice in build 4.2.6.401 describes.
The game’s global generatorreseeded with 1790000000 · 31-bit outputs
- 01428991966
- 11930012957
- 21056160899
- 31924511133
- 4799111086
- 51919418955
- 62130897800
- 7352640671
The library enginemt19937 · seed 5489 · 32-bit outputs
- 03499211612shuffle
- 1581869302shuffle
- 23890346734shuffle
- 33586334585
- 4545404204
- 54161255391
- 63922919429
- 7949333985
Egypt’s portal list ra, explorer, explorer, pharaoh → ra, pharaoh, explorer, explorer
Step through
- 1Two sequences. Each generator moves only when something takes a value from it.
- 2Game draws. Three values taken from the game’s generator move it to position 3; the library engine has not moved.
- 3A library shuffle. Shuffling Egypt’s four-slot portal list takes 3 outputs of the library engine; the game’s generator has not moved.
- 4A clock reseed. Seeding the game’s generator with time(NULL) = 1790000000 replaces its state: it starts again from that seed’s first output. The library engine keeps its position.
One extra draw moves the next answer
Imagine one generator whose next values are assigned to a glove roll, a loot roll, and a placement choice. If another operation consumes a value before the loot roll, the loot routine receives a different part of the sequence.
No direct call from one feature to the other is necessary. Shared state is enough. An unused result still advances the generator if the draw already happened.
The on-kill reward routine illustrates this: it can draw even when the plant has no corresponding reward rate. An artifact branch can likewise draw values and then overwrite them. The number of draws consumed by a feature is part of its effect on later consumers.
A shuffle adds another variable. Permuting a list of entries makes distribution calls, and rejection sampling can consume more than one engine value per call. Changing the candidate count can therefore change how far the stream advances.
The game’s global generatorseed 4357 · 31-bit outputs
- 02146374468glove roll
- 1699692587another draw
- 21213834231loot roll
- 31920714022placement choice
- 4994957275
- 52082945813
| as planned | as drawn | |
|---|---|---|
| glove roll | 0 · 2146374468 | 0 · 2146374468 |
| loot roll | 1 · 699692587 | 2 · 1213834231 |
| placement choice | 2 · 1213834231 | 3 · 1920714022 |
Step through
- 1As planned. The glove roll, the loot roll and the placement choice take outputs 0, 1 and 2.
- 2One extra draw. Another operation takes output 1 first: the loot roll now receives output 2 and the placement choice output 3, though neither feature calls the other.
Many features reset the same generator
The shared game generator has 38 reseed call sites, and 30 pass time(NULL). This is a pattern across plants, loot, and artifacts. The clock has one-second resolution, and every reseed replaces the generator's accumulated state.33Clock reseeding is shared across systemsThe branch census finds 38 calls to the shared seed wrapper, 30 passing time(NULL). Attributed clock callers include the weighted loot selector, its coin-spring sibling, the Gashapon spin, three instruction-equivalent Zomboss loot implementations, and two Gardener Grass paths. The loot selector reseeds immediately before its bounded draw; Gashapon reseeds at spin entry before its later selection logic. These sites all replace state at 0x104e3e580. A second reseed discards earlier intervening draws; only draws after that reseed can shift a subsequent consumer. The libc++ shuffle engine is a separate object.0x1034fca380x10198019c0x10197f7a40x1010952480x1000e172c0x101eeab000x10229a9c80x100b21ec80x100b24b1c
All of these callers reset the same object. A loot routine can therefore change the random state later read by a plant or artifact, just as a plant can change the state later used by loot.
Two draws with the same bound, each taken immediately after a reseed to the same second, receive the same first result. Draws between those two resets are discarded by the second reset. A draw taken after the second reset but before the next consumer does change which output that consumer receives.
Gardener Grass adds an ordering mistake
Gardener Grass's fire routine reseeds before checking whether the plant is level 5. Below that level, it does not take the optional draw: it changes the shared state without using a value itself.44A firing routine resets unrelated random stateGardener Grass’s fire routine calls time(NULL) and reseeds before checking whether the plant is level 5. The lower-level branch takes no draw after this reset. Its effect-object critical-hit path also reseeds immediately before a bounded-100 draw. Equal seeds yield the same first result; an intervening draw after a reseed can change which output a consumer receives.0x100b21e980x100b21ec80x100b248980x100b24b1c
Moving that level check earlier would remove this particular no-draw reset. It would not fix the wider coupling: the other clock-seeded callers would still replace the shared state. The general issue is a local feature resetting a process-wide dependency.
Reseeded with 179000000031-bit outputs
- 01428991966draw → 66
- 11930012957other
- 21056160899other
- 31924511133
- 4799111086
- 51919418955
Reseeded with 1790000000 again31-bit outputs
- 01428991966draw → 66
- 11930012957
- 21056160899
- 31924511133
- 4799111086
- 51919418955
Reseeded with 179000000131-bit outputs
- 01811357021draw → 21
- 11236193707
- 2396618650
- 31172432243
- 41635916231
- 5158427370
Step through
- 1Reseed, then draw. A caller reseeds with 1790000000 and draws at once: output 0 of that state gives 66 under the bound 100.
- 2Draws in between. Other consumers take outputs 1 and 2 of the same state.
- 3A reseed in the same second. Another caller reseeds with 1790000000: the state is replaced, outputs 1 and 2 no longer matter, and its first draw with the same bound repeats 66.
- 4The next second. A reseed with 1790000001 starts another sequence; its first draw gives 21.
A good generator cannot repair the wrong selection loop
Gashapon's five-entry selector builds cumulative weights but draws again as it visits each entry. This is different from drawing once and finding the interval containing that value.
For cumulative totals and total weight , its idealized selection shares are
With five equal weights, those shares are 20%, 32%, 28.8%, 15.36%, and 3.84%. The input weights are equal, but the output frequencies are not.55Redrawing inside a cumulative scanThe five-entry selector constructs cumulative weights, but its loop back-edge returns to the random draw. Entry i receives product(1-Cj/T) for earlier entries times Ci/T. With five equal weights the idealized shares are 0.2, 0.32, 0.288, 0.1536, and 0.0384. This equal-weight example demonstrates the algorithm; it is not an attribution of the artifact’s unbound resource weight vectors. The inclusive float endpoint also permits a no-selection remainder.0x1010952240x1010953e40x10109540c
This is a much larger distortion than the tiny modulo bias at ordinary small bounds. A cumulative-weight selector should draw once and find the interval containing that value; drawing again at each comparison changes the distribution.
Notes
The evidence behind each claim: the code path, and the native addresses where it was traced.
- 1
The shared game generator
The game-owned MT19937 starts at 4357, returns its low 31 bits, uses modulo for bounded integers, and divides floating draws by 2147483647.0 before float32 conversion. A branch census finds 38 calls to the global seed wrapper, including 30 that pass time(NULL). Separate object-owned generators and a random_device-seeded process engine also exist. These game reseeds do not reach libc++’s shuffle engine.
0x1034ff9440x103608ff40x1036090b40x10360921c - 2
The separate libc++ shuffle stream
The imported __rs_get and __rs_default symbols identify two-argument std::random_shuffle. Its forward permutation uses uniform_int_distribution with rejection. Live PlayCover captures record raw outputs matching mt19937 seed 5489, complete permutations and selections; the host-library probe is separate evidence. Recorded iPad outcomes corroborate specified routes without identifying the loaded iPad library. Evolution and Modern portals share this library stream.
0x1009a92800x1001210440x10001bf38 - 3
Clock reseeding is shared across systems
The branch census finds 38 calls to the shared seed wrapper, 30 passing time(NULL). Attributed clock callers include the weighted loot selector, its coin-spring sibling, the Gashapon spin, three instruction-equivalent Zomboss loot implementations, and two Gardener Grass paths. The loot selector reseeds immediately before its bounded draw; Gashapon reseeds at spin entry before its later selection logic. These sites all replace state at 0x104e3e580. A second reseed discards earlier intervening draws; only draws after that reseed can shift a subsequent consumer. The libc++ shuffle engine is a separate object.
0x1034fca380x10198019c0x10197f7a40x1010952480x1000e172c0x101eeab000x10229a9c80x100b21ec80x100b24b1c - 4
A firing routine resets unrelated random state
Gardener Grass’s fire routine calls time(NULL) and reseeds before checking whether the plant is level 5. The lower-level branch takes no draw after this reset. Its effect-object critical-hit path also reseeds immediately before a bounded-100 draw. Equal seeds yield the same first result; an intervening draw after a reseed can change which output a consumer receives.
0x100b21e980x100b21ec80x100b248980x100b24b1c - 5
Redrawing inside a cumulative scan
The five-entry selector constructs cumulative weights, but its loop back-edge returns to the random draw. Entry i receives product(1-Cj/T) for earlier entries times Ci/T. With five equal weights the idealized shares are 0.2, 0.32, 0.288, 0.1536, and 0.0384. This equal-weight example demonstrates the algorithm; it is not an attribution of the artifact’s unbound resource weight vectors. The inclusive float endpoint also permits a no-selection remainder.
0x1010952240x1010953e40x10109540c