A Universe from Three Sectors

reverse-engineering programming nostalgia

Tell me the sector numbers of StarDock, Terra, and Rylos in your TradeWars 2002 game, and I’ll hand you back the entire map. All thousand sectors. Every warp lane between them. The location and class of all three hundred and eighty ports. Not a good guess. The exact map, the same one your BBS generated the night it ran Big Bang.

That shouldn’t be possible. The map felt like the one unknowable thing about the game, the part you spent turns and whole evenings charting by hand. But it was sitting in plain sight the entire time.

This is the same game I’ve been reconstructing from its binaries for the better part of a year. TradeWars 2002, the BBS door I lost too many teenage nights to. That project is what made this one possible, so I’ll start there.

Big Bang

Every TradeWars game began the same way. Before the door ever opened to callers, the sysop ran a program called BIGBANG.EXE. It laid out the universe: picked the special sectors, strung a thousand-odd warp lanes between random sectors, scattered the ports across the map, and wrote the whole thing into the game’s data files. Then the sysop deleted it, or didn’t, and the universe stood frozen until the next re-bang months later.

The map was random because Big Bang seeded a random number generator and let it run. Everyone knew this much: same seed, same universe. The docs said so, and warned sysops to change the seed every re-bang or they’d get the same galaxy back. The seed was a knob you turned at creation. Forward only. Nobody talked about turning it the other way, because turning it the other way looked pointless. The universe was already sitting in the data files. If you wanted to know it, you explored it.

And explore it people did. A whole ecosystem of helper tools grew up to do exactly this. TWX Proxy, the zero-turn-map scripts, the CIM reports: they sat between you and the game, watched the sectors scroll past as you moved, and quietly filed away everything they saw. That was the state of the art for thirty years. To know the map, walk the map. It works, it’s clever, and it completely sidesteps the question I want to ask, which is: where did the seed go?

Where the seed went

When the sysop left it set to randomize, Big Bang didn’t reach for anything exotic. Turbo Pascal’s Randomize reads the DOS clock. The hour, the minute, the second, the hundredths of a second, packed into one 32-bit value. That’s your seed. From there a plain linear congruential generator grinds it forward, one step at a time:

RandSeed := RandSeed * 134775813 + 1

Every call to Random turns the crank once and hands back a slice of the result. Pick two sectors to warp together, two turns of the crank. Place a port, another turn. By the time Big Bang finishes it’s cranked a few thousand times, and every one of those cranks was fixed, completely, by where the crank started.

So the universe is a function of one 32-bit number. That part is obvious. The non-obvious part is how little of that number is real. It came from a clock, and a clock only reads so many values: twenty-four hours, sixty minutes, sixty seconds, a hundred hundredths. Multiply it out and you get 8,640,000 possible seeds. Every TradeWars universe ever generated with a randomized seed is one of those 8.64 million. That’s about twenty-three bits. And really it’s fewer, because the DOS clock’s sub-second field is coarser than it looks. It ticks about eighteen times a second, not a hundred, so a lot of those 8.64 million never actually come up.

Call it twenty-odd bits. The whole thousand-sector galaxy fits in a number smaller than a single frame of a JPEG. You can try every possibility faster than you can read this paragraph. The only question is what you check each guess against.

Three sectors is enough

You need something observable. Something Big Bang produced early, that you can read off the map without charting all of it.

The landmark sectors are the place to look. The very first thing Big Bang does, before a single warp lane, is roll for the special sectors: StarDock, Terra, Rylos, and the Ferrengi homeworld. Those are literally the first four numbers it produces. And they’re exactly the sectors a player knows. You dock at StarDock constantly. You find Terra and Rylos early and never forget where they are. They’re the most-remembered coordinates in the game.

They’re also safe to read, because the map is the one thing that never moves. Ports get busted, ships come and go, sectors change hands, mines and beacons get dropped and cleared. But the warp lanes and the port locations are frozen the instant Big Bang finishes. Those landmarks aren’t game state you have to catch in a particular moment. They’re generator output, fixed for the life of the universe.

So the recovery is brute force, nothing subtler. For each of the ~8.64 million clock seeds, run Big Bang’s opening moves and check whether it puts StarDock at 431, Terra at 186, Rylos at 244. Throw away the seeds that don’t. On my machine this takes a few seconds, in Python, single-threaded, no cleverness.

The whole recovery is a couple dozen lines. This is the core of it, the generator’s opening rolls and a sweep over every reading the clock could give:

MULT = 0x08088405   # Borland's LCG multiplier: RandSeed = RandSeed*MULT + 1

def landmarks(seed):
    # Big Bang's opening rolls: StarDock, Terra, Rylos.
    # Each is Random(990)+11, re-rolled to stay distinct.
    s = seed & 0xFFFFFFFF
    def roll():
        nonlocal s
        s = (s * MULT + 1) & 0xFFFFFFFF
        return (s >> 16) % 990 + 11
    a = roll()
    b = roll()
    while b == a: b = roll()
    c = roll()
    while c in (a, b): c = roll()
    return a, b, c

def clock_seeds():
    # every value the DOS clock can hand Randomize: hh:mm:ss.cc packed into 32 bits
    for h in range(24):
        for m in range(60):
            for sec in range(60):
                for cs in range(100):
                    yield ((sec << 8 | cs) << 16) | (h << 8 | m)

target = (431, 186, 244)   # StarDock, Terra, Rylos, straight off the map
print([hex(seed) for seed in clock_seeds() if landmarks(seed) == target])
# -> ['0xc00']

One seed falls out. Turning that seed back into the full map is the long part, the part that needed the byte-exact reconstruction, so it’s not here. This much just finds the number.

How many seeds survive? I ran that across every possible universe, not just mine:

Landmarks you know Universes with a unique seed
2 (StarDock, Terra) 0.005%, about 8,700 candidates each
3 (+ Rylos) 99.984%
4 (+ Ferrengi home) 99.999%

Two landmarks isn’t enough; you’re left with thousands of candidate seeds. But the third one collapses it. StarDock, Terra, and Rylos together pin the seed uniquely for all but a sliver of possible universes, and the Ferrengi homeworld, or any single sector you’ve actually charted, mops up the rest. Three sectors and the whole thing falls open.

And once you have the seed you don’t stop at the landmarks. You keep cranking. Replay the warp passes and you get every sector’s connections. Keep going into the port pass and you get every port’s sector and class. The seed doesn’t give you part of the map. It gives you all of it, in the order Big Bang built it.

Proving it wasn’t a fluke

It’s easy to write a program that reproduces one universe you already have and call it a day. That proves nothing. So I held this to the same standard as the rest of the project: it has to match the original binary, exactly.

I generated a fresh universe with the real, unmodified BIGBANG.EXE, cracked its seed from the landmark sectors alone, and rebuilt the map from scratch. Then I compared, sector by sector, against what the original actually wrote.

All thousand sectors’ warps matched. All thousand sectors’ ports matched too. The full class distribution, seventy-six class-1 ports on down to nineteen class-8s, each in exactly the sector Big Bang chose. There’s no partial credit in a thousand-out-of-a-thousand match. Across that many independent draws, either your model of the generator is exact or it desynchronizes into garbage a few sectors in. It didn’t desynchronize. And the recovered seed decoded right back to the clock the run started on: noon, on the dot.

The honest part

I want to be careful about what this is, because it would be easy to oversell.

This is not cryptanalysis. A 32-bit linear congruential generator seeded from a clock is about the weakest randomness you can build, and brute-forcing a few million possibilities was easy in 1994 and is trivial now. If the whole story were “I brute-forced a small seed space,” a security person would rightly shrug.

The hard part, the part that took months and isn’t really in this post, was recovering the exact sequence of Random calls Big Bang makes. Which sector it rolls first. When it re-rolls a collision. How the passes that repair connectivity decide to add a warp, and precisely how many times they crank when they do. Get any of that wrong by a single draw and the rebuilt universe scrambles into noise almost immediately. That sequence only exists inside a byte-exact reconstruction of BIGBANG.EXE, and building that reconstruction is the real work. The seed crack is the easy last mile on a long, unglamorous road.

A couple of caveats, so I don’t overstate the reach. A careful sysop could type in a full 32-bit seed by hand instead of letting the clock choose; a few did. Then the search is the whole four billion instead of a few million. Still minutes on a modern machine, but no longer instant. Almost nobody bothered, because the program told you to randomize and people randomized.

And this is the early DOS line specifically, though all of it. I checked versions 0.98, 1.01, and 1.03, and they carry the same generator: the same tool cracks and rebuilds every one without a change. Under the same clock, 1.01 and 1.03 produce byte-identical universes, and 0.98 differs only in one planet’s stats, with the entire warp map and every port in the exact same place. The later branches I haven’t tried: the 1995 v2, John Pritchett’s 1997 v3 rewrite, the TWGS server that grew out of it. They kept the same bargain, randomize a seed and the universe is fixed, so the weakness plausibly follows. But rebuilding those maps exactly would mean reversing each generator the way I did this one.

What I couldn’t find

I went looking for whether anyone had done this before, and I couldn’t find it. The forward direction is documented everywhere: same seed, same universe. Recovering the state of a weak generator from its output is a known trick in general. Exploration-based mapping is the whole helper-tool tradition. But not the specific inverse. Take a real TradeWars universe, recover its discarded clock seed from a few sectors, rebuild the exact map. I found no record of anyone doing it. I can’t prove a negative; some sysop in 1996 may have done exactly this and never written it down, and if that’s you, I’d love to hear about it. But as far as the public record goes, the map was always something you walked, never something you solved.

What it means

Not much, practically. The boards are mostly gone, the sysops randomized their seeds like the docs told them to, and the helper tools solved the real problem, knowing where to trade, a long time ago. Nobody needs this.

But I like what it says about the map. For thirty years it was the one part of TradeWars that felt genuinely yours to discover, the fog you cleared one jump at a time. The fog was never thick. A thousand sectors, three hundred and eighty ports, every warp lane you charted at two in the morning: all of it folded up inside a wristwatch reading from the night your BBS ran Big Bang. It just took reconstructing a dead compiler’s output to the byte before you could unfold it.

The universe was twenty-three bits the whole time.

Discussion