01 · Problem
What is it for?
Growth rules need to operate on complete generations while keeping randomness reproducible.
02 · Mental model
How should you think about it?
An axiom is generation zero. Each next generation rewrites every old symbol independently.
Rewrites symbol sequences with normalized weights and injectable randomness. Output can grow exponentially. Rendering and caller-specific output budgets are separate from grammar rewriting.
03 · Control flow
What happens in which order?
- Start from the axiom.
- Find rules for each old symbol.
- Choose one rule by normalized weight.
- Append its complete replacement sequence.
- Repeat for the requested generation count.
04 · Building blocks
The important contracts
LSystem / generateRewrite an axiom using validated weighted rules.Rule / SymbolTypeDescribe symbols, replacements and relative weights.05 · Build it yourself
Implement a small core
Start with this small independent implementation, then use the steps below to make its contract explicit.
- Start from the axiom.
- Find rules for each old symbol.
- Choose one rule by normalized weight.
- Append its complete replacement sequence.
- Repeat for the requested generation count.
type Rule<S> = { weight: number; replacement: readonly S[] };
function expand<S>(
input: readonly S[],
rules: ReadonlyMap<S, readonly Rule<S>[]>,
random: () => number,
) {
const output: S[] = [];
for (const symbol of input) {
const rule = chooseWeighted(rules.get(symbol) ?? [], random());
output.push(...(rule?.replacement ?? [symbol]));
}
return output;
}06 · Verify
What should you test?
- Check exact output after several generations.
- Preserve symbols without matching rules.
- Reject invalid iterations and invalid rule weights.
Try it yourself
Turn the first verification claim into a failing test, implement the smallest fix, then add a boundary case.
Check exact output after several generations.
Compare your approach
Your check should fail when the stated invariant is deliberately broken, and pass again after the fix. Keep setup deterministic and assert an observable result.
07 · Limits
Trade-offs and failure modes
- Output size can grow exponentially.
- Rewriting and turtle rendering are separate stages.
- Limit output size before exposing generation to uncontrolled input.
Output can grow exponentially. Rendering and caller-specific output budgets are separate from grammar rewriting.
08 · Keep exploring
Source code and related ideas
yarn tsx packages/lsystem/examples/basic.ts