Controlled chance
This section treats randomness as a design material: choose a repeatable seed, choose the shape of the choices, and save exact parameters when a seed is not enough.
- Lesson: control repeatable variation
- Practice: predict, run, and repair
- Exercise: build a tested six-image edition
Lesson
One rule can produce a family
One seed generates six related editions; shape and position distinguish motifs without relying on color.
A computer’s random-number generator behaves more like an elaborate card shuffle than magic. A seed chooses the starting shuffle. The same seed and same rules produce the same sequence again, giving a varied image a repeatable identity.
A distribution decides how engine output becomes useful choices. Uniform choices treat equal ranges equally; weighted choices favor some outcomes; center-biased choices make the middle more common. These are compositional tools, not a substitute for choosing a composition.
Engine, seed, and distribution have separate jobs
A random engine is a repeatable state machine:
std::mt19937 engine(seed);
The standard defines the sequence for
std::mt19937
.
It is pseudo-random and repeatable, not secret.
A distribution maps that sequence into a range. The exercise uses
std::uniform_real_distribution<float>
for positions and radii. Exact distribution mapping is not required to match
between every C++ standard-library implementation, even when the engine and seed
match.
That creates three different replay promises:
- Concept replay: a seed and algorithm describe the same process.
- Same-build replay: the same source, library build, parameters, and seed reproduce the same serialized model.
- Cross-toolchain parameter replay: saved generated records recreate the exact model without running the distribution again.
The adapter uses
openFrameworks file utilities
when S saves edition-parameters.txt. The CONTROLLED_CHANCE_V1 format stores
seed metadata and exactly six ordered editions. Its parser rejects wrong
versions, missing or extra records, non-finite numbers, unknown motifs, bad
ranges, and noncontiguous indices.
Generate records before drawing
The model stores choices without any drawing call:
struct MarkParameter {
float x_unit;
float y_unit;
float radius;
Motif motif;
};
struct Edition {
int index;
std::vector<MarkParameter> marks;
};
Separating records from rendering lets tests inspect values and another program parse saved parameters. Drawing then becomes a separate visual interpretation.
Independent choices are only the first step
This exercise draws each mark’s position, radius, and motif separately. That is a useful first model, but adding more independent random properties usually makes a sketch less coherent, not more artful.
A richer composition can make choices in a hierarchy:
composition -> region or cluster -> individual mark
The composition might choose one dominant direction and one deliberate empty area. Each region inherits that direction with a small change. Marks inherit their region’s palette and vary only spacing or width. The random values now share causes, so they produce family resemblance rather than confetti.
Keep this section’s exercise focused on seeds, distributions, and replay. After the light, time, and media units, section 16 returns to chance and builds a full spatial grammar from shared panel seams, inherited angles, bounded variation, and intentional quiet space.
Weighted tickets make hierarchy explicit
Motifs use an integer from
std::uniform_int_distribution<int>
between 0 and 9:
0 1 2 3 4 5 -> dot (six tickets)
6 7 8 -> dash (three tickets)
9 -> ring (one ticket)
The intended proportions are 60%, 30%, and 10%. Exact ticket boundaries are testable; a small random sample’s observed percentages are not a reliable oracle.
Averaging uniforms creates a visible distribution shape
One uniform coordinate spreads values evenly in expectation. Averaging two uniform values favors the center:
center_biased = (uniform_a + uniform_b) / 2
Few pairs average near 0 or 1; many average near 0.5. This triangular shape creates a denser middle and quieter edges. The property test checks all pairs on a fixed grid instead of trusting a lucky random sample.
One continuing engine creates editions 0 through 5. All six share the same rules
but receive different records. Responsive panel geometry maps normalized values
after generation and reserves maximum radius, half the stroke, and an outer
margin. A panel smaller than 64 × 64 or any non-finite parameter is invalid.
Practice
Predict the choice rules and inspect a known-good edition without using the section unit-test runner.
1. Work the ticket and layout numbers
- Tickets 5, 6, 8, and 9 map to dot, dash, dash, and ring.
- Uniform values 0.2 and 0.8 average to 0.5.
- Six editions with 20 marks each contain 120 records.
- With maximum radius 8, the inset is
8 + 1.5 + 2 = 11.5pixels. - A seed is enough for same-build replay; saved parameters are needed for this section’s exact cross-toolchain replay.
Predict each result first. Then draw ten ticket boxes and shade the six/three/one groups so the hierarchy is visible.
2. Build and explore the working example
scripts/section-06.sh generate --project solution
scripts/section-06.sh build --project solution --configuration Release
On Windows Developer PowerShell:
.\scripts\section-06.ps1 generate -Project solution
.\scripts\section-06.ps1 build -Project solution -Configuration Release
Open the app. Press R and confirm the same edition returns. Press N and
identify what changes while the six-panel grammar remains. Press S, then open
edition-parameters.txt and find its version, seed, edition indices, normalized
positions, radii, and motif codes.
3. Repair a weighted boundary
In exercises/06-controlled-chance/shared/edition_model.cpp, temporarily change:
if (ticket >= 0 && ticket < 6) return Motif::dot;
to:
if (ticket >= 0 && ticket < 5) return Motif::dot;
Before rebuilding, write the new ticket counts. Build and run the solution, then
inspect several N variations and compare the motif hierarchy with your ten-box
diagram. Restore < 6 and rebuild; do not regenerate.
If that was your only intended edit:
git restore -- exercises/06-controlled-chance/shared/edition_model.cpp
That command discards every uncommitted change in the named file.
Exercise
Problem: create a six-image seeded edition
Build exactly six related images from one seeded model. Choose a seed, marks per
edition, radius range, and palette, then create a visual grammar distinct from
the starter’s dots/dashes/rings and the solution’s connected routes. Preserve
the explicit six/three/one ticket rule, center-biased positions, R/N/S
controls, responsive bounds, and portable serialization contract.
Use the
Exercise 06 brief, starter, tests, and solution
as the authoritative requirements. Begin in
starter/src/design/edition_design.cpp, then edit starter/src/ofApp.cpp.
Run the unit tests
Linux or macOS, using both compilers when available:
CXX=g++ tests/run-section-06-tests.sh
CXX=clang++ tests/run-section-06-tests.sh
Windows Developer PowerShell:
.\tests\run-section-06-tests.ps1
The tests check same-build seed replay, exactly six editions, legal ranges and
motifs, exact six/three/one ticket mapping, the center-biased distribution
property, stable serialization round trips, strict seed grammar, malformed
input rejection, finite three-pixel stroke-aware bounds at 64 × 64, invalid
smaller viewports, and your design choices. They compile the starter design and
do not compare screenshots.
After tests pass, generate and build the starter. Exercise R, N, and S,
inspect the saved text, and view all six panels at minimum, narrow, square, and
wide sizes.
Quick visual check
R,N, andSwork from the keyboard; no panel flashes.- Motif roles and edition differences remain legible without color alone.
- Ink/background and accent/background contrast are suitable.
- Exactly six images remain clear at minimum, narrow, square, and wide sizes.
- Geometry differs from starter and solution in more than palette.
- Capture alt text names six images, density shape, motif encoding, and palette.
- Saved parameters record the inspected model; reused work remains credited.
If you get stuck
If the sketch changes unexpectedly, print the seed and confirm the model uses the seeded engine rather than a global generator. If every image looks alike, verify that rendering reads the generated records. For a weighting failure, draw tickets 0 through 9 and inspect the first boundary that differs.