diff --git a/Bosses.py b/Bosses.py index 77146e53..d4395a26 100644 --- a/Bosses.py +++ b/Bosses.py @@ -6,14 +6,10 @@ from Fill import FillError def BossFactory(boss: str, player: int) -> Optional[Boss]: - if boss is None: - return None if boss in boss_table: enemizer_name, defeat_rule = boss_table[boss] return Boss(boss, enemizer_name, defeat_rule, player) - - logging.error('Unknown Boss: %s', boss) - return None + raise Exception('Unknown Boss: %s', boss) def ArmosKnightsDefeatRule(state, player: int): @@ -153,7 +149,7 @@ boss_table = { def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool: - #blacklist approach + # blacklist approach if boss in {"Agahnim", "Agahnim2", "Ganon"}: return False @@ -169,8 +165,8 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> if boss in {"Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"}: return False - elif dungeon_name == 'Skull Woods' : - if boss == "Trinexx": + elif dungeon_name == 'Skull Woods': + if boss == "Trinexx": return False return True @@ -179,7 +175,7 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> def place_boss(world, player: int, boss: str, location: str, level: Optional[str]): if location == 'Ganons Tower' and world.mode[player] == 'inverted': location = 'Inverted Ganons Tower' - logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else '')) + logging.info('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else '')) world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player) @@ -206,12 +202,49 @@ def place_bosses(world, player: int): all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']] - if world.boss_shuffle[player] in ["basic", "normal"]: + shuffle_mode = world.boss_shuffle[player] + already_placed_bosses = [] + if ";" in shuffle_mode: + bosses = shuffle_mode.split(";") + shuffle_mode = bosses.pop() + for boss in bosses: + if "-" in boss: + loc, boss = boss.split("-") + boss = boss.title() + level = None + if loc.split(" ")[-1] in {"top", "middle", "bottom"}: + # split off level + loc = loc.split(" ") + level = loc[-1] + loc = " ".join(loc[:-1]) + loc = loc.title() + if can_place_boss(boss, loc, level) and [loc, level] in boss_locations: + place_boss(world, player, boss, loc, level) + already_placed_bosses.append(boss) + boss_locations.remove([loc, level]) + else: + Exception("Cannot place", boss, "at", loc, level, "for player", player) + else: + boss = boss.title() + boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations) + + if shuffle_mode == "none": + return # vanilla bosses come pre-placed + + if shuffle_mode in ["basic", "normal"]: if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] else: # all bosses present, the three duplicates chosen at random bosses = all_bosses + [world.random.choice(placeable_bosses) for _ in range(3)] + # there is probably a better way to do this + while already_placed_bosses: + # remove already manually placed bosses, to prevent for example triple Lanmolas + boss = already_placed_bosses.pop() + if boss in bosses: + bosses.remove(boss) + # there may be more bosses than locations at this point, depending on manual placement + logging.debug('Bosses chosen %s', bosses) world.random.shuffle(bosses) @@ -223,7 +256,7 @@ def place_bosses(world, player: int): bosses.remove(boss) place_boss(world, player, boss, loc, level) - elif world.boss_shuffle[player] == "chaos": # all bosses chosen at random + elif shuffle_mode == "chaos": # all bosses chosen at random for loc, level in boss_locations: try: boss = world.random.choice( @@ -234,20 +267,28 @@ def place_bosses(world, player: int): else: place_boss(world, player, boss, loc, level) - elif world.boss_shuffle[player] == "singularity": + elif shuffle_mode == "singularity": primary_boss = world.random.choice(placeable_bosses) - remaining_boss_locations = [] - for loc, level in boss_locations: - # place that boss where it can go - if can_place_boss(primary_boss, loc, level): - place_boss(world, player, primary_boss, loc, level) - else: - remaining_boss_locations.append((loc, level)) + remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, boss_locations) if remaining_boss_locations: # pick a boss to go into the remaining locations remaining_boss = world.random.choice([boss for boss in placeable_bosses if all( can_place_boss(boss, loc, level) for loc, level in remaining_boss_locations)]) - for loc, level in remaining_boss_locations: - place_boss(world, player, remaining_boss, loc, level) + remaining_boss_locations, _ = place_where_possible(world, player, remaining_boss, remaining_boss_locations) + if remaining_boss_locations: + raise Exception("Unfilled boss locations!") else: - raise FillError(f"Could not find boss shuffle mode {world.boss_shuffle[player]}") + raise FillError(f"Could not find boss shuffle mode {shuffle_mode}") + + +def place_where_possible(world, player: int, boss: str, boss_locations): + remainder = [] + placed_bosses = [] + for loc, level in boss_locations: + # place that boss where it can go + if can_place_boss(boss, loc, level): + place_boss(world, player, boss, loc, level) + placed_bosses.append(boss) + else: + remainder.append((loc, level)) + return remainder, placed_bosses diff --git a/Dungeons.py b/Dungeons.py index 9e4111e8..7ff62447 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -9,7 +9,7 @@ def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == "universal" else small_keys, dungeon_items, player) - dungeon.boss = BossFactory(default_boss, player) + dungeon.boss = BossFactory(default_boss, player) if default_boss else None for region in dungeon.regions: world.get_region(region, player).dungeon = dungeon dungeon.world = world diff --git a/Mystery.py b/Mystery.py index d74c9973..58e7d677 100644 --- a/Mystery.py +++ b/Mystery.py @@ -10,6 +10,7 @@ import ModuleUpdate ModuleUpdate.update() +import Bosses from Utils import parse_yaml from Rom import Sprite from EntranceRandomizer import parse_arguments @@ -247,7 +248,7 @@ def get_choice(option, root, value=None) -> typing.Any: if any(root[option].values()): return interpret_on_off( random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]) - raise RuntimeError(f"All options specified in {option} are weighted as zero.") + raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.") def handle_name(name: str): @@ -260,6 +261,20 @@ def prefer_int(input_data: str) -> typing.Union[str, int]: except: return input_data + +available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in + {'Agahnim', 'Agahnim2', 'Ganon'}} + +boss_shuffle_options = {None: 'none', + 'none': 'none', + 'simple': 'basic', + 'full': 'normal', + 'random': 'chaos', + 'singularity': 'singularity', + 'duality': 'singularity' + } + + def roll_settings(weights): ret = argparse.Namespace() if "linked_options" in weights: @@ -386,14 +401,22 @@ def roll_settings(weights): ret.item_functionality = get_choice('item_functionality', weights) - ret.shufflebosses = {None: 'none', - 'none': 'none', - 'simple': 'basic', - 'full': 'normal', - 'random': 'chaos', - 'singularity': 'singularity', - 'duality': 'singularity' - }[get_choice('boss_shuffle', weights)] + boss_shuffle = get_choice('boss_shuffle', weights) + + if boss_shuffle in boss_shuffle_options: + ret.shufflebosses = boss_shuffle_options[boss_shuffle] + else: + options = boss_shuffle.lower().split(";") + remainder_shuffle = "none" # vanilla + bosses = [] + for boss in options: + if boss in boss_shuffle_options: + remainder_shuffle = boss + elif boss not in available_boss_names and not "-" in boss: + raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.") + else: + bosses.append(boss) + ret.shufflebosses = ";".join(bosses + [remainder_shuffle]) ret.enemy_shuffle = {'none': False, 'shuffled': 'shuffled',