Bug hunt: Why you only need Paris to beat Pizza Tycoon (1994)

The new release of Pizza Legacy v0.1.0 introduces the ability to win the game, just like the original.

How to win the game is a question I hadn't really considered when playing Pizza Tycoon (1994) in my childhood. I had fun just opening restaurants and designing pizzas, aided by a healthy dose of arms dealing to fund the expansion of my pizza empire. But winning? Never happened, probably never thought about it.

As I started the reimplementation project I looked at the data files that come with Pizza Tycoon and found the graphic file ENDE.VGA and the text file ENDE.E, which tell us what happens when you win:

ENDE.VGA; a man in a golden chair wearing a crown and a
                big smile, with a bikini clad woman on either side of him, one
                looking admiringly at him and the other winking at us.

ENDE.VGA (ende being German for end).

The first line of ENDE.E:

You have achieved the unthinkable! You are King of the whole Western fast food market! You are the one, the only PIZZA TYCOON.

So although this makes it clear that there is a way to win the game, it doesn't tell us how exactly (other than being the king of the whole western fast food market, but that could mean a lot of things).

I just ignored this question for a long time since I had so much to implement before even worrying about how the player would win, but at some point I came across a Reddit post titled I 'beat' Pizza Tycoon? (Why did this happen?). After finding this post I got curious, but not curious enough to do a proper investigation, so I looked up the game-over conditions in the assembly and used Claude to analyse it, which suggested:

end_of_week_processing checks once per week whether the current player has >= 5% market share in ALL 10 cities.

This fit my mental model: it made sense from a gameplay perspective and it fit with the text I had seen from ENDE.E. It didn't match what the Reddit user was reporting though, claiming only to have restaurants in Paris and Berlin, but maybe they had omitted some details about what they were doing, or had a corrupt save game? I reached out but didn't hear back.

Something doesn't add up

Later, during playtesting of my own engine I was watching Pizza Tycoon - Worldwide Pizza King - Episode Ten, which shows YouTuber AppleSauce playing in Paris when suddenly the win sequence triggered.

Because the entire playthrough leading to their victory was recorded, I could clearly see that they did not in fact have restaurants in all cities in the game, so something else was going on. I was now actually getting closer to needing to implement the winning scenario in Pizza Legacy, so, back to the assembly for a proper in-depth look to see what was going on.

The relevant code lives in a function I named end_of_week_processing (I've spent the past 15 years slowly documenting the assembly of the game's executable PT.EXE; the names are mine or IDA generated). This function is called once per in-game week for each human player, and it does things like checking for bankruptcy, weekly profit transfers of the cities you're not currently in, and the victory condition check.

A screenshot of the IDA reverse engineering tool running on
             macOS, showing the assembly code (reproduced below) execution
             paths, showing six blocks of code with arrows between them to show
             how the execution moves through the code depending on various
             conditions.

Screenshot of IDA (The Interactive Disassembler) showing the code below in graph view.

The victory condition check is the following:

loc_546CD:
        xor     bl, bl          ; city index = 0 (Paris)
        jmp     short loc_546F3 ; start loop

loc_546D1:
        movzx   eax, bh         ; bh = current player index
        imul    eax, 0x0A       ; player * 10 (10 cities)
        movzx   edx, bl         ; city index
        cmp     byte ptr marketshare_percentages[edx+eax], 5
        jb      short loc_546F8 ; market share < 5%? exit loop
        push    0x8C            ; GAME_OVER_YOU_WON constant
        call    schedule_fullscreen_status_update ; YOU WIN
        add     esp, 4
        inc     bl

loc_546F3:
        cmp     bl, 0x0A        ; loop while city < 10
        jb      short loc_546D1 ; jump to loop implementation

loc_546F8:                      ; post-loop code
        movzx   edx, bh
        ...

In C that would look something like:

int number_of_cities = 10;
for (int city_id = 0; city_id < number_of_cities; city_id++) {
    if (marketshare_percentages[player_id][city_id] < 5) {
        break;
    }
    schedule_fullscreen_status_update(GAME_OVER_YOU_WON);
}

The issue is that schedule_fullscreen_status_update(GAME_OVER_YOU_WON) executes inside the loop instead of after successful completion. This wasn't "all cities need at least 5% market share". All you need is city 0 to have at least 5% market share!

Testing it myself

This new understanding explains both the Reddit post and the YouTube video. To be sure, I tested it myself, and indeed in my own save game with more than 5% market share in six different cities nothing happened until I also opened a couple of restaurants in Paris, and boom, for the first time in my life, I had won Pizza Tycoon.

I was curious to see if this behavior was the same in the original Pizza Connection (in German).

I could have tried to verify this in the German executable as well, but I've only documented the assembly of the English PT.EXE; the German PC.EXE was built with a different compiler and has a very different layout, so locating the equivalent victory-condition code would have required a substantial amount of reverse engineering. Instead I tried to just reach at least 5% market share in Paris through playing.

To get my market share up quickly (because I was investigating, not looking to spend an evening actually playing the game), I needed money. The fastest way is weapons dealing: buy cheap, sell expensive, hope you don't get caught.

Instead of doing the hard work myself, I did some light cheating by buying a bomb, saving the game, then buying another bomb, saving again, and doing that once more. I wrote a quick Python script that compared the three save-game files and printed any differing values side-by-side. From there I looked for all fields that changed by exactly one on every subsequent save game (I figured finding a single byte with the bomb count was easier than trying to find the double with the money).

At offset 0x2E0 of the save game I found the values 158, 159 and 160 respectively. Opening the save game in a Hex editor, changing the value, and loading it, confirmed that this is indeed where the number of bombs is stored, with an offset of 157: If the byte is 157 you have 0 bombs, a value of 158 means you have 1 bomb. Because the value is stored in a single byte, decreasing it below 157 causes it to wrap around, so setting it to 156 gives you 255 bombs.

I set the value to 141, to give myself 240 bombs (because it's fastest to sell them in batches of 20), sold them all, and now I had the capital needed to start expanding without investing too much time.

Within a short while I got to well over 5% market share in Paris without the game ending, so the bug seems to be specific to the English version of Pizza Tycoon.

Screenshot of the market share screen in the German Pizza
                 Connection, showing player one (me) with a bar close to 1
                 (10%), indicating I have more than 5% market share but the
                 game is not over yet.

Despite what the title says the Y axis here is 0 to 100, not 0 to 10, so when you reach the 1 that's 10%.

Design or accident?

I'm reasonably sure that the original logic was to require the player to hold at least 5% market share in all ten cities at the same time. The loop structure which exits on failure makes sense for that interpretation; if they just wanted the first city they could have checked index 0 directly instead of going through the trouble of writing a loop that never iterates beyond the first entry.

So why does the game hand you the win for Paris alone? The English Pizza Tycoon isn't quite the same game as the German original. Terry Greer, graphics artist at MicroProse at the time, recalls that Pizza Connection was a German game that MicroProse wanted to rebadge and add to their tycoon brand. The win condition was reworked somewhere in that process: the German original doesn't have the bug, only the MicroProse-handled English release does.

My guess is that someone tried to relax the requirement and let you win the game if you reached 5% market share in any city, but then introduced a bug. The result isn't any city, it's Paris specifically, simply because Paris is city 0, the first one the loop checks. That it landed on Paris looks accidental: MicroProse localized the game for the English-speaking market, even swapping in three US cities (Baltimore, New York and Chicago, replacing Moscow, Zürich and Athens). If they wanted to hand-pick a winning city, I would expect one of those instead of the capital of France.

Why make winning easier in the first place? One possibility is that it was commercial. Pizza Tycoon wasn't a standalone release for MicroProse; it was part of a Tycoon Series, alongside Chris Sawyer's Transport Tycoon and the earlier Sid Meier's Railroad Tycoon, all sold under the MicroProse banner at roughly the same time. A full-page ad in Computer Gaming World (issue 126, January 1995) stacks all three boxes under a red Now it's your turn. banner and pitches them as PC games that let you buy, build and control your own empire, under the MicroProse tagline Anything is possible.

A MicroProse magazine advertisement: a red banner
                     reading 'Now it's your turn.' above the box art for
                     Transport Tycoon, Pizza Tycoon and Sid Meier's Railroad
                     Tycoon (Classic), with the text 'Introducing the Tycoon
                     Series from MicroProse. PC games that let you buy, build
                     and control your own empire.'

MicroProse's Tycoon Series ad, Computer Gaming World #126 (January 1995). Scan by Patrick Bregger via MobyGames.

As part of their localization, MicroProse also translated the ending text; in German it ends with a sarcastic credits roll, where the team thanks some people, including their compiler vendor for building the right bugs into new compiler versions at exactly the right moment to drive the devs to despair and then tells you to just start over because it was so nice.

The English version gets rid of all that, and after crowning you the one and only PIZZA TYCOON, it asks what you will do now and answers its own question for you:

ENDE.VGA; The same screenshot as before but with the
                     following text on it: "But you, you are already
                     having visions of branching out into... TRANSPORT!!!"
But you, you are already having visions of branching out into... TRANSPORT!!!
Just think of it - trains, planes and automobiles, all owned and controlled by you.

Probably the explanation is a combination: a game pitched at a different, more casual audience that someone simply wanted to be easier to finish, and localizing the ending text presented an opportunity to add some cross-sell. Either way the attempt backfired, leaving Paris as the accidental gatekeeper to the whole game.

How does Pizza Legacy deal with this

After 30 years, Pizza Tycoon players can finally win the game the way I think the developers intended: become an actual Pizza Tycoon instead of just dominating the French.

If the Pizza Legacy option fix legacy bugs is on you will have to reach 5% market share in all cities, and with the option off the original behavior is preserved: reaching 5% market share in just Paris wins the game. For the next release I think I'll allow a toggle between just all cities and any city.

There are already quite a few bug fixes and modernizations in Pizza Legacy; you can find the full list in MODERNIZATIONS.md in the repo.

The newly released Pizza Legacy v0.1.0 is the first release that actually allows you to win, see the release announcement blog post for details on what's new in this release.