Simulating a City's Traffic When Your CPU Has Three Million Instructions to Spare
Source: hackernews
The Pizza Tycoon traffic system writeup is a good reminder that some of the most interesting engineering decisions in game history were made under constraints so severe that the problem had to be redefined before it could be solved. Pizza Tycoon shipped in 1994, targeting a 386DX at 25 MHz. That processor executed somewhere between three and five million integer instructions per second, had no hardware floating-point unit unless you bolted on a 387 coprocessor, and addressed memory through a segmented 16-bit model that made anything over 640KB a bureaucratic headache. Simulating a city’s worth of vehicle traffic inside those limits is not an optimization challenge. It is a design challenge dressed up as one.
What 25 MHz Actually Means
A 386DX-25 is often described in relative terms, but the absolute numbers are more revealing. At 25 MHz with a CPI (cycles per instruction) of roughly two to four for integer operations, you get around six to twelve million clock cycles per frame at 60 fps, or twelve to twenty-five million at 30 fps. A naive individual-agent traffic simulation, where each vehicle maintains position, velocity, a route, and collision state, might easily cost a few hundred instructions per vehicle per update. With a city of even modest density, say two hundred active vehicles, that is forty thousand instructions per frame just for movement, before pathfinding, rendering, or anything else.
Pathfinding makes it worse. A breadth-first search across a city-scale tile grid, even a small 64x64 map, touches thousands of nodes. Run that for every vehicle on every frame and the simulation budget is gone before the screen has been drawn. This is the core tension that every DOS-era city sim had to resolve.
The Flow Model and Why It Works
The canonical solution, used in various forms by SimCity (1989), Transport Tycoon (1994), and Pizza Tycoon, is to abandon the idea of simulating individual vehicles and instead simulate traffic as a fluid flowing through a network. Each road tile or segment carries a density value representing the aggregate load on that piece of road. That density evolves according to simple rules: high-density sources (commercial zones, delivery destinations) push traffic outward; low-density sinks absorb it. The result is a scalar field over the road network that updates incrementally each game tick.
From an implementation standpoint, this is far cheaper than agent simulation. Updating a density field over N road tiles costs O(N) operations, each involving a small number of additions and comparisons. For a tile grid where roads make up perhaps 20 to 30 percent of the map, N might be a few hundred to a few thousand. That is a manageable budget. The simulation also degrades gracefully: if you skip an update cycle because the frame is already expensive, the density values are slightly stale, not catastrophically wrong.
The visual representation of individual cars can still exist on top of this model, and often did. Cars are spawned and despawned according to density values rather than being individually routed from origin to destination. A tile with high traffic density generates cars at higher frequency. Those cars follow the road geometry using simple local rules: move forward, turn if the next tile is also a road, prefer lower-density tiles at intersections. No global pathfinding required. The behavior looks like traffic because traffic, at sufficient density, is mostly local behavior anyway.
Time-Slicing and the Simulation Budget
A technique common across the DOS simulation genre is time-slicing: not everything is updated on every frame. SimCity’s source code, released years later, shows clearly that different subsystems run on different intervals. Traffic density might update every eight ticks. Crime might update every sixteen. Population growth every thirty-two. This keeps any single frame from being dominated by simulation overhead, and it matches the temporal scale of the phenomena being modeled. Traffic changes faster than population, so it gets more frequent updates, but still not every frame.
Pizza Tycoon would have faced similar design decisions. Delivery routes, customer demand, and vehicle movements all operate on different timescales. The simulation budget could be partitioned accordingly. An order placed at a restaurant does not need instant traffic rerouting; the delivery window is measured in game-minutes, not frames.
This approach has an interesting side effect: the simulation becomes more stable under load. When the frame rate drops because of a complex scene, the simulation ticks less frequently, which means each simulation update still has roughly the same real-time compute budget. The city slows down rather than breaking.
Fixed-Point Arithmetic and the Missing FPU
Without a floating-point coprocessor, any calculation involving non-integer values had to use fixed-point arithmetic. The standard approach was to scale values up by a power of two, do integer arithmetic, then shift back down. A road density value might be stored as an integer in the range 0 to 255 (fitting in a single byte), with the conceptual range 0.0 to 1.0 mapped linearly. Addition, comparison, and table lookup all work natively. Multiplication requires a shift. Division is rare enough to precompute or avoid.
This constraint had architectural consequences. Data structures tended toward byte arrays and packed integers rather than structs with float members. Tile maps stored simulation state alongside rendering data in compact formats. A single byte per road tile could encode both the tile type and the current traffic load. Memory locality was essentially free because everything was already packed.
For pathfinding, fixed-point costs mean Dijkstra’s algorithm was expensive not because of floating point, but because of the branching and random memory access patterns involved in priority queue operations. Many DOS games replaced Dijkstra with simpler gradient descent: pre-compute a distance field from each destination point, then route vehicles by always moving to the adjacent tile with the smallest distance value. The pre-computation cost is paid once, and routing a vehicle is O(path length) with tiny constants, mostly array indexing and comparison.
Spatial Partitioning for Free
The tile grid, which the rendering system already required, doubled as a spatial index. Collision detection, neighbor queries, and line-of-sight calculations all reduced to array lookups. Checking whether a vehicle could move to the next tile was a single memory read. Finding all vehicles within a radius was a bounded loop over a rectangular region of tiles. Nothing needed a spatial hash or a scene graph. The grid was the scene graph.
This is worth dwelling on because modern game development has largely separated the rendering representation from the simulation representation. Physics engines work in continuous coordinates. Pathfinding uses navigation meshes. AI uses behavior trees with floating-point state. In 1994, the tile grid was the single source of truth for everything, and that unification was a feature, not a limitation. Every system could share the same data structure, the same coordinate system, and the same cache lines.
The Approximation Is the Design
What makes the Pizza Tycoon approach interesting beyond the technical details is the philosophical stance it required. The goal was not to simulate traffic accurately. It was to simulate traffic convincingly, where convincingly means that the player’s mental model of cause and effect holds up under play. More deliveries cause congestion. Congestion causes late deliveries. Late deliveries cost revenue. That causal chain needs to be real, but the mechanism producing it can be as abstract as necessary.
This is a different kind of fidelity than what modern simulation games often target. A flow-based traffic model cannot tell you which specific vehicle is blocking which intersection. It cannot produce realistic emergent traffic jams from individual driver behavior, the way the Nagel-Schreckenberg cellular automaton model can. But for a game about running pizza restaurants, none of that matters. What matters is that the city feels alive and that the player’s decisions have legible consequences.
The constraint forced clarity about what the simulation was actually for. It is a lesson that scales surprisingly well beyond DOS-era hardware. Budget constraints, whether measured in MHz, battery life, or cloud compute costs, tend to produce the same result: a forced conversation about which parts of the model are load-bearing and which are decoration. The Pizza Tycoon developers had that conversation in 1993, with a 386 sitting on the desk next to them, and they got the answer right.
Echoes in Modern Systems
The techniques are not entirely historical. Flow-based traffic models still appear in city simulation games, including Cities: Skylines, which added individual agent simulation on top of an underlying flow model to handle large cities without destroying frame budgets. Time-sliced simulation updates appear in almost every open-world game, where NPC AI, physics, and world simulation run on staggered intervals. The gradient descent pathfinding approach, often called influence maps or potential fields in contemporary AI literature, is a standard tool in real-time strategy games.
The 25 MHz CPU is gone, but the design pattern it forced into existence is still doing work. The insight that you can approximate a complex system with a cheaper model, as long as the model preserves the causal relationships that matter to the user, turns out to be as relevant at gigahertz speeds as it was at megahertz ones. The scale of what is tractable has changed. The shape of the problem has not.