From 64dfb294421b52ab4706c4b9717af8421730b91e Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 19 Mar 2024 21:29:51 -0400 Subject: [PATCH] cleanup + act plando fixes --- worlds/ahit/DeathWishLocations.py | 20 +++-- worlds/ahit/DeathWishRules.py | 26 +++--- worlds/ahit/Items.py | 18 ++-- worlds/ahit/Locations.py | 13 +-- worlds/ahit/Options.py | 15 ++-- worlds/ahit/Regions.py | 133 ++++++++++++++++++++---------- worlds/ahit/Rules.py | 79 +++++++++--------- worlds/ahit/test/TestActs.py | 2 +- worlds/ahit/test/TestBase.py | 5 -- worlds/ahit/test/__init__.py | 5 ++ 10 files changed, 189 insertions(+), 127 deletions(-) delete mode 100644 worlds/ahit/test/TestBase.py diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 75758ab8533..00902262ad8 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -2,10 +2,12 @@ from .Regions import connect_regions, create_region from BaseClasses import Region, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule -from worlds.AutoWorld import World -from typing import List +from typing import List, TYPE_CHECKING from .Locations import death_wishes +if TYPE_CHECKING: + from . import HatInTimeWorld + dw_prereqs = { "So You're Back From Outer Space": ["Beat the Heat"], @@ -81,17 +83,17 @@ "So You're Back From Outer Space", "Encore! Encore!", "Snatcher's Hit List", - "Vault Codes in the Wind", + "Vault Codes in the Wind", "10 Seconds until Self-Destruct", "Killing Two Birds", "Zero Jumps", - "Boss Rush", + "Boss Rush", "Bird Sanctuary", - "The Mustache Gauntlet", + "The Mustache Gauntlet", "Wound-Up Windmill", - "Camera Tourist", - "Rift Collapse: Deep Sea", - "Cruisin' for a Bruisin'", + "Camera Tourist", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", "Seal the Deal", ] @@ -144,7 +146,7 @@ } -def create_dw_regions(world: World): +def create_dw_regions(world: "HatInTimeWorld"): if world.options.DWExcludeAnnoyingContracts.value > 0: for name in annoying_dws: world.get_excluded_dws().append(name) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index b49f3cee911..1418af676b1 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -1,13 +1,17 @@ -from worlds.AutoWorld import World, CollectionState +from worlds.AutoWorld import CollectionState from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData from .DeathWishLocations import dw_prereqs, dw_candles from BaseClasses import Entrance, Location, ItemClassification from worlds.generic.Rules import add_rule, set_rule -from typing import List, Callable +from typing import List, Callable, TYPE_CHECKING from .Regions import act_chapters from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes +if TYPE_CHECKING: + from . import HatInTimeWorld + + # Any speedruns expect the player to have Sprint Hat dw_requirements = { "Beat the Heat": LocData(umbrella=True), @@ -98,7 +102,7 @@ } -def set_dw_rules(world: World): +def set_dw_rules(world: "HatInTimeWorld"): if "Snatcher's Hit List" not in world.get_excluded_dws() \ or "Camera Tourist" not in world.get_excluded_dws(): set_enemy_rules(world) @@ -221,7 +225,7 @@ def set_dw_rules(world: World): world.player) -def modify_dw_rules(world: World, name: str): +def modify_dw_rules(world: "HatInTimeWorld", name: str): difficulty: Difficulty = get_difficulty(world) main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) @@ -267,7 +271,7 @@ def modify_dw_rules(world: World, name: str): set_candle_dw_rules(name, world) -def get_total_dw_stamps(state: CollectionState, world: World) -> int: +def get_total_dw_stamps(state: CollectionState, world: "HatInTimeWorld") -> int: if world.options.DWShuffle.value > 0: return 999 # no stamp costs in death wish shuffle @@ -290,7 +294,7 @@ def get_total_dw_stamps(state: CollectionState, world: World) -> int: return count -def set_candle_dw_rules(name: str, world: World): +def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) @@ -327,7 +331,7 @@ def set_candle_dw_rules(name: str, world: World): add_rule(full_clear, lambda state: state.has(coin, world.player)) -def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: +def get_zero_jump_clear_count(state: CollectionState, world: "HatInTimeWorld") -> int: total: int = 0 for name in act_chapters.keys(): @@ -349,7 +353,7 @@ def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: return total -def get_reachable_enemy_count(state: CollectionState, world: World) -> int: +def get_reachable_enemy_count(state: CollectionState, world: "HatInTimeWorld") -> int: count: int = 0 for enemy in hit_list.keys(): if enemy in bosses: @@ -361,7 +365,7 @@ def get_reachable_enemy_count(state: CollectionState, world: World) -> int: return count -def can_reach_all_bosses(state: CollectionState, world: World) -> bool: +def can_reach_all_bosses(state: CollectionState, world: "HatInTimeWorld") -> bool: for boss in bosses: if not state.has(boss, world.player): return False @@ -369,7 +373,7 @@ def can_reach_all_bosses(state: CollectionState, world: World) -> bool: return True -def create_enemy_events(world: World): +def create_enemy_events(world: "HatInTimeWorld"): no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() for enemy, regions in hit_list.items(): @@ -413,7 +417,7 @@ def create_enemy_events(world: World): add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) -def set_enemy_rules(world: World): +def set_enemy_rules(world: "HatInTimeWorld"): no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() for enemy, regions in hit_list.items(): diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 6b0ccba7ea8..5411aec68f5 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -1,12 +1,14 @@ from BaseClasses import Item, ItemClassification -from worlds.AutoWorld import World from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem from .Locations import get_total_locations from .Rules import get_difficulty -from typing import Optional, List, Dict +from typing import Optional, List, Dict, TYPE_CHECKING +if TYPE_CHECKING: + from . import HatInTimeWorld -def create_itempool(world: World) -> List[Item]: + +def create_itempool(world: "HatInTimeWorld") -> List[Item]: itempool: List[Item] = [] if not world.is_dw_only() and world.options.HatItems.value == 0: calculate_yarn_costs(world) @@ -87,7 +89,7 @@ def create_itempool(world: World) -> List[Item]: return itempool -def calculate_yarn_costs(world: World): +def calculate_yarn_costs(world: "HatInTimeWorld"): mw = world.multiworld min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) @@ -107,7 +109,7 @@ def calculate_yarn_costs(world: World): world.options.YarnAvailable.value += (max_cost + world.options.MinExtraYarn.value) - available_yarn -def item_dlc_enabled(world: World, name: str) -> bool: +def item_dlc_enabled(world: "HatInTimeWorld", name: str) -> bool: data = item_table[name] if data.dlc_flags == HatDLC.none: @@ -122,12 +124,12 @@ def item_dlc_enabled(world: World, name: str) -> bool: return False -def create_item(world: World, name: str) -> Item: +def create_item(world: "HatInTimeWorld", name: str) -> Item: data = item_table[name] return HatInTimeItem(name, data.classification, data.code, world.player) -def create_multiple_items(world: World, name: str, count: int = 1, +def create_multiple_items(world: "HatInTimeWorld", name: str, count: int = 1, item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: data = item_table[name] @@ -139,7 +141,7 @@ def create_multiple_items(world: World, name: str, count: int = 1, return itemlist -def create_junk_items(world: World, count: int) -> List[Item]: +def create_junk_items(world: "HatInTimeWorld", count: int) -> List[Item]: trap_chance = world.options.TrapChance.value junk_pool: List[Item] = [] junk_list: Dict[str, int] = {} diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 6e6e041bc27..9d148f64c08 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -1,11 +1,14 @@ -from worlds.AutoWorld import World from .Types import HatDLC, HatType, LocData, Difficulty -from typing import Dict +from typing import Dict, TYPE_CHECKING from .Options import TasksanityCheckCount +if TYPE_CHECKING: + from . import HatInTimeWorld + TASKSANITY_START_ID = 2000300204 -def get_total_locations(world: World) -> int: + +def get_total_locations(world: "HatInTimeWorld") -> int: total: int = 0 if not world.is_dw_only(): @@ -34,7 +37,7 @@ def get_total_locations(world: World) -> int: return total -def location_dlc_enabled(world: World, location: str) -> bool: +def location_dlc_enabled(world: "HatInTimeWorld", location: str) -> bool: data = location_table.get(location) or event_locs.get(location) if data.dlc_flags == HatDLC.none: @@ -53,7 +56,7 @@ def location_dlc_enabled(world: World, location: str) -> bool: return False -def is_location_valid(world: World, location: str) -> bool: +def is_location_valid(world: "HatInTimeWorld", location: str) -> bool: if not location_dlc_enabled(world, location): return False diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index b6b4eaa7862..d58a12ded20 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,10 +1,13 @@ -from typing import List +from typing import List, TYPE_CHECKING from dataclasses import dataclass -from worlds.AutoWorld import World, PerGameCommonOptions +from worlds.AutoWorld import PerGameCommonOptions from Options import Range, Toggle, DeathLink, Choice, OptionDict +if TYPE_CHECKING: + from . import HatInTimeWorld + -def adjust_options(world: World): +def adjust_options(world: "HatInTimeWorld"): world.options.HighestChapterCost.value = max( world.options.HighestChapterCost.value, world.options.LowestChapterCost.value) @@ -81,7 +84,7 @@ def adjust_options(world: World): world.options.DWTimePieceRequirement.value = 0 -def get_total_time_pieces(world: World) -> int: +def get_total_time_pieces(world: "HatInTimeWorld") -> int: count: int = 40 if world.is_dlc1(): count += 6 @@ -566,13 +569,13 @@ class DWExcludeAnnoyingBonuses(Toggle): - Zero Jumps - Bird Sanctuary - Wound-Up Windmill - - Vault Codes in the Wind + - Vault Codes in the Wind - Boss Rush - Camera Tourist - The Mustache Gauntlet - Rift Collapse: Deep Sea - Cruisin' for a Bruisin' - - Seal the Deal""" + - Seal the Deal""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0c1c1e2a4de..038a4ef3ba2 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -1,11 +1,13 @@ -from worlds.AutoWorld import World from BaseClasses import Region, Entrance, ItemClassification, Location from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard -import typing +from typing import TYPE_CHECKING, List, Dict from .Rules import set_rift_rules, get_difficulty +if TYPE_CHECKING: + from . import HatInTimeWorld + # ChapterIndex: region chapter_regions = { @@ -268,7 +270,7 @@ } -def create_regions(world: World): +def create_regions(world: "HatInTimeWorld"): w = world p = world.player @@ -422,7 +424,7 @@ def create_regions(world: World): create_thug_shops(w) -def create_rift_connections(world: World, region: Region): +def create_rift_connections(world: "HatInTimeWorld", region: Region): i = 1 for name in rift_access_regions[region.name]: act_region = world.multiworld.get_region(name, world.player) @@ -436,7 +438,7 @@ def create_rift_connections(world: World, region: Region): world.multiworld.get_entrance(entrance.name, world.player) -def create_tasksanity_locations(world: World): +def create_tasksanity_locations(world: "HatInTimeWorld"): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) id_start: int = TASKSANITY_START_ID for i in range(world.options.TasksanityCheckCount.value): @@ -444,15 +446,37 @@ def create_tasksanity_locations(world: World): ship_shape.locations.append(location) -def is_valid_plando(world: World, region: str) -> bool: - if region in blacklisted_acts.values() or region not in act_entrances.keys(): +def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = False) -> bool: + # Duplicated keys will throw an exception for us, but we still need to check for duplicated values + if is_candidate: + found_list: List = [] + old_region = region + for name in world.options.ActPlando.keys(): + act = world.options.ActPlando.get(name) + if act == old_region: + region = name + found_list.append(name) + + if len(found_list) == 0: + return False + + if len(found_list) > 1: + raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Duplicated act plando mapping found for act: \"{old_region}\"") + elif region not in world.options.ActPlando.keys(): return False - if region not in world.options.ActPlando.keys(): + if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region): return False act = world.options.ActPlando.get(region) - if act in blacklisted_acts.values() or act not in act_entrances.keys(): + try: + world.multiworld.get_region(region, world.player) + world.multiworld.get_region(act, world.player) + except KeyError: + return False + + if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act): return False # Don't allow plando-ing things onto the first act that aren't completable with nothing @@ -471,24 +495,27 @@ def is_valid_plando(world: World, region: str) -> bool: return False # Don't allow straight up impossible mappings - if region == "The Illness has Spread" and act == "Alpine Free Roam": + if (region == "Time Rift - Curly Tail Trail" + or region == "Time Rift - The Twilight Bell" + or region == "The Illness has Spread") \ + and act == "Alpine Free Roam": return False - if region == "Rush Hour" and act == "Nyakuza Free Roam": + if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") \ + and act == "Nyakuza Free Roam": return False - if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam": + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": return False - if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + if region == "Time Rift - Deep Sea" and act == "Bon Voyage!": return False - return any(a.name == world.options.ActPlando.get(region) for a in - world.multiworld.get_regions(world.player)) + return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player)) -def randomize_act_entrances(world: World): - region_list: typing.List[Region] = get_act_regions(world) +def randomize_act_entrances(world: "HatInTimeWorld"): + region_list: List[Region] = get_act_regions(world) world.random.shuffle(region_list) separate_rifts: bool = bool(world.options.ActRandomizer.value == 1) @@ -509,23 +536,41 @@ def randomize_act_entrances(world: World): region_list.remove(region) region_list.append(region) + for name in world.options.ActPlando.keys(): + try: + world.multiworld.get_region(name, world.player) + except KeyError: + print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Act \"{name}\" does not exist in the multiworld." + f"Possible reasons are typos, case-sensitivity, or DLC options.") + for region in region_list.copy(): if region.name in world.options.ActPlando.keys(): - if is_valid_plando(world, region.name): + try: + act = world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player) + except KeyError: + print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Act \"{world.options.ActPlando.get(region.name)}\" does not exist in the multiworld." + f"Possible reasons are typos, case-sensitivity, or DLC options.") + continue + + if is_valid_plando(world, region.name) and is_valid_plando(world, act.name, True): region_list.remove(region) region_list.append(region) + region_list.remove(act) + region_list.append(act) else: print(f"[WARNING] ActPlando " - f"({world.multiworld.get_player_name(world.player)}) - " - f"{region.name}: {world.options.ActPlando.get(region.name)} " - f"is an invalid or disallowed act plando combination!") + f"({world.multiworld.get_player_name(world.player)}) - " + f"\"{region.name}: {world.options.ActPlando.get(region.name)}\" " + f"is an invalid or disallowed act plando combination!") # Reverse the list, so we can do what we want to do first region_list.reverse() - shuffled_list: typing.List[Region] = [] - mapped_list: typing.List[Region] = [] - rift_dict: typing.Dict[str, Region] = {} + shuffled_list: List[Region] = [] + mapped_list: List[Region] = [] + rift_dict: Dict[str, Region] = {} first_chapter: Region = get_first_chapter_region(world) has_guaranteed: bool = False @@ -543,7 +588,7 @@ def randomize_act_entrances(world: World): and "Free Roam" not in act_entrances[region.name]: continue - if region.name in world.options.ActPlando.keys() and is_valid_plando(world, region.name): + if is_valid_plando(world, region.name): has_guaranteed = True i = 0 @@ -555,14 +600,14 @@ def randomize_act_entrances(world: World): mapped_list.append(region) # Look for candidates to map this act to - candidate_list: typing.List[Region] = [] + candidate_list: List[Region] = [] for candidate in region_list: # We're mapping something to the first act, make sure it is valid if not has_guaranteed: if candidate.name not in guaranteed_first_acts: continue - if candidate.name in world.options.ActPlando.values(): + if is_valid_plando(world, candidate.name, True): continue # Not completable without Umbrella @@ -579,7 +624,7 @@ def randomize_act_entrances(world: World): has_guaranteed = True break - if region.name in world.options.ActPlando.keys() and is_valid_plando(world, region.name): + if is_valid_plando(world, region.name): candidate_list.clear() candidate_list.append( world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player)) @@ -666,7 +711,7 @@ def randomize_act_entrances(world: World): set_rift_rules(world, rift_dict) -def connect_time_rift(world: World, time_rift: Region, exit_region: Region): +def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): count: int = len(rift_access_regions[time_rift.name]) i: int = 1 while i <= count: @@ -676,8 +721,8 @@ def connect_time_rift(world: World, time_rift: Region, exit_region: Region): i += 1 -def get_act_regions(world: World) -> typing.List[Region]: - act_list: typing.List[Region] = [] +def get_act_regions(world: "HatInTimeWorld") -> List[Region]: + act_list: List[Region] = [] for region in world.multiworld.get_regions(world.player): if region.name in chapter_act_info.keys(): if not is_act_blacklisted(world, region.name): @@ -686,9 +731,9 @@ def get_act_regions(world: World) -> typing.List[Region]: return act_list -def is_act_blacklisted(world: World, name: str) -> bool: - plando: bool = name in world.options.ActPlando.keys() \ - or name in world.options.ActPlando.values() +def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: + plando: bool = name in world.options.ActPlando.keys() and is_valid_plando(world, name) \ + or name in world.options.ActPlando.values() and is_valid_plando(world, name, True) if name == "The Finale": return not plando and world.options.EndGoal.value == 1 @@ -702,7 +747,7 @@ def is_act_blacklisted(world: World, name: str) -> bool: return name in blacklisted_acts.values() -def create_region(world: World, name: str) -> Region: +def create_region(world: "HatInTimeWorld", name: str) -> Region: reg = Region(name, world.player, world.multiworld) for (key, data) in location_table.items(): @@ -726,7 +771,7 @@ def create_region(world: World, name: str) -> Region: return reg -def create_badge_seller(world: World) -> Region: +def create_badge_seller(world: "HatInTimeWorld") -> Region: badge_seller = Region("Badge Seller", world.player, world.multiworld) world.multiworld.regions.append(badge_seller) count: int = 0 @@ -782,7 +827,7 @@ def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Reg entrance.connect(exit_region) -def create_region_and_connect(world: World, +def create_region_and_connect(world: "HatInTimeWorld", name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region: reg: Region = create_region(world, name) @@ -800,17 +845,17 @@ def create_region_and_connect(world: World, return reg -def get_first_chapter_region(world: World) -> Region: - start_chapter: ChapterIndex = world.options.StartingChapter.value +def get_first_chapter_region(world: "HatInTimeWorld") -> Region: + start_chapter: ChapterIndex = ChapterIndex(world.options.StartingChapter.value) return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) -def get_act_original_chapter(world: World, act_name: str) -> Region: +def get_act_original_chapter(world: "HatInTimeWorld", act_name: str) -> Region: return world.multiworld.get_region(act_chapters[act_name], world.player) # Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game -def update_chapter_act_info(world: World, original_region: Region, new_region: Region): +def update_chapter_act_info(world: "HatInTimeWorld", original_region: Region, new_region: Region): original_act_info = chapter_act_info[original_region.name] new_act_info = chapter_act_info[new_region.name] world.act_connections[original_act_info] = new_act_info @@ -825,7 +870,7 @@ def get_shuffled_region(self, region: str) -> str: return name -def create_thug_shops(world: World): +def create_thug_shops(world: "HatInTimeWorld"): min_items: int = min(world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value) max_items: int = max(world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value) count: int = -1 @@ -867,7 +912,7 @@ def create_thug_shops(world: World): world.set_nyakuza_thug_items(thug_items) -def create_events(world: World) -> int: +def create_events(world: "HatInTimeWorld") -> int: count: int = 0 for (name, data) in event_locs.items(): @@ -892,7 +937,7 @@ def create_events(world: World) -> int: return count -def create_event(name: str, item_name: str, region: Region, world: World) -> Location: +def create_event(name: str, item_name: str, region: Region, world: "HatInTimeWorld") -> Location: event = HatInTimeLocation(world.player, name, None, region) region.locations.append(event) event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index ff6e279167e..1f24961d5c3 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,11 +1,14 @@ -from worlds.AutoWorld import World, CollectionState +from worlds.AutoWorld import CollectionState from worlds.generic.Rules import add_rule, set_rule from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ shop_locations, event_locs, snatcher_coins from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC from BaseClasses import Location, Entrance, Region -import typing +from typing import TYPE_CHECKING, List, Callable, Union, Dict +if TYPE_CHECKING: + from . import HatInTimeWorld + act_connections = { "Mafia Town - Act 2": ["Mafia Town - Act 1"], @@ -31,14 +34,14 @@ } -def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: +def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) -> bool: if world.options.HatItems.value > 0: return state.has(hat_type_to_item[hat], world.player) return state.count("Yarn", world.player) >= get_hat_cost(world, hat) -def get_hat_cost(world: World, hat: HatType) -> int: +def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int: cost: int = 0 costs = world.get_hat_yarn_costs() for h in world.get_hat_craft_order(): @@ -49,20 +52,20 @@ def get_hat_cost(world: World, hat: HatType) -> int: return cost -def can_sdj(state: CollectionState, world: World): +def can_sdj(state: CollectionState, world: "HatInTimeWorld"): return can_use_hat(state, world, HatType.SPRINT) -def painting_logic(world: World) -> bool: +def painting_logic(world: "HatInTimeWorld") -> bool: return world.options.ShuffleSubconPaintings.value > 0 # -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert -def get_difficulty(world: World) -> Difficulty: +def get_difficulty(world: "HatInTimeWorld") -> Difficulty: return Difficulty(world.options.LogicDifficulty.value) -def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool: +def has_paintings(state: CollectionState, world: "HatInTimeWorld", count: int, allow_skip: bool = True) -> bool: if not painting_logic(world): return True @@ -74,35 +77,35 @@ def has_paintings(state: CollectionState, world: World, count: int, allow_skip: return state.count("Progressive Painting Unlock", world.player) >= count -def zipline_logic(world: World) -> bool: +def zipline_logic(world: "HatInTimeWorld") -> bool: return world.options.ShuffleAlpineZiplines.value > 0 -def can_use_hookshot(state: CollectionState, world: World): +def can_use_hookshot(state: CollectionState, world: "HatInTimeWorld"): return state.has("Hookshot Badge", world.player) -def can_hit(state: CollectionState, world: World, umbrella_only: bool = False): +def can_hit(state: CollectionState, world: "HatInTimeWorld", umbrella_only: bool = False): if world.options.UmbrellaLogic.value == 0: return True return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) -def can_surf(state: CollectionState, world: World): +def can_surf(state: CollectionState, world: "HatInTimeWorld"): return state.has("No Bonk Badge", world.player) -def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool: +def has_relic_combo(state: CollectionState, world: "HatInTimeWorld", relic: str) -> bool: return state.has_group(relic, world.player, len(world.item_name_groups[relic])) -def get_relic_count(state: CollectionState, world: World, relic: str) -> int: +def get_relic_count(state: CollectionState, world: "HatInTimeWorld", relic: str) -> int: return state.count_group(relic, world.player) # Only use for rifts -def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: +def can_clear_act(state: CollectionState, world: "HatInTimeWorld", act_entrance: str) -> bool: entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) if not state.can_reach(entrance.connected_region, "Region", world.player): return False @@ -114,12 +117,12 @@ def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bo return world.multiworld.get_location(name, world.player).access_rule(state) -def can_clear_alpine(state: CollectionState, world: World) -> bool: +def can_clear_alpine(state: CollectionState, world: "HatInTimeWorld") -> bool: return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \ and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player) -def can_clear_metro(state: CollectionState, world: World) -> bool: +def can_clear_metro(state: CollectionState, world: "HatInTimeWorld") -> bool: return state.has("Nyakuza Intro Cleared", world.player) \ and state.has("Yellow Overpass Station Cleared", world.player) \ and state.has("Yellow Overpass Manhole Cleared", world.player) \ @@ -130,14 +133,14 @@ def can_clear_metro(state: CollectionState, world: World) -> bool: and state.has("Pink Paw Manhole Cleared", world.player) -def set_rules(world: World): +def set_rules(world: "HatInTimeWorld"): # First, chapter access starting_chapter = ChapterIndex(world.options.StartingChapter.value) world.set_chapter_cost(starting_chapter, 0) # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale - chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, - ChapterIndex.SUBCON, ChapterIndex.ALPINE] + chapter_list: List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, + ChapterIndex.SUBCON, ChapterIndex.ALPINE] final_chapter = ChapterIndex.FINALE if world.options.EndGoal.value == 2: @@ -333,7 +336,7 @@ def set_rules(world: World): add_rule(loc, lambda state: can_use_hookshot(state, world)) - dummy_entrances: typing.List[Entrance] = [] + dummy_entrances: List[Entrance] = [] for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not world.is_dlc1(): @@ -342,11 +345,11 @@ def set_rules(world: World): i: int = 1 entrance: Entrance = world.multiworld.get_entrance(key, world.player) region: Region = entrance.connected_region - access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] + access_rules: List[Callable[[CollectionState], bool]] = [] dummy_entrances.append(entrance) # Entrances to this act that we have to set access_rules on - entrances: typing.List[Entrance] = [] + entrances: List[Entrance] = [] for act in acts: act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) @@ -358,7 +361,7 @@ def set_rules(world: World): # Copy access rules from act completions if "Free Roam" not in required_region.name: - rule: typing.Callable[[CollectionState], bool] + rule: Callable[[CollectionState], bool] name = f"Act Completion ({required_region.name})" rule = world.multiworld.get_location(name, world.player).access_rule access_rules.append(rule) @@ -380,7 +383,7 @@ def set_rules(world: World): world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) -def set_specific_rules(world: World): +def set_specific_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), lambda state: state.has("Time Piece", world.player, 12) and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) @@ -411,7 +414,7 @@ def set_specific_rules(world: World): set_expert_rules(world) -def set_moderate_rules(world: World): +def set_moderate_rules(world: "HatInTimeWorld"): # Moderate: Gallery without Brewing Hat set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True) @@ -488,7 +491,7 @@ def set_moderate_rules(world: World): set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) -def set_hard_rules(world: World): +def set_hard_rules(world: "HatInTimeWorld"): # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT) @@ -545,7 +548,7 @@ def set_hard_rules(world: World): and state.has("Metro Ticket - Pink", world.player)) -def set_expert_rules(world: World): +def set_expert_rules(world: "HatInTimeWorld"): # Finale Telescope with no hats set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) @@ -623,7 +626,7 @@ def set_expert_rules(world: World): and state.has("Metro Ticket - Pink", world.player)) -def set_mafia_town_rules(world: World): +def set_mafia_town_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) or state.can_reach("Down with the Mafia!", "Region", world.player) @@ -683,7 +686,7 @@ def set_mafia_town_rules(world: World): and state.has("Scooter Badge", world.player), "or") -def set_botb_rules(world: World): +def set_botb_rules(world: "HatInTimeWorld"): if world.options.UmbrellaLogic.value == 0 and get_difficulty(world) < Difficulty.MODERATE: set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) @@ -695,7 +698,7 @@ def set_botb_rules(world: World): lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) -def set_subcon_rules(world: World): +def set_subcon_rules(world: "HatInTimeWorld"): set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.DWELLER)) @@ -733,7 +736,7 @@ def set_subcon_rules(world: World): add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1)) -def set_alps_rules(world: World): +def set_alps_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING)) @@ -753,7 +756,7 @@ def set_alps_rules(world: World): lambda state: can_clear_alpine(state, world)) -def set_dlc1_rules(world: World): +def set_dlc1_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), lambda state: can_use_hookshot(state, world)) @@ -763,7 +766,7 @@ def set_dlc1_rules(world: World): or state.can_reach("Ship Shape", "Region", world.player)) -def set_dlc2_rules(world: World): +def set_dlc2_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: state.has("Metro Ticket - Green", world.player) or state.has("Metro Ticket - Blue", world.player)) @@ -786,7 +789,7 @@ def set_dlc2_rules(world: World): lambda state: state.has("Metro Ticket - Yellow", world.player), "or") -def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]): +def reg_act_connection(world: "HatInTimeWorld", region: Union[str, Region], unlocked_entrance: Union[str, Entrance]): reg: Region entrance: Entrance if isinstance(region, str): @@ -804,7 +807,7 @@ def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked # See randomize_act_entrances in Regions.py # Called before set_rules -def set_rift_rules(world: World, regions: typing.Dict[str, Region]): +def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. for entrance in regions["Time Rift - Gallery"].entrances: @@ -890,7 +893,7 @@ def set_rift_rules(world: World, regions: typing.Dict[str, Region]): # Basically the same as above, but without the need of the dict since we are just setting defaults # Called if Act Rando is disabled -def set_default_rift_rules(world: World): +def set_default_rift_rules(world: "HatInTimeWorld"): for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances: add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) @@ -964,7 +967,7 @@ def set_default_rift_rules(world: World): add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) -def set_event_rules(world: World): +def set_event_rules(world: "HatInTimeWorld"): for (name, data) in event_locs.items(): if not is_location_valid(world, name): continue diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index 28e1a430d4b..b49259c947a 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -1,6 +1,6 @@ from worlds.ahit.Regions import act_chapters from worlds.ahit.Rules import act_connections -from worlds.ahit.test.TestBase import HatInTimeTestBase +from . import HatInTimeTestBase class TestActs(HatInTimeTestBase): diff --git a/worlds/ahit/test/TestBase.py b/worlds/ahit/test/TestBase.py deleted file mode 100644 index 1eb4dd65554..00000000000 --- a/worlds/ahit/test/TestBase.py +++ /dev/null @@ -1,5 +0,0 @@ -from test.TestBase import WorldTestBase - - -class HatInTimeTestBase(WorldTestBase): - game = "A Hat in Time" diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py index e69de29bb2d..67b750a65c7 100644 --- a/worlds/ahit/test/__init__.py +++ b/worlds/ahit/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class HatInTimeTestBase(WorldTestBase): + game = "A Hat in Time"