import base64 import Utils import settings from copy import deepcopy from worlds.AutoWorld import World, WebWorld from BaseClasses import Region, Location, Item, ItemClassification, Tutorial from . import client from .rom import generate_output, SuperMarioLand2ProcedurePatch from .options import SML2Options from .locations import (locations, location_name_to_id, level_name_to_id, level_id_to_name, START_IDS, coins_coords, auto_scroll_max) from .items import items from .sprites import level_sprites from .sprite_randomizer import randomize_enemies, randomize_platforms from .logic import has_pipe_up, has_pipe_down, has_pipe_left, has_pipe_right, has_level_progression, is_auto_scroll from . import logic class MarioLand2Settings(settings.Group): class SML2RomFile(settings.UserFilePath): """File name of the Super Mario Land 2 1.0 ROM""" description = "Super Mario Land 2 - 6 Golden Coins (USA, Europe) 1.0 ROM File" copy_to = "Super Mario Land 2 - 6 Golden Coins (USA, Europe).gb" md5s = [SuperMarioLand2ProcedurePatch.hash] rom_file: SML2RomFile = SML2RomFile(SML2RomFile.copy_to) class MarioLand2WebWorld(WebWorld): setup_en = Tutorial( "Multiworld Setup Guide", "A guide to playing Super Mario Land 2 with Archipelago.", "English", "setup_en.md", "setup/en", ["Alchav"] ) tutorials = [setup_en] class MarioLand2World(World): """Super Mario Land 2 is a classic platformer that follows Mario on a quest to reclaim his castle from the villainous Wario. This iconic game features 32 levels, unique power-ups, and introduces Wario as Mario's arch-rival.""" # -ChatGPT game = "Super Mario Land 2" settings_key = "sml2_options" settings: MarioLand2Settings location_name_to_id = location_name_to_id item_name_to_id = {item_name: ID for ID, item_name in enumerate(items, START_IDS)} web = MarioLand2WebWorld() item_name_groups = { "Level Progression": { item_name for item_name in items if item_name.endswith(("Progression", "Secret", "Secret 1", "Secret 2")) and "Auto Scroll" not in item_name }, "Bells": {item_name for item_name in items if "Bell" in item_name}, "Golden Coins": {"Mario Coin", "Macro Coin", "Space Coin", "Tree Coin", "Turtle Coin", "Pumpkin Coin"}, "Coins": {"1 Coin", *{f"{i} Coins" for i in range(2, 169)}}, "Powerups": {"Mushroom", "Fire Flower", "Carrot"}, "Difficulties": {"Easy Mode", "Normal Mode"}, "Auto Scroll Traps": {item_name for item_name in items if "Auto Scroll" in item_name and "Cancel" not in item_name}, "Cancel Auto Scrolls": {item_name for item_name in items if "Cancel Auto Scroll" in item_name}, } location_name_groups = { "Bosses": { "Tree Zone 5 - Boss", "Space Zone 2 - Boss", "Macro Zone 4 - Boss", "Pumpkin Zone 4 - Boss", "Mario Zone 4 - Boss", "Turtle Zone 3 - Boss" }, "Normal Exits": {location for location in locations if locations[location]["type"] == "level"}, "Secret Exits": {location for location in locations if locations[location]["type"] == "secret"}, "Bells": {location for location in locations if locations[location]["type"] == "bell"}, "Coins": {location for location in location_name_to_id if "Coin" in location} } options_dataclass = SML2Options options: SML2Options generate_output = generate_output def __init__(self, world, player: int): super().__init__(world, player) self.auto_scroll_levels = [] self.num_coin_locations = [] self.max_coin_locations = {} self.sprite_data = {} self.coin_fragments_required = 0 def generate_early(self): self.sprite_data = deepcopy(level_sprites) if self.options.randomize_enemies: randomize_enemies(self.sprite_data, self.random) if self.options.randomize_platforms: randomize_platforms(self.sprite_data, self.random) if self.options.marios_castle_midway_bell: self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell" if self.options.auto_scroll_chances == "vanilla": self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)] else: self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances) for _ in range(32)] self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0 unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"] if not self.options.shuffle_midway_bells: unbeatable_scroll_levels.append("Pumpkin Zone 1") for level, i in enumerate(self.auto_scroll_levels): if i == 1: if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"): self.auto_scroll_levels[level] = 2 elif self.options.auto_scroll_mode == "chaos": if (self.options.accessibility == "full" and level_id_to_name[level] in unbeatable_scroll_levels): self.auto_scroll_levels[level] = 2 else: self.auto_scroll_levels[level] = self.random.randint(1, 3) elif (self.options.accessibility == "full" and level_id_to_name[level] in unbeatable_scroll_levels): self.auto_scroll_levels[level] = 0 if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key: self.auto_scroll_levels[level] = 3 def create_regions(self): menu_region = Region("Menu", self.player, self.multiworld) self.multiworld.regions.append(menu_region) created_regions = [] for location_name, data in locations.items(): region_name = location_name.split(" -")[0] if region_name in created_regions: region = self.multiworld.get_region(region_name, self.player) else: region = Region(region_name, self.player, self.multiworld) if region_name == "Tree Zone Secret Course": region_to_connect = self.multiworld.get_region("Tree Zone 2", self.player) elif region_name == "Space Zone Secret Course": region_to_connect = self.multiworld.get_region("Space Zone 1", self.player) elif region_name == "Macro Zone Secret Course": region_to_connect = self.multiworld.get_region("Macro Zone 1", self.player) elif region_name == "Pumpkin Zone Secret Course 1": region_to_connect = self.multiworld.get_region("Pumpkin Zone 2", self.player) elif region_name == "Pumpkin Zone Secret Course 2": region_to_connect = self.multiworld.get_region("Pumpkin Zone 3", self.player) elif region_name == "Turtle Zone Secret Course": region_to_connect = self.multiworld.get_region("Turtle Zone 2", self.player) elif region_name.split(" ")[-1].isdigit() and int(region_name.split(" ")[-1]) > 1: region_to_connect = self.multiworld.get_region(" ".join(region_name.split(" ")[:2]) + f" {int(region_name.split(' ')[2]) - 1}", self.player) else: region_to_connect = menu_region region_to_connect.connect(region) self.multiworld.regions.append(region) created_regions.append(region_name) if location_name == "Mario's Castle - Midway Bell" and not self.options.marios_castle_midway_bell: continue region.locations.append(MarioLand2Location(self.player, location_name, self.location_name_to_id[location_name], region)) self.multiworld.get_region("Macro Zone Secret Course", self.player).connect( self.multiworld.get_region("Macro Zone 4", self.player)) self.multiworld.get_region("Macro Zone 4", self.player).connect( self.multiworld.get_region("Macro Zone Secret Course", self.player)) castle = self.multiworld.get_region("Mario's Castle", self.player) wario = MarioLand2Location(self.player, "Mario's Castle - Wario", parent=castle) castle.locations.append(wario) wario.place_locked_item(MarioLand2Item("Wario Defeated", ItemClassification.progression, None, self.player)) if self.options.coinsanity: coinsanity_checks = self.options.coinsanity_checks.value self.num_coin_locations = [[region, 1] for region in created_regions if region != "Mario's Castle"] self.max_coin_locations = {region: len(coins_coords[region]) for region in created_regions if region != "Mario's Castle"} if self.options.accessibility == "full" or self.options.auto_scroll_mode == "always": for level in self.max_coin_locations: if level in auto_scroll_max and self.auto_scroll_levels[level_name_to_id[level]] in (1, 3): if isinstance(auto_scroll_max[level], tuple): self.max_coin_locations[level] = min( auto_scroll_max[level][int(self.options.shuffle_midway_bells.value)], self.max_coin_locations[level]) else: self.max_coin_locations[level] = min(auto_scroll_max[level], self.max_coin_locations[level]) coinsanity_checks = min(sum(self.max_coin_locations.values()), coinsanity_checks) for i in range(coinsanity_checks - 31): self.num_coin_locations.sort(key=lambda region: self.max_coin_locations[region[0]] / region[1]) self.num_coin_locations[-1][1] += 1 coin_locations = [] for level, coins in self.num_coin_locations: if self.max_coin_locations[level]: coin_thresholds = self.random.sample(range(1, self.max_coin_locations[level] + 1), coins) coin_locations += [f"{level} - {i} Coin{'s' if i > 1 else ''}" for i in coin_thresholds] for location_name in coin_locations: region = self.multiworld.get_region(location_name.split(" -")[0], self.player) region.locations.append(MarioLand2Location(self.player, location_name, self.location_name_to_id[location_name], parent=region)) def set_rules(self): entrance_rules = { "Menu -> Space Zone 1": lambda state: state.has("Hippo Bubble", self.player) or (state.has("Carrot", self.player) and not is_auto_scroll(state, self.player, "Hippo Zone")), "Space Zone 1 -> Space Zone Secret Course": lambda state: state.has("Space Zone Secret", self.player), "Space Zone 1 -> Space Zone 2": lambda state: has_level_progression(state, "Space Zone Progression", self.player), "Tree Zone 1 -> Tree Zone 2": lambda state: has_level_progression(state, "Tree Zone Progression", self.player), "Tree Zone 2 -> Tree Zone Secret Course": lambda state: state.has("Tree Zone Secret", self.player), "Tree Zone 2 -> Tree Zone 3": lambda state: has_level_progression(state, "Tree Zone Progression", self.player, 2), "Tree Zone 4 -> Tree Zone 5": lambda state: has_level_progression(state, "Tree Zone Progression", self.player, 3), "Macro Zone 1 -> Macro Zone Secret Course": lambda state: state.has("Macro Zone Secret 1", self.player), "Macro Zone Secret Course -> Macro Zone 4": lambda state: state.has("Macro Zone Secret 2", self.player), "Macro Zone 1 -> Macro Zone 2": lambda state: has_level_progression(state, "Macro Zone Progression", self.player), "Macro Zone 2 -> Macro Zone 3": lambda state: has_level_progression(state, "Macro Zone Progression", self.player, 2), "Macro Zone 3 -> Macro Zone 4": lambda state: has_level_progression(state, "Macro Zone Progression", self.player, 3), "Macro Zone 4 -> Macro Zone Secret Course": lambda state: state.has("Macro Zone Secret 2", self.player), "Pumpkin Zone 1 -> Pumpkin Zone 2": lambda state: has_level_progression(state, "Pumpkin Zone Progression", self.player), "Pumpkin Zone 2 -> Pumpkin Zone Secret Course 1": lambda state: state.has("Pumpkin Zone Secret 1", self.player), "Pumpkin Zone 2 -> Pumpkin Zone 3": lambda state: has_level_progression(state, "Pumpkin Zone Progression", self.player, 2), "Pumpkin Zone 3 -> Pumpkin Zone Secret Course 2": lambda state: state.has("Pumpkin Zone Secret 2", self.player), "Pumpkin Zone 3 -> Pumpkin Zone 4": lambda state: has_level_progression(state, "Pumpkin Zone Progression", self.player, 3), "Mario Zone 1 -> Mario Zone 2": lambda state: has_level_progression(state, "Mario Zone Progression", self.player), "Mario Zone 2 -> Mario Zone 3": lambda state: has_level_progression(state, "Mario Zone Progression", self.player, 2), "Mario Zone 3 -> Mario Zone 4": lambda state: has_level_progression(state, "Mario Zone Progression", self.player, 3), "Turtle Zone 1 -> Turtle Zone 2": lambda state: has_level_progression(state, "Turtle Zone Progression", self.player), "Turtle Zone 2 -> Turtle Zone Secret Course": lambda state: state.has("Turtle Zone Secret", self.player), "Turtle Zone 2 -> Turtle Zone 3": lambda state: has_level_progression(state, "Turtle Zone Progression", self.player, 2), } if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt": # Require the other coins just to ensure they are being added to start inventory properly, # and so they show up in Playthrough as required entrance_rules["Menu -> Mario's Castle"] = lambda state: (state.has_all( ["Tree Coin", "Space Coin", "Macro Coin", "Pumpkin Coin", "Turtle Coin"], self.player) and state.has("Mario Coin Fragment", self.player, self.coin_fragments_required)) else: entrance_rules["Menu -> Mario's Castle"] = lambda state: state.has_from_list_unique([ "Tree Coin", "Space Coin", "Macro Coin", "Pumpkin Coin", "Mario Coin", "Turtle Coin" ], self.player, self.options.required_golden_coins) for entrance, rule in entrance_rules.items(): self.multiworld.get_entrance(entrance, self.player).access_rule = rule for location in self.multiworld.get_locations(self.player): if location.name.endswith(("Coins", "Coin")): rule = getattr(logic, location.parent_region.name.lower().replace(" ", "_") + "_coins", None) if rule: coins = int(location.name.split(" ")[-2]) location.access_rule = lambda state, coin_rule=rule, num_coins=coins: \ coin_rule(state, self.player, num_coins) else: rule = getattr(logic, location.name.lower().replace( " - ", "_").replace(" ", "_").replace("'", ""), None) if rule: location.access_rule = lambda state, loc_rule=rule: loc_rule(state, self.player) self.multiworld.completion_condition[self.player] = lambda state: state.has("Wario Defeated", self.player) def create_items(self): item_counts = { "Space Zone Progression": 1, "Space Zone Secret": 1, "Tree Zone Progression": 3, "Tree Zone Secret": 1, "Macro Zone Progression": 3, "Macro Zone Secret 1": 1, "Macro Zone Secret 2": 1, "Pumpkin Zone Progression": 3, "Pumpkin Zone Secret 1": 1, "Pumpkin Zone Secret 2": 1, "Mario Zone Progression": 3, "Turtle Zone Progression": 2, "Turtle Zone Secret": 1, "Mushroom": 1, "Fire Flower": 1, "Carrot": 1, "Space Physics": 1, "Hippo Bubble": 1, "Water Physics": 1, "Super Star Duration Increase": 2, "Mario Coin Fragment": 0, } if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt": # There are 5 Zone Progression items that can be condensed. item_counts["Mario Coin Fragment"] = 1 + ((5 * self.options.mario_coin_fragment_percentage) // 100) if self.options.coinsanity: coin_count = sum([level[1] for level in self.num_coin_locations]) max_coins = sum(self.max_coin_locations.values()) if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt": removed_coins = (coin_count * self.options.mario_coin_fragment_percentage) // 100 coin_count -= removed_coins item_counts["Mario Coin Fragment"] += removed_coins # Randomly remove some coin items for variety coin_count -= (coin_count // self.random.randint(100, max(100, coin_count))) if coin_count: coin_bundle_sizes = [max_coins // coin_count] * coin_count remainder = max_coins - sum(coin_bundle_sizes) for i in range(remainder): coin_bundle_sizes[i] += 1 for a, b in zip(range(1, len(coin_bundle_sizes), 2), range(2, len(coin_bundle_sizes), 2)): split = self.random.randint(1, coin_bundle_sizes[a] + coin_bundle_sizes[b] - 1) coin_bundle_sizes[a], coin_bundle_sizes[b] = split, coin_bundle_sizes[a] + coin_bundle_sizes[b] - split for coin_bundle_size in coin_bundle_sizes: item_name = f"{coin_bundle_size} Coin{'s' if coin_bundle_size > 1 else ''}" if item_name in item_counts: item_counts[item_name] += 1 else: item_counts[item_name] = 1 if self.options.shuffle_golden_coins == "shuffle": for item in self.item_name_groups["Golden Coins"]: item_counts[item] = 1 elif self.options.shuffle_golden_coins == "mario_coin_fragment_hunt": for item in ("Tree Coin", "Space Coin", "Macro Coin", "Pumpkin Coin", "Turtle Coin"): self.multiworld.push_precollected(self.create_item(item)) else: for item, location_name in ( ("Mario Coin", "Mario Zone 4 - Boss"), ("Tree Coin", "Tree Zone 5 - Boss"), ("Space Coin", "Space Zone 2 - Boss"), ("Macro Coin", "Macro Zone 4 - Boss"), ("Pumpkin Coin", "Pumpkin Zone 4 - Boss"), ("Turtle Coin", "Turtle Zone 3 - Boss") ): location = self.multiworld.get_location(location_name, self.player) location.place_locked_item(self.create_item(item)) location.address = None location.item.code = None if self.options.shuffle_midway_bells: for item in [item for item in items if "Midway Bell" in item]: if item != "Mario's Castle Midway Bell" or self.options.marios_castle_midway_bell: item_counts[item] = 1 if self.options.difficulty_mode == "easy_to_normal": item_counts["Normal Mode"] = 1 elif self.options.difficulty_mode == "normal_to_easy": item_counts["Easy Mode"] = 1 if self.options.shuffle_pipe_traversal == "single": item_counts["Pipe Traversal"] = 1 elif self.options.shuffle_pipe_traversal == "split": item_counts["Pipe Traversal - Right"] = 1 item_counts["Pipe Traversal - Left"] = 1 item_counts["Pipe Traversal - Up"] = 1 item_counts["Pipe Traversal - Down"] = 1 else: self.multiworld.push_precollected(self.create_item("Pipe Traversal")) if any(self.auto_scroll_levels): if self.options.auto_scroll_mode == "global_trap_item": item_counts["Auto Scroll"] = 1 elif self.options.auto_scroll_mode == "global_cancel_item": item_counts["Cancel Auto Scroll"] = 1 else: for level, i in enumerate(self.auto_scroll_levels): if i == 3: item_counts[f"Auto Scroll - {level_id_to_name[level]}"] = 1 elif i == 2: item_counts[f"Cancel Auto Scroll - {level_id_to_name[level]}"] = 1 for item in self.multiworld.precollected_items[self.player]: if item.name in item_counts and item_counts[item.name] > 0: item_counts[item.name] -= 1 location_count = len(self.multiworld.get_unfilled_locations(self.player)) items_to_add = location_count - sum(item_counts.values()) if items_to_add > 0: mario_coin_frags = 0 if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt": mario_coin_frags = (items_to_add * self.options.mario_coin_fragment_percentage) // 100 item_counts["Mario Coin Fragment"] += mario_coin_frags item_counts["Super Star Duration Increase"] += items_to_add - mario_coin_frags elif items_to_add < 0: if self.options.coinsanity: for i in range(1, 168): coin_name = f"{i} Coin{'s' if i > 1 else ''}" if coin_name in item_counts: amount_to_remove = min(-items_to_add, item_counts[coin_name]) item_counts[coin_name] -= amount_to_remove items_to_add += amount_to_remove if items_to_add >= 0: break double_progression_items = ["Tree Zone Progression", "Macro Zone Progression", "Pumpkin Zone Progression", "Mario Zone Progression", "Turtle Zone Progression"] self.random.shuffle(double_progression_items) while sum(item_counts.values()) > location_count: if double_progression_items: double_progression_item = double_progression_items.pop() item_counts[double_progression_item] -= 2 item_counts[double_progression_item + " x2"] = 1 continue if self.options.auto_scroll_mode in ("level_trap_items", "level_cancel_items", "chaos"): auto_scroll_item = self.random.choice([item for item in item_counts if "Auto Scroll" in item]) level = auto_scroll_item.split("- ")[1] self.auto_scroll_levels[level_name_to_id[level]] = 0 del item_counts[auto_scroll_item] continue raise Exception(f"Too many items in the item pool for Super Mario Land 2 player {self.player_name}") # item = self.random.choice(list(item_counts)) # item_counts[item] -= 1 # if item_counts[item] == 0: # del item_counts[item] # self.multiworld.push_precollected(self.create_item(item)) self.coin_fragments_required = max((item_counts["Mario Coin Fragment"] * self.options.mario_coin_fragments_required_percentage) // 100, 1) for item_name, count in item_counts.items(): self.multiworld.itempool += [self.create_item(item_name) for _ in range(count)] def fill_slot_data(self): return { "energy_link": self.options.energy_link.value } def create_item(self, name: str) -> Item: return MarioLand2Item(name, items[name], self.item_name_to_id[name], self.player) def get_filler_item_name(self): return "1 Coin" def modify_multidata(self, multidata: dict): rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', 'utf8')[:21] rom_name.extend([0] * (21 - len(rom_name))) new_name = base64.b64encode(bytes(rom_name)).decode() multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name] class MarioLand2Location(Location): game = "Super Mario Land 2" class MarioLand2Item(Item): game = "Super Mario Land 2"