· 7 min read ·

Emergent Traffic on 25 MHz: The Engineering Behind Pizza Tycoon's Streets

Source: hackernews

The Pizza Legacy blog published a breakdown of how Pizza Tycoon, the 1994 Austrian-developed DOS tycoon game, handled its city traffic on hardware that peaked at 25 MHz. It is a short article, but it pulls on a thread worth unraveling fully: how developers in that era achieved plausible emergent behavior from systems that, by modern standards, barely have enough clock budget to render a single frame.

The Hardware Budget

A 486DX at 25 MHz delivers somewhere between 25 and 40 MIPS for integer work. The integrated FPU on the DX variant could do roughly 3 to 5 MFLOPS double-precision, but games avoided it where possible because floating-point instructions cost 10 to 100 cycles each, and a game loop could not afford that. Everything time-sensitive was done in fixed-point: velocities stored as integers scaled by a power of two, positions tracked as tile coordinates rather than world-space floats.

The L1 cache on the 486 was 8 KB, unified for instructions and data. This single number shaped most simulation design decisions. If your hot data fit in 8 KB, tile reads cost 1 cycle each. If it overflowed into L2 or, worse, into DRAM, you were paying 30 to 60 cycle penalties per miss at a time when 60 cycles represented nearly a quarter of your per-frame budget on a 60 Hz target. Staying under 8 KB was not a guideline; it was an architectural constraint that good DOS developers treated as a first-class design requirement.

Pizza Tycoon ran at minimum on a 486/25 with 4 MB of RAM under MS-DOS. The city was a tile grid, each tile encoded in a byte or two: road type, connectivity flags for the four cardinal neighbors, occupancy state, one-way designation. A 64-by-64 tile grid is 4,096 bytes, comfortably inside the L1 cache after the first pass. Once warmed, scanning the entire road network cost almost nothing.

Two Categories of Movement

The game had two categories of moving object: player-owned delivery vehicles, which needed to go somewhere specific, and ambient traffic, which just needed to look like a city. These two categories have completely different computational profiles, and merging them into a single routing system would have been a mistake.

Delivery vehicles required real pathfinding. A BFS or Dijkstra over a tile graph of a few thousand nodes is not expensive, and it was computed exactly once per delivery dispatch, with the result cached as a waypoint list. The vehicle followed that list tick by tick without re-evaluating anything. Occasional replanning happened on route invalidation, not on a timer.

Ambient traffic is where the interesting constraint-driven design lives. A car driving through a city with no particular destination does not need to know the optimal path to anywhere. It needs to move convincingly through the road network without clustering in one place and without deadlocking at intersections. The solution that developers across the industry converged on was the local junction rule: at each intersection, a vehicle selects its next direction based only on the connectivity of its current tile and a small bias toward continuing straight. No path data is computed or stored. The cost per vehicle per tick is a handful of integer operations.

This is structurally a cellular automaton rule. The vehicle’s next state depends only on local state: its current position, current direction, and the occupancy and connectivity of the tiles immediately ahead. The global behavior that emerges from thousands of these local decisions, vehicles flowing through the network, building up density on arterials, thinning out on side streets, looks like traffic. At city-block scale, a population-level approximation of flow is indistinguishable from individual simulation.

The Density Layer

Individual vehicle movement alone does not produce the right visual density. Traffic should look heavier on main roads and lighter on back streets, independent of how many agent sprites happen to be in frame at a given moment. The solution was a separate traffic density value per road tile, maintained in parallel with vehicle agent positions.

Each simulation tick, a diffusion pass ran over road tiles: a tile’s density value moved toward a weighted average of itself and its neighbors, with a small decay applied each step. This is a discrete approximation of the Lighthill-Whitham-Richards flow model, a 1955 fluid-dynamics analogy for traffic that describes density waves propagating through a road network. The game did not implement the partial differential equation; it used a five-operation integer approximation per tile per tick. On a 64-by-64 road grid with perhaps 1,000 road tiles active, that pass costs under 5,000 instructions, finishing in well under a millisecond at 25 MHz.

The density values served two purposes: visual effects (more sprites, slower-looking movement in dense areas) and AI routing hints. Delivery vehicles consulting a density layer when choosing between equivalent-length paths would naturally prefer less congested routes, producing emergent traffic load balancing without any explicit congestion-avoidance code.

Tiered Update Scheduling

Not every subsystem needed to run every frame, and this scheduling decision matters more than almost any algorithmic choice when working under a fixed CPU budget.

Rendering ran every frame. Vehicle position updates ran every two or three frames, cutting the number of agent evaluations by half or more. Density diffusion ran every five to ten frames, on the premise that traffic density is a slow-moving quantity that does not need per-frame precision. Economic simulation, customer demand, ingredient costs, all ran on timescales measured in seconds of real time.

The 8253 Programmable Interval Timer fired at 18.2 Hz by default on DOS machines; games reprogrammed it to higher rates for finer timing control. The important property was that simulation ticks ran at a fixed rate independent of rendering frame rate. If the machine was slow, frames dropped; the simulation stayed deterministic.

How Other Games of the Era Handled the Same Problem

SimCity, released five years earlier and updated through SimCity 2000 in 1993, took the opposite approach to vehicle agents: there were none. Traffic in SimCity was a density value computed through stochastic trip sampling. Simulated trips originated at residential zones and random-walked the road network toward commercial and industrial destinations, incrementing a counter on each tile traversed. The visual cars moving along roads were decorative sprites disconnected entirely from the simulation values. Will Wright’s stated position was that players needed to understand traffic as a phenomenon, not watch individual drivers, and that the abstraction held at city scale.

Transport Tycoon, released the same year as Pizza Tycoon, solved a different version of the problem because its vehicles had fixed routes on track or road networks with explicit origin and destination points. Chris Sawyer wrote the entire game in x86 assembly, using depth-limited depth-first search for vehicle pathfinding, jump tables for vehicle state machine dispatch, and bit-packed tile arrays for map storage. The assembly implementation gave roughly three to five times the throughput of equivalent C code, which is why Transport Tycoon ran at over 70 frames per second on a 486/33 with hundreds of vehicles active.

The academic work running parallel to all of this is the Nagel-Schreckenberg model, published in 1992 by physicists Kai Nagel and Michael Schreckenberg. NaSch describes freeway traffic as a one-dimensional cellular automaton: vehicles have integer velocities, accelerate toward a maximum, brake based on the gap ahead, and apply a small random slowdown to produce realistic stop-and-go waves. The rules are entirely local; global phenomena like phantom traffic jams emerge without any global coordination. Pizza Tycoon’s ambient traffic was not using NaSch, but the underlying insight is identical: local rules at each cell, emergent global behavior, O(N) computation per tick.

The four NaSch update rules, applied in parallel to every vehicle each timestep, illustrate how little is required:

1. Accelerate:   v = min(v + 1, v_max)
2. Brake:        v = min(v, gap)          // gap = empty cells ahead
3. Randomize:    if rand() < p: v = max(v - 1, 0)
4. Move:         position += v

That is the entire model. No global state, no path planning, no communication between agents. The traffic jams that appear are a consequence of the braking rule interacting with the randomization rule at sufficient density; no one designed them in.

What the Constraint Actually Bought

The reason these old simulation games reward close technical reading is not nostalgia. The constraint-first design process they followed, starting from CPU and cache budgets and working backward to algorithms, produced solutions that remain relevant when you need to simulate large agent populations in real time. The specific numbers change; the underlying requirements do not.

Games running on 25 MHz hardware were forced to find algorithms that scaled linearly with agent count, used only local information per agent, and kept hot data inside cache. A modern traffic simulation running millions of agents on a server faces the same requirements, not from clock speed constraints but from memory bandwidth and cache pressure at scale. The local-rule cellular automaton approach that DOS developers arrived at by necessity is the same approach that large-scale agent simulation researchers continue to favor today.

Pizza Tycoon’s traffic system is documented at pizzalegacy.nl, and the write-up is worth reading for the specific implementation choices. The broader lesson from that article is that constraint-first algorithm design, starting from cache and clock budgets, tends to produce solutions worth understanding on their own terms.

Was this interesting?