Score Manipulation

From Super Mario World Speedrunning Wiki
Jump to: navigation, search

Overview

Score Manipulation is a very high-level technique that involves planning out exactly how many points you get in every level, so that your total score is a value that takes a minimal time for the game to calculate and draw to the screen. Managing the player's score is a small portion of all of the things the game needs to do every frame, but if the score is not optimal, it can cause the game to lag more often during laggy portions of the run. This mainly includes the spotlight and fadeout at the end of every goal tape march, as well as every keyhole animation. However, certain levels are particularly laggy on their own, such as Back Door, Forest of Illusion 2, and Sunken Ghost Ship. Having an optimal score while playing these levels will reduce lag.

A good score manipulation route will have the player collect or purposefully *not* collect certain items without losing any time (for example, whether to collect a Dragon Coin or to jump over it). However, due to how little of time score manipulation actually saves and how much extra effort it takes to pay attention to your score at all times, most runners forego extremely optimal score manipulation. A couple more general rules are easier to follow to get the most beneficial improvements with the least amount of effort:

Score Calculation Algorithm

The player's score is stored as a 24-bit hexadecimal number in memory. This number is converted into a 6-digit decimal number every frame and displayed on the status bar (the last zero in the score is basically just for looks). There is no hardware in the Super Nintendo to manage numbers this large, so this conversion is done in software. It's not a slow algorithm, but it isn't fast by any means. To make matters worse, it is run every single frame. This means, if you have a score that makes this algorithm take a long time, there is less time for the game to process other game things, like physics, collisions, etc. This makes the game more likely to lag.

Pseudocode

Here is a simplified version of the code that runs to convert the score from hexadecimal to decimal. You can find the original code here.

let tempScore <- score
for i from 5 to 0 do
   let digit = 0
   while tempScore - 10^i > 0 do
      tempScore <- tempScore - 10^i
      digit <- digit + 1
   end
   set status bar score at the 10^i's place to digit
end
let k <- 5
while status bar score at the 10^k's place is 0
   set status bar score at the 10^k's place to an empty tile
   k <- k - 1
end

Essentially, for each of the digits in the score, that number in the score determines how many times the temporary score will be adjusted by a power of 10. For example, a score of 386750 will:

  • (0) subtract 100000 from the score 1 time
  • (3) subtract 10000 from the score 4 times
  • (8) subtract 1000 from the score 9 times
  • (6) subtract 100 from the score 7 times
  • (7) subtract 10 from the score 8 times
  • (5) subtract 1 from the score 6 times.

Notice that there will always be one subtraction first, so the amount of times is that digit plus one.

The game will also have to replace the leading zero with an empty tile, so that the score shows 386750 instead of 0386750.

Runtime

Here is a formula for the amount of time (in microseconds) the system needs to display a given score with the digits of ABCDEF0, where M is the number of leading zeros that turn into empty tiles. Note that the formula is slightly different on different versions of the game (due to ROM alignment):

Japanese version: cycles = 2948 + 154 * M + 484 * (A + B + C + D + E + F)
PAL v1.1 version: cycles = 2948 + 154 * M + 484 * A + 496 * (B + C + D + E + F)
All other versions: cycles = 2948 + 154 * M + 484 * A + 490 * B + 496 * (C + D + E + F)

subtract 6 cycles if score is 0

NTSC systems: time_in_us = cycles / 21.47727
PAL systems: time_in_us = cycles / 21.28137

Here are some various scores with their runtime on various versions of the game:

Score (J) Cycle Count (J) Runtime (us) (J) CPU Usage (%) (U) Cycle Count (U) Runtime (us) (U) CPU Usage (%) (E0) Cycle Count (E0) Runtime (us) (E0) CPU Usage (%) (E1) Cycle Count (E1) Runtime (us) (E1) CPU Usage (%)
0 3866 180.0 1.082 3866 180.0 1.082 3866 181.7 0.908 3866 181.7 0.908
99950 18744 872.7 5.245 19128 890.6 5.352 19128 898.8 4.495 19128 898.8 4.495
100150 6490 302.2 1.816 6568 305.8 1.838 6568 308.6 1.543 6574 308.9 1.545
1000000 3432 159.8 0.960 3432 159.8 0.960 3432 161.3 0.806 3432 161.3 0.806
9999990 29084 1354.2 8.138 29570 1376.8 8.274 29570 1389.5 6.948 29624 1392.0 6.961

For reference, one frame is 357366 cycles, or 16639.3 microseconds long (425568 cycles, or 19997.2 us in PAL regions).

You are reading the table correctly--on the NTSC versions of the game when you have the maximum score of 9999990, over 8% of the CPU's available time is going towards drawing the score. To make matters worse, when playing in 2-player mode, on Luigi's turn, Mario's score is drawn to the status bar before it gets immediately overwritten by Luigi's score. So the impact of score lag on Luigi is two-fold.

Theory

As you can see, the higher a digit is in the score, the more times the algorithm has to loop and subtract a power of ten from the score. This means that scores with low digits take the least amount of time to convert and draw. However, the empty tiles that precede smaller scores also take extra time to draw. This means the most optimal score is exactly 1000000. The least optimal score is the maximum score of 9999990. In general, you can estimate how "good" of a score you have by summing all of the digits of the score together. For example, the score of 386750 has a value of about 29. From this, you can tell that a score of 100000 is much better than a score of 99950, since the values for those scores are 1 and 32, which are vastly different. If you had a score of 99950, you can spin jump on an enemy to kill it, gaining 200 points. This would put you at 100150, with a value of 7, which is still much better than 32.