Mar 4, 2023

Super Sprint (NES) Analysis

 


I've spent the last few days mapping out a lot of the RAM and a handful of code for Tengen's port of the arcade classic Super Sprint.

Let's talk about the car mechanics. What is it about upgrades in games being practically worthless? Nearly every upgrade in this game is not worth the effort to get it. Tire traction simply affects how quickly you can turn while moving. Each level of traction control improves turning by 1/32 of a step. In other words, after 32 steps, you will have turned 5 degrees more than with the next level of traction down. You actually turn slower the higher your engine's RPM, typically 1/16 of a step slower. You turn the fastest when at a complete stop, regardless of tire traction. 

Tire traction is a worthless upgrade on any tracks with embankments. On the arcade, these were sections of slabbed track typically. In the NES port, they are typically sections of track with large dirt patches along the outside edge, probably to resemble mounds of dirt under the track. When a car reaches one of these embankments, the tire traction is immediately maximized. It never resets - they forgot to put in code to reset it once you leave the embankment! 

Acceleration level is a bit difficult to rate. Each level of acceleration improves the rate at which your car's RPM will increase. RPM increases pretty quickly as it is, so this is arguably a trivial stat. However, RPM determine speed, so the faster you can get those RPM up, the faster you will go.

Top speed has a bit of a misnomer. It's just your speed. There is no gradual gain. Each level affects how fast your car is going relative to the RPM. At level 1, a car will top out at 3.875pps. This goes up to roughly 4.36pps at level 2. At max level, a car's top speed is only 4.844pps. The diminishing returns in this upgrade are laughable, however the mid-range speeds do improve somewhat more steadily. As I said when discussing acceleration, you may be spending considerable time in the mid range at later stages.

Some guides for Super Sprint will warn against upgrading Top Speed, stating the higher it is, the more likely you are to crash and explode. The simple fact of the matter is, the higher your speed, the less time you have to react to oncoming collisions. A car's speed has no bearing on whether it explodes or not. Explosive crashes are determined by how many RPM the car is running and if the car hit the wall at the exact angle opposite the normal (i.e., 180 degrees opposite the normal). One might therefore conclude it would be best to max out Top Speed first rather than acceleration, but the terminal point is #0F00 RPM. Considering max RPM is #1F00, even with level 1 Acceleration it doesn't take much to crash into a wall. 

Tile layouts for each screen are pretty straightforward. Each sequence of titles starts with a "length" byte. If the value is less than #80, the program will place the tile denoted by the next byte that many times. For example, #2000 would place tile #00 32 times (1 row). If the value is greater than #80, the value is ANDed by #7f and the program will place that many tiles based on the next series of bytes. For example, #83001415 would place tile #00, then tile #14, then tile #15. The data tends to limit repeating tiles to 32 times, but that shouldn't matter. For example, many tracks start with #20002000, but I don't see any reason not to use #4000 to save two bytes -- probably personal taste. Note that the winner screen tiles are referenced four times, since the pointer is also used to keep track of which racer won. The same goes for the upgrades screen (but for only player 1 and player 2).

Each track is actually mapped out for the AI. You may have noticed the AI typically stays within the middle of the track, unless it gets knocked out of position. This is because each track is mapped out for the AI, like so:

Track 6 AI map
Rendering of AI map for track 6

Each arrow corresponds to the direction the AI car should turn toward when on that particular tile. The numbers above each arrow correspond to one of eight degrees of speed the AI should try to reach for that position on the track. There are 15 target speeds, but only targets 4, 6, 8, 10, 12, 13, 14, and 15 are used. It's all very roundabout, but this way they could fit a majority of speeds in three bits.

The AI has a racing level and a speed level. The racing level affects what the target RPM is for the AI. This increases by up to three levels between races. When it reaches level 16, it resets and increases the speed level. This affects how fast the AI cars move as their RPM increase. There are only 4 speed levels, but the speed increases are significant.

The white car is your rival. The red car is expected to lose. The blue car may occasionally win when controlled by the CPU. The white car is the only CPU racer "following" the race. It considers the red car the weakest racer and will increase its own racing level if it falls more at least 4 checkpoints behind. If it is ahead of the red car, it will then do the same with the blue car. Once the white car has pulled ahead of the red and blue car, it will then increase its racing level if the yellow car - player 1 - is at least four positions ahead. These are just temporary increases, so it will maintain the standard racing level as long as it is three or fewer positions behind any car.

In addition to the AI map, there is also a terrain map, which maps the behavior of each tile to a specific function in the program, as well as the direction of the normal vector of the wall. For example, a tile mapped to terrain #20 corresponds to a vertical wall on the left, which has a normal vector angle of 0 degrees, whereas a tile mapped to terrain #14 corresponds to the interior of the outside wall of a left turn and will has a normal vector of 315 degrees. Unrestricted terrains, such as #00, have a normal of 355 degrees, but they wouldn't map to a function that relies on the normal. Many tracks just boil down to vertical wall, horizontal wall, or diagonal wall. Diagonal wall tracks typically just compare (x mod 8) to (y mod 8) with some slight adjustments to either x or y in order to determine if the car is on the track. As an example, here is the terrain mapping for the first track:

Rendering of tile terrain behavior map for track 1

As you can see on the terrain mapping, some tiles map to the range of #40 to #4E, each of which is conspicuously arranged in a line (more or less). These are the checkpoints which determine if the player is actually following the track. Each checkpoint has a predetermined direction (±90 degrees) a car must be moving when crossing it in order for the racer's position to advance. This is a very simple method of updating track position on winding tracks which makes it difficult to more accurately track who is in first, who's in second, and so on. The developers would have to alter the code slightly to allow for more than 15 checkpoints. In some other racing games, the program has an array of checkpoints and calculates the car's distance from the next checkpoint to determine where it is, which would be easier to modify than the method seen here. However, Super Sprint's method does have its perks, as seen here:

Example of checkpoint on shortcut

As you can see in one of the tracks with a shortcut, checkpoint 5 not only cuts across the actual track, but it also cuts across the dirt shortcut. This would be much more difficult using a checkpoint array.

Super Sprint does keep track of which car is in the lead, flashing "P1" if the yellow car is leading, or "P2" if the blue car is leading and controlled by a player (or in attract mode). The info is only updated when a car's position advances. Due to how spread out checkpoints can be, this means there can be a significant delay between when a car pulls ahead and when the leader is highlighted.

Notice how the track typically has a solid shadows on the left and bottom, but overpasses cast dithered shadows. This was not due to the NES's limited palette, but was actually done to create the illusion of driving under the overpass.

Palette attributes of overpass tiles

Most track tiles use either palette 0 or palette 1, but overpasses use palette 2, as seen in the image above. The difference between palette 0 and palette 2 is that black is swapped out with grey. In the other three palettes, there is no grey at all - that's the transparency. When a car is just about to pass through an overpass, it is switched to low priority. This means the black and yellow pixels (as well as green) will be rendered over it. Since the grey bits are typically the transparency, they won't cover the car. However, since the overpass explicitly uses solid grey instead of transparency, it will cover up the car entirely. The program uses specific terrain mappings for toggling sprite priority. There are also specific terrain mappings to tell the program where the hidden rails under the overpass are. 

Terrain Mappings
They were pretty lax on their terrain handling. For example, you may find #16 and #17 together, but you won't necessarily find #14 and #15 together. There's also the weird issues with some tiles clearly being 95% obstruction, but the code treating it as only 75%.

00    =    $E86D    Open road (RTS)
01    =    $F287    Protrusion in top-right └
02    =    $F25F    Protrusion in top-left  ┘
03    =    $F273    Protrusion in bottom-left  ┐
04    =    $F29B    Protrusion in bottom-right ┌
05    =    $F2B2    Wall in top half
06    =    $F2C4    Wall in left half
07    =    $F2D3    Wall in bottom half
08    =    $F2E5    Wall in right half
09    =    $F305    Wall in bottom-left
0A    =    $F2F1    Wall in bottom-right
0B    =    $F319    Wall in top-right
0C    =    $F32D    Wall in top-left
0D    =    $F350    Divider left
0E    =    $F341    Divider bottom
0F-13            unused
14    =    $F365    Wall diagonal top-left upper
15    =    $F35F    Wall diagonal top-left lower
16    =    $F383    Wall diagonal bottom-right upper
17    =    $F37D    Wall diagonal bottom-right lower
18    =    $F3A0    Wall diagonal bottom-left upper
19    =    $F39A    Wall diagonal bottom-left lower
1A    =    $F3BB    Wall diagonal top-right upper
1B    =    $F3B5    Wall diagonal top-right lower
1C    =    $F2AF    Wall in top half (check tile below)
1D    =    $F2BE    Wall in left half (check tile right)
1E    =    $F2D0    Wall in bottom half (check tile above)
1F    =    $F2DF    Wall in right half (check tile left)
20-27 =    $E86E    Grass (lower bits set normal vector)
28    =    $F413    Embankment
29    =    $F41A    Overpass corner (bounces player)
2A    =    $F434    Overpass left edge
2B    =             unused (Underpass left edge?)
2C    =    $F443    Overpass bottom Edge
2D    =             unused (Underpass bottom edge?)
2E    =    $F452    Overpass right edge
2F    =             unused (Underpass right edge?)
30    =    $F461    Overpass top Edge
31-3F =             unused (Underpass top edge?)
40-4D =    $E9D4    Checkpoint (lower bits set order) 
4E    =    $EA06    Finish line

Each overpass must have a checkpoint on each side. The checkpoint has additional information that tells the car which tiles bordering the overpass it is allowed to pass. For example, a checkpoint might specify tiles #2C and #30, meaning the car is allowed to go vertically across the overpass. Judging from the tile map above, it is likely the programmers intended separate tiles for overpasses and underpasses, but then decided to handle sprite priority separate from those specific tiles, likely to make track designing a tad simpler.

There is a bug in the finish line terrain (#4E) code which allows the player to cheat the lap counter by rapidly tapping the Start button while over a finish line. If done right, the player can increment the lap counter for any car by up to 4 laps. The cause of this seems to be because Super Sprint runs parallel code. It's not uncommon for interrupt requests to be used in order to pause one function, run some other code, then go back to the interrupted function. The issue with this is the programmer needs to be careful not to alter any RAM currently in use at the time of the interrupt. This is what happened in Super Sprint. Byte $001B temporarily holds the terrain value, but it is also used as a pass-through variable when polling the gamepads. The gamepads are polled during the interrupt, which is expedited when the Start button is pressed. If no buttons are pressed on gamepad 2 when the game is paused at just the right moment, the program will still be running the code for terrain #4E, but with a value of #00 inside $001B instead of #4E. Checkpoints verify the current checkpoint is the same as the target checkpoint ($00B2). In the case of the finish line, when both values match, it sets the target checkpoint to #00 and increases the lap counter. When the glitch occurs, since the target checkpoint is #00 and the temporary variable $001B is also #00, the lap count is increased again. I "fixed" this by setting 0x1DBA9, 0x1DBB5, 0x1DBBF, and 0x1DBCB each to #13.

Wall physics themselves are dubious. Many walls will ignore the normal vector if the car hitting it is controlled by the CPU. While this can occasionally be detrimental for the CPU, it effectively means the CPU will rarely carom wildly off walls. If a car caroms into a wall when its direction is within 90 degrees of the normal, it will effectively come to a stop unless it is practically finished caroming. Otherwise, the program  reflects the angle of the car across the normal. If it is in line with the normal, the car explodes. If it is within 35 degrees of the normal, it is rotated to the normal, else if it is within 57 degrees of the normal, it is rotated to 56 degrees of from the normal. I don't know why it's so complicated, but I guess it had something to do with trying to make crashes slightly more realistic.

Bytes $009D-$00A0 are a weird bunch, used at crossroads and overpasses. Only two bits are used - bit 0 and 6. Bit 0 is set when disabling racecar and cyclone collision detection. Bit 6 is set when it wants to tell the program to render the car on the foreground, and also to give it greater height (earlier draw priority) than the other cars; the car is pushed to the background if bit 6 is not set at an intersection tile. The program tends to either set bit 0 alone, or both together. I don't think I have seen it set bit 6 alone. Cars are also prohibited from picking up the wrench or bonus points when bit 0 alone is set, in case they spawned on an overpass.

Another feature of setting bit 0 is it forces the CPU to maintain the same direction while it is set. Even if it changes direction while caroming off walls, it will steer itself back into whatever direction it had when bit 0 was set. I experienced this "bug" one time during regular gameplay, where a car that just approached an intersection got hit by another car and sent flying into a wall, preventing the game from clearing bit 0, which caused the car to ceaselessly drive head-on into a wall until the race ended. 

One other bug can happen with the inverse. Byte $035D is set to #00 when crossing an intersection checkpoint. Normally byte $035D is set to #80 when a car is driving the right direction through a checkpoint, but it gets set to #00 if the car is not going the right way. If a CPU-controlled car is at an intersection checkpoint and byte $35D is cleared, bit 0 will not get set and the CPU will steer through the intersection. The way intersections are designed in Super Sprint is the second pass is the main direction of the intersection. In other words, if a CPU exits the checkpoint before bit 0 is set, it will cut the intersection. Since this results in the CPU taking the checkpoints out of order, the next time it crosses the finish line will not count.

The rate the car actually turns (its sprite) looks bugged due to typos or logic errors to me. I've been trying to make sense of it, but I can't. It has to be poor logic (because it happens twice). The rate at which a player can turn is based on the direction the sprite is facing minus the direction the track is going (from the AI's map). When trying to turn left, it turns at full speed if the difference is greater than 180 degrees, or if the difference is greater than 1260 degrees. If it's greater than 1260 degrees, it's naturally greater than 180 degrees as well. Inversely, when trying to turn right, it turns at full speed if the difference is less than 180 degrees or if the difference is less than 1260 degrees. If it's greater than 180 but less than 1260, it's still less than 1260, so why bother with the less-than-180 check, unless it's a logic error. I thought they got the BCC and the BCS switched on the 1260 (-180 degrees) check, which would be a quick fix setting 0x1DC53 to #B0 and 0x1DC6C to #90 in the ROM, but after making those changes the game was practically unplayable. It took drifting to a whole new level, but not in a good way. It probably needs an abs() subroutine, but I am leaving it as it is for now.

The player scores are literally just the text. For example, if the HUD shows a score of 13750, the actual score is #00F6F8FCFAF5. When you cross a checkpoint or pick up a point bonus, it adds 1 to the next-to-last byte. If the value equals #FF, it subtracts 10 and adds 1 to the next byte, and so forth. If you somehow manage to exceed 999990 points, the game wraps back around to 000000 (with all zeroes visible). There are no rewards for collecting points. 

Wrenches spawn when P1 crosses the finish line on laps 1 and 4, if one is not already active. For each wrench, a flag is set so the program doesn't spawn more than one (the routine fires every frame the car is over the finish line). If the first wrench hasn't been picked up by the time the second wrench spawns, it disappears - because both wrenches share the same variables.

I may update this post later as I delve deeper into the program. For now, I hope someone finds this sort of rubberducking of mine helpful.


No comments:

Post a Comment

©TheouAegis Productions™. Powered by Blogger.