The insides of retro games: Punch-Out for NES
Part 1. Passwords
The Mike Tyson's Punch-Out game for NES uses a password system that allows players to continue the game from a certain point. Each password consists of 10 digits, which can range from 0 to 9. A game can accept two types of passwords, which I call “normal” and “special” passwords. Special passwords are certain combinations of 10 numbers to which the game reacts in a unique way. A complete list of special passwords looks like this:
- 075 541 6113 - telephone signal "busy" 1
- 800 422 2602 - telephone signal "busy" 2
- 206 882 2040 - telephone signal "busy" 3
- 135 792 4680 - a game in a hidden tournament: "Another World Circuit" (for the password to be adopted, you must hold down the Select button and press A + B)
- 106 113 0120 - display of titles (for the password to be accepted, you must hold down the Select button and press A + B)
- 007 373 5963 - takes the player to battle with Mike Tyson
The second type of passwords accepted by the game is regular passwords. In ordinary passwords encoded progress, which the player has achieved in the game. In a normal password, the following game data is encoded:
- Number of career victories
- The number of career losses
- Number of wins by knockout
- Next opponent
As an example for studying the generation of passwords, we use a game with 24 wins, 1 loss, 19 knockouts and starting in a world tournament with a fight against Super Macho Man.
The process of coding the state of a game into a password begins with collecting the number of victories, losses and knockouts into the buffer. The game represents each number in the form of a binary-decimal code , formed from 8 bits per digit and 2 digits for each value. That is, for 24 wins, one byte is needed with a value of 2 and a second byte with a value of 4. The same happens with pairs of bytes for losses and knockouts, that is, a total of 6 data bytes are obtained. In the diagram below, these 6 bytes are indicated with values in both decimal and binary.
The next step is to generate a checksum for these 6 bytes. The checksum byte is calculated by adding 6 individual bytes and subtracting the result from 255. In our case, 2 + 4 + 0 + 1 + 1 + 9 = 17, that is, 255 - 17 = 238.
Then we write several bits of 6 bytes to the new buffer . This buffer can be perceived as one 28-bit intermediate value, which we will step by step fill. Bits from the first buffer are divided into groups of two and moved to different hard-coded positions of the second buffer. This is the first of several steps, the only task of which is simple obfuscation of data in order to make it harder for players to generate passwords.
Note that not all bits from the original buffer are transferred to the new intermediate buffer. These bits are ignored, because it is known that they are always equal to 0. Due to the rules of the game, the number of losses is enough to transmit only 2 bits of information in the password. If the total amount of losses reaches 3, then the game over comes and the player does not receive the password. Therefore, the number of losses is sufficient to describe the numbers 0, 1 and 2, and for this only 2 bits are enough.
Then we write other pairs of bits to the intermediate buffer. The first four pairs are taken from the previously calculated checksum value. Another pair is taken from the value of the enemy. The opponent's value is a number indicating which opponent the player will fight after entering the password. You can use three possible enemy values:
0 - DON FLAMENCO (first fight of the tournament major)
1 - PISTON HONDA (first fight of the world tournament)
2 - SUPER MACHO MAN (last fight of the world tournament)
Since we wanted to generate a password that pushes us against Super Macho Man, then as the value of the enemy we use 2. Then the checksum bits and enemy values are written into the intermediate bits as follows:
The next step is to perform several cyclic permutations of the intermediate bits to the left. One cyclic permutation to the left means that all bits are shifted one position to the left, and the bit, being the leftmost, moves and becomes the rightmost one. To calculate the number of permutations to the left, we take the sum of the value of the enemy and the number of losses, add 1 and take the remainder of the division by 3 of this result. In our case, we get 2 + 1 + 1 = 4. Then the remainder of 4/3 is 1, so we cycle the intermediate bits to the left one time.
At this stage, the intermediate bits are thoroughly mixed and it's time to start breaking them in order to get the numbers that make up the password. Passwords should consist of 10 digits, so we break the 28 intermediate bits into 10 separate numbers, which we will call the password values P0, P1, P2, etc. Each of the first nine values of the password receive 3 bits of data, and the last gets only one of the intermediate bits. To complete the finished password value, we will also include bits indicating the number of permutations performed at the previous stage.
Finally, we add to each value of the password a unique hard offset in the code. The finished password digit will be the remainder of this amount from division by 10. For example, in the seventh position we use offset 1, that is, we get 5 + 1 = 6, and the final digit will be the remainder from 6/10, that is 6. In the fourth position we use offset 7, that is, we get 5 + 7 = 12, and the final figure is equal to the remainder of 12/10, that is, 2.
So, we got ready-made password digits, which can be checked in the game.
The process of decoding passwords back to the number of wins / losses / knockouts and the value of the opponent is simply performing the steps described above in reverse order. I will leave it as a task for readers. However, the game has two noticeable errors that it makes when decoding and checking the passwords entered by the player.
The first error occurs in the very first step of decoding a password, that is, when subtracting offsets to return to the password values. The initial password values contained 3 data bits each, that is, their values must have been in the range of 0-7 before applying the offsets. However, the player can enter a password, which, after subtracting the offset, results in a password value of 8 or 9 (dividing by 10 with the rest). Instead of immediately rejecting such a password, the game mistakenly does not check this case and allows you to add an extra data bit to the password value that can pollute the set of intermediate bits so that the passwords are no longer unique. Since certain intermediate bits can be set either by the corresponding digit of the password, OR by the additional bit of the adjacent password value, there are many passwords, which is converted to the same set of intermediate bits. That is why you can find different passwords that give the same in-game result, although they should have been unique.
The second error is a bug in logic that the game uses to verify data after decoding a password. The game tries to apply the following conditions:
- the checksum stored in the password corresponds to the checksum, which should be obtained by taking into account the number of wins / losses / knockouts stored in the password
- Loss value is 0, 1 or 2
- enemy value is 0, 1 or 2
- the number of cyclic permutations stored in the password is the correct number, taking into account the value of losses and the value of the opponent, stored in the password
- all numbers of wins / losses / knockouts stored in the password are in the interval 0-9
- wins> = 3
- wins> = knockouts
If any of these conditions are not met, then the game must reject the password. However, in the implementation of the final check there is a bug (namely, when checking the numbers encoded by BCD.) Instead of checking for victory> = knockouts, the game allows for cases when the upper number of wins is 0, the lower number of wins> = 3, and the upper number of knockouts is less than the lower number victories. For example, a record with 3 wins, 0 losses and 23 knockouts will be accepted by the game (which proves the password 099 837 5823), although it must be rejected (because it is impossible to win 23 fights by knockout if you won only 3 fights.)
The private details of this coding scheme are unique to Punch-Out, but the general idea of obtaining important bits of the game state, converting them with the ability to recover for obfuscating the original state and then using them to generate a certain number of characters to demonstrate to the player as a password is a fairly universal approach. You can use checksums so that random password changes (for example, if a player fails) would most often lead to its rejection, rather than creating another password with a random game state.
Part 2. Punch-Out overview
Every fighter in Mike Tyson's Punch-Out !!! controlled by one or more interpreted byte-code scripts. The player character, Little Mac, executes a simple script that contains the logic of each action available to the player (dodging, blocks, strikes, etc.) Opponents are controlled by 3 levels of independent scripts, which together create a character's behavior.
The highest-level enemy script is executed during all 3 rounds of the battle and controls the largest changes in the behavior of the enemy. I will call this script "match script". Its main task is the choice of behaviors that the enemy will perform in response to various events during the battle. For example, a certain behavior will instantly start after the enemy rises after a knockdown, or when a player has run out of hearts and he becomes tired. These behaviors are recorded in a table and are invoked by the game engine in response to relevant events. Also, the match script sets the initial values for configuration options related to the complexity of the battle (for example, the amount of time that the enemy remains vulnerable after a missed strike.) Finally,
A lower level enemy script is a “script of behavior”. This level is responsible for the sequence of certain hits and attacks that the enemy must perform within the framework of the current behavior (set by the match script.) The behavior scripts execute commands like “put the right jab, pause for 28 frames, randomly put the left or right uppercut, repeat everything it's 5 times. " Also, the script has commands to read and write to any address in the memory from the game engine, so the behavior can be very dynamic.
The lowest-level script of opponents is the “script of animations”. Such scripts perform the details of each individual strike, block or special attack as part of the behavior (set by the behavior script.) At this level, there are commands such as “assign the sprite 23 to the current animation frame of the enemy, move it down and to the right by 1 pixel every second frame in over the next 10 frames, change the frame of the animation to the sprite 24, reproduce the sound effect 7 ". In addition to animation commands, animation scripts also perform sequences of various gameplay state changes, which are closely related to the movements of opponents. For example, in a long animation of a special attack, an animation script may insert commands that make an adversary vulnerable to knockdowns with a single blow over a very short period of time. Like behavior scripts,
Little Mac Script
Executed by the character of the player Little Mac script is most similar to the opponents animation scripts. It changes the current frame of the reflected animation and moves the player around the screen. Like animation scripts, the Little Mac script executes sequences of certain gameplay events, for example, at which particular point in time the Mac should hit the opponent, or when it should perform a block or dodge. The Little Mac script controls the input of the player, just as the behavior scripts control the opponent's animation scripts.
Each of these four scripts is processed by its own interpreter. Although many of them have the same functionality, for example, basic control and direct memory access, each system implements its own version and does not share the common code (or space of opcodes) with other systems. This allows each type of script to be very specific and effectively use a small set of targeted commands. Script data makes up approximately 22% of the non-graphic data of the game cartridge (the machine code for the game engine takes up only 17%), so it was very important that the scripts have a compact appearance.
Part 3. Match Punch-Out Script
Match script controls the behavior of the enemy at the highest level. The main operation that he performs over and over again is waiting for a certain time of the round and making changes to the opponent’s configuration data at this moment. The video shows the first round of the first battle against Bald Bull, along with the match script that controls his overall behavior.
There are three main operations that a match script can perform. The first is to wait until the timer of the round reaches a certain value. The second is to ask whether the current behavior of the enemy has changed. Behaviors are recorded in the memory configuration table in memory, and then called at different times by the match scripts and the game engine itself. There are two segments of behavior used by match scripts in the table. I call them "basic" behavior and "special" behavior. Special behaviors are, for example, the Bull Charge of an opponent of Bald Bull or Honda Rush of an opponent of Piston Honda, and the basic behaviors are the usual strikes made by the enemy during the rest of the time. The specific behavior scripts used to implement these types of behavior can be changed by the match script right during the fight,
The peculiarity of the behavior change performed by the match scripts is that they can be replaced by the behavior changes requested by the game engine. The game engine uses four behavioral segments to query for new behaviors when a Mac loses all its hearts and gets tired, and also when an adversary rises after a knockdown. If the match script fulfilled a request to change behavior, but one of these four events of the game engine occurs before the request is processed (requests cannot be processed until the enemy enters the waiting state), the game engine sets the desired behavior and the match script request is discarded. Some fighters, such as Bald Bull, in a short period of time several times ask for special behavior. It seems that this is necessary only to reduce the likelihood
The third main operation of the match script is memory patching. Most memory patches affect the battle configuration table, where behavior scripts are logged. In addition to the sets of behaviors, the table contains data related to the complexity of the battle. For example, when the timer in the video reaches 0:30, Bald Bull changes its security settings. This leads to the fact that the player can no longer deceive him by pressing up and then performing a blow to the body. In addition, match scripts have the ability to patch arbitrary memory addresses, but this feature is used only once - at the beginning of the second round with Mike Tyson, so that the player gets a star the first time he hits him in standby mode.
Part 4. Punch-Out Behavior Script
Now we will look at scripts of behaviors that are directly involved in the implementation of behaviors.
The video shows the interpretation of how the script could look like the opponent's Piston Honda 1 behaviors in English teams.
Behavior scripts are responsible for the sequential launch of animations in the same way that match scripts were responsible for launching behaviors. The team
animplays a specific animation, and the team
anim_rndperforms an animation, randomly selected from a list of 8 options. In the video shown above, at the moment of random selection from the list of options, the selected option is momentarily highlighted in red. When Piston Honda deals its first two jabs, it is used for each of them
anim. After that, it uses
anim_rndto randomly select from a set containing 6 hook animations and 2 empty animations. As a result, he performs a hook in 75% of the time, and does nothing in 25% of the time.
From the point of view of the script, the behavior of the animations is reproduced synchronously, because when the animation system is not idle, the script interpreter is paused.
Execution Management Commands
There are several commands that modify the execution of the script itself. Commands
pausecan pause the execution of scripts for a certain number of frames, or for the number of frames randomly selected from a list of 2 options.
There are various branching commands that, when certain conditions are met, optionally transfer to different parts of the behavior script. A team
branch_rndhas a given probability that a branch will occur every time it is executed. A special case of probabilistic branching is a team
branch_alwaysthat has one hundred percent branching probability.
A simple looping mechanism is built into the behavior script interpreter. The command
set_loop_countsets the current value of the cycle counter. With each command
branch_while_loopit reduces the value of the loop counter by one and performs branching only if the value of the counter is greater than zero.
The last type of branch checks the contents of the memory to make a decision about branching. Piston Honda uses this command
branch_mem_testto check whether its last hit in particular behavior was successful. If the hit hit the target, then it performs branching for the next hit. If the hit was not successful, then he uses the command
branch_while_loopto continue to beat, only when 5 unsuccessful punches have accumulated.
There are two commands that behavior scripts can control the behavior system itself. The command is
begin_behavior_mainused to complete the current running behavior and start the main behavior. This is different from the branching behavior in the script, because the part of the script that is considered the current "main" behavior can be changed during the match by the match script (see the previous part of the article about match scripts).
Another behavior related command is
enable_behavior_change. When starting a new behavior, it starts from the blocking state, when all further requests for changing the behavior are blocked. Using the command, the
enable_behavior_changescript signals that it is ready to allow execution of other behaviors. For example, in the special behavior of the Piston Honda team
enable_behavior_changenever runs, so if the Mac gets tired at this time, the special behavior continues. However, knockdown events bypass this system, so if during the special behavior of Piston Honda the main character is knocked down, the behavior will be changed in any case.