Super Mario 64: Basic testing (#4335)

---------

Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
josephwhite
2026-05-09 10:46:53 -04:00
committed by GitHub
parent b59e52a103
commit 2bd572c23d
7 changed files with 402 additions and 33 deletions
+8
View File
@@ -79,6 +79,14 @@ sm64_secrets_to_level = {secret: level for (level,secret) in sm64_level_to_secre
sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level }
sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets }
# Levels with at least one star without a movement rule
# Currently excluding WF, HMC, WDW, TTM, THI, TTC, and RR
valid_move_randomizer_start_courses = [
"Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain",
"Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land",
"Dire, Dire Docks", "Snowman's Land"
]
def create_regions(multiworld: MultiWorld, options: SM64Options, player: int):
regSS = Region("Menu", player, multiworld, "Castle Area")
create_default_locs(regSS, locSS_table)
+26 -32
View File
@@ -5,7 +5,8 @@ from ..generic.Rules import add_rule, set_rule
from .Locations import location_table
from .Options import SM64Options
from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\
sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances,\
valid_move_randomizer_start_courses
from .Items import action_item_data_table
def shuffle_dict_keys(multiworld: MultiWorld, dictionary: dict) -> dict:
@@ -28,40 +29,33 @@ def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_r
def set_rules(multiworld: MultiWorld, options: SM64Options, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int):
randomized_level_to_paintings = sm64_level_to_paintings.copy()
randomized_level_to_secrets = sm64_level_to_secrets.copy()
valid_move_randomizer_start_courses = [
"Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain",
"Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land",
"Dire, Dire Docks", "Snowman's Land"
] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR
if options.area_rando >= 1: # Some randomization is happening, randomize Courses
randomized_level_to_paintings = shuffle_dict_keys(multiworld,sm64_level_to_paintings)
# If not shuffling later, ensure a valid start course on move randomizer
if options.area_rando < 3 and move_rando_bitvec > 0:
swapdict = randomized_level_to_paintings.copy()
invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses}
fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld)
fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld)
if options.area_rando == 2: # Randomize Secrets as well
if options.area_rando > options.area_rando.option_Off: # Some randomization is happening, randomize Courses
randomized_level_to_paintings = shuffle_dict_keys(multiworld, sm64_level_to_paintings)
if options.area_rando == options.area_rando.option_Courses_and_Secrets_Separate: # Randomize Secrets as well
randomized_level_to_secrets = shuffle_dict_keys(multiworld, sm64_level_to_secrets)
randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets}
if options.area_rando == 3: # Randomize Courses and Secrets in one pool
randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets} # Concatenate courses and secrets for rest
if options.area_rando == options.area_rando.option_Courses_and_Secrets: # Randomize Courses and Secrets in one pool
randomized_entrances = shuffle_dict_keys(multiworld, randomized_entrances)
# Guarantee first entrance is a course
swapdict = randomized_entrances.copy()
if move_rando_bitvec == 0:
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, multiworld)
else:
invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses}
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld)
fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld)
# Guarantee BITFS is not mapped to DDD
fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, multiworld)
# Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave":
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, multiworld)
else:
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, multiworld)
# Now, fix assignment if necessary
swapdict = randomized_entrances.copy()
if move_rando_bitvec == 0:
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, multiworld)
else:
invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses}
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, multiworld)
fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, multiworld)
# Guarantee BITFS is not mapped to DDD
fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, multiworld)
# Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave":
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, multiworld)
else:
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, multiworld)
# Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code
# Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise
+1 -1
View File
@@ -139,7 +139,7 @@ class SM64World(World):
if self.move_rando_bitvec & (1 << itemdata.code - double_jump_bitvec_offset)]
def generate_basic(self):
if not (self.options.buddy_checks):
if not self.options.buddy_checks:
self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB"))
self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF"))
self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB"))
View File
+8
View File
@@ -0,0 +1,8 @@
from test.bases import WorldTestBase
from .. import SM64World
class SM64TestBase(WorldTestBase):
game = "Super Mario 64"
world: SM64World
+81
View File
@@ -0,0 +1,81 @@
from .bases import SM64TestBase
from .. import Options
from ..Regions import sm64_entrances_to_level, sm64_level_to_entrances
# Access to Locations/Areas/Entrances by Power Star count in star_cost
class StarCostAccessTestBase(SM64TestBase):
run_default_tests = False
options = {
"progressive_keys": Options.ProgressiveKeys.option_false,
"enable_locked_paintings": Options.EnableLockedPaintings.option_false,
# Test for access would mean access to entrance/painting,
# not level itself for the sake of entrance rando.
"area_rando": Options.AreaRandomizer.option_Courses_and_Secrets,
}
def test_BoB_entrance_access(self):
# Always accessible, no stars needed.
bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
bob_entrance_id = self.world.area_connections[bob_level_id]
bob_entrance = sm64_level_to_entrances[bob_entrance_id]
self.assertTrue(self.can_reach_region(bob_entrance))
def test_MIPS1_access(self):
# Requires Basement Key and Power Stars calculated in star_costs["MIPS1Cost"]
self.assertFalse(self.can_reach_location("MIPS 1"))
self.collect([self.get_item_by_name("Basement Key")])
self.assertFalse(self.can_reach_location("MIPS 1"))
mips1_cost = self.world.star_costs["MIPS1Cost"]
self.collect([self.get_item_by_name("Power Star")] * mips1_cost)
self.assertTrue(self.can_reach_location("MIPS 1"))
def test_MIPS2_access(self):
# Requires Basement Key and Power Stars calculated in star_costs["MIPS2Cost"]
self.assertFalse(self.can_reach_location("MIPS 2"))
self.collect([self.get_item_by_name("Basement Key")])
self.assertFalse(self.can_reach_location("MIPS 2"))
mips2_cost = self.world.star_costs["MIPS2Cost"]
self.collect([self.get_item_by_name("Power Star")] * mips2_cost)
self.assertTrue(self.can_reach_location("MIPS 2"))
def test_BitDW_entrance_access(self):
# Requires Power Stars calculated in star_costs["FirstBowserDoorCost"]
bitdw_level_id = sm64_entrances_to_level["Bowser in the Dark World"]
bitdw_entrance_id = self.world.area_connections[bitdw_level_id]
bitdw_entrance = sm64_level_to_entrances[bitdw_entrance_id]
self.assertFalse(self.can_reach_region(bitdw_entrance))
bitdw_cost = self.world.star_costs["FirstBowserDoorCost"]
self.collect([self.get_item_by_name("Power Star")] * bitdw_cost)
self.assertTrue(self.can_reach_region(bitdw_entrance))
# Since BitFS is locked behind "DDD: Board Bowser's Sub", we just need to test DDD.
def test_DDD_entrance_access(self):
# Requires Basement Key and Power Stars calculated in star_costs["BasementDoorCost"]
ddd_level_id = sm64_entrances_to_level["Dire, Dire Docks"]
ddd_entrance_id = self.world.area_connections[ddd_level_id]
ddd_entrance = sm64_level_to_entrances[ddd_entrance_id]
self.assertFalse(self.can_reach_region(ddd_entrance))
self.collect([self.get_item_by_name("Basement Key")])
self.assertFalse(self.can_reach_region(ddd_entrance))
bitfs_cost = self.world.star_costs["BasementDoorCost"]
self.collect([self.get_item_by_name("Power Star")] * bitfs_cost)
self.assertTrue(self.can_reach_region(ddd_entrance))
def test_Floor3_access(self):
# Requires Second Floor Key and Power Stars calculated in star_costs["SecondFloorDoorCost"]
self.assertFalse(self.can_reach_region("Third Floor"))
self.collect([self.get_item_by_name("Second Floor Key")])
self.assertFalse(self.can_reach_region("Third Floor"))
floor3_cost = self.world.star_costs["StarsToFinish"]
self.collect([self.get_item_by_name("Power Star")] * floor3_cost)
self.assertTrue(self.can_reach_region("Third Floor"))
def test_BitS_entrance_access(self):
# Requires Second Floor Key and Power Stars calculated in star_costs["StarsToFinish"]
self.assertFalse(self.can_reach_region("Bowser in the Sky"))
self.collect([self.get_item_by_name("Second Floor Key")])
self.assertFalse(self.can_reach_region("Bowser in the Sky"))
bits_cost = self.world.star_costs["StarsToFinish"]
self.collect([self.get_item_by_name("Power Star")] * bits_cost)
self.assertTrue(self.can_reach_region("Third Floor"))
self.assertTrue(self.can_reach_region("Bowser in the Sky"))
+278
View File
@@ -0,0 +1,278 @@
from .bases import SM64TestBase
from .. import Options
from ..Locations import loc100Coin_table, location_table
from ..Regions import sm64_entrances_to_level, sm64_level_to_paintings, sm64_level_to_secrets, \
valid_move_randomizer_start_courses
valid_move_randomizer_start_entrances = {
level: entrance
for (level, entrance) in sm64_entrances_to_level.items()
if level in valid_move_randomizer_start_courses
}
# Coin Star Logic
class EnableCoinStarsTestBase(SM64TestBase):
options = {
"enable_coin_stars": Options.EnableCoinStars.option_on
}
# Ensure Coin Star locations are created
def test_coin_star_locations(self):
possible_locations = self.world.location_names
for loc in loc100Coin_table:
# Use subtest to force all locations to be tested
with self.subTest("Location created", location=loc):
self.assertIn(loc, possible_locations)
class DisableCoinStarsTestBase(SM64TestBase):
options = {
"enable_coin_stars": Options.EnableCoinStars.option_off
}
# Ensure Coin Star locations are not created
def test_coin_star_locations(self):
possible_locations = self.world.get_locations()
for loc in loc100Coin_table:
# Use subtest to force all locations to be tested
with self.subTest("Location not created", location=loc):
self.assertNotIn(loc, possible_locations)
class VanillaCoinStarsTestBase(SM64TestBase):
options = {
"enable_coin_stars": Options.EnableCoinStars.option_vanilla
}
# Ensure Coin Star locations are created
def test_coin_star_locations(self):
possible_locations = self.world.location_names
for loc in loc100Coin_table:
# Use subtest to force all locations to be tested
with self.subTest("Location created", location=loc):
self.assertIn(loc, possible_locations)
# Vanilla Coin Stars should give the player their own Power Stars
def test_items_in_coin_star_locations(self):
for loc in loc100Coin_table:
# Use subtest to force all locations to be tested
with self.subTest("Location created", location=loc):
item_in_loc = self.world.get_location(loc).item
self.assertEqual(item_in_loc.name, "Power Star")
# By default, these test bases are single player multiworld.
# In any other case, we should test that they belong to their respective worlds.
# Exclamation Boxes
class ExclamationBoxesOnTestBase(SM64TestBase):
options = {
"exclamation_boxes": Options.ExclamationBoxes.option_true,
}
class ExclamationBoxesOffTestBase(SM64TestBase):
options = {
"exclamation_boxes": Options.ExclamationBoxes.option_false,
}
# Should populate the boxes with the players own 1Up Mushrooms
def test_items_in_exclamation_box_locations(self):
# Get 1Up Block locations
loc1ups_table = {name for name in location_table.keys() if "1Up Block" in name}
for loc in loc1ups_table:
# Use subtest to force all locations to be tested
with self.subTest("Location has own 1Up Mushroom.", location=loc):
item_in_loc = self.world.get_location(loc).item
self.assertEqual(item_in_loc.name, "1Up Mushroom")
# By default, these test bases are single player multiworld.
# In any other case, we should test that they belong to their respective worlds.
# Entrance Randomizer
class EntranceRandoOffTestBase(SM64TestBase):
options = {
"area_rando": Options.AreaRandomizer.option_Off
}
# Ensure entrance rando disabled
def test_BoB_entrance(self):
bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
self.assertEqual(self.world.area_connections[bob_level_id], bob_level_id)
def test_BitFS_entrance(self):
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id)
class EntranceRandoCourseTestBase(SM64TestBase):
options = {
"area_rando": Options.AreaRandomizer.option_Courses_Only
}
def test_BoB_entrance(self):
bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
# BoB goes to a painting, not a secret
self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
def test_BitFS_entrance(self):
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
# BitFS is a secret (aka not a course), unaffected by Course Only entrance rando.
self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id)
class EntranceRandoSeparateTestBase(SM64TestBase):
options = {
"area_rando": Options.AreaRandomizer.option_Courses_and_Secrets_Separate
}
def test_BoB_entrance(self):
bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
# BoB goes to a painting, not a secret
self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
def test_BitFS_entrance(self):
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
# BitFS goes to a secret, not a painting
self.assertIn(self.world.area_connections[bitfs_level_id], sm64_level_to_secrets.keys())
self.assertNotIn(self.world.area_connections[bitfs_level_id], sm64_level_to_paintings.keys())
# BitFS does not go to DDD
self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
class EntranceRandoAllTestBase(SM64TestBase):
options = {
"area_rando": Options.AreaRandomizer.option_Courses_and_Secrets
}
def test_BitFS_entrance(self):
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
# BitFS does not go to DDD
self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
# Completion Type
class CompletionLastBowserTestBase(SM64TestBase):
options = {
"completion_type": Options.CompletionType.option_Last_Bowser_Stage
}
class CompletionAllBowserTestBase(SM64TestBase):
options = {
"completion_type": Options.CompletionType.option_All_Bowser_Stages
}
# Option Combos
# Smallest Power Star count possible
class MinimumStarsPossibleTestBase(SM64TestBase):
options = {
"amount_of_stars": Options.AmountOfStars.range_start,
"enable_move_rando": Options.EnableMoveRandomizer.option_true,
"exclamation_boxes": Options.ExclamationBoxes.option_false,
"enable_coin_stars": Options.EnableCoinStars.option_off
}
# There will be less Power Stars than filler with this low of a star count
def test_stars_vs_filler(self):
filler_count = len(self.get_items_by_name("1Up Mushroom"))
star_count = len(self.get_items_by_name("Power Star"))
self.assertGreater(filler_count, star_count)
# Entrance + Move Randos
class CourseEntrancesMoveTestBase(SM64TestBase):
options = {
"enable_move_rando": Options.EnableMoveRandomizer.option_true,
"area_rando": Options.AreaRandomizer.option_Courses_Only
}
def test_BoB_entrance(self):
bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
# BoB goes to a course, not a secret.
self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
# BoB goes to level with at least one star without a movement rule.
self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values())
def test_BitFS_entrance(self):
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
# BitFS is a secret (aka not a course), unaffected by Course Only entrance rando.
self.assertEqual(self.world.area_connections[bitfs_level_id], bitfs_level_id)
def test_WF_entrance(self):
wf_level_id = sm64_entrances_to_level["Whomp's Fortress"]
# WF goes to level with at least one star without a movement rule.
self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values())
class SeparateEntrancesMoveTestBase(SM64TestBase):
options = {
"enable_move_rando": Options.EnableMoveRandomizer.option_true,
"area_rando": Options.AreaRandomizer.option_Courses_and_Secrets_Separate
}
def test_BoB_entrance(self):
bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
# BoB goes to a course, not a secret.
self.assertNotIn(self.world.area_connections[bob_level_id], sm64_level_to_secrets.keys())
self.assertIn(self.world.area_connections[bob_level_id], sm64_level_to_paintings.keys())
# BoB goes to level with at least one star without a movement rule.
self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values())
def test_BitFS_entrance(self):
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
# BitFS does not go to DDD.
self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
def test_WF_entrance(self):
wf_level_id = sm64_entrances_to_level["Whomp's Fortress"]
# WF goes to level with at least one star without a movement rule.
self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values())
class AllEntrancesMoveTestBase(SM64TestBase):
options = {
"enable_move_rando": Options.EnableMoveRandomizer.option_true,
"area_rando": Options.AreaRandomizer.option_Courses_and_Secrets
}
def test_BoB_entrance(self):
bob_level_id = sm64_entrances_to_level["Bob-omb Battlefield"]
# BoB goes to level with at least one star without a movement rule.
self.assertIn(self.world.area_connections[bob_level_id], valid_move_randomizer_start_entrances.values())
def test_BitFS_entrance(self):
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
# BitFS does not go to DDD.
self.assertIsNot(self.world.area_connections[bitfs_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
def test_WF_entrance(self):
wf_level_id = sm64_entrances_to_level["Whomp's Fortress"]
# WF goes to level with at least one star without a movement rule.
self.assertIn(self.world.area_connections[wf_level_id], valid_move_randomizer_start_entrances.values())
def test_CotMC_entrance(self):
cotmc_level_id = sm64_entrances_to_level["Cavern of the Metal Cap"]
# CotMC does not go to HMC.
self.assertIsNot(self.world.area_connections[cotmc_level_id], sm64_entrances_to_level["Hazy Maze Cave"])
# If BitFS -> HMC, CotMC does not go to DDD.
bitfs_level_id = sm64_entrances_to_level["Bowser in the Fire Sea"]
if self.world.area_connections[bitfs_level_id] == sm64_entrances_to_level["Hazy Maze Cave"]:
self.assertIsNot(self.world.area_connections[cotmc_level_id], sm64_entrances_to_level["Dire, Dire Docks"])
# No Strict Requirements
class NoStrictRequirementsTestBase(SM64TestBase):
options = {
"enable_move_rando": Options.EnableMoveRandomizer.option_true,
"buddy_checks": Options.BuddyChecks.option_true,
"strict_move_requirements": Options.StrictMoveRequirements.option_false,
"strict_cap_requirements": Options.StrictCapRequirements.option_false,
"strict_cannon_requirements": Options.StrictCannonRequirements.option_false,
}