From 82e764e548d33f3ba96948cbf2f6d742957dda93 Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:14:21 -0500 Subject: [PATCH] SM64: Move Randomizer Content Update (#2569) * Super Mario 64: Move Randomizer Update Co-authored-by: RBman <139954693+RBmans@users.noreply.github.com> Signed-off-by: Magnemania * Fixed logic for Vanish Cap Under the Moat Signed-off-by: Magnemania --- worlds/sm64ex/Items.py | 17 +- worlds/sm64ex/Options.py | 88 ++++++--- worlds/sm64ex/Regions.py | 196 +++++++++++--------- worlds/sm64ex/Rules.py | 379 ++++++++++++++++++++++++++++++-------- worlds/sm64ex/__init__.py | 82 ++++++--- 5 files changed, 546 insertions(+), 216 deletions(-) diff --git a/worlds/sm64ex/Items.py b/worlds/sm64ex/Items.py index 5b429a23cdc3..546f1abd316b 100644 --- a/worlds/sm64ex/Items.py +++ b/worlds/sm64ex/Items.py @@ -16,6 +16,21 @@ class SM64Item(Item): "1Up Mushroom": 3626184 } +action_item_table = { + "Double Jump": 3626185, + "Triple Jump": 3626186, + "Long Jump": 3626187, + "Backflip": 3626188, + "Side Flip": 3626189, + "Wall Kick": 3626190, + "Dive": 3626191, + "Ground Pound": 3626192, + "Kick": 3626193, + "Climb": 3626194, + "Ledge Grab": 3626195 +} + + cannon_item_table = { "Cannon Unlock BoB": 3626200, "Cannon Unlock WF": 3626201, @@ -29,4 +44,4 @@ class SM64Item(Item): "Cannon Unlock RR": 3626214 } -item_table = {**generic_item_table, **cannon_item_table} \ No newline at end of file +item_table = {**generic_item_table, **action_item_table, **cannon_item_table} \ No newline at end of file diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 8a10f3edea55..d9a877df2b37 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,9 +1,10 @@ import typing from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice - +from .Items import action_item_table class EnableCoinStars(DefaultOnToggle): - """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything""" + """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything. + Removes 15 locations from the pool.""" display_name = "Enable 100 Coin Stars" @@ -13,56 +14,63 @@ class StrictCapRequirements(DefaultOnToggle): class StrictCannonRequirements(DefaultOnToggle): - """If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy - Checks are enabled""" + """If disabled, Stars that expect cannons may have to be acquired without them. + Has no effect if Buddy Checks and Move Randomizer are disabled""" display_name = "Strict Cannon Requirements" class FirstBowserStarDoorCost(Range): - """How many stars are required at the Star Door to Bowser in the Dark World""" + """What percent of the total stars are required at the Star Door to Bowser in the Dark World""" + display_name = "First Star Door Cost %" range_start = 0 - range_end = 50 - default = 8 + range_end = 40 + default = 7 class BasementStarDoorCost(Range): - """How many stars are required at the Star Door in the Basement""" + """What percent of the total stars are required at the Star Door in the Basement""" + display_name = "Basement Star Door %" range_start = 0 - range_end = 70 - default = 30 + range_end = 50 + default = 25 class SecondFloorStarDoorCost(Range): - """How many stars are required to access the third floor""" + """What percent of the total stars are required to access the third floor""" + display_name = 'Second Floor Star Door %' range_start = 0 - range_end = 90 - default = 50 + range_end = 70 + default = 42 class MIPS1Cost(Range): - """How many stars are required to spawn MIPS the first time""" + """What percent of the total stars are required to spawn MIPS the first time""" + display_name = "MIPS 1 Star %" range_start = 0 - range_end = 40 - default = 15 + range_end = 35 + default = 12 class MIPS2Cost(Range): - """How many stars are required to spawn MIPS the second time.""" + """What percent of the total stars are required to spawn MIPS the second time.""" + display_name = "MIPS 2 Star %" range_start = 0 - range_end = 80 - default = 50 + range_end = 70 + default = 42 class StarsToFinish(Range): - """How many stars are required at the infinite stairs""" - display_name = "Endless Stairs Stars" + """What percent of the total stars are required at the infinite stairs""" + display_name = "Endless Stairs Star %" range_start = 0 - range_end = 100 - default = 70 + range_end = 90 + default = 58 class AmountOfStars(Range): - """How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set""" + """How many stars exist. + If there aren't enough locations to hold the given total, the total will be reduced.""" + display_name = "Total Power Stars" range_start = 35 range_end = 120 default = 120 @@ -83,11 +91,13 @@ class BuddyChecks(Toggle): class ExclamationBoxes(Choice): - """Include 1Up Exclamation Boxes during randomization""" + """Include 1Up Exclamation Boxes during randomization. + Adds 29 locations to the pool.""" display_name = "Randomize 1Up !-Blocks" option_Off = 0 option_1Ups_Only = 1 + class CompletionType(Choice): """Set goal for game completion""" display_name = "Completion Goal" @@ -96,17 +106,32 @@ class CompletionType(Choice): class ProgressiveKeys(DefaultOnToggle): - """Keys will first grant you access to the Basement, then to the Secound Floor""" + """Keys will first grant you access to the Basement, then to the Second Floor""" display_name = "Progressive Keys" +class StrictMoveRequirements(DefaultOnToggle): + """If disabled, Stars that expect certain moves may have to be acquired without them. Only makes a difference + if Move Randomization is enabled""" + display_name = "Strict Move Requirements" + +def getMoveRandomizerOption(action: str): + class MoveRandomizerOption(Toggle): + """Mario is unable to perform this action until a corresponding item is picked up. + This option is incompatible with builds using a 'nomoverando' branch.""" + display_name = f"Randomize {action}" + return MoveRandomizerOption + sm64_options: typing.Dict[str, type(Option)] = { "AreaRandomizer": AreaRandomizer, + "BuddyChecks": BuddyChecks, + "ExclamationBoxes": ExclamationBoxes, "ProgressiveKeys": ProgressiveKeys, "EnableCoinStars": EnableCoinStars, - "AmountOfStars": AmountOfStars, "StrictCapRequirements": StrictCapRequirements, "StrictCannonRequirements": StrictCannonRequirements, + "StrictMoveRequirements": StrictMoveRequirements, + "AmountOfStars": AmountOfStars, "FirstBowserStarDoorCost": FirstBowserStarDoorCost, "BasementStarDoorCost": BasementStarDoorCost, "SecondFloorStarDoorCost": SecondFloorStarDoorCost, @@ -114,7 +139,10 @@ class ProgressiveKeys(DefaultOnToggle): "MIPS2Cost": MIPS2Cost, "StarsToFinish": StarsToFinish, "death_link": DeathLink, - "BuddyChecks": BuddyChecks, - "ExclamationBoxes": ExclamationBoxes, - "CompletionType" : CompletionType, + "CompletionType": CompletionType, } + +for action in action_item_table: + # HACK: Disable randomization of double jump + if action == 'Double Jump': continue + sm64_options[f"MoveRandomizer{action.replace(' ','')}"] = getMoveRandomizerOption(action) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index d426804c30f3..c04b862fa757 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -1,5 +1,4 @@ import typing - from enum import Enum from BaseClasses import MultiWorld, Region, Entrance, Location @@ -8,7 +7,8 @@ locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \ locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \ locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ - locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table + locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table + class SM64Levels(int, Enum): BOB_OMB_BATTLEFIELD = 91 @@ -55,7 +55,7 @@ class SM64Levels(int, Enum): SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock", SM64Levels.RAINBOW_RIDE: "Rainbow Ride" } -sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() } +sm64_paintings_to_level = {painting: level for (level, painting) in sm64_level_to_paintings.items() } # sm64secrets is a dict of secret areas, same format as sm64paintings sm64_level_to_secrets: typing.Dict[SM64Levels, str] = { @@ -68,152 +68,163 @@ class SM64Levels(int, Enum): SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea", SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow" } -sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() } +sm64_secrets_to_level = {secret: level for (level,secret) in sm64_level_to_secrets.items() } -sm64_entrances_to_level = { **sm64_paintings_to_level, **sm64_secrets_to_level } -sm64_level_to_entrances = { **sm64_level_to_paintings, **sm64_level_to_secrets } +sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level } +sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets } def create_regions(world: MultiWorld, player: int): regSS = Region("Menu", player, world, "Castle Area") - create_default_locs(regSS, locSS_table, player) + create_default_locs(regSS, locSS_table) world.regions.append(regSS) regBoB = create_region("Bob-omb Battlefield", player, world) - create_default_locs(regBoB, locBoB_table, player) + create_locs(regBoB, "BoB: Big Bob-Omb on the Summit", "BoB: Footrace with Koopa The Quick", + "BoB: Mario Wings to the Sky", "BoB: Behind Chain Chomp's Gate", "BoB: Bob-omb Buddy") + create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins") if (world.EnableCoinStars[player].value): - regBoB.locations.append(SM64Location(player, "BoB: 100 Coins", location_table["BoB: 100 Coins"], regBoB)) - world.regions.append(regBoB) + create_locs(regBoB, "BoB: 100 Coins") regWhomp = create_region("Whomp's Fortress", player, world) - create_default_locs(regWhomp, locWhomp_table, player) + create_locs(regWhomp, "WF: Chip Off Whomp's Block", "WF: Shoot into the Wild Blue", "WF: Red Coins on the Floating Isle", + "WF: Fall onto the Caged Island", "WF: Blast Away the Wall") + create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy") if (world.EnableCoinStars[player].value): - regWhomp.locations.append(SM64Location(player, "WF: 100 Coins", location_table["WF: 100 Coins"], regWhomp)) - world.regions.append(regWhomp) + create_locs(regWhomp, "WF: 100 Coins") regJRB = create_region("Jolly Roger Bay", player, world) - create_default_locs(regJRB, locJRB_table, player) + create_locs(regJRB, "JRB: Plunder in the Sunken Ship", "JRB: Can the Eel Come Out to Play?", "JRB: Treasure of the Ocean Cave", + "JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy") + jrb_upper = create_subregion(regJRB, 'JRB: Upper', "JRB: Red Coins on the Ship Afloat") if (world.EnableCoinStars[player].value): - regJRB.locations.append(SM64Location(player, "JRB: 100 Coins", location_table["JRB: 100 Coins"], regJRB)) - world.regions.append(regJRB) + create_locs(jrb_upper, "JRB: 100 Coins") regCCM = create_region("Cool, Cool Mountain", player, world) - create_default_locs(regCCM, locCCM_table, player) + create_default_locs(regCCM, locCCM_table) if (world.EnableCoinStars[player].value): - regCCM.locations.append(SM64Location(player, "CCM: 100 Coins", location_table["CCM: 100 Coins"], regCCM)) - world.regions.append(regCCM) + create_locs(regCCM, "CCM: 100 Coins") regBBH = create_region("Big Boo's Haunt", player, world) - create_default_locs(regBBH, locBBH_table, player) + create_locs(regBBH, "BBH: Go on a Ghost Hunt", "BBH: Ride Big Boo's Merry-Go-Round", + "BBH: Secret of the Haunted Books", "BBH: Seek the 8 Red Coins") + bbh_third_floor = create_subregion(regBBH, "BBH: Third Floor", "BBH: Eye to Eye in the Secret Room") + create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion") if (world.EnableCoinStars[player].value): - regBBH.locations.append(SM64Location(player, "BBH: 100 Coins", location_table["BBH: 100 Coins"], regBBH)) - world.regions.append(regBBH) + create_locs(regBBH, "BBH: 100 Coins") regPSS = create_region("The Princess's Secret Slide", player, world) - create_default_locs(regPSS, locPSS_table, player) - world.regions.append(regPSS) + create_default_locs(regPSS, locPSS_table) regSA = create_region("The Secret Aquarium", player, world) - create_default_locs(regSA, locSA_table, player) - world.regions.append(regSA) + create_default_locs(regSA, locSA_table) regTotWC = create_region("Tower of the Wing Cap", player, world) - create_default_locs(regTotWC, locTotWC_table, player) - world.regions.append(regTotWC) + create_default_locs(regTotWC, locTotWC_table) regBitDW = create_region("Bowser in the Dark World", player, world) - create_default_locs(regBitDW, locBitDW_table, player) - world.regions.append(regBitDW) + create_default_locs(regBitDW, locBitDW_table) - regBasement = create_region("Basement", player, world) - world.regions.append(regBasement) + create_region("Basement", player, world) regHMC = create_region("Hazy Maze Cave", player, world) - create_default_locs(regHMC, locHMC_table, player) + create_locs(regHMC, "HMC: Swimming Beast in the Cavern", "HMC: Metal-Head Mario Can Move!", + "HMC: Watch for Rolling Rocks", "HMC: Navigating the Toxic Maze","HMC: 1Up Block Past Rolling Rocks") + hmc_red_coin_area = create_subregion(regHMC, "HMC: Red Coin Area", "HMC: Elevate for 8 Red Coins") + create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit") if (world.EnableCoinStars[player].value): - regHMC.locations.append(SM64Location(player, "HMC: 100 Coins", location_table["HMC: 100 Coins"], regHMC)) - world.regions.append(regHMC) + create_locs(hmc_red_coin_area, "HMC: 100 Coins") regLLL = create_region("Lethal Lava Land", player, world) - create_default_locs(regLLL, locLLL_table, player) + create_locs(regLLL, "LLL: Boil the Big Bully", "LLL: Bully the Bullies", + "LLL: 8-Coin Puzzle with 15 Pieces", "LLL: Red-Hot Log Rolling") + create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano") if (world.EnableCoinStars[player].value): - regLLL.locations.append(SM64Location(player, "LLL: 100 Coins", location_table["LLL: 100 Coins"], regLLL)) - world.regions.append(regLLL) + create_locs(regLLL, "LLL: 100 Coins") regSSL = create_region("Shifting Sand Land", player, world) - create_default_locs(regSSL, locSSL_table, player) + create_locs(regSSL, "SSL: In the Talons of the Big Bird", "SSL: Shining Atop the Pyramid", "SSL: Inside the Ancient Pyramid", + "SSL: Free Flying for 8 Red Coins", "SSL: Bob-omb Buddy", + "SSL: 1Up Block Outside Pyramid", "SSL: 1Up Block Pyramid Left Path", "SSL: 1Up Block Pyramid Back") + create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle") if (world.EnableCoinStars[player].value): - regSSL.locations.append(SM64Location(player, "SSL: 100 Coins", location_table["SSL: 100 Coins"], regSSL)) - world.regions.append(regSSL) + create_locs(regSSL, "SSL: 100 Coins") regDDD = create_region("Dire, Dire Docks", player, world) - create_default_locs(regDDD, locDDD_table, player) + create_locs(regDDD, "DDD: Board Bowser's Sub", "DDD: Chests in the Current", "DDD: Through the Jet Stream", + "DDD: The Manta Ray's Reward", "DDD: Collect the Caps...") + ddd_moving_poles = create_subregion(regDDD, "DDD: Moving Poles", "DDD: Pole-Jumping for Red Coins") if (world.EnableCoinStars[player].value): - regDDD.locations.append(SM64Location(player, "DDD: 100 Coins", location_table["DDD: 100 Coins"], regDDD)) - world.regions.append(regDDD) + create_locs(ddd_moving_poles, "DDD: 100 Coins") regCotMC = create_region("Cavern of the Metal Cap", player, world) - create_default_locs(regCotMC, locCotMC_table, player) - world.regions.append(regCotMC) + create_default_locs(regCotMC, locCotMC_table) regVCutM = create_region("Vanish Cap under the Moat", player, world) - create_default_locs(regVCutM, locVCutM_table, player) - world.regions.append(regVCutM) + create_default_locs(regVCutM, locVCutM_table) regBitFS = create_region("Bowser in the Fire Sea", player, world) - create_default_locs(regBitFS, locBitFS_table, player) - world.regions.append(regBitFS) + create_subregion(regBitFS, "BitFS: Upper", *locBitFS_table.keys()) - regFloor2 = create_region("Second Floor", player, world) - world.regions.append(regFloor2) + create_region("Second Floor", player, world) regSL = create_region("Snowman's Land", player, world) - create_default_locs(regSL, locSL_table, player) + create_default_locs(regSL, locSL_table) if (world.EnableCoinStars[player].value): - regSL.locations.append(SM64Location(player, "SL: 100 Coins", location_table["SL: 100 Coins"], regSL)) - world.regions.append(regSL) + create_locs(regSL, "SL: 100 Coins") regWDW = create_region("Wet-Dry World", player, world) - create_default_locs(regWDW, locWDW_table, player) + create_locs(regWDW, "WDW: Express Elevator--Hurry Up!") + wdw_top = create_subregion(regWDW, "WDW: Top", "WDW: Shocking Arrow Lifts!", "WDW: Top o' the Town", + "WDW: Secrets in the Shallows & Sky", "WDW: Bob-omb Buddy") + create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown") if (world.EnableCoinStars[player].value): - regWDW.locations.append(SM64Location(player, "WDW: 100 Coins", location_table["WDW: 100 Coins"], regWDW)) - world.regions.append(regWDW) + create_locs(wdw_top, "WDW: 100 Coins") regTTM = create_region("Tall, Tall Mountain", player, world) - create_default_locs(regTTM, locTTM_table, player) + ttm_middle = create_subregion(regTTM, "TTM: Middle", "TTM: Scary 'Shrooms, Red Coins", "TTM: Blast to the Lonely Mushroom", + "TTM: Bob-omb Buddy", "TTM: 1Up Block on Red Mushroom") + ttm_top = create_subregion(ttm_middle, "TTM: Top", "TTM: Scale the Mountain", "TTM: Mystery of the Monkey Cage", + "TTM: Mysterious Mountainside", "TTM: Breathtaking View from Bridge") if (world.EnableCoinStars[player].value): - regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM)) - world.regions.append(regTTM) - - regTHIT = create_region("Tiny-Huge Island (Tiny)", player, world) - create_default_locs(regTHIT, locTHI_table, player) + create_locs(ttm_top, "TTM: 100 Coins") + + create_region("Tiny-Huge Island (Huge)", player, world) + create_region("Tiny-Huge Island (Tiny)", player, world) + regTHI = create_region("Tiny-Huge Island", player, world) + create_locs(regTHI, "THI: The Tip Top of the Huge Island", "THI: 1Up Block THI Small near Start") + thi_pipes = create_subregion(regTHI, "THI: Pipes", "THI: Pluck the Piranha Flower", "THI: Rematch with Koopa the Quick", + "THI: Five Itty Bitty Secrets", "THI: Wiggler's Red Coins", "THI: Bob-omb Buddy", + "THI: 1Up Block THI Large near Start", "THI: 1Up Block Windy Area") + thi_large_top = create_subregion(thi_pipes, "THI: Large Top", "THI: Make Wiggler Squirm") if (world.EnableCoinStars[player].value): - regTHIT.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHIT)) - world.regions.append(regTHIT) - regTHIH = create_region("Tiny-Huge Island (Huge)", player, world) - world.regions.append(regTHIH) + create_locs(thi_large_top, "THI: 100 Coins") regFloor3 = create_region("Third Floor", player, world) world.regions.append(regFloor3) regTTC = create_region("Tick Tock Clock", player, world) - create_default_locs(regTTC, locTTC_table, player) + create_locs(regTTC, "TTC: Stop Time for Red Coins") + ttc_lower = create_subregion(regTTC, "TTC: Lower", "TTC: Roll into the Cage", "TTC: Get a Hand", "TTC: 1Up Block Midway Up") + ttc_upper = create_subregion(ttc_lower, "TTC: Upper", "TTC: Timed Jumps on Moving Bars", "TTC: The Pit and the Pendulums") + ttc_top = create_subregion(ttc_upper, "TTC: Top", "TTC: Stomp on the Thwomp", "TTC: 1Up Block at the Top") if (world.EnableCoinStars[player].value): - regTTC.locations.append(SM64Location(player, "TTC: 100 Coins", location_table["TTC: 100 Coins"], regTTC)) - world.regions.append(regTTC) + create_locs(ttc_top, "TTC: 100 Coins") regRR = create_region("Rainbow Ride", player, world) - create_default_locs(regRR, locRR_table, player) + create_locs(regRR, "RR: Swingin' in the Breeze", "RR: Tricky Triangles!", + "RR: 1Up Block Top of Red Coin Maze", "RR: 1Up Block Under Fly Guy", "RR: Bob-omb Buddy") + rr_maze = create_subregion(regRR, "RR: Maze", "RR: Coins Amassed in a Maze") + create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow") + create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky") if (world.EnableCoinStars[player].value): - regRR.locations.append(SM64Location(player, "RR: 100 Coins", location_table["RR: 100 Coins"], regRR)) - world.regions.append(regRR) + create_locs(rr_maze, "RR: 100 Coins") regWMotR = create_region("Wing Mario over the Rainbow", player, world) - create_default_locs(regWMotR, locWMotR_table, player) - world.regions.append(regWMotR) + create_default_locs(regWMotR, locWMotR_table) regBitS = create_region("Bowser in the Sky", player, world) - create_default_locs(regBitS, locBitS_table, player) - world.regions.append(regBitS) + create_locs(regBitS, "Bowser in the Sky 1Up Block") + create_subregion(regBitS, "BitS: Top", "Bowser in the Sky Red Coins") def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): @@ -227,9 +238,30 @@ def connect_regions(world: MultiWorld, player: int, source: str, target: str, ru sourceRegion.exits.append(connection) connection.connect(targetRegion) + def create_region(name: str, player: int, world: MultiWorld) -> Region: - return Region(name, player, world) + region = Region(name, player, world) + world.regions.append(region) + return region + + +def create_subregion(source_region: Region, name: str, *locs: str) -> Region: + region = Region(name, source_region.player, source_region.multiworld) + connection = Entrance(source_region.player, name, source_region) + source_region.exits.append(connection) + connection.connect(region) + source_region.multiworld.regions.append(region) + create_locs(region, *locs) + return region + + +def set_subregion_access_rule(world, player, region_name: str, rule): + world.get_entrance(world, player, region_name).access_rule = rule + + +def create_default_locs(reg: Region, default_locs: dict): + create_locs(reg, *default_locs.keys()) + -def create_default_locs(reg: Region, locs, player): - reg_names = [name for name, id in locs.items()] - reg.locations += [SM64Location(player, loc_name, location_table[loc_name], reg) for loc_name in locs] +def create_locs(reg: Region, *locs: str): + reg.locations += [SM64Location(reg.player, loc_name, location_table[loc_name], reg) for loc_name in locs] diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index fedd5b7a6ebd..f2b8e0bcdf2d 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,36 +1,59 @@ -from ..generic.Rules import add_rule -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 +from typing import Callable, Union, Dict, Set -def shuffle_dict_keys(world, obj: dict) -> dict: - keys = list(obj.keys()) - values = list(obj.values()) +from BaseClasses import MultiWorld +from ..generic.Rules import add_rule, set_rule +from .Locations import location_table +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 +from .Items import action_item_table + +def shuffle_dict_keys(world, dictionary: dict) -> dict: + keys = list(dictionary.keys()) + values = list(dictionary.values()) world.random.shuffle(keys) - return dict(zip(keys,values)) + return dict(zip(keys, values)) -def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, - swapdict: dict, world): +def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_regions: Set[str], + swapdict: Dict[SM64Levels, str], world): if entrance_map[entrance] in invalid_regions: # Unlucky :C - replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items() + replacement_regions = [(rand_entrance, rand_region) for rand_entrance, rand_region in swapdict.items() if rand_region not in invalid_regions] - rand_region, rand_entrance = world.random.choice(replacement_regions) + rand_entrance, rand_region = world.random.choice(replacement_regions) old_dest = entrance_map[entrance] entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest - swapdict[rand_region] = entrance - swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region + swapdict[entrance], swapdict[rand_entrance] = rand_region, old_dest + swapdict.pop(entrance) -def set_rules(world, player: int, area_connections: dict): +def set_rules(world, 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 world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) + # If not shuffling later, ensure a valid start course on move randomizer + if world.AreaRandomizer[player].value < 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, world) + fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world) + if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) - randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets } + randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets} if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool - randomized_entrances = shuffle_dict_keys(world,randomized_entrances) - swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() } + randomized_entrances = shuffle_dict_keys(world, randomized_entrances) # Guarantee first entrance is a course - fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world) + swapdict = randomized_entrances.copy() + if move_rando_bitvec == 0: + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world) + 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, world) + fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world) # Guarantee BITFS is not mapped to DDD fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world) # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD. @@ -43,27 +66,34 @@ def set_rules(world, player: int, area_connections: dict): # Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()}) randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()} - + + rf = RuleFactory(world, player, move_rando_bitvec) + connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1)) connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3)) connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3)) connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12)) connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1)) - connect_regions(world, player, "Menu", randomized_entrances_s["The Secret Aquarium"], lambda state: state.has("Power Star", player, 3)) + connect_regions(world, player, randomized_entrances_s["Jolly Roger Bay"], randomized_entrances_s["The Secret Aquarium"], + rf.build_rule("SF/BF | TJ & LG | MOVELESS & TJ")) connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10)) - connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) + connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], + lambda state: state.has("Power Star", player, star_costs["FirstBowserDoorCost"])) connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"]) connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"]) connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"]) - connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) + connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], + lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"])) connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"]) - connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"]) - connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"], + rf.build_rule("GP")) + connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], + lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) @@ -72,66 +102,127 @@ def set_rules(world, player: int, area_connections: dict): connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"]) connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"]) connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"]) - connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island (Huge)") - connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island (Tiny)") + connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island") + connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island") - connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value)) + connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, star_costs["SecondFloorDoorCost"])) connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"]) connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"]) connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"]) - connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) + connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, star_costs["StarsToFinish"])) - #Special Rules for some Locations - add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player)) - add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("DDD: Pole-Jumping for Red Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player)) + # Course Rules + # Bob-omb Battlefield + rf.assign_rule("BoB: Island", "CANN | CANNLESS & WC & TJ | CAPLESS & CANNLESS & LJ") + rf.assign_rule("BoB: Mario Wings to the Sky", "CANN & WC | CAPLESS & CANN") + rf.assign_rule("BoB: Behind Chain Chomp's Gate", "GP | MOVELESS") + # Whomp's Fortress + rf.assign_rule("WF: Tower", "{{WF: Chip Off Whomp's Block}}") + rf.assign_rule("WF: Chip Off Whomp's Block", "GP") + rf.assign_rule("WF: Shoot into the Wild Blue", "WK & TJ/SF | CANN") + rf.assign_rule("WF: Fall onto the Caged Island", "CL & {WF: Tower} | MOVELESS & TJ | MOVELESS & LJ | MOVELESS & CANN") + rf.assign_rule("WF: Blast Away the Wall", "CANN | CANNLESS & LG") + # Jolly Roger Bay + rf.assign_rule("JRB: Upper", "TJ/BF/SF/WK | MOVELESS & LG") + rf.assign_rule("JRB: Red Coins on the Ship Afloat", "CL/CANN/TJ/BF/WK") + rf.assign_rule("JRB: Blast to the Stone Pillar", "CANN+CL | CANNLESS & MOVELESS | CANN & MOVELESS") + rf.assign_rule("JRB: Through the Jet Stream", "MC | CAPLESS") + # Cool, Cool Mountain + rf.assign_rule("CCM: Wall Kicks Will Work", "TJ/WK & CANN | CANNLESS & TJ/WK | MOVELESS") + # Big Boo's Haunt + rf.assign_rule("BBH: Third Floor", "WK+LG | MOVELESS & WK") + rf.assign_rule("BBH: Roof", "LJ | MOVELESS") + rf.assign_rule("BBH: Secret of the Haunted Books", "KK | MOVELESS") + rf.assign_rule("BBH: Seek the 8 Red Coins", "BF/WK/TJ/SF") + rf.assign_rule("BBH: Eye to Eye in the Secret Room", "VC") + # Haze Maze Cave + rf.assign_rule("HMC: Red Coin Area", "CL & WK/LG/BF/SF/TJ | MOVELESS & WK") + rf.assign_rule("HMC: Pit Islands", "TJ+CL | MOVELESS & WK & TJ/LJ | MOVELESS & WK+SF+LG") + rf.assign_rule("HMC: Metal-Head Mario Can Move!", "LJ+MC | CAPLESS & LJ+TJ | CAPLESS & MOVELESS & LJ/TJ/WK") + rf.assign_rule("HMC: Navigating the Toxic Maze", "WK/SF/BF/TJ") + rf.assign_rule("HMC: Watch for Rolling Rocks", "WK") + # Lethal Lava Land + rf.assign_rule("LLL: Upper Volcano", "CL") + # Shifting Sand Land + rf.assign_rule("SSL: Upper Pyramid", "CL & TJ/BF/SF/LG | MOVELESS") + rf.assign_rule("SSL: Free Flying for 8 Red Coins", "TJ/SF/BF & TJ+WC | TJ/SF/BF & CAPLESS | MOVELESS") + # Dire, Dire Docks + rf.assign_rule("DDD: Moving Poles", "CL & {{Bowser in the Fire Sea Key}} | TJ+DV+LG+WK & MOVELESS") + rf.assign_rule("DDD: Through the Jet Stream", "MC | CAPLESS") + rf.assign_rule("DDD: Collect the Caps...", "VC+MC | CAPLESS & VC") + # Snowman's Land + rf.assign_rule("SL: Snowman's Big Head", "BF/SF/CANN/TJ") + rf.assign_rule("SL: In the Deep Freeze", "WK/SF/LG/BF/CANN/TJ") + rf.assign_rule("SL: Into the Igloo", "VC & TJ/SF/BF/WK/LG | MOVELESS & VC") + # Wet-Dry World + rf.assign_rule("WDW: Top", "WK/TJ/SF/BF | MOVELESS") + rf.assign_rule("WDW: Downtown", "NAR & LG & TJ/SF/BF | CANN | MOVELESS & TJ+DV") + rf.assign_rule("WDW: Go to Town for Red Coins", "WK | MOVELESS & TJ") + rf.assign_rule("WDW: Quick Race Through Downtown!", "VC & WK/BF | VC & TJ+LG | MOVELESS & VC & TJ") + rf.assign_rule("WDW: Bob-omb Buddy", "TJ | SF+LG | NAR & BF/SF") + # Tall, Tall Mountain + rf.assign_rule("TTM: Top", "MOVELESS & TJ | LJ/DV & LG/KK | MOVELESS & WK & SF/LG | MOVELESS & KK/DV") + rf.assign_rule("TTM: Blast to the Lonely Mushroom", "CANN | CANNLESS & LJ | MOVELESS & CANNLESS") + # Tiny-Huge Island + rf.assign_rule("THI: Pipes", "NAR | LJ/TJ/DV/LG | MOVELESS & BF/SF/KK") + rf.assign_rule("THI: Large Top", "NAR | LJ/TJ/DV | MOVELESS") + rf.assign_rule("THI: Wiggler's Red Coins", "WK") + rf.assign_rule("THI: Make Wiggler Squirm", "GP | MOVELESS & DV") + # Tick Tock Clock + rf.assign_rule("TTC: Lower", "LG/TJ/SF/BF/WK") + rf.assign_rule("TTC: Upper", "CL | SF+WK") + rf.assign_rule("TTC: Top", "CL | SF+WK") + rf.assign_rule("TTC: Stomp on the Thwomp", "LG & TJ/SF/BF") + rf.assign_rule("TTC: Stop Time for Red Coins", "NAR | {TTC: Lower}") + # Rainbow Ride + rf.assign_rule("RR: Maze", "WK | LJ & SF/BF/TJ | MOVELESS & LG/TJ") + rf.assign_rule("RR: Bob-omb Buddy", "WK | MOVELESS & LG") + rf.assign_rule("RR: Swingin' in the Breeze", "LG/TJ/BF/SF") + rf.assign_rule("RR: Tricky Triangles!", "LG/TJ/BF/SF") + rf.assign_rule("RR: Cruiser", "WK/SF/BF/LG/TJ") + rf.assign_rule("RR: House", "TJ/SF/BF/LG") + rf.assign_rule("RR: Somewhere Over the Rainbow", "CANN") + # Cavern of the Metal Cap + rf.assign_rule("Cavern of the Metal Cap Red Coins", "MC | CAPLESS") + # Vanish Cap Under the Moat + rf.assign_rule("Vanish Cap Under the Moat Switch", "WK/TJ/BF/SF/LG | MOVELESS") + rf.assign_rule("Vanish Cap Under the Moat Red Coins", "TJ/BF/SF/LG/WK & VC | CAPLESS & WK") + # Bowser in the Fire Sea + rf.assign_rule("BitFS: Upper", "CL") + rf.assign_rule("Bowser in the Fire Sea Red Coins", "LG/WK") + rf.assign_rule("Bowser in the Fire Sea 1Up Block Near Poles", "LG/WK") + # Wing Mario Over the Rainbow + rf.assign_rule("Wing Mario Over the Rainbow Red Coins", "TJ+WC") + rf.assign_rule("Wing Mario Over the Rainbow 1Up Block", "TJ+WC") + # Bowser in the Sky + rf.assign_rule("BitS: Top", "CL+TJ | CL+SF+LG | MOVELESS & TJ+WK+LG") + # 100 Coin Stars if world.EnableCoinStars[player]: - add_rule(world.get_location("DDD: 100 Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player)) - add_rule(world.get_location("SL: Into the Igloo", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("RR: Somewhere Over the Rainbow", player), lambda state: state.has("Cannon Unlock RR", player)) - - if world.AreaRandomizer[player] or world.StrictCannonRequirements[player]: - # If area rando is on, it may not be possible to modify WDW's starting water level, - # which would make it impossible to reach downtown area without the cannon. - add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player)) - add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player)) - add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player)) - - if world.StrictCapRequirements[player]: - add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player)) - add_rule(world.get_location("HMC: Metal-Head Mario Can Move!", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("JRB: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("SSL: Free Flying for 8 Red Coins", player), lambda state: state.has("Wing Cap", player)) - add_rule(world.get_location("DDD: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("Cavern of the Metal Cap Red Coins", player), lambda state: state.has("Metal Cap", player)) - if world.StrictCannonRequirements[player]: - add_rule(world.get_location("WF: Blast Away the Wall", player), lambda state: state.has("Cannon Unlock WF", player)) - add_rule(world.get_location("JRB: Blast to the Stone Pillar", player), lambda state: state.has("Cannon Unlock JRB", player)) - add_rule(world.get_location("CCM: Wall Kicks Will Work", player), lambda state: state.has("Cannon Unlock CCM", player)) - add_rule(world.get_location("TTM: Blast to the Lonely Mushroom", player), lambda state: state.has("Cannon Unlock TTM", player)) - if world.StrictCapRequirements[player] and world.StrictCannonRequirements[player]: - # Ability to reach the floating island. Need some of those coins to get 100 coin star as well. - add_rule(world.get_location("BoB: Find the 8 Red Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) - add_rule(world.get_location("BoB: Shoot to the Island in the Sky", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) - if world.EnableCoinStars[player]: - add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) - - #Rules for Secret Stars - add_rule(world.get_location("Wing Mario Over the Rainbow Red Coins", player), lambda state: state.has("Wing Cap", player)) - add_rule(world.get_location("Wing Mario Over the Rainbow 1Up Block", player), lambda state: state.has("Wing Cap", player)) + rf.assign_rule("BoB: 100 Coins", "CANN & WC | CANNLESS & WC & TJ") + rf.assign_rule("WF: 100 Coins", "GP | MOVELESS") + rf.assign_rule("JRB: 100 Coins", "GP & {JRB: Upper}") + rf.assign_rule("HMC: 100 Coins", "GP") + rf.assign_rule("SSL: 100 Coins", "{SSL: Upper Pyramid} | GP") + rf.assign_rule("DDD: 100 Coins", "GP") + rf.assign_rule("SL: 100 Coins", "VC | MOVELESS") + rf.assign_rule("WDW: 100 Coins", "GP | {WDW: Downtown}") + rf.assign_rule("TTC: 100 Coins", "GP") + rf.assign_rule("THI: 100 Coins", "GP") + rf.assign_rule("RR: 100 Coins", "GP & WK") + # Castle Stars add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12)) add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25)) add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35)) - if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value: - (world.MIPS2Cost[player].value, world.MIPS1Cost[player].value) = (world.MIPS1Cost[player].value, world.MIPS2Cost[player].value) - add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value)) - add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value)) + if star_costs["MIPS1Cost"] > star_costs["MIPS2Cost"]: + (star_costs["MIPS2Cost"], star_costs["MIPS1Cost"]) = (star_costs["MIPS1Cost"], star_costs["MIPS2Cost"]) + rf.assign_rule("MIPS 1", "DV | MOVELESS") + rf.assign_rule("MIPS 2", "DV | MOVELESS") + add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS1Cost"])) + add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS2Cost"])) + + world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player) if world.CompletionType[player] == "last_bowser_stage": world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) @@ -139,3 +230,145 @@ def set_rules(world, player: int, area_connections: dict): world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \ state.can_reach("Bowser in the Fire Sea", 'Region', player) and \ state.can_reach("Bowser in the Sky", 'Region', player) + + +class RuleFactory: + + world: MultiWorld + player: int + move_rando_bitvec: bool + area_randomizer: bool + capless: bool + cannonless: bool + moveless: bool + + token_table = { + "TJ": "Triple Jump", + "LJ": "Long Jump", + "BF": "Backflip", + "SF": "Side Flip", + "WK": "Wall Kick", + "DV": "Dive", + "GP": "Ground Pound", + "KK": "Kick", + "CL": "Climb", + "LG": "Ledge Grab", + "WC": "Wing Cap", + "MC": "Metal Cap", + "VC": "Vanish Cap" + } + + class SM64LogicException(Exception): + pass + + def __init__(self, world, player, move_rando_bitvec): + self.world = world + self.player = player + self.move_rando_bitvec = move_rando_bitvec + self.area_randomizer = world.AreaRandomizer[player].value > 0 + self.capless = not world.StrictCapRequirements[player] + self.cannonless = not world.StrictCannonRequirements[player] + self.moveless = not world.StrictMoveRequirements[player] or not move_rando_bitvec > 0 + + def assign_rule(self, target_name: str, rule_expr: str): + target = self.world.get_location(target_name, self.player) if target_name in location_table else self.world.get_entrance(target_name, self.player) + cannon_name = "Cannon Unlock " + target_name.split(':')[0] + try: + rule = self.build_rule(rule_expr, cannon_name) + except RuleFactory.SM64LogicException as exception: + raise RuleFactory.SM64LogicException( + f"Error generating rule for {target_name} using rule expression {rule_expr}: {exception}") + if rule: + set_rule(target, rule) + + def build_rule(self, rule_expr: str, cannon_name: str = '') -> Callable: + expressions = rule_expr.split(" | ") + rules = [] + for expression in expressions: + or_clause = self.combine_and_clauses(expression, cannon_name) + if or_clause is True: + return None + if or_clause is not False: + rules.append(or_clause) + if rules: + if len(rules) == 1: + return rules[0] + else: + return lambda state: any(rule(state) for rule in rules) + else: + return None + + def combine_and_clauses(self, rule_expr: str, cannon_name: str) -> Union[Callable, bool]: + expressions = rule_expr.split(" & ") + rules = [] + for expression in expressions: + and_clause = self.make_lambda(expression, cannon_name) + if and_clause is False: + return False + if and_clause is not True: + rules.append(and_clause) + if rules: + if len(rules) == 1: + return rules[0] + return lambda state: all(rule(state) for rule in rules) + else: + return True + + def make_lambda(self, expression: str, cannon_name: str) -> Union[Callable, bool]: + if '+' in expression: + tokens = expression.split('+') + items = set() + for token in tokens: + item = self.parse_token(token, cannon_name) + if item is True: + continue + if item is False: + return False + items.add(item) + if items: + return lambda state: state.has_all(items, self.player) + else: + return True + if '/' in expression: + tokens = expression.split('/') + items = set() + for token in tokens: + item = self.parse_token(token, cannon_name) + if item is True: + return True + if item is False: + continue + items.add(item) + if items: + return lambda state: state.has_any(items, self.player) + else: + return False + if '{{' in expression: + return lambda state: state.can_reach(expression[2:-2], "Location", self.player) + if '{' in expression: + return lambda state: state.can_reach(expression[1:-1], "Region", self.player) + item = self.parse_token(expression, cannon_name) + if item in (True, False): + return item + return lambda state: state.has(item, self.player) + + def parse_token(self, token: str, cannon_name: str) -> Union[str, bool]: + if token == "CANN": + return cannon_name + if token == "CAPLESS": + return self.capless + if token == "CANNLESS": + return self.cannonless + if token == "MOVELESS": + return self.moveless + if token == "NAR": + return not self.area_randomizer + item = self.token_table.get(token, None) + if not item: + raise Exception(f"Invalid token: '{item}'") + if item in action_item_table: + if self.move_rando_bitvec & (1 << (action_item_table[item] - action_item_table['Double Jump'])) == 0: + # This action item is not randomized. + return True + return item + diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index ab7409a324c3..e54a4b7a9103 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -1,7 +1,7 @@ import typing import os import json -from .Items import item_table, cannon_item_table, SM64Item +from .Items import item_table, action_item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location from .Options import sm64_options from .Rules import set_rules @@ -35,14 +35,44 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 8 + data_version = 9 required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] option_definitions = sm64_options + number_of_stars: int + move_rando_bitvec: int + filler_count: int + star_costs: typing.Dict[str, int] + def generate_early(self): + max_stars = 120 + if (not self.multiworld.EnableCoinStars[self.player].value): + max_stars -= 15 + self.move_rando_bitvec = 0 + for action, itemid in action_item_table.items(): + # HACK: Disable randomization of double jump + if action == 'Double Jump': continue + if getattr(self.multiworld, f"MoveRandomizer{action.replace(' ','')}")[self.player].value: + max_stars -= 1 + self.move_rando_bitvec |= (1 << (itemid - action_item_table['Double Jump'])) + if (self.multiworld.ExclamationBoxes[self.player].value > 0): + max_stars += 29 + self.number_of_stars = min(self.multiworld.AmountOfStars[self.player].value, max_stars) + self.filler_count = max_stars - self.number_of_stars + self.star_costs = { + 'FirstBowserDoorCost': round(self.multiworld.FirstBowserStarDoorCost[self.player].value * self.number_of_stars / 100), + 'BasementDoorCost': round(self.multiworld.BasementStarDoorCost[self.player].value * self.number_of_stars / 100), + 'SecondFloorDoorCost': round(self.multiworld.SecondFloorStarDoorCost[self.player].value * self.number_of_stars / 100), + 'MIPS1Cost': round(self.multiworld.MIPS1Cost[self.player].value * self.number_of_stars / 100), + 'MIPS2Cost': round(self.multiworld.MIPS2Cost[self.player].value * self.number_of_stars / 100), + 'StarsToFinish': round(self.multiworld.StarsToFinish[self.player].value * self.number_of_stars / 100) + } + # Nudge MIPS 1 to match vanilla on default percentage + if self.number_of_stars == 120 and self.multiworld.MIPS1Cost[self.player].value == 12: + self.star_costs['MIPS1Cost'] = 15 self.topology_present = self.multiworld.AreaRandomizer[self.player].value def create_regions(self): @@ -50,7 +80,7 @@ def create_regions(self): def set_rules(self): self.area_connections = {} - set_rules(self.multiworld, self.player, self.area_connections) + set_rules(self.multiworld, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec) if self.topology_present: # Write area_connections to spoiler log for entrance, destination in self.area_connections.items(): @@ -72,31 +102,29 @@ def create_item(self, name: str) -> Item: return item def create_items(self): - starcount = self.multiworld.AmountOfStars[self.player].value - if (not self.multiworld.EnableCoinStars[self.player].value): - starcount = max(35,self.multiworld.AmountOfStars[self.player].value-15) - starcount = max(starcount, self.multiworld.FirstBowserStarDoorCost[self.player].value, - self.multiworld.BasementStarDoorCost[self.player].value, self.multiworld.SecondFloorStarDoorCost[self.player].value, - self.multiworld.MIPS1Cost[self.player].value, self.multiworld.MIPS2Cost[self.player].value, - self.multiworld.StarsToFinish[self.player].value) - self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,starcount)] - self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(starcount,120 - (15 if not self.multiworld.EnableCoinStars[self.player].value else 0))] - + # 1Up Mushrooms + self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,self.filler_count)] + # Power Stars + self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,self.number_of_stars)] + # Keys if (not self.multiworld.ProgressiveKeys[self.player].value): key1 = self.create_item("Basement Key") key2 = self.create_item("Second Floor Key") self.multiworld.itempool += [key1, key2] else: self.multiworld.itempool += [self.create_item("Progressive Key") for i in range(0,2)] - - wingcap = self.create_item("Wing Cap") - metalcap = self.create_item("Metal Cap") - vanishcap = self.create_item("Vanish Cap") - self.multiworld.itempool += [wingcap, metalcap, vanishcap] - + # Caps + self.multiworld.itempool += [self.create_item(cap_name) for cap_name in ["Wing Cap", "Metal Cap", "Vanish Cap"]] + # Cannons if (self.multiworld.BuddyChecks[self.player].value): self.multiworld.itempool += [self.create_item(name) for name, id in cannon_item_table.items()] - else: + # Moves + self.multiworld.itempool += [self.create_item(action) + for action, itemid in action_item_table.items() + if self.move_rando_bitvec & (1 << itemid - action_item_table['Double Jump'])] + + def generate_basic(self): + if not (self.multiworld.BuddyChecks[self.player].value): 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")) @@ -108,9 +136,7 @@ def create_items(self): self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI")) self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR")) - if (self.multiworld.ExclamationBoxes[self.player].value > 0): - self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,29)] - else: + if (self.multiworld.ExclamationBoxes[self.player].value == 0): self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom")) @@ -147,14 +173,10 @@ def get_filler_item_name(self) -> str: def fill_slot_data(self): return { "AreaRando": self.area_connections, - "FirstBowserDoorCost": self.multiworld.FirstBowserStarDoorCost[self.player].value, - "BasementDoorCost": self.multiworld.BasementStarDoorCost[self.player].value, - "SecondFloorDoorCost": self.multiworld.SecondFloorStarDoorCost[self.player].value, - "MIPS1Cost": self.multiworld.MIPS1Cost[self.player].value, - "MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value, - "StarsToFinish": self.multiworld.StarsToFinish[self.player].value, + "MoveRandoVec": self.move_rando_bitvec, "DeathLink": self.multiworld.death_link[self.player].value, - "CompletionType" : self.multiworld.CompletionType[self.player].value, + "CompletionType": self.multiworld.CompletionType[self.player].value, + **self.star_costs } def generate_output(self, output_directory: str):