· 7 min read ·

When the Map Does the Routing: Traffic Simulation on a 25 MHz Budget

Source: hackernews

The pizzalegacy.nl writeup on Pizza Tycoon’s traffic system is exactly the kind of retrocomputing archaeology that earns 292 points on Hacker News. Not because it is nostalgic, but because the problem it describes is genuinely hard, and the solution turns out to be clever in ways that still apply.

Pizza Tycoon shipped in 1994, developed by German studio Starbyte Software and published by Microprose. It was a business simulation: you opened pizza shops, managed staff and ingredients, and ran delivery routes through a simulated city. That city had traffic. Background cars moved through streets, stopped at intersections, and gave the world the visual impression of life. Doing that on hardware that would struggle to run a browser tab today required thinking about the problem differently.

What 25 MHz Actually Means

The Intel 486DX at 25 MHz delivered roughly 20 to 25 million integer operations per second. That sounds like a lot until you account for what else was consuming those cycles. VGA memory writes were slow; updating a 320x200 framebuffer could eat a significant fraction of your frame budget by itself. DOS had no preemptive multitasking and no GPU, so every pixel, every simulation step, and every audio sample was the CPU’s problem.

At 30 frames per second, your frame budget is about 33 milliseconds. Realistically, 30 to 40 percent of that went to rendering. What remained for simulation, input handling, and everything else was closer to 20 milliseconds. At 25 MIPS, that is roughly 500,000 integer instructions total per frame. Not per system, not per feature: total.

The 486SX variant, which many machines of that era actually shipped with, lacked a floating-point unit entirely. Software floating-point emulation was so slow that it was effectively unavailable for any inner loop. Everything that mattered had to be integers.

The Shape of the Problem

Simulating traffic means answering a few questions on every frame: where is each car, where should it go next, and what happens when two cars want the same space? For a city with dozens of background vehicles, the naive approach of running a pathfinding algorithm per car per frame is obviously impossible. Even a simple breadth-first search over a modest city grid, run for 50 cars 30 times a second, would exhaust the entire CPU budget before rendering a single pixel.

The question is not how to make pathfinding faster. The question is how to avoid pathfinding at runtime entirely.

How SimCity Solved It

SimCity (1989), whose source code was released by EA in 2008, did not simulate individual cars at all. Its traffic model operated on road tiles, not vehicles. Each road tile accumulated a traffic density value computed from the population density of nearby residential, commercial, and industrial zones. High-density areas generated high traffic density on adjacent roads. Cars shown on screen were a visual layer, drawn at positions derived from traffic density, not from any actual vehicle state.

This is the aggregate model: O(tiles) updates rather than O(cars) times O(pathfinding). It scales well, it looks convincing at the zoom level SimCity used, and it costs almost nothing at runtime because the expensive computation, the zone-to-road density propagation, ran infrequently and was spread across many frames.

The tradeoff is fidelity. You cannot ask SimCity where a specific car is going because no specific car exists. For a city-builder, that is fine. For a game where your delivery vehicles share streets with background traffic, it might not be.

Transport Tycoon’s Approach

Transport Tycoon, also released in 1994 and written by Chris Sawyer in hand-optimized x86 assembly, did simulate individual vehicles. But it avoided per-frame pathfinding by separating route planning from movement. Routes were computed once, on schedule, and vehicles followed pre-cached waypoint sequences. A road vehicle knew its next waypoint, followed it, then read the next one. Routing only ran again when the player changed a vehicle’s orders or when the network changed.

Sawyer’s assembly background gave him a different CPU budget than most developers. A routine written in tight assembly might use a third the instructions of the equivalent C code. That changes what is possible, but the fundamental insight is the same: separate the expensive computation from the per-frame loop.

Baking the Answer Into the Map

For a game like Pizza Tycoon, where background traffic needs to feel realistic but does not need to be individually tracked by the player, the elegant solution is to encode routing information in the map data itself. Each road tile gets a preferred exit direction, or a set of valid exit directions weighted by destination type. When a car enters a tile, it reads that tile’s direction data and knows immediately where to go next. No search, no graph traversal: just a table lookup.

This is what is now called a flow field, though the term was not in wide use in 1994. The concept appeared in Valve’s 2013 GDC talk on Left 4 Dead 2’s navigation system, which popularized it for modern RTS and crowd simulation. But the underlying idea, pre-computing directional gradient information into the environment so that agents can follow it cheaply at runtime, is older than the name. It is the natural solution when you have many agents, a static or slowly-changing graph, and a tight per-frame budget.

Building the flow field happens once at map load time, or whenever the road network changes. The per-frame cost per car is reduced to a tile index lookup and a direction read. On a 25 MHz 486, that is a handful of instructions. For 50 cars at 30 frames per second, the total cost is negligible.

The Other Half of the Budget: Intersections

Flow fields handle straight movement well. Intersections are harder because multiple cars may want the same tile at the same time, and the right-of-way logic needs to produce consistent, visually sensible behavior without being expensive.

The standard approach in this era was to treat intersection tiles as stateful objects rather than running per-car collision detection. An intersection tile held a flag indicating whether it was occupied and, in more sophisticated implementations, a queue of waiting cars. A car requesting entry to an intersection checked the flag; if clear, it set the flag and entered. When it exited, it cleared the flag. Simple semaphore logic, no search involved.

More sophisticated games added a timer-based slot system: intersections granted access on a rotating basis, much like a simplified traffic light with no visual representation. Cars that could not enter waited in place. The result looked like real traffic courtesy because it produced the same surface behavior: cars yielding to each other at junctions.

Staggered Updates and Fixed Pools

Two more techniques that appear throughout DOS-era simulation code are staggered updates and fixed object pools.

Staggered updates mean that not every car is updated every frame. With 50 cars divided across 5 update groups, each group updates once every 5 frames. At 30 fps that is a 6 Hz update rate per car, which is fast enough to look smooth at typical city-view zoom levels and cuts the per-frame simulation cost by 80 percent.

Fixed pools mean that the maximum number of traffic cars is hard-coded at compile time. There is no heap allocation during gameplay. A pool of, say, 64 car structs is allocated at startup; cars are activated and deactivated by flipping a flag. This eliminates allocator overhead, keeps the car data in a contiguous block that is cache-friendly even by 486 standards (the 486 had an 8KB unified L1 cache), and makes the worst-case frame time predictable.

Fixed-Point Arithmetic

With no floating-point unit available on many target machines, smooth sub-tile movement required fixed-point arithmetic. A car’s position might be stored as a 16.16 fixed-point value: the high 16 bits are the tile coordinate, the low 16 bits are the sub-tile fractional position. Addition and subtraction work identically to integers. Multiplication requires a shift after the multiply to correct the scale. The result is smooth-looking motion at the cost of slightly more complex arithmetic, but still entirely integer operations.

This was so standard in DOS game development that most developers had personal fixed-point math headers they carried between projects. The Doom source code, released in 1997 and representative of mid-90s practice, is full of FRACUNIT constants and FixedMul calls for exactly this reason.

What Constraint Forces

The techniques in Pizza Tycoon’s traffic system, whether described exactly as above or with local variations, are not workarounds for weak hardware. They are correct approaches to the problem of multi-agent navigation in a static environment. Flow fields, pre-cached routes, aggregate density models, staggered updates: modern game engines use all of them, not because modern hardware is weak but because these approaches are algorithmically better than running per-agent pathfinding every frame.

The 25 MHz constraint did not produce clever solutions by magic. It forced developers to ask the right question: what is the minimum computation actually necessary to produce the desired behavior? The answer, in this case, was to move as much work as possible out of the per-frame loop and into the map data itself. The map becomes not just a visual description of the world but a compiled program that agents execute by moving through it.

That framing, the environment as encoded behavior, shows up in behavior trees, navigation meshes, and ECS architectures today. The hardware has changed by several orders of magnitude. The insight has not.

Was this interesting?