All journal articles
From Uniformity to Diversity: Engineering Richer Procedural Worlds
By Pavel Platonov · NorthernByte
MISsimulation
TypeScript
Cloudflare Workers
Gaussian Distributions
Procedural Generation
Data Modeling
MISsimulation is a business computing simulation set in the context of
a fictional election campaign. Its scenario generator creates the
data teams use to practice analysis and decision-making. The data was
valid, but each generated island felt much the same: similar resident
groups, political tendencies, and geographic patterns.
The campaign is the setting; the engineering challenge was to make
each scenario distinct while keeping its data coherent and
reproducible for teaching and comparison.
The Tech Stack and Starting Point
The generator runs on Cloudflare Workers using TypeScript. The core
engine uses a seeded random approach with Gaussian distributions for
demographic modeling. We output both CSV files and MySQL
dumps containing everything from voter registration to social media
networks.
The generator already used deterministic seeding, so the same seed
produced the same output. It could build populations of more than
5,000 residents in memory within the request budget.
The problem was that we were using the same four demographic cluster
templates for every single island. It worked, but it was predictable.
Problem 1: The Apolitical Rate Was Hardcoded
Originally, every island had roughly the same percentage of
politically disengaged residents because we calculated it the same way
every time. This created unrealistic uniformity across different
"worlds" that should have had distinct political cultures.
The fix: We implemented a weighted random apolitical
target using a power curve:
const apoliticalTarget = _.clamp(
0.1 + Math.pow(Math.max(randUnit(880), 0), 1.4) * 0.4,
0.1,
0.5
);
This samples a target between 10% and 50%. Raising the random draw
to 1.4 favors values toward the lower end while leaving room for
higher targets. Because the draw is seeded, a given island keeps the
same target when regenerated.
But here's the tricky part: we couldn't just set a global variable and
walk away. We needed to recalculate the "quiet majority" cluster's
population share to hit that target while keeping all other
demographic clusters intact:
const apoliticalOther = otherClusters.reduce((sum, cluster) => {
return sum + cluster.share * cluster.apoliticalProbability;
}, 0);
const quietProb = Math.max(0.05, quietCluster.apoliticalProbability);
let desiredShare = (this.targetApoliticalRate - apoliticalOther) / quietProb;
desiredShare = _.clamp(desiredShare, 0.05, 0.8);
This dynamically adjusts cluster shares to achieve the target rate.
Some islands end up with highly engaged populations, others are
dominated by politically indifferent residents.
Problem 2: Template Fatigue
Nine demographic archetypes lived in our codebase, but we only ever
used the same four (plus the quiet majority baseline). Every island
got the exact same mix.
The technical constraint: we needed deterministic generation for
testing. If you generate an island with seed 12345 today, it needs to
be identical tomorrow.
The solution: Keep all nine templates but randomly
select four based on the island's seed:
const optionalSelection = optionalTemplates
.map((template, index) => ({ template, score: randUnit(500 + index) }))
.sort((a, b) => a.score - b.score)
.slice(0, optionalSlots)
.map((entry) => entry.template);
Each template gets a seeded random score, we sort by that score, and
take the top four. Same seed = same selection every time. Different
seed = different cluster composition.
This means one island might combine "young progressives" with "rural
conservatives" and "wealthy moderates," while another gets "retirees,"
"working-class liberals," and "suburban independents." The
nine-choose-four selection gives us 126 possible template mixes
before the apolitical target and geographic weights vary.
Problem 3: Geographic Distribution Was Too Uniform
Our area assignment code gave every geographic area roughly equal
population. Real places don't work that way. City centers concentrate
population. Rural areas spread thin. Some suburbs boom while others
stagnate.
The original code multiplied cluster affinities by 1.0 for baseline,
with minor adjustments. Too conservative.
The fix: We added a seeded area diversity bias with
base weights between 0.65 and 2.0:
this.config.areas.forEach((area, index) => {
let base = 0.65 + localRand(index * 19 + 1) * 1.35;
// Urban preference adjustment
if (['Central', 'Downtown', 'City Center'].includes(area)) {
base *= 0.9 + this.config.urbanRuralSkew * 0.5;
}
bias[area] = base;
});
Then during resident assignment, we apply this bias on top of cluster
affinities:
if (this.areaDiversityBias) {
areas.forEach((area, index) => {
const modifier = this.areaDiversityBias![area];
if (modifier !== undefined) {
const jitter = 0.9 + (index % 5) * 0.02;
areaScores[area] *= modifier * jitter;
}
});
}
The same area can now receive a different share of residents under
different seeds, while each island remains reproducible.
Bonus: Performance Instrumentation
We also switched from Date.now() to
performance.now() for timing metrics:
const getTimestamp: () => number = (() => {
try {
if (typeof globalThis !== 'undefined') {
const perf = (globalThis as any).performance;
if (perf && typeof perf.now === 'function') {
return () => perf.now();
}
}
} catch (error) {
// fallback
}
return () => Date.now();
})();
This gives the timing breakdown finer resolution when profiling
generation work inside a Worker request.
The Results
Before these changes, the main sources of repetition were:
- The same four optional demographic templates appeared in every island.
- The apolitical target stayed near the same value.
- Area weights produced similar population patterns across seeds.
Now the seed controls three separate choices:
- An apolitical target within the configured 10% to 50% range.
- Four optional templates selected from nine candidates.
- Area weights that vary from one island to another.
Reusing a seed under the same generator version recreates the same
scenario. Changing it can change the resident mix and geographic
distribution while preserving the structure of the files students
analyze.
Those changes give instructors varied datasets for the same analysis
tasks. The reproducible seed lets a team revisit a scenario and lets
us diagnose a generation issue without losing the conditions that
produced it.
November 2, 2025
Pavel P.