diff --git a/BaseClasses.py b/BaseClasses.py index 9c583350..7717cc77 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -24,24 +24,13 @@ class MultiWorld(): plando_connections: List[PlandoConnection] er_seeds: Dict[int, str] - def __init__(self, players: int, shuffle, logic, mode, swords, difficulty, item_functionality, timer, - progressive, - goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): + def __init__(self, players: int): self.random = random.Random() # world-local random state is saved in case of future use a # persistently running program with multiple worlds rolling concurrently self.players = players self.teams = 1 - self.shuffle = shuffle.copy() - self.logic = logic.copy() - self.mode = mode.copy() - self.swords = swords.copy() - self.difficulty = difficulty.copy() - self.item_functionality = item_functionality.copy() - self.timer = timer.copy() - self.progressive = progressive - self.goal = goal.copy() - self.algorithm = algorithm + self.algorithm = 'balanced' self.dungeons = [] self.regions = [] self.shops = [] @@ -60,13 +49,9 @@ class MultiWorld(): self.aga_randomness = True self.lock_aga_door_in_escape = False self.save_and_quit_from_boss = True - self.accessibility = accessibility.copy() - self.shuffle_ganon = shuffle_ganon - self.fix_gtower_exit = self.shuffle_ganon - self.retro = retro.copy() - self.custom = custom - self.customitemarray: List[int] = customitemarray - self.hints = hints.copy() + self.custom = False + self.customitemarray = [] + self.shuffle_ganon = True self.dynamic_regions = [] self.dynamic_locations = [] self.spoiler = Spoiler(self) @@ -75,6 +60,18 @@ class MultiWorld(): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val set_player_attr('_region_cache', {}) + set_player_attr('shuffle', "vanilla") + set_player_attr('logic', "noglitches") + set_player_attr('mode', 'open') + set_player_attr('swords', 'random') + set_player_attr('difficulty', 'normal') + set_player_attr('item_functionality', 'normal') + set_player_attr('timer', False) + set_player_attr('goal', 'ganon') + set_player_attr('progressive', 'on') + set_player_attr('accessibility', 'items') + set_player_attr('retro', False) + set_player_attr('hints', True) set_player_attr('player_names', []) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) @@ -144,6 +141,10 @@ class MultiWorld(): #for i in range(players): # self.worlds.append(worlds.alttp.ALTTPWorld({}, i)) + def copy(self) -> MultiWorld: + import copy + return copy.deepcopy(self) + def secure(self): self.random = secrets.SystemRandom() diff --git a/Gui.py b/Gui.py index 69ae2f26..e0a7d1e1 100755 --- a/Gui.py +++ b/Gui.py @@ -153,7 +153,7 @@ def guiMain(args=None): goalFrame = Frame(drowDownFrame) goalVar = StringVar() goalVar.set('ganon') - goalOptionMenu = OptionMenu(goalFrame, goalVar, 'ganon', 'pedestal', 'dungeons', 'triforcehunt', + goalOptionMenu = OptionMenu(goalFrame, goalVar, 'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal') goalOptionMenu.pack(side=RIGHT) goalLabel = Label(goalFrame, text='Game goal') diff --git a/Main.py b/Main.py index d68abde5..07900f01 100644 --- a/Main.py +++ b/Main.py @@ -56,9 +56,7 @@ def main(args, seed=None): start = time.perf_counter() # initialize the world - world = MultiWorld(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, - args.item_functionality, args.timer, args.progressive.copy(), args.goal, args.algorithm, - args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) + world = MultiWorld(args.multi) logger = logging.getLogger('') world.seed = get_seed(seed) @@ -67,6 +65,26 @@ def main(args, seed=None): else: world.random.seed(world.seed) + world.shuffle = args.shuffle.copy() + world.logic = args.logic.copy() + world.mode = args.mode.copy() + world.swords = args.swords.copy() + world.difficulty = args.difficulty.copy() + world.item_functionality = args.item_functionality.copy() + world.timer = args.timer.copy() + world.progressive = args.progressive.copy() + world.goal = args.goal.copy() + if hasattr(args, "algorithm"): # current GUI options + world.algorithm = args.algorithm + world.shuffleganon = args.shuffleganon + world.custom = args.custom + world.customitemarray = args.customitemarray + + world.accessibility = args.accessibility.copy() + world.retro = args.retro.copy() + + world.hints = args.hints.copy() + world.remote_items = args.remote_items.copy() world.mapshuffle = args.mapshuffle.copy() world.compassshuffle = args.compassshuffle.copy() @@ -100,14 +118,14 @@ def main(args, seed=None): world.triforce_pieces_required = args.triforce_pieces_required.copy() world.shop_shuffle = args.shop_shuffle.copy() world.shop_shuffle_slots = args.shop_shuffle_slots.copy() - world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()} + world.progression_balancing = args.progression_balancing.copy world.shuffle_prizes = args.shuffle_prizes.copy() world.sprite_pool = args.sprite_pool.copy() world.dark_room_logic = args.dark_room_logic.copy() world.plando_items = args.plando_items.copy() world.plando_texts = args.plando_texts.copy() world.plando_connections = args.plando_connections.copy() - world.er_seeds = args.er_seeds.copy() + world.er_seeds = getattr(args, "er_seeds", {}) world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy() world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() @@ -124,8 +142,6 @@ def main(args, seed=None): world.er_seeds[player] = "vanilla" elif seed.startswith("group-"): # renamed from team to group to not confuse with existing team name use world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player])) - elif seed.startswith("team-"): # TODO: remove on breaking_changes - world.er_seeds[player] = get_same_seed(world, (shuffle, seed, world.retro[player], world.mode[player], world.logic[player])) elif not args.race: world.er_seeds[player] = seed elif seed: # race but with a set seed, ignore set seed and use group logic instead @@ -155,11 +171,6 @@ def main(args, seed=None): item = ItemFactory(tok.strip(), player) if item: world.push_precollected(item) - # item in item_table gets checked in mystery, but not CLI - so we double-check here - world.local_items[player] = {item.strip() for item in args.local_items[player].split(',') if - item.strip() in item_table} - world.non_local_items[player] = {item.strip() for item in args.non_local_items[player].split(',') if - item.strip() in item_table} # enforce pre-defined local items. if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: @@ -255,7 +266,7 @@ def main(args, seed=None): logger.info('Placing Dungeon Items.') - if args.algorithm in ['balanced', 'vt26'] or any( + if world.algorithm in ['balanced', 'vt26'] or any( list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())): fill_dungeons_restrictive(world) @@ -264,13 +275,13 @@ def main(args, seed=None): logger.info('Fill the world.') - if args.algorithm == 'flood': + if world.algorithm == 'flood': flood_items(world) # different algo, biased towards early game progress items - elif args.algorithm == 'vt25': + elif world.algorithm == 'vt25': distribute_items_restrictive(world, False) - elif args.algorithm == 'vt26': + elif world.algorithm == 'vt26': distribute_items_restrictive(world, True) - elif args.algorithm == 'balanced': + elif world.algorithm == 'balanced': distribute_items_restrictive(world, True) logger.info("Filling Shop Slots") @@ -517,6 +528,7 @@ def main(args, seed=None): def copy_world(world): # ToDo: Not good yet + # delete now? ret = MultiWorld(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.item_functionality, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) @@ -663,7 +675,7 @@ def copy_dynamic_regions_and_locations(world, ret): def create_playthrough(world): # create a copy as we will modify it old_world = world - world = copy_world(world) + world = world.copy() # get locations containing progress items prog_locations = {location for location in world.get_filled_locations() if location.item.advancement} diff --git a/Mystery.py b/Mystery.py index 24f9d4cc..5d673ae5 100644 --- a/Mystery.py +++ b/Mystery.py @@ -13,10 +13,11 @@ from worlds.generic import PlandoItem, PlandoConnection ModuleUpdate.update() from Utils import parse_yaml -from worlds.alttp.Rom import Sprite from worlds.alttp.EntranceRandomizer import parse_arguments from Main import main as ERmain from Main import get_seed, seeddigits +import Options +from worlds import lookup_any_item_name_to_id from worlds.alttp.Items import item_name_groups, item_table from worlds.alttp import Bosses from worlds.alttp.Text import TextTable @@ -173,16 +174,13 @@ def main(args=None, callback=ERmain): weights_cache[path][key] = option name_counter = Counter() + erargs.player_settings = {} for player in range(1, args.multi + 1): path = player_path_cache[player] if path: try: settings = settings_cache[path] if settings_cache[path] else \ roll_settings(weights_cache[path], args.plando) - if settings.sprite and not os.path.isfile(settings.sprite) and not Sprite.get_sprite_from_name( - settings.sprite): - logging.warning( - f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.") if args.pre_roll: import yaml if path == args.weights: @@ -206,7 +204,10 @@ def main(args=None, callback=ERmain): yaml.dump(pre_rolled, f) for k, v in vars(settings).items(): if v is not None: - getattr(erargs, k)[player] = v + try: + getattr(erargs, k)[player] = v + except AttributeError: + setattr(erargs, k, {player: v}) except Exception as e: raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e else: @@ -251,9 +252,6 @@ def main(args=None, callback=ERmain): with open(os.path.join(args.outputpath if args.outputpath else ".", f"mystery_result_{seed}.yaml"), "wt") as f: yaml.dump(important, f) - erargs.skip_progression_balancing = {player: not balanced for player, balanced in - erargs.progression_balancing.items()} - del (erargs.progression_balancing) callback(erargs, seed) @@ -311,14 +309,10 @@ available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if leve boss_shuffle_options = {None: 'none', 'none': 'none', - 'simple': 'basic', 'basic': 'basic', - 'full': 'normal', 'normal': 'normal', - 'random': 'chaos', 'chaos': 'chaos', - 'singularity': 'singularity', - 'duality': 'singularity' + 'singularity': 'singularity' } @@ -420,7 +414,7 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.") -def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses"))): +def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses", ))): if "pre_rolled" in weights: pre_rolled = weights["pre_rolled"] @@ -459,9 +453,37 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b ret = argparse.Namespace() ret.name = get_choice('name', weights) - + ret.accessibility = get_choice('accessibility', weights) ret.game = get_choice("game", weights, "A Link to the Past") + ret.local_items = set() + for item_name in weights.get('local_items', []): + items = item_name_groups.get(item_name, {item_name}) + for item in items: + if item in lookup_any_item_name_to_id: + ret.local_items.add(item) + else: + raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.") + + ret.non_local_items = set() + for item_name in weights.get('non_local_items', []): + items = item_name_groups.get(item_name, {item_name}) + for item in items: + if item in lookup_any_item_name_to_id: + ret.non_local_items.add(item) + else: + raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.") + + if ret.game == "A Link to the Past": + roll_alttp_settings(ret, weights, plando_options) + elif ret.game == "Hollow Knight": + for option_name, option in Options.hollow_knight_options.items(): + setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True))) + else: + raise Exception(f"Unsupported game {ret.game}") + return ret + +def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): glitches_required = get_choice('glitches_required', weights) if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']: logging.warning("Only NMG, OWG and No Logic supported") @@ -500,8 +522,6 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b 'universal' if 'u' in dungeon_items else 's' in dungeon_items) ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items) - ret.accessibility = get_choice('accessibility', weights) - entrance_shuffle = get_choice('entrance_shuffle', weights, 'vanilla') if entrance_shuffle.startswith('none-'): ret.shuffle = 'vanilla' @@ -510,12 +530,11 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b goal = get_choice('goals', weights, 'ganon') ret.goal = {'ganon': 'ganon', - 'fast_ganon': 'crystals', - 'dungeons': 'dungeons', + 'crystals': 'crystals', + 'bosses': 'bosses', 'pedestal': 'pedestal', 'ganon_pedestal': 'ganonpedestal', 'triforce_hunt': 'triforcehunt', - 'triforce-hunt': 'triforcehunt', # deprecated, moving all goals to `_` 'local_triforce_hunt': 'localtriforcehunt', 'ganon_triforce_hunt': 'ganontriforcehunt', 'local_ganon_triforce_hunt': 'localganontriforcehunt', @@ -582,36 +601,12 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b boss_shuffle = get_choice('boss_shuffle', weights) ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options) - ret.enemy_shuffle = {'none': False, - 'shuffled': 'shuffled', - 'random': 'chaos', - 'chaosthieves': 'chaosthieves', - 'chaos': 'chaos', - True: True, - False: False, - None: False - }[get_choice('enemy_shuffle', weights, False)] + ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False)) ret.killable_thieves = get_choice('killable_thieves', weights, False) ret.tile_shuffle = get_choice('tile_shuffle', weights, False) ret.bush_shuffle = get_choice('bush_shuffle', weights, False) - # legacy support for enemy shuffle - if type(ret.enemy_shuffle) == str: - if ret.enemy_shuffle == 'shuffled': - ret.killable_thieves = True - elif ret.enemy_shuffle == 'chaos': - ret.killable_thieves = True - ret.bush_shuffle = True - ret.tile_shuffle = True - elif ret.enemy_shuffle == "chaosthieves": - ret.killable_thieves = bool(random.randint(0, 1)) - ret.bush_shuffle = True - ret.tile_shuffle = True - ret.enemy_shuffle = True - - # end of legacy block - ret.enemy_damage = {None: 'default', 'default': 'default', 'shuffled': 'shuffled', @@ -648,7 +643,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b get_choice("turtle_rock_medallion", weights, "random")] for index, medallion in enumerate(ret.required_medallions): - ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"}\ + ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \ .get(medallion.lower(), None) if not ret.required_medallions[index]: raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}") @@ -672,30 +667,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b if get_choice("local_keys", weights, "l" in dungeon_items): # () important for ordering of commands, without them the Big Keys section is part of the Small Key else - ret.local_items = (item_name_groups["Small Keys"] if ret.keyshuffle else set()) \ - | item_name_groups["Big Keys"] if ret.bigkeyshuffle else set() - else: - ret.local_items = set() - for item_name in weights.get('local_items', []): - items = item_name_groups.get(item_name, {item_name}) - for item in items: - if item in item_table: - ret.local_items.add(item) - else: - raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.") - - ret.local_items = ",".join(ret.local_items) - - ret.non_local_items = set() - for item_name in weights.get('non_local_items', []): - items = item_name_groups.get(item_name, {item_name}) - for item in items: - if item in item_table: - ret.non_local_items.add(item) - else: - raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.") - - ret.non_local_items = ",".join(ret.non_local_items) + ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set() + ret.local_items |= item_name_groups["Big Keys"] if ret.bigkeyshuffle else set() ret.plando_items = [] if "items" in plando_options: @@ -704,7 +677,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b if item not in item_table: raise Exception(f"Could not plando item {item} as the item was not recognized") if location not in location_table and location not in key_drop_data: - raise Exception(f"Could not plando item {item} at location {location} as the location was not recognized") + raise Exception( + f"Could not plando item {item} at location {location} as the location was not recognized") ret.plando_items.append(PlandoItem(item, location, location_world, from_pool, force)) options = weights.get("plando_items", []) @@ -798,8 +772,6 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b else: ret.quickswap = True ret.sprite = "Link" - return ret - if __name__ == '__main__': main() diff --git a/Options.py b/Options.py index 7a9259a1..04c21e3a 100644 --- a/Options.py +++ b/Options.py @@ -39,6 +39,10 @@ class Option(metaclass=AssembleOptions): def __bool__(self): return bool(self.value) + @classmethod + def from_any(cls, data: typing.Any): + raise NotImplementedError + class Toggle(Option): option_false = 0 @@ -54,6 +58,13 @@ class Toggle(Option): else: return cls(1) + @classmethod + def from_any(cls, data: typing.Any): + if type(data) == str: + return cls.from_text(data) + else: + return cls(data) + def __eq__(self, other): if isinstance(other, Toggle): return self.value == other.value @@ -75,7 +86,6 @@ class Choice(Option): @classmethod def from_text(cls, text: str) -> Choice: - for optionname, value in cls.options.items(): if optionname == text.lower(): return cls(value) @@ -83,6 +93,10 @@ class Choice(Option): f'Could not find option "{text}" for "{cls.__name__}", ' f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}') + @classmethod + def from_any(cls, data: typing.Any): + return cls.from_text(data) + class Logic(Choice): option_no_glitches = 0 @@ -151,6 +165,52 @@ keyshuffle = Toggle bigkeyshuffle = Toggle hints = Toggle +RandomizeDreamers = Toggle +RandomizeSkills = Toggle +RandomizeCharms = Toggle +RandomizeKeys = Toggle +RandomizeGeoChests = Toggle +RandomizeMaskShards = Toggle +RandomizeVesselFragments = Toggle +RandomizeCharmNotches = Toggle +RandomizePaleOre = Toggle +RandomizeRancidEggs = Toggle +RandomizeRelics = Toggle +RandomizeMaps = Toggle +RandomizeStags = Toggle +RandomizeGrubs = Toggle +RandomizeWhisperingRoots = Toggle +RandomizeRocks = Toggle +RandomizeSoulTotems = Toggle +RandomizePalaceTotems = Toggle +RandomizeLoreTablets = Toggle +RandomizeLifebloodCocoons = Toggle + +hollow_knight_randomize_options: typing.Dict[str, Option] = { + "RandomizeDreamers" : RandomizeDreamers, + "RandomizeSkills" : RandomizeSkills, + "RandomizeCharms" : RandomizeCharms, + "RandomizeKeys" : RandomizeKeys, + "RandomizeGeoChests" : RandomizeGeoChests, + "RandomizeMaskShards" : RandomizeMaskShards, + "RandomizeVesselFragments" : RandomizeVesselFragments, + "RandomizeCharmNotches" : RandomizeCharmNotches, + "RandomizePaleOre" : RandomizePaleOre, + "RandomizeRancidEggs" : RandomizeRancidEggs, + "RandomizeRelics" : RandomizeRelics, + "RandomizeMaps" : RandomizeMaps, + "RandomizeStags" : RandomizeStags, + "RandomizeGrubs" : RandomizeGrubs, + "RandomizeWhisperingRoots" : RandomizeWhisperingRoots, + "RandomizeRocks" : RandomizeRocks, + "RandomizeSoulTotems" : RandomizeSoulTotems, + "RandomizePalaceTotems" : RandomizePalaceTotems, + "RandomizeLoreTablets" : RandomizeLoreTablets, + "RandomizeLifebloodCocoons" : RandomizeLifebloodCocoons, +} + +hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options} + if __name__ == "__main__": import argparse diff --git a/WebHostLib/static/static/playerSettings.json b/WebHostLib/static/static/playerSettings.json index f2d16169..ced97395 100644 --- a/WebHostLib/static/static/playerSettings.json +++ b/WebHostLib/static/static/playerSettings.json @@ -27,11 +27,11 @@ }, { "name": "Fast Ganon (Pyramid Always Open)", - "value": "fast_ganon" + "value": "crystals" }, { - "name": "All Dungeons", - "value": "dungeons" + "name": "All Bosses", + "value": "bosses" }, { "name": "Master Sword Pedestal", diff --git a/WebHostLib/static/static/weightedSettings.json b/WebHostLib/static/static/weightedSettings.json index a9bd1fca..7121247e 100644 --- a/WebHostLib/static/static/weightedSettings.json +++ b/WebHostLib/static/static/weightedSettings.json @@ -356,9 +356,9 @@ "description": "Kill Ganon in his lair. The hole is always open, but you may still require some crystals to damage him.", "defaultValue": 0 }, - "dungeons": { - "keyString": "goals.dungeons", - "friendlyName": "All Dungeons", + "bosses": { + "keyString": "goals.bosses", + "friendlyName": "All Bosses", "description": "Defeat the boss of all dungeons, defeat Agahnim in both Castle Tower and Ganon's Tower, then defeat Ganon in his lair.", "defaultValue": 0 }, diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index a12d308f..ea8a87ae 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -12,8 +12,7 @@ from worlds.alttp.Rules import set_rules class TestDungeon(unittest.TestCase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1) self.starting_regions = [] # Where to start exploring self.remove_exits = [] # Block dungeon exits self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/hollow_knight/__init__.py b/test/hollow_knight/__init__.py index 842d321b..e539b799 100644 --- a/test/hollow_knight/__init__.py +++ b/test/hollow_knight/__init__.py @@ -7,8 +7,7 @@ from test.TestBase import TestBase class TestVanilla(TestBase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1) self.world.game[1] = "Hollow Knight" create_regions(self.world, 1) gen_hollow(self.world, 1) \ No newline at end of file diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index 624b7290..08f3c8cd 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -12,9 +12,9 @@ from test.TestBase import TestBase class TestInverted(TestBase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'inverted'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1) self.world.difficulty_requirements[1] = difficulties['normal'] + self.world.mode[1] = "inverted" create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) create_shops(self.world, 1) diff --git a/test/inverted/TestInvertedBombRules.py b/test/inverted/TestInvertedBombRules.py index 716b8e9d..aecab857 100644 --- a/test/inverted/TestInvertedBombRules.py +++ b/test/inverted/TestInvertedBombRules.py @@ -12,8 +12,8 @@ from worlds.alttp.Rules import set_inverted_big_bomb_rules class TestInvertedBombRules(unittest.TestCase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'inverted'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1) + self.world.mode[1] = "inverted" self.world.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/inverted_minor_glitches/TestInvertedMinor.py b/test/inverted_minor_glitches/TestInvertedMinor.py index a9b6ef09..e28d18c2 100644 --- a/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/test/inverted_minor_glitches/TestInvertedMinor.py @@ -12,9 +12,9 @@ from test.TestBase import TestBase class TestInvertedMinor(TestBase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'minorglitches'}, {1: 'inverted'}, {1: 'random'}, {1: 'normal'}, - {1: 'normal'}, {1: False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1: False}, False, None, {1: False}) + self.world = MultiWorld(1) + self.world.mode[1] = "inverted" + self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index c4a8e59e..5dd3fa6e 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -12,8 +12,9 @@ from test.TestBase import TestBase class TestInvertedOWG(TestBase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'owglitches'}, {1: 'inverted'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1) + self.world.logic[1] = "owglitches" + self.world.mode[1] = "inverted" self.world.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index 71f05f24..c7efa5ff 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -12,9 +12,8 @@ from test.TestBase import TestBase class TestMinor(TestBase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'minorglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, - {1: 'normal'}, {1: False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1: False}, False, None, {1: False}) + self.world = MultiWorld(1) + self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index 22feedaa..824d72f8 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -12,9 +12,9 @@ from test.TestBase import TestBase class TestVanillaOWG(TestBase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'owglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1) self.world.difficulty_requirements[1] = difficulties['normal'] + self.world.logic[1] = "owglitches" create_regions(self.world, 1) create_dungeons(self.world, 1) create_shops(self.world, 1) diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index 9b0e4060..a5d53e1b 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -12,8 +12,8 @@ from test.TestBase import TestBase class TestVanilla(TestBase): def setUp(self): - self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1) + self.world.logic[1] = "noglitches" self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/worlds/__init__.py b/worlds/__init__.py index 172b5b20..8c2f0939 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -8,6 +8,7 @@ __all__ = {"lookup_any_item_id_to_name", from .alttp.Items import lookup_id_to_name as alttp from .hk.Items import lookup_id_to_name as hk lookup_any_item_id_to_name = {**alttp, **hk} +lookup_any_item_name_to_id = {name: id for id, name in lookup_any_item_id_to_name.items()} from .alttp import Regions diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index 57f1e772..f3bfa456 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -59,7 +59,7 @@ def parse_arguments(argv, no_defaults=False): Vanilla: Swords are in vanilla locations. ''') parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', - choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'], + choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'], help='''\ Select completion goal. (default: %(default)s) Ganon: Collect all crystals, beat Agahnim 2 then @@ -317,8 +317,8 @@ def parse_arguments(argv, no_defaults=False): ''') parser.add_argument('--suppress_rom', help='Do not create an output rom file.', action='store_true') parser.add_argument('--gui', help='Launch the GUI', action='store_true') - parser.add_argument('--skip_progression_balancing', action='store_true', default=defval(False), - help="Skip Multiworld Progression balancing.") + parser.add_argument('--progression_balancing', action='store_true', default=defval(False), + help="Enable Multiworld Progression balancing.") parser.add_argument('--skip_playthrough', action='store_true', default=defval(False)) parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core')) parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos', @@ -407,7 +407,7 @@ def parse_arguments(argv, no_defaults=False): 'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', - 'heartbeep', "skip_progression_balancing", "triforce_pieces_available", + 'heartbeep', "progression_balancing", "triforce_pieces_available", "triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots", "required_medallions", "plando_items", "plando_texts", "plando_connections", "er_seeds", diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index cc730c4e..638c8824 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -226,7 +226,7 @@ for diff in {'easy', 'normal', 'hard', 'expert'}: def generate_itempool(world, player: int): if world.difficulty[player] not in difficulties: raise NotImplementedError(f"Diffulty {world.difficulty[player]}") - if world.goal[player] not in {'ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', + if world.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}: raise NotImplementedError(f"Goal {world.goal[player]}") if world.mode[player] not in {'open', 'standard', 'inverted'}: diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 7f0d4181..bd6971a6 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -779,7 +779,7 @@ def patch_rom(world, rom, player, team, enemized): rom.write_int16(0x15DB5 + 2 * offset, 0x0640) elif room_id == 0x00d6 and world.fix_trock_exit[player]: rom.write_int16(0x15DB5 + 2 * offset, 0x0134) - elif room_id == 0x000c and world.fix_gtower_exit: # fix ganons tower exit point + elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point rom.write_int16(0x15DB5 + 2 * offset, 0x00A4) else: rom.write_int16(0x15DB5 + 2 * offset, link_y) @@ -1403,8 +1403,8 @@ def patch_rom(world, rom, player, team, enemized): rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected elif world.goal[player] in ['ganonpedestal']: rom.write_byte(0x18003E, 0x06) - elif world.goal[player] in ['dungeons']: - rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat + elif world.goal[player] in ['bosses']: + rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat elif world.goal[player] in ['crystals']: rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals else: @@ -2234,8 +2234,8 @@ def write_strings(rom, world, player, team): else: tt['sign_ganons_tower'] = f'You need {world.crystals_needed_for_gt[player]} crystals to enter.' - if world.goal[player] == 'dungeons': - tt['sign_ganon'] = 'You need to complete all the dungeons.' + if world.goal[player] == 'bosses': + tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.' elif world.goal[player] == 'ganonpedestal': tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.' elif world.goal[player] == "ganon": diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index c3e0ac98..39bfc4e6 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -59,8 +59,8 @@ def set_rules(world, player): else: raise NotImplementedError(f'Not implemented yet: Logic - {world.logic[player]}') - if world.goal[player] == 'dungeons': - # require all dungeons to beat ganon + if world.goal[player] == 'bosses': + # require all bosses to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player)) elif world.goal[player] == 'ganon': # require aga2 to beat ganon