Introduction

This page is dedicated to document aspects of the DOS port of Prince of Persia (PoP) that pertain to TAS and speedrunning.

External Resources

Abundant information exists regarding all other aspects of this game, its history, and trivia. Here I document a the most important:

Table of Contents

Emulation

Tools

Casual gamers are advised to use SDLPoP to conveniently and quickly run the game over Windows or Linux. Speedrunners can use DosBox to accurately emulate and time their runs competitively.

TAS Emulation

For TASing, however, the game requires a precise, reproducible environment. Here I provide the currently better tested setup, based on the instructions given in this article.

Additional tools

Running the Game

PCem Configuration

The late '90s package can be used, except in late90s.cfg you need to change mem_size from 32768 to 8192, as the game does not need that much, and it makes the savestates and boot (memcheck) time smaller.
Also don't forget to set the cdrom_path to point to the game image and hdc_fn to point to late90s.img (full path without quotes, spaces are fine).

LibTAS Configuration

Don't be like me and make sure the following configurations are activated in libTAS

Launching libTAS

To run libTAS, you can use the following command:
libTAS /usr/bin/pcem --config ${PWD}/late90s.cfg

Launching the game

Once you go through booting the PCem machine, you will be greeted by the DOS command line. The quickest way to launch the game is by running it straight from the CD-ROM, by running the following commands:
D:
cd prince
cd prince1
prince

Memory Analysis

If you are going to TAS this game, you absolutely need to keep track of important variables. To achieve this, you need to use memory analysis tools, like Cheat Engine. Unfortunately, CE does not work natively on Linux, so you need any of the following alternatives:

Attaching PINCE to PCem

To attach PINCE to the game, first install PINCE and then run it using the following command:
bash PINCE.sh
Then, attach it to PCem by following these steps:

Looking for the Hook

After launching the game, press the "Pause" button during the game's loading phase. At this point, use PINCE to look for the following array of bytes:
00 00 00 b0 1d 2b 0f 00 00 00
A single memory address should be found.
This address will be the hook address. This value has no intrinsic meaning, other than being the address from which we will obtain other relevant addresses.

Relevant Addresses

Use a programmer calculator to calculate the addresses of the following relevant values, based on their offset with regards to the hook address.

Game Mechanics

This section discusses game mechanics that are relevant to effective TASing.

Game Phases

Game Loading

As soon as the game is launched, it starts loading all the relevant files from the CDROM (or floppy disk or hard disk drive) into memory. At this stage the game displays the following text:
As soon as the game finishes loading, it transitions towards the title screen.

Title Screen & Demo

The title screen is the first image that appears after the loading phase. As long as no keys are pressed, the credits and backstory will show. Later, a randomized demo sequence will be shown. After the demo sequence finishes, the title screen is displayed again.
You come back to the title screen after the game restarts. This can happen in three situations:

Copy Protection Level

If you play an original copy of the game, you will need to pass a playable copy protection level between levels 1 and 2. This level appears only once in the session.) after you pass the level, you won't be prompted to pass it again later if you restart the game.
In this level, a total of 14 potions are presented to you. Your task is to drink the correct potion, i.e., the one whose letter corresponds to the first letter of the word within the original user manual indicated in the prompt at the bottom of the screen. After you drink the correct potion, the exit door will open and you can progress to level 2. If you drink the wrong potion, you'll lose half your life. If you drink two wrong potions you'll die and the game will restart.
Funnily enough, you can beat the protection scheme very quickly by restarting and advancing (shift+L) repeatedly until the closest potion is the correct one, as shown in this video.

Cutscenes

As soon as the current level finishes (screen goes to black) and the next level starts, the clock will remain stopped. This includes the cutscenes that play during some of the levels.
The following table shows what are the game cutscenes and their duration
TransitionDurationDescription
Level 1->244 FramesPrincess stands anxiously while the sand clock runs
Level 3->444 FramesPrincess lays anxiously while the sand clock runs
Level 5->644 FramesPrincess stands anxiously while the sand clock runs
Level 7->879 FramesPrincess releases mouse to aid our hero
Level 8->990 FramesPrincess receives a successful mouse
Level 11->1244 FramesPrincess stands anxiously while the sand clock runs

Levels

The game consists of 15 playable levels.

Hall of Fame

The hall of fame (HoF) appears once you beat the game, after the last 'hug cutscene' and the closing credits.
The HoF, like many arcade machines of its era, is dedicated as a persistent record of the best players. After beating the game, the player will be given the opportunity of inputting their names. The entry will be stored in the order of score (the best on top). The store reflects how much time remained at the moment of defeating Jaffar, as explained in later.

Game Timer

Both the human and tool-assisted speedruns of the game use the In-Game Timer (IGT), as opposed to wall-clock time, for measuring which run is faster. There are multiple reasons for the game's speedrunning community to have reached this decision:

Timer Logic

At the very beginning of the game, the timer is initialized with the following values:
Each game frame, the number of ticks is reduced by one. When the number of ticks left reaches 001, the number of minutes left is reduced by one, and the tick count comes back to 711. The following progression illustrates the Minute/Tick progression from start to game over:
59:711
59:710
...
59:002
59:001
58:711
...
01:002
01:001
00:711 <- GAME OVER
When the minutes left counter reaches zero, the game is lost and the bad ending scene is displayed.

Timer Bug

Those keen eyed individuals might have noted that the timer logic is flawed. In total, 711 ticks pass for each reduction in the minute timer. This bug has the following interpretations:
Anybody looking to TAS this game and squeeze every single frame from IGT needs to keep this bug in mind, as it might affect their final movie length calculations.

Exceptions

The game timer will run as described, except for the following situations:

HoF Score Calculation

The score is calculated as the remaining minutes/seconds remaining at the time of beating Jaffar in level 13. If the timer was 49:01 at the time of Jaffar's death, the HoF will display 48:00 as the player's score. If the timer was 48:711, time HoF will display 47:59. The time displayed by the HoF is crucial as proof of score for a TAS, where the improvement can be registered at a second level. Should a TAS improve on another at the tick level (where the tick does not pass the second barrier), the TASer / judge would have to provide proof of improvement by accessing the internal timer as shown before.

Random Number Generator (RNG)

The game contains a single RNG from which all pseudo-randomized events are determined. Although most of the game elements that use the RNG are not relevant to the outcome of a play (i.e., they are mere decoration), one crucial gameplay element is affected by it: the guard's fighting actions. Therefore, any changes in RNG will make a TAS movie desync whereas a different one might work, exactly and only at the points where guard fights are involved.

Internal State

The game's pseudo random events are determined by a single RNG whose internal state is a 64-bit integer. Just like any other RNG, this state is updated every time the RNG is used to generate a new number, such that it produces a (mostly) different number every time it is queried.

Initialization Logic

Upon launching the game, the 4 bytes of memory space corresponding to the internal state start uninitialized. Before displaying the Title screen for the first time, the game calls the RNG update function. Inside this function, there is a check whereas, if the current value of the internal state:
At this point in the game initialization phase, the game banks on the fact that a zero value for the timer is the most likely scenario (especially on a fresh system boot) or the operating system clears previous memory pages before assigning them to a new process, we can assume that querying the system clock will be the most likely course of action.
The DOS function used to query the clock will return the number of microseconds passed since the start of the day. The system date does not count towards setting the initial RNG state.

Update Logic

The way in which the internal state is updated is given by the formula:
state = state * 0x000343FD + 0x000269EC3
Nevertheless, given that the state is a 64-bit integer, both the multiplication and addition are subject to overflow. Effectively they are passed through a modulo filter after each calculation, such that, purely mathematically, the calculation corresponds to:
state = ( ( (state * 0x000343FD) % 0x100000000) + 0x000269EC3 ) % 0x100000000
The combination of these operations make it so that each state number maps to another number within the 64-bit range that appears to be entirely random. The relation is bijective, such that it is possible to calculate an undoing of the random state by the following operation:
state = (state + 0xFFd9613D) * 0xB9B33155
Similarly, to account for overflow:
state = ( ( (state + 0xFFd9613D) % 0x100000000) * 0xB9B33155 ) % 0x100000000
All events in the game update the RNG state using the above formula. However, some events trigger it more than once in between frames.

Active RNG Elements

Guard RNG Behavior

Guards are the only element in the game that can change the outcome of a TAS movie if the RNG diverges. That is because the guards' fighting actions are determined by a set of action probabilities. These actions (attack, wait, respond, advance) contain a trigger probability that goes from 0 to 100, where 0 means that it won't ever be triggered, or; 100, meaning it will always be triggered. Every guard in the game contains a different set of probabilities based on their skill, where the skills are determined by the following table.
Any intermediate probability will trigger an action (or not) depending on whether the outcome of random(0,255) is equal or smaller than the probability value. This query updates the internal RNG state once per frame, when the guard is in the middle of a fight (not when he is simply en garde).

Falling Tiles (Level 13)

At the very beginning of level 13, the tiles on the ceiling will start breaking and falling over the running hero. This is assumed to be Jaffar stomping on them to hurt and bothering you (indeed, many runs have been lost to these tiles breaking the player's run).
To determine the order in which these tiles are activated, the RNG is queried once per each.

Passive RNG Elements

Loose Tiles

If the game runs on Sound Blaster or another synthetized sound device (not PC Speaker), the game's loose tiles will affect the RNG. Every time once such tile is activated, either by disturbing from afar it or pressing on it until it falls, the RNG will be queried to determine which of the 3 possible sounds will play. This randomness contributes to the game's innovative life-full feel. However, the logic affects the RNG state non-deterministically.
The game stores the value of the last loose sound produced, to prevent reproducing it twice consecutively. This value is stored as a single byte that starts uninitialized at the beginning of the game. Once the first loose tile is activated (either in level one or the demonstration level), its value will oscillate between the values: 19, 20, and 21. These are indexes to the synthetized sample corresponding to three slightly different tile sounds.
To determine the new loose tile sound, the game uses the following logic, simplified as:
do 
 looseTileSound = random(0:2) + 19
while looseTileSound == lastLooseTileSound
lastLooseTileSound = looseTileSound 
Since it can be expected that the initial value of this variable is zero at the beginning of the game, this aspect of RNG cannot be manipulated a priori. However, it plays a crucial role in reproducibility as, even if you enter the level with the same RNG state, it might diverge upon hitting one of these tiles.

Torches

Every torch visible on screen triggers, each, an RNG update to determine the shape of its flame in the next frame. Every update is deterministic as this selection allows the flame shape to repeat between two consecutive frames.
On the frame where the hero enters a new room, the number of torches in the new room is the number of RNG updates that will be calculated.

Potions

Potions produce a smoke effect that repeats deterministically and does not affect RNG at every frame. Instead, the only update on the RNG is produced upon entering a room where a potion exists, to determine the initial phase of the pattern. This is done to prevent multiple potions to all produce the same pattern at the same time. This is the case for all types of potions (+1HP, -1HP, +1MaxHP, Slowfall).
The RNG will not be affected upon drinking the potion, but it also won't be updated upon reentering the room.

Sword

Twice in the game you can observe a pickable sword laying on the ground: once in level 1 --the sword you use to fight throughout the game--, and; once in level 12 -- the shadow's sword. To denote its sharpness, the game will add a twinkling effect on the sword every so often.
To determine whether the sword should twinkle, the game checks:
The fact that updating the RNG or not depends on a previous result, makes this a non-deterministic effect on its internal state. This is not a problem, however, as current TAS skips the first sword, and picks up the second one without any RNG-based effects on the actions required to do so.

Stars

What? Stars?. Yes, these ones:
They twinkle every 3 or so frames. Which one of them twinkles is determined by the game's RNG. This check updates its internal state once.

Other RNG Elements

Copy Protection Letters

The position and letters hovering over the potions in level 15 are determined at the start of the game. The index of each of the potions is shown below.
The letter positioning uses (and affects) the RNG internal state a non-deterministic amount of times, given the following pseudo-code:
// N is the size of the vector containing letters from A to Z
// Letters might be repeated in this vector to increase their selection likelihood
N = 39 

// Advances the RNG state by one
correctPotion = random(0,13) 

for idx in 0:13
  do letter = random(0,N) while letter has not been picked up before
  PotionLetter[idx] = letter
end for
The important aspect of the code above is that the letter selection code above will access the RNG a non-deterministic number of times (i.e., until the letter picked has not been picked before). This is of crucial aspect of manipulating RNG in a TAS, as the process needs to be replicated exactly as in the game code.

Cutscenes

The RNG state is modified even during cutscenes, since they contain two torches and a set of stars in the window. For some reason, the number of RNG state updates per frame is inconsistent on different cutscenes. Here is an exact progression of the number of RNG updates:
/* 01->02 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 03->04 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 05->06 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 07->08 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 08->09 */  {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
/* 11->12 */  {1, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}

Invariant Rooms

There are rooms where no RNG elements exist and therefore will keep the RNG state fixed until you exit.

GameResources/DOS/PrinceOfPersia last edited by feos on 2/4/2024 9:47 AM
Page History Latest diff List referrers View Source