Naijapolis

Engineering blog · Part 3 of 9 · Simulation

The city’s heartbeat

Power grids, service coverage, mood and the monthly tick: the simulation that makes a town feel alive.

4 min read · 21 October 2026

All nine parts
  1. One file, no framework
  2. A kernel you can test
  3. The city’s heartbeat
  4. Ground, water and light
  5. Highlife from maths
  6. Sharing without trust
  7. Shipping and staying up
  8. Growing trees, flowers and water
  9. Waste and the politics of rubbish

A city builder lives or dies on whether the town feels alive. Under the cartoon streets there is a small, honest simulation. Here are its main ideas, with the real algorithms simplified.

Keeping the lights on: a graph problem

Power poles and generators are the nodes of a graph. The rule is simple: every pole links to its three nearest neighbours within a maximum distance, and then electricity flows outwards from every generator. Whatever the flow reaches is powered; whatever it cannot reach is dark.

gap too wideGgeneratorpowereddark
The generator (G) lights every pole it is connected to. The pair on the right is only a short way off, but nobody has built the poles to bridge the gap, so they stay dark. Players spot this on the map and fix it by placing one more pole.

This is a flood fill, the same algorithm a paint bucket uses. It is cheap, and it explains itself visually, which matters: a rule the player can see on the map is a rule the player trusts.

function linkGrid(nodes, maxLink) {
  // 1. link each node to its three nearest neighbours in range
  nodes.forEach((a, i) => {
    nodes
      .map((b, j) => ({ j, d: Math.hypot(a.x - b.x, a.z - b.z) }))
      .filter(o => o.j !== i && o.d <= maxLink)
      .sort((p, q) => p.d - q.d)
      .slice(0, 3)
      .forEach(o => connect(nodes, i, o.j));        // links both ways
  });
  // 2. flood outwards from every generator
  const queue = [];
  nodes.forEach((n, i) => { if (n.gen > 0) { n.live = true; queue.push(i) } });
  while (queue.length) {
    const n = nodes[queue.pop()];
    n.adj.forEach(j => { if (!nodes[j].live) { nodes[j].live = true; queue.push(j) } });
  }
  return nodes;
}

Having power is not all-or-nothing, though. Supply is compared with demand across the city, and batteries add a share of their stored charge. The result is a ratio between 0 and 1, so a town that is slightly short of power is slightly unhappy, not plunged into darkness.

Services fade with distance

Clinics, schools, police posts and water points each have a reach and a strength. For any home we add up the strength of every building that reaches it, but the strength fades the further the home is from the service:

10strengthat the buildingedge of reachdistancefades to 60% at the edgenothing beyond
A service is strongest right outside its door and weaker at the edge of its reach. The amount of fade is one tunable number, so a designer can change how forgiving the game is without touching code.
function coverAt(providers, x, z, need) {
  let v = 0;
  for (const p of providers) {
    const d = Math.hypot(p.x - x, p.z - z);
    if (d <= p.reach) v += p.strength[need] * p.efficiency * (1 - FALLOFF * d / p.reach);
  }
  return Math.min(1, v);      // two clinics do not make you more than fully covered
}

Why linear? Because it is easy to explain ("the further away, the weaker") and easy to draw as a ring around the building. Every number lives in a tuning file that can be edited and reloaded, so balance is a conversation, not a code change.

From coverage to mood to score

For each home the game looks at six needs: power, water, safety, health, education and roads. Each becomes a number between 0 and 1 and they are blended into the home’s mood. The city’s score and its one-to-five star rating come from the homes’ moods, jobs and the way the town is run. There is no hidden machinery: when a family is unhappy, the card says which need is short.

The monthly tick

1. A month passesthe clock fires the bus2. Gather the factspower nodes, services, roads3. Per home: coverage, then moodsix needs, each 0 to 14. City score and starsplus a yearly report card5. Events, if the gates are openfloods, strikes, elections, festivalsnext month
One pass of the simulation. The loop is the same every month; what changes over a town’s life is how many events pass the gate in step 5.

Everything runs on the month. Systems subscribe to the month event rather than running their own timers, which keeps them in step with each other and makes pausing or fast-forwarding trivial. The simulation does not draw anything: it reads the world, computes numbers, writes numbers back, and lets the renderer notice.

Floods: risk as a landscape

Rain is risky only where water would gather. The flat island has a few low basins, each a point and a radius; a spot’s flood risk is highest at a basin’s centre and fades to nothing at its rim. The shoreline adds a second source of risk close to the sea. Drainage and sea walls lower the risk locally.

function floodRisk(x, z, { basins, limit, factor = 1 }) {
  let r = 0;
  for (const [bx, bz, radius] of basins) {
    const d = Math.hypot(x - bx, z - bz);
    if (d < radius) r = Math.max(r, 1.15 * (1 - d / radius));   // deepest in the middle
  }
  const shore = Math.min(limit - Math.abs(x), limit - Math.abs(z));
  if (shore < 9) r = Math.max(r, 0.9 * (9 - shore) / 9);        // and near the sea
  return Math.min(1, r * factor);
}

On cities built on shaped ground (Part 4), the same question is answered from the height map instead: low ground next to the water floods first.

The yearly report card

Once a year the council grades the town A to E. Stars count most, then whether the books balance, whether floods hit, and how happy people are. The grade pays a bonus, so doing well makes the next year easier, while doing badly makes it harder but never hopeless. A kind game is allowed to be gentle about failure.

What we learned

  1. Prefer rules a player can see. A flood fill on a map explains itself; a hidden multiplier does not.
  2. Put every tunable number in one place, with a plain-language description. Designers will use it; you will thank them for it.
  3. Let time be one event, not many timers.

The next part is about drawing this world, and about making it run on a phone that is also doing three other things.

Now go and build something.

It takes about a minute to lay your first road.

Play Game