RAScript Tutorials

Etron By Etron.

Tutorial #8 – Challenges Part 2

Overview

This tutorial will show a few examples of how to create speedruns and other time related challenges. This is the second tutorial on challenges and it will build on the concepts introduced in Tutorial #7 - Challenges Part 1. The game Super Mario World was chosen for this tutorial because it is a platforming game where you can replay levels after they are beaten making it perfect for reattempting a speedrun. Additionally, the game can be completed quickly by using secret exits to bypass the standard progression route making it possible to quickly jump to the final confrontation with Bowser.

Leaderboard Analogy

Similar to the last tutorial will use leaderboard terminology for the different types of conditions used in a challenge. Each achievement will have a start, cancel, and submit events that control when the achievement is primed. For more information on these events refer to the previous tutorial.

Start

The start condition for a challenge achievement is an event that occurs right before the challenge has started.

Cancel

The cancel conditions for a challenge achievement are events that will reset the hit from the start condition.

Submit

The submit condition for a challenge achievement is the event that occurs once the challenge has been completed.

Example #8A: Yoshi’s Island 1 Speedrun

Yoshi’s Island 1 is the first level in Super Mario World and is one of the easiest levels to beat in the game. The game gives you 300 seconds to complete the level however, once you reduce that time limit it becomes much more challenging. When deciding on time limits for speedrun achievements be sure to add a small buffer of extra time to allow for a few mistakes. Player’s can spend years perfecting a speedrun which is an unreasonable expectation for the average player. Leave the world record times for leaderboards instead.

// Super Mario World
// #ID = 228

// $0000CE: [3 byte] 24-bit pointer to level's sprite data
function LevelPointer() => tbyte(0x0000CE)

// $000100: Game mode. Anything 00 to 0a is title screen
//          Goes from 2314 to 2315 at new game start
function GameMode() => byte(0x000100)

// $000DB2: 2 player game flag
function Player2() => byte(0x000DB2) 

// $000DD5: Level Beat
//          [00] = Default value. 
//          [01] = Beat the level using the normal exit.
//          [02] = Beat the level using the secret exit. 
//          [80] = Exiting the level by using Start Select or by dying.
//          [E0] = Save prompt popup in the overworld.
//          Fun Fact: Donut Ghost House has normal and secret exit in reverse.
function LevelBeat() => byte(0x000DD5)

// $000F31: [3 bytes] Time (BCD)
function TimeX00() => byte(0x000F31)
function Time0X0() => byte(0x000F32)
function Time00X() => byte(0x000F33)
function Time() 
{
    return TimeX00() * 100 +
        Time0X0() * 10 +
        Time00X()
}

// $0013BF: 8-bit Level ID
function LevelID() => byte(0x0013BF)

// $0013CF: if = 40 player is entering stage from mid point
function MidPoint() => byte(0x0013CF)

// Shortcut for when a player starts a certain level
function StartLevel(level) 
{
    return GameMode() > 0xa &&
        Player2() == 0 &&
        LevelID() == level &&
        MidPoint() != 0x40 &&
        prev(LevelPointer()) == 0 &&
        LevelPointer() != 0
}

// Shortcut for completing a level (defaults to normal exit)
function ExitLevel(exit = 1)
{
    return prev(LevelBeat()) == 0 &&
        LevelBeat() == exit
}

// Speedrun challenge to complete the level parameter in under the time parameter
// Start: When the player start the level parameter
// Cancels: When the player returns to the world map, title screen, or time elapses
// Submits: When the player exits the level
function LevelSpeedrun(level, time)
{
    start = once(StartLevel(level))
        
    cancel = never(GameMode() == 0) &&
        never(LevelPointer() == 0) &&
        never(Time() < time)
    
    submit = trigger_when(ExitLevel())
    
    return  start && cancel && submit
}

achievement(
     "Example #8A - Yoshi's Island 1 Speedrun",
     "Complete Yoshi's Island 1 in under 240 seconds",
     0,
     LevelSpeedrun(0x29, 240)
)

Start

The start condition for this challenge occurs when the player starts Yoshi’s Island 1 from the beginning in single player mode. It’s worth noting that most levels have a checkpoint in the middle that, if the player hits the white tape between the midpoint goal posts, acts as a starting location when the player die’s before completing the level. Starting from the midpoint would negate the speedrun challenge so the StartLevel(level) function only triggers when starting from the beginning of the level. By using a shortcut function like StartLevel(level) we can easily change which level the achievement is coded for by changing the level ID parameter.

Cancel

The cancel conditions for this challenge achievement are when the player leaves the level, a game over occurs, or the timer expires. The achievement uses the level pointer to know when a player is in a level. When the level is loaded the level pointer is not null (zero) and when the level is unloaded the pointer returns to null (zero). In this case the pointer’s location is unimportant since we use the level ID to know which level is loaded.

For games that include a level timer like Super Mario World it’s a good idea to use that timer for speedruns. The player can check their progress with the in-game clock to know how much time they have left to finish the speedrun. The time display is an unpacked Binary Coded Decimal (BCD) so the code uses the function Time() to convert it to base 10 decimal. If the game does not include a timer then you can create an internal timer which will be shown in the next example.

Submit

The submit condition for this challenge achievement is when the player exits the level through a normal exit. Some levels have two exits so we need to specify the type of exit. The ExitLevel(exit) function is a shortcut which defaults to the normal exit if exit = 1 and the secret exit if exit = 2. Note that this event will occur every time the player completes a level however, the achievement will only be awarded if the start condition hit is still active.

Scripts: Example #8A script

Example #8B: World Speedrun

The ultimate goal of a speedrun is to complete a game in the shortest time possible. Games often have shortcuts and optional items to collect so you can beat the game without 100% completing it. At a minimum, Speedruns are categorized by either 100% runs which required you to complete every level and collect every item, Any% runs which allow skipping levels and items, and Glitch runs which allow the player use any in-game glitch at their disposal. Depending on the game there are often many more types of speedruns to attempt.

Super Mario World has a total of 96 normal and secret exits which, at the time of writing, took oosui 1h 21m 17s 530ms to get the 100% world record. This is a very long time to base an achievement on so a better candidate is the 11 Exit speedrun is which, at the time of writing, took Tsuake 9m 42s 983ms for the world record. The 11 Exit is a glitchless run which requires you to take a shortcut through the Star World to skip ahead to Bowser’s Castle. When adding a time buffer to a multi-level speedrun it’s a good idea to allow for several errors in each level. Even with adding over six minutes to world record, this example would still be a huge challenge to beat for the average player. Unless the game is very short a full game speedrun like this example would fit better in a Bonus set.

// Super Mario World
// #ID = 228

// $000100: Game mode. Anything 00 to 0a is title screen
//          As a word is goes from 2314 to 2315 at new game start
function GameMode() => byte(0x000100)
function GameModeWord() => word(0x000100)

// $000DB2: 2 player game flag
function Player2() => byte(0x000DB2) 

// $0013BF: 8-bit Level ID
function LevelID() => byte(0x0013BF)

// $0013F9: Mario's layer (3 = game end)
function MariosLayer() => byte(0x13F9)

// $001F2E: Stages Cleared
function StagesCleared() => byte(0x001F2E)

// Shortcut for when a player starts a new game file
function NewGame() 
{
    return prev(GameModeWord()) == 2314 && 
        GameModeWord() == 2315 && 
        StagesCleared() == 0 && 
        Player2() == 0
}

// Shortcut for when a player beats bowser and wins the game
function EndGame()
{
    return StagesCleared() > 0 &&
        LevelID() == 49 && 
        MariosLayer() == 3 && 
        GameMode() == 2
}

// Converts the time minutes:seconds:frames to frames (NTSC = 1/60s, PAL = 1/50s)
function TimeSpanNTSC(minutes, seconds, frames) => minutes * 3600 + seconds * 60 + frames
function TimeSpanPAL(minutes, seconds, frames) => minutes * 2500 + seconds * 50 + frames

// Speedrun challenge to complete Super Mario World in under the time parameter
// Start: When the player creates a new game file
// Cancels: When the player returns to the title screen on time elapses
// Submits: When the player defeats bowser
function WorldSpeedrun(time)
{
    start = once(NewGame())
        
    cancel = never(GameMode() == 0) &&
        never(repeated(time, GameMode() > 0x0a))
    
    submit = trigger_when(EndGame())
    
    return  start && cancel && submit
}

achievement(
     "Example #8B - World Speedrun",
     "Defeat Bowser (front door) starting from a new save game file in under 16 minutes",
     0,
     WorldSpeedrun(TimeSpanNTSC(16,0,0))
)

Start

The start condition for this challenge occurs when the player starts a new game file. The shortcut function NewGame() checks that no exits are cleared and that only one player is active when the game mode changes.

Cancel

The cancel conditions for this challenge achievement are when a game over occurs or the internal timer expires. Since Super Mario World does not have any overall timer we have setup a timer internal to the achievement code. The code never(repeated(time, GameMode() > 0x0a)) will reset the achievement after the parameter time frames while player is in-game. Note that both US and Japanese games are linked so we use the TimeSpanNTSC() function which converts the time to frames for a 60hz system. If the linked version was from the Europe region then use the TimeSpanPAL() function which converts the time to frames for a 50hz system. As a rule the NTSC and PAL games should not be linked to the same set since players on a 50Hz system will have an advantage over players on a 60Hz system.

Submit

The submit condition for this challenge achievement is when the player beats bowser. The EndGame() checks that the player is on the final level when Mario changes layers during the end game cinematics. The achievement specifies the Front Door entrance since it’s possible to enter Bowser’s Castle from the Back Door which skips the first half of the level. Note that an additional check is added that at least one exit has been cleared to ensure that the player didn’t use a glitch to skip to the end.

Scripts: Example #8B script

Example #8C: Small Boy Challenge

The small boy challenge is to complete a level (or the whole game) without using a power up or riding Yoshi. Super Mario World has power ups like the fire flower or the cape which can trivialize completing a level. For example, using the cape for the Yoshi’s Island 1 Speedrun would make the challenge too easy since you can fly over the entire level. Additionally, being powered up gives Mario one more hit point therefore the small boy challenge is also a damageless challenge. Being small does have an advantage, small Mario has a smaller hitbox meaning that small Mario can pass by obstacles that would hit a powered up Mario.

Homework #8

Using similar logic from Example 8A create an achievement to beat any level as small Mario and without Yoshi.

Useful Memory

To complete the homework problem you’ll need the memory addresses:
0x000019 -> [8-bit] Player’s Power: 0 = Small, 1 = Super, 2 = Cape, 3 = Flower
0x00187A -> [8-bit] Sitting on Yoshi: 0 = None, 1 = On Yoshi 2 = On Yoshi turning
0x0013BF -> [8-bit] Level ID

ID Level ID Level ID Level
0x01 Vanilla Secret 2 0x1B Chocolate Fortress 0x38 Valley Ghost House
0x02 Vanilla Secret 3 0x1C Chocolate Island 5 0x39 Valley of Bowser 2
0x03 Top Secret Area 0x1D Chocolate Island 4 0x3A Valley of Bowser 1
0x04 Donut Ghost House 0x20 Roy’s Castle 0x3B Chocolate Secret
0x05 Donut Plains 3 0x20 Iggy’s Castle 0x3C Vanilla Dome 2
0x06 Donut Plains 4 0x21 Choco-Ghost House 0x3D Vanilla Dome 4
0x07 Morton’s Castle 0x22 Chocolate Island 1 0x3E Vanilla Dome 1
0x09 Donut Plains 2 0x23 Chocolate Island 3 0x40 Lemmy’s Castle
0x0A Donut Secret 1 0x24 Chocolate Island 2 0x41 Forest Ghost House
0x0B Vanilla Fortress 0x26 Yoshi’s Island 4 0x42 Forrest of Illusion 1
0x0C Butter Bridge 1 0x27 Yoshi’s Island 3 0x43 Forrest of Illusion 4
0x0D Butter Bridge 2 0x29 Yoshi’s Island 1 0x44 Forrest of Illusion 2
0x0E Ludwig’s Castle 0x2A Yoshi’s Island 2 0x46 Forrest Secret Area
0x0F Cheese Bridge Area 0x2B Vanilla Ghost House 0x47 Forrest of Illusion 3
0x10 Cookie Mountain 0x2D Vanilla Secret 1    
0x11 Soda Lake 0x2E Vanilla Dome 3    
0x13 Donut Secret House 0x2F Donut Secret 2    
0x15 Donut Plains 1 0x33 Valley of Bowser 4    
0x18 Sunken Ghost Ship 0x37 Valley of Bowser 3    
0x1A Wendy’s Castle 0x38 Valley Ghost House    

Solution

Tutorial #8 Solution


Tutorial #0 - Getting Started
Tutorial #1 - Memory Basics
Tutorial #2 - Add Hits
Tutorial #3 - Bit Flags
Tutorial #4 - Arithmetic Operations
Tutorial #5 - Pointers
Tutorial #6 - If/Else
Tutorial #7 - Challenges Part 1

All RAScript Tutorials can be found here.