From ed6b7b26704965cd20db5a951fa9568c15480fa2 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 14 Jan 2024 06:48:30 -0800 Subject: [PATCH 1/7] Zillion: remove old option access from item link validation (#2673) * Zillion: remove old option access from item link validation and a little bit a cleaning in other stuff nearby * one option access missed --- worlds/zillion/__init__.py | 20 ++++++++++---------- worlds/zillion/logic.py | 13 ++++++++----- worlds/zillion/options.py | 30 +++++++++++++++--------------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 3f441d12ab34..d30bef144464 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -4,7 +4,7 @@ import settings import threading import typing -from typing import Any, Dict, List, Literal, Set, Tuple, Optional, cast +from typing import Any, Dict, List, Set, Tuple, Optional, cast import os import logging @@ -12,7 +12,7 @@ MultiWorld, Item, CollectionState, Entrance, Tutorial from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import ZillionOptions, ZillionStartChar, validate +from .options import ZillionOptions, validate from .id_maps import item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id @@ -225,7 +225,7 @@ def access_rule_wrapped(zz_loc_local: ZzLocation, loc.access_rule = access_rule if not (limited_skill >= zz_loc.req): loc.progress_type = LocationProgressType.EXCLUDED - self.multiworld.exclude_locations[p].value.add(loc.name) + self.options.exclude_locations.value.add(loc.name) here.locations.append(loc) self.my_locations.append(loc) @@ -288,15 +288,15 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: if group["game"] == "Zillion": assert "item_pool" in group item_pool = group["item_pool"] - to_stay: Literal['Apple', 'Champ', 'JJ'] = "JJ" + to_stay: Chars = "JJ" if "JJ" in item_pool: assert "players" in group group_players = group["players"] - start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char")) - players_start_chars = [ - (player, start_chars[player].current_option_name) - for player in group_players - ] + players_start_chars: List[Tuple[int, Chars]] = [] + for player in group_players: + z_world = multiworld.worlds[player] + assert isinstance(z_world, ZillionWorld) + players_start_chars.append((player, z_world.options.start_char.get_char())) start_char_counts = Counter(sc for _, sc in players_start_chars) # majority rules if start_char_counts["Apple"] > start_char_counts["Champ"]: @@ -304,7 +304,7 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: elif start_char_counts["Champ"] > start_char_counts["Apple"]: to_stay = "Champ" else: # equal - choices: Tuple[Literal['Apple', 'Champ', 'JJ'], ...] = ("Apple", "Champ") + choices: Tuple[Chars, ...] = ("Apple", "Champ") to_stay = multiworld.random.choice(choices) for p, sc in players_start_chars: diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index 305546c78b62..dcbc6131f1a9 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,9 +1,11 @@ -from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter +from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter + from BaseClasses import CollectionState + +from zilliandomizer.logic_components.items import Item, items from zilliandomizer.logic_components.locations import Location from zilliandomizer.randomizer import Randomizer -from zilliandomizer.logic_components.items import Item, items -from .region import ZillionLocation + from .item import ZillionItem from .id_maps import item_name_to_id @@ -18,11 +20,12 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int: returns a hash of the player and of the set locations with their items """ + from . import ZillionWorld z_world = cs.multiworld.worlds[p] - my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations")) + assert isinstance(z_world, ZillionWorld) _hash = p - for z_loc in my_locations: + for z_loc in z_world.my_locations: zz_name = z_loc.zz_loc.name zz_item = z_loc.item.zz_item \ if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \ diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index cb861e962128..97f8b817f77c 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,13 +1,14 @@ from collections import Counter from dataclasses import dataclass -from typing import Dict, Tuple +from typing import ClassVar, Dict, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice -from zilliandomizer.options import \ - Options as ZzOptions, char_to_gun, char_to_jump, ID, \ - VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts +from zilliandomizer.options import ( + Options as ZzOptions, char_to_gun, char_to_jump, ID, + VBLR as ZzVBLR, Chars, ItemCounts as ZzItemCounts +) from zilliandomizer.options.parsing import validate as zz_validate @@ -107,6 +108,15 @@ class ZillionStartChar(Choice): display_name = "start character" default = "random" + _name_capitalization: ClassVar[Dict[int, Chars]] = { + option_jj: "JJ", + option_apple: "Apple", + option_champ: "Champ", + } + + def get_char(self) -> Chars: + return ZillionStartChar._name_capitalization[self.value] + class ZillionIDCardCount(Range): """ @@ -348,16 +358,6 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": # that should be all of the level requirements met - name_capitalization: Dict[str, Chars] = { - "jj": "JJ", - "apple": "Apple", - "champ": "Champ", - } - - start_char = options.start_char - start_char_name = name_capitalization[start_char.current_key] - assert start_char_name in chars - starting_cards = options.starting_cards room_gen = options.room_gen @@ -371,7 +371,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": max_level.value, False, # tutorial skill, - start_char_name, + options.start_char.get_char(), floppy_req.value, options.continues.value, bool(options.randomize_alarms.value), From 6ac3d5c6511182282637535bb853da6ec95c15f2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Jan 2024 21:24:34 +0100 Subject: [PATCH 2/7] Core: set consistent server defaults (#2566) --- WebHostLib/templates/generate.html | 10 +++++----- settings.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 33f8dbc09e6c..53d98dfae6ba 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -69,8 +69,8 @@

Generate Game{% if race %} (Race Mode){% endif %}

@@ -185,12 +185,12 @@

Generate Game{% if race %} (Race Mode){% endif %}

+ +
+
- -
-
diff --git a/settings.py b/settings.py index acae86095cda..c58eadf155d7 100644 --- a/settings.py +++ b/settings.py @@ -597,8 +597,8 @@ class LogNetwork(IntEnum): disable_item_cheat: Union[DisableItemCheat, bool] = False location_check_points: LocationCheckPoints = LocationCheckPoints(1) hint_cost: HintCost = HintCost(10) - release_mode: ReleaseMode = ReleaseMode("goal") - collect_mode: CollectMode = CollectMode("goal") + release_mode: ReleaseMode = ReleaseMode("auto") + collect_mode: CollectMode = CollectMode("auto") remaining_mode: RemainingMode = RemainingMode("goal") auto_shutdown: AutoShutdown = AutoShutdown(0) compatibility: Compatibility = Compatibility(2) @@ -673,7 +673,7 @@ class Race(IntEnum): spoiler: Spoiler = Spoiler(3) glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here? race: Race = Race(0) - plando_options: PlandoOptions = PlandoOptions("bosses") + plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") class SNIOptions(Group): From ad074490bcb07aeec17edf871d4f26ad0835aabe Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Jan 2024 21:30:00 +0100 Subject: [PATCH 3/7] Test: add location access rule benchmark (#2433) --- test/benchmark/__init__.py | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 test/benchmark/__init__.py diff --git a/test/benchmark/__init__.py b/test/benchmark/__init__.py new file mode 100644 index 000000000000..5f890e85300d --- /dev/null +++ b/test/benchmark/__init__.py @@ -0,0 +1,127 @@ +import time + + +class TimeIt: + def __init__(self, name: str, time_logger=None): + self.name = name + self.logger = time_logger + self.timer = None + self.end_timer = None + + def __enter__(self): + self.timer = time.perf_counter() + return self + + @property + def dif(self): + return self.end_timer - self.timer + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.end_timer: + self.end_timer = time.perf_counter() + if self.logger: + self.logger.info(f"{self.dif:.4f} seconds in {self.name}.") + + +if __name__ == "__main__": + import argparse + import logging + import gc + import collections + import typing + + # makes this module runnable from its folder. + import sys + import os + sys.path.remove(os.path.dirname(__file__)) + new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.chdir(new_home) + sys.path.append(new_home) + + from Utils import init_logging, local_path + local_path.cached_path = new_home + from BaseClasses import MultiWorld, CollectionState, Location + from worlds import AutoWorld + from worlds.AutoWorld import call_all + + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + + class BenchmarkRunner: + gen_steps: typing.Tuple[str, ...] = ( + "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + rule_iterations: int = 100_000 + + if sys.version_info >= (3, 9): + @staticmethod + def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + else: + @staticmethod + def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + + def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: + with TimeIt(f"{test_location.game} {self.rule_iterations} " + f"runs of {test_location}.access_rule({state_name})", logger) as t: + for _ in range(self.rule_iterations): + test_location.access_rule(state) + # if time is taken to disentangle complex ref chains, + # this time should be attributed to the rule. + gc.collect() + return t.dif + + def main(self): + for game in sorted(AutoWorld.AutoWorldRegister.world_types): + summary_data: typing.Dict[str, collections.Counter[str]] = { + "empty_state": collections.Counter(), + "all_state": collections.Counter(), + } + try: + multiworld = MultiWorld(1) + multiworld.game[1] = game + multiworld.player_name = {1: "Tester"} + multiworld.set_seed(0) + multiworld.state = CollectionState(multiworld) + args = argparse.Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(getattr(option, "default")) + }) + multiworld.set_options(args) + + gc.collect() + for step in self.gen_steps: + with TimeIt(f"{game} step {step}", logger): + call_all(multiworld, step) + gc.collect() + + locations = sorted(multiworld.get_unfilled_locations()) + if not locations: + continue + + all_state = multiworld.get_all_state(False) + for location in locations: + time_taken = self.location_test(location, multiworld.state, "empty_state") + summary_data["empty_state"][location.name] = time_taken + + time_taken = self.location_test(location, all_state, "all_state") + summary_data["all_state"][location.name] = time_taken + + total_empty_state = sum(summary_data["empty_state"].values()) + total_all_state = sum(summary_data["all_state"].values()) + + logger.info(f"{game} took {total_empty_state/len(locations):.4f} " + f"seconds per location in empty_state and {total_all_state/len(locations):.4f} " + f"in all_state. (all times summed for {self.rule_iterations} runs.)") + logger.info(f"Top times in empty_state:\n" + f"{self.format_times_from_counter(summary_data['empty_state'])}") + logger.info(f"Top times in all_state:\n" + f"{self.format_times_from_counter(summary_data['all_state'])}") + + except Exception as e: + logger.exception(e) + + runner = BenchmarkRunner() + runner.main() From 5b93db121f1e73387286cdebbe0993b931038215 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 15 Jan 2024 06:29:30 +0300 Subject: [PATCH 4/7] Stardew Valley: Added missing rule on the club card (#2722) --- worlds/stardew_valley/rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index f56dec39a1f0..88aa13f31471 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -170,6 +170,8 @@ def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOp logic.received("Bus Repair").simplify()) MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player), logic.received(Wallet.skull_key).simplify()) + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_casino, player), + logic.received("Club Card").simplify()) for floor in range(25, 200 + 25, 25): MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player), logic.can_mine_to_skull_cavern_floor(floor).simplify()) From 6d393fe42b49d7d9ba8b6c513306a0f92209f2ba Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sun, 14 Jan 2024 22:47:32 -0500 Subject: [PATCH 5/7] TLOZ: update to new options API (#2714) --- worlds/tloz/ItemPool.py | 20 ++++++++++---------- worlds/tloz/Options.py | 14 +++++++------- worlds/tloz/Rules.py | 13 +++++++------ worlds/tloz/__init__.py | 11 ++++++----- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 456598edecef..5b90e99722df 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -94,17 +94,17 @@ def get_pool_core(world): # Starting Weapon start_weapon_locations = starting_weapon_locations.copy() final_starting_weapons = [weapon for weapon in starting_weapons - if weapon not in world.multiworld.non_local_items[world.player]] + if weapon not in world.options.non_local_items] if not final_starting_weapons: final_starting_weapons = starting_weapons starting_weapon = random.choice(final_starting_weapons) - if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: + if world.options.StartingPosition == StartingPosition.option_safe: placed_items[start_weapon_locations[0]] = starting_weapon - elif world.multiworld.StartingPosition[world.player] in \ + elif world.options.StartingPosition in \ [StartingPosition.option_unsafe, StartingPosition.option_dangerous]: - if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous: + if world.options.StartingPosition == StartingPosition.option_dangerous: for location in dangerous_weapon_locations: - if world.multiworld.ExpandedPool[world.player] or "Drop" not in location: + if world.options.ExpandedPool or "Drop" not in location: start_weapon_locations.append(location) placed_items[random.choice(start_weapon_locations)] = starting_weapon else: @@ -115,7 +115,7 @@ def get_pool_core(world): # Triforce Fragments fragment = "Triforce Fragment" - if world.multiworld.ExpandedPool[world.player]: + if world.options.ExpandedPool: possible_level_locations = [location for location in all_level_locations if location not in level_locations[8]] else: @@ -125,15 +125,15 @@ def get_pool_core(world): if location in possible_level_locations: possible_level_locations.remove(location) for level in range(1, 9): - if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: + if world.options.TriforceLocations == TriforceLocations.option_vanilla: placed_items[f"Level {level} Triforce"] = fragment - elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons: + elif world.options.TriforceLocations == TriforceLocations.option_dungeons: placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment else: pool.append(fragment) # Level 9 junk fill - if world.multiworld.ExpandedPool[world.player] > 0: + if world.options.ExpandedPool > 0: spots = random.sample(level_locations[8], len(level_locations[8]) // 2) for spot in spots: junk = random.choice(list(minor_items.keys())) @@ -142,7 +142,7 @@ def get_pool_core(world): # Finish Pool final_pool = basic_pool - if world.multiworld.ExpandedPool[world.player]: + if world.options.ExpandedPool: final_pool = { item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0) for item in set(basic_pool) | set(minor_items) | set(take_any_items) diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py index 96bd3e296dca..58a50ec35929 100644 --- a/worlds/tloz/Options.py +++ b/worlds/tloz/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Option, DefaultOnToggle, Choice +from dataclasses import dataclass +from Options import Option, DefaultOnToggle, Choice, PerGameCommonOptions class ExpandedPool(DefaultOnToggle): @@ -32,9 +33,8 @@ class StartingPosition(Choice): option_dangerous = 2 option_very_dangerous = 3 - -tloz_options: typing.Dict[str, type(Option)] = { - "ExpandedPool": ExpandedPool, - "TriforceLocations": TriforceLocations, - "StartingPosition": StartingPosition -} +@dataclass +class TlozOptions(PerGameCommonOptions): + ExpandedPool: ExpandedPool + TriforceLocations: TriforceLocations + StartingPosition: StartingPosition diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 12bf466bce99..b94002f25da2 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -11,6 +11,7 @@ def set_rules(tloz_world: "TLoZWorld"): player = tloz_world.player world = tloz_world.multiworld + options = tloz_world.options # Boss events for a nicer spoiler log play through for level in range(1, 9): @@ -23,7 +24,7 @@ def set_rules(tloz_world: "TLoZWorld"): # No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons for i, level in enumerate(tloz_world.levels[1:10]): for location in level.locations: - if world.StartingPosition[player] < StartingPosition.option_dangerous \ + if options.StartingPosition < StartingPosition.option_dangerous \ or location.name not in dangerous_weapon_locations: add_rule(world.get_location(location.name, player), lambda state: state.has_group("weapons", player)) @@ -66,7 +67,7 @@ def set_rules(tloz_world: "TLoZWorld"): lambda state: state.has("Recorder", player)) add_rule(world.get_location("Level 7 Boss", player), lambda state: state.has("Recorder", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), lambda state: state.has("Recorder", player)) add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), @@ -75,13 +76,13 @@ def set_rules(tloz_world: "TLoZWorld"): lambda state: state.has("Recorder", player)) for location in food_locations: - if world.ExpandedPool[player] or "Drop" not in location: + if options.ExpandedPool or "Drop" not in location: add_rule(world.get_location(location, player), lambda state: state.has("Food", player)) add_rule(world.get_location("Level 8 Item (Magical Key)", player), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) @@ -106,13 +107,13 @@ def set_rules(tloz_world: "TLoZWorld"): for location in stepladder_locations: add_rule(world.get_location(location, player), lambda state: state.has("Stepladder", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: for location in stepladder_locations_expanded: add_rule(world.get_location(location, player), lambda state: state.has("Stepladder", player)) # Don't allow Take Any Items until we can actually get in one - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Take Any Item Left", player), lambda state: state.has_group("candles", player) or state.has("Raft", player)) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 6e8927c4e7b9..f6aa71523992 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -13,7 +13,7 @@ from .Items import item_table, item_prices, item_game_ids from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations -from .Options import tloz_options +from .Options import TlozOptions from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late from .Rules import set_rules from worlds.AutoWorld import World, WebWorld @@ -63,7 +63,8 @@ class TLoZWorld(World): This randomizer shuffles all the items in the game around, leading to a new adventure every time. """ - option_definitions = tloz_options + options_dataclass = TlozOptions + options = TlozOptions settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False @@ -132,7 +133,7 @@ def create_regions(self): for i, level in enumerate(level_locations): for location in level: - if self.multiworld.ExpandedPool[self.player] or "Drop" not in location: + if self.options.ExpandedPool or "Drop" not in location: self.levels[i + 1].locations.append( self.create_location(location, self.location_name_to_id[location], self.levels[i + 1])) @@ -144,7 +145,7 @@ def create_regions(self): self.levels[level].locations.append(boss_event) for location in major_locations: - if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location: + if self.options.ExpandedPool or "Take Any" not in location: overworld.locations.append( self.create_location(location, self.location_name_to_id[location], overworld)) @@ -311,7 +312,7 @@ def get_filler_item_name(self) -> str: return self.multiworld.random.choice(self.filler_items) def fill_slot_data(self) -> Dict[str, Any]: - if self.multiworld.ExpandedPool[self.player]: + if self.options.ExpandedPool: take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item From d10f8f66c7288787553d3929157bf03b100f3d42 Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:48:44 -0700 Subject: [PATCH 6/7] Shivers: Fix rule logic for location 'Final Riddle: Guillotine Dropped' (#2706) --- worlds/shivers/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 62f4cd6a077f..57488ff33314 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -151,7 +151,7 @@ def get_rules_lookup(player: int): "Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player), "Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player), "Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player), - "Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player) + "Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player)) }, "elevators": { "Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player)) From 518b04c08eb45d6dad3db6d8ae5075e15c5c48fc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:17:46 +0100 Subject: [PATCH 7/7] SoE: minor typing and style fixes (#2724) * SoE: fix typing for tests * SoE: explicitly export pyevermizer To support loading the module from source (rather than module) we import pyevermizer from `__init__.py` in other files. This has been an implicit export and `mypy --strict` disables implicit exports, so we export it explicitly now. * SoE: fix style in patch.py * SoE: remove unused imports * SoE: fix format mistakes * SoE: cleaner typing in SoEOptions.flags as suggested by beauxq --- worlds/soe/__init__.py | 9 +++++---- worlds/soe/options.py | 8 +++++--- worlds/soe/patch.py | 2 +- worlds/soe/test/__init__.py | 4 ++-- worlds/soe/test/test_access.py | 6 +++--- worlds/soe/test/test_goal.py | 12 ++++++------ worlds/soe/test/test_oob.py | 4 ++-- worlds/soe/test/test_sequence_breaks.py | 4 ++-- worlds/soe/test/test_traps.py | 3 ++- 9 files changed, 28 insertions(+), 24 deletions(-) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index b431e471e2e9..74387fb1be80 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -13,12 +13,15 @@ from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_item_rule, set_rule from .logic import SoEPlayerLogic -from .options import AvailableFragments, Difficulty, EnergyCore, RequiredFragments, SoEOptions, TrapChance +from .options import Difficulty, EnergyCore, SoEOptions from .patch import SoEDeltaPatch, get_base_rom_path if typing.TYPE_CHECKING: from BaseClasses import MultiWorld, CollectionState +__all__ = ["pyevermizer", "SoEWorld"] + + """ In evermizer: @@ -158,7 +161,7 @@ class RomFile(settings.SNESRomPath): class SoEWorld(World): """ Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a - space station where the final boss must be defeated. + space station where the final boss must be defeated. """ game: typing.ClassVar[str] = "Secret of Evermore" options_dataclass = SoEOptions @@ -370,8 +373,6 @@ def generate_basic(self) -> None: self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? def generate_output(self, output_directory: str) -> None: - from dataclasses import asdict - player_name = self.multiworld.get_player_name(self.player) self.connect_name = player_name[:32] while len(self.connect_name.encode('utf-8')) > 32: diff --git a/worlds/soe/options.py b/worlds/soe/options.py index 0436b17618e7..cb9e9bb6de23 100644 --- a/worlds/soe/options.py +++ b/worlds/soe/options.py @@ -1,8 +1,8 @@ from dataclasses import dataclass, fields from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol -from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, ProgressionBalancing, \ - Range, Toggle +from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \ + ProgressionBalancing, Range, Toggle # typing boilerplate @@ -294,5 +294,7 @@ def flags(self) -> str: for field in fields(self): option = getattr(self, field.name) if isinstance(option, (EvermizerFlag, EvermizerFlags)): - flags += getattr(self, field.name).to_flag() + assert isinstance(option, Option) + # noinspection PyUnresolvedReferences + flags += option.to_flag() return flags diff --git a/worlds/soe/patch.py b/worlds/soe/patch.py index 8270f2d86dfa..a322de2af65f 100644 --- a/worlds/soe/patch.py +++ b/worlds/soe/patch.py @@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str: return file_name -def read_rom(stream: BinaryIO, strip_header: bool=True) -> bytes: +def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytes: """Reads rom into bytearray and optionally strips off any smc header""" data = stream.read() if strip_header and len(data) % 0x400 == 0x200: diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index b3ba7018e48d..1ab852163053 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -6,7 +6,7 @@ class SoETestBase(WorldTestBase): game = "Secret of Evermore" def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (), - satisfied=True) -> None: + satisfied: bool = True) -> None: """ Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True. Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True @@ -19,7 +19,7 @@ def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: self.assertFalse(self.can_reach_location(location), f"{location} is reachable but shouldn't be") - def testRocketPartsExist(self): + def testRocketPartsExist(self) -> None: """Tests that rocket parts exist and are unique""" self.assertEqual(len(self.get_items_by_name("Gauge")), 1) self.assertEqual(len(self.get_items_by_name("Wheel")), 1) diff --git a/worlds/soe/test/test_access.py b/worlds/soe/test/test_access.py index 81b8818eb528..f1d6ee993b34 100644 --- a/worlds/soe/test/test_access.py +++ b/worlds/soe/test/test_access.py @@ -4,10 +4,10 @@ class AccessTest(SoETestBase): @staticmethod - def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]): + def _resolveGourds(gourds: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]: return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers] - def test_bronze_axe(self): + def test_bronze_axe(self) -> None: gourds = { "Pyramid bottom": (118, 121, 122, 123, 124, 125), "Pyramid top": (140,) @@ -16,7 +16,7 @@ def test_bronze_axe(self): items = [["Bronze Axe"]] self.assertAccessDependency(locations, items) - def test_bronze_spear_plus(self): + def test_bronze_spear_plus(self) -> None: locations = ["Megataur"] items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]] self.assertAccessDependency(locations, items) diff --git a/worlds/soe/test/test_goal.py b/worlds/soe/test/test_goal.py index 885c2a74ef14..bb64b8eca759 100644 --- a/worlds/soe/test/test_goal.py +++ b/worlds/soe/test/test_goal.py @@ -8,7 +8,7 @@ class TestFragmentGoal(SoETestBase): "required_fragments": 20, } - def test_fragments(self): + def test_fragments(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) # 0 fragments fragments = self.get_items_by_name("Energy Core Fragment") @@ -24,11 +24,11 @@ def test_fragments(self): self.assertEqual(self.count("Energy Core Fragment"), 21) self.assertBeatable(True) - def test_no_weapon(self): + def test_no_weapon(self) -> None: self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"]) self.assertBeatable(False) - def test_no_rocket(self): + def test_no_rocket(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"]) self.assertBeatable(False) @@ -38,16 +38,16 @@ class TestShuffleGoal(SoETestBase): "energy_core": "shuffle", } - def test_core(self): + def test_core(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) self.collect_by_name(["Energy Core"]) self.assertBeatable(True) - def test_no_weapon(self): + def test_no_weapon(self) -> None: self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"]) self.assertBeatable(False) - def test_no_rocket(self): + def test_no_rocket(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"]) self.assertBeatable(False) diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py index 969e93d4f6af..3c1a2829de8e 100644 --- a/worlds/soe/test/test_oob.py +++ b/worlds/soe/test/test_oob.py @@ -6,7 +6,7 @@ class OoBTest(SoETestBase): """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic.""" options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"} - def test_oob_access(self): + def test_oob_access(self) -> None: in_logic = self.options["out_of_bounds"] == "logic" # some locations that just need a weapon + OoB @@ -37,7 +37,7 @@ def test_oob_access(self): self.collect_by_name("Diamond Eye") self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) - def test_oob_goal(self): + def test_oob_goal(self) -> None: # still need Energy Core with OoB if sequence breaks are not in logic for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: self.collect_by_name(item) diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py index 8a7f9c64ede8..2da8c9242cb9 100644 --- a/worlds/soe/test/test_sequence_breaks.py +++ b/worlds/soe/test/test_sequence_breaks.py @@ -6,7 +6,7 @@ class SequenceBreaksTest(SoETestBase): """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic.""" options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"} - def test_sequence_breaks_access(self): + def test_sequence_breaks_access(self) -> None: in_logic = self.options["sequence_breaks"] == "logic" # some locations that just need any weapon + sequence break @@ -30,7 +30,7 @@ def test_sequence_breaks_access(self): self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead self.assertEqual(self.can_reach_location("Escape"), in_logic) - def test_sequence_breaks_goal(self): + def test_sequence_breaks_goal(self) -> None: in_logic = self.options["sequence_breaks"] == "logic" # don't need Energy Core with sequence breaks in logic diff --git a/worlds/soe/test/test_traps.py b/worlds/soe/test/test_traps.py index f83a37be8223..7babd4522b30 100644 --- a/worlds/soe/test/test_traps.py +++ b/worlds/soe/test/test_traps.py @@ -32,7 +32,8 @@ def test_dataclass(self) -> None: def test_trap_count(self) -> None: """Test that total trap count is correct""" - self.assertEqual(self.options["trap_count"], len(self.get_items_by_name(self.option_name_to_item_name.values()))) + self.assertEqual(self.options["trap_count"], + len(self.get_items_by_name(self.option_name_to_item_name.values()))) class TestTrapAllZeroChance(Bases.TrapTestBase):