Population Enforcer
Maintaining stable population levels across a distributed simulation presents unique challenges. How do you ensure thousands of virtual actors maintain realistic demographic patterns while avoiding jarring sudden changes that break immersion? The solution I've integrated into the system is what I've called the population enforcer which is a control system that gradually adjusts actor populations to maintain target levels within acceptable variance bands.
The enforcer operates as a feedback loop, continuously monitoring each node's current population against its configured target. When populations drift outside acceptable ranges, it makes small, natural-looking adjustments through births and deaths rather than teleporting actors in and out of existence. This approach preserves the simulation of an organic, evolving world while maintaining the statistical properties that is necessary for consistent behavior.
Configuration Architecture
Each simulation node requires careful configuration to balance realism with computational efficiency. The population enforcer reads these parameters from JSON files that define everything from target population sizes to mortality curves and demographic distributions.
node417.json
These numbers are an example and don't represent the full Node-417 config.
{
"node_id": "NODE417",
"target_population": 5000,
"sigma_pct": 0.05,
"max_daily_adjust_pct": 0.02,
"id_prefix": "n417",
"age_distribution": {
"mean_years": 34,
"stddev_years": 18,
"min_years": 0,
"max_years": 95
},
"mortality": {
"per_year": [
{ "max_age_months": 144, "rate": 0.002 },
{ "max_age_months": 600, "rate": 0.006 },
{ "max_age_months": 900, "rate": 0.015 },
{ "max_age_months": 1200, "rate": 0.06 },
{ "max_age_months": 2400, "rate": 0.18 }
],
"random_jitter_pct": 0.25
}
}
The sigma_pct parameter defines our tolerance band—in this case, 5% of the target population. With a target of 5000 actors, the enforcer will take action if the population drifts below 4750 or above 5250. The max_daily_adjust_pct acts as a rate limiter, preventing sudden demographic shocks by capping adjustments to 2% of the target population per simulation day.
The age distribution follows a normal distribution with realistic parameters for a modern population. Meanwhile, the mortality rates increase with age brackets, starting low for children and young adults before climbing steeply for elderly actors. The random jitter prevents deaths from occurring on predictable schedules, adding the randomness that makes populations feel alive.
Validation and Data Integrity
Before any enforcement can occur, the system must validate that node configurations are well-formed and contain sensible values. To do this I've brought in the use of Ajv for JSON Schema validation (which I use extensively in other parts of the game), to provide early detection of configuration errors that could corrupt the simulation state downstream.
node_meta.schema.json
"$id": "nodeMeta.v1",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": [
"node_id",
"target_population",
"sigma_pct",
"max_daily_adjust_pct",
"default_location",
"id_prefix",
"mortality",
"age_distribution"
],
"properties": {
"node_id": { "type": "string", "minLength": 1 },
"target_population": { "type": "number", "exclusiveMinimum": 0 },
"sigma_pct": { "type": "number", "minimum": 0, "maximum": 1 },
"max_daily_adjust_pct": { "type": "number", "minimum": 0, "maximum": 1 },
"default_location": { "type": "string", "minLength": 1 },
"id_prefix": { "type": "string", "pattern": "^[a-z0-9\\-]{2,16}$" },
...
This schema validation serves as a critical gatekeeper. As new simulation nodes come online or existing nodes receive configuration updates, malformed data gets caught before it can propagate into the live simulation. Parameters like sigma_pct and max_daily_adjust_pct are constrained to sensible ranges (0-1), while string fields like node_id must contain actual content.
The validation step also enforces consistency in ID prefixes, ensuring that actor identifiers follow predictable patterns across nodes. This becomes important when actors migrate between nodes or when debugging population issues across the distributed system.
File-Based Concurrency Control
Once validation passes, the enforcer must acquire exclusive access to modify population data. Unlike many systems that rely on database transactions, our simulation uses file-based storage for performance reasons—this means implementing our own concurrency control through filesystem locks.
The locking mechanism prevents race conditions when multiple processes might attempt to modify population data simultaneously. This is particularly important during node startup sequences or when administrative tools need to inspect population state while enforcement is running.
With the lock acquired, the system reads the current list of living actors from the seed log. This log serves as the authoritative record of which actors currently exist in the simulation.
Seeds as Stable Identities
Every actor in Node-417 exists as a deterministic seed—a numeric value that serves as both unique identifier and random number generator state. This design choice has profound implications for how the population enforcer operates.
Rather than storing complete actor records with names, ages, relationships, and other complex state, the enforcer only needs to track these numeric seeds. Other simulation systems can regenerate full actor details from seeds when needed, but population management operates at this lower level of abstraction.
NODE417_seeds.log
The seed log contains just timestamps and seed values—nothing more. This minimal format keeps I/O operations fast and makes it easy to count living actors or select candidates for mortality events.
2025-09-07T04:16:10.783Z 3577854293
2025-09-07T04:16:10.803Z 1532579522
2025-09-07T04:16:10.822Z 178803927
2025-09-07T04:16:10.840Z 1850681338
2025-09-07T04:16:10.861Z 2973940622
Each line represents an actor's "birth" in the simulation—the moment their seed was added to the active population. The enforcer can scan this file to determine current population levels and select actors for natural death events based on their age (derived from the timestamp) and other seed-determined characteristics.
This seed-based approach also enables interesting debugging capabilities. When investigating population anomalies, developers can regenerate the exact state of any actor at any point in time, since the seed determines all random events in that actor's life history.
Hydration vs Enforcement Modes
The population enforcer operates in two distinct modes depending on the current state of the simulation node. New nodes or those recovering from data loss require hydration—rapid population growth to reach target levels immediately. Most nodes during normal operation need only routine enforcement—small adjustments to maintain population stability.
The system automatically detects which mode to use by comparing current population to target levels. If the population is dramatically below target (typically more than 50% under), hydration mode activates and rapidly generates new actors until the population reaches the target band. This prevents new nodes from spending days or weeks gradually building up to operational levels.
During routine enforcement, the system respects the max_daily_adjust_pct rate limit, making only small changes that maintain the illusion of natural population dynamics. Actors are born and die at realistic rates rather than materializing suddenly.
The Enforcement Algorithm
When routine enforcement runs, it follows a carefully orchestrated sequence designed to maintain demographic realism while meeting population targets.
First, the system computes mortality weights for each living actor based on their age, health factors derived from their seed, and the configured mortality rates. Younger actors receive lower weights, making them less likely to die, while elderly actors face increasingly high mortality chances.
The natural death process then runs, probabilistically selecting actors for removal based on these weights. This isn't a simple random selection—the system uses the mortality curves defined in the configuration to ensure deaths follow realistic age patterns. A 25-year-old actor might have a 0.6% annual death chance, while an 85-year-old faces 18% annual mortality.
After processing natural deaths, the enforcer calculates how many births are needed to maintain population levels within the target band. This birth allocation system (which deserves its own detailed explanation in a future post) considers factors like seasonal birth patterns, demographic momentum, and resource availability.
Finally, new seeds are generated for birth events and appended to the seed log. Associated metadata files are updated atomically to ensure the simulation state remains consistent even if the process is interrupted.
It's worth noting that in the current implementation I would classify the death handling as the weak point. It handles mortality directly within the population enforcer, but this responsibility will likely migrate to a dedicated death-handling system as the simulation grows more complex. Death events have implications beyond simple population counting—they affect families, economies, and social networks in ways that deserve specialized attention and they should not be lazily drawn down based on a population enforcement scheme.
There are also future plans for emigration and immigration for actors to transition between nodes seamlessly but this will be a larger more concerted effort as there are many moving parts to handle for that type of process.
Reporting and Observability
After each enforcement cycle, the system generates detailed reports for monitoring and debugging. These reports capture the before and after states, document all changes made, and provide metrics for tracking population stability over time.
Example report for a target population of 10 in Node-417:
{
"ok": true,
"world_ms": 63331775537904,
"world_day": 39770630,
"world_date": "Cycle-06 Day 30, Year 3977",
"reports": [
{
"node": "NODE417",
"before": 10,
"after": 10,
"target": 10,
"band": [
10,
10
],
"dailyCap": 1,
"applied": {
"natural_deaths": 1,
"allocatedSize": 1,
"allocatedType": "births"
}
}
]
}
This reporting allows me to easily debug enforcement logic by examining exactly what changes were made during each cycle and better monitoring for population stability in the production environments, though I have more extensive reporting there as well.
The world_ms and world_day fields anchor each report in simulation time, making it possible to correlate population changes with other events occurring in the virtual world. The band array shows the current acceptable population range, while dailyCap indicates the maximum number of changes that could have been made given the rate limiting configuration.
Looking Forward
The population enforcer represents just one piece of a much larger demographic simulation system. In future posts I plan to go deeper into the birth allocation algorithms, actor migration between nodes, and the complex social networks that emerge from these simple population controls.
The ultimate goal remains unchanged: maintaining stable, realistic populations that provide a convincing foundation for the rich world of Node-417. The population enforcer ensures that foundation stays solid, even as everything else in the simulation continues to evolve.