From f10163e7d2ba79f512f39ba47d21c40a89774006 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 29 Sep 2021 09:12:23 +0200 Subject: [PATCH] SoE: implement logic --- worlds/soe/Logic.py | 52 ++++++++++++++ worlds/soe/Options.py | 7 ++ worlds/soe/__init__.py | 149 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 worlds/soe/Logic.py create mode 100644 worlds/soe/Options.py create mode 100644 worlds/soe/__init__.py diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py new file mode 100644 index 00000000..90d52cf1 --- /dev/null +++ b/worlds/soe/Logic.py @@ -0,0 +1,52 @@ +from BaseClasses import MultiWorld +from ..AutoWorld import LogicMixin +from typing import Set +# TODO: import Options +# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? + +from . import pyevermizer + +# TODO: resolve/flatten/expand rules to get rid of recursion below where possible +# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) +rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] +# Logic.items are all items excluding non-progression items and duplicates +item_names: Set[str] = set() +items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items()) + if item.name not in item_names and not item_names.add(item.name)] + + +# when this module is loaded, this mixin will extend BaseClasses.CollectionState +class SecretOfEvermoreLogic(LogicMixin): + def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: + """ + Returns reached count of one of evermizer's progress steps based on + collected items. i.e. returns 0-3 for P_DE based on items giving CHECK_BOSS,DIAMOND_EYE_DROP + """ + n = 0 + for item in items: + for pvd in item.provides: + if pvd[1] == progress: + if self.has(item.name, player): + n += self.item_count(item.name, player) * pvd[0] + if n >= max_count > 0: + return n + for rule in rules: + for pvd in rule.provides: + if pvd[1] == progress and pvd[0] > 0: + has = True + for req in rule.requires: + if not self._soe_has(req[1], world, player, req[0]): + has = False + break + if has: + n += pvd[0] + if n >= max_count > 0: + return n + return n + + def _soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: + """ + Returns True if count of an evermizer progress steps are reached based + on collected items. i.e. 2 * P_DE + """ + return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py new file mode 100644 index 00000000..57b32bd3 --- /dev/null +++ b/worlds/soe/Options.py @@ -0,0 +1,7 @@ +import typing +from Options import Option + +# TODO: add options + +soe_options: typing.Dict[str, type(Option)] = { +} diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py new file mode 100644 index 00000000..9628ba50 --- /dev/null +++ b/worlds/soe/__init__.py @@ -0,0 +1,149 @@ +from .Options import soe_options +from ..AutoWorld import World +from ..generic.Rules import set_rule +from BaseClasses import Region, Location, Entrance, Item +import typing +from . import Logic # load logic mixin + +try: + import pyevermizer # from package +except ImportError: + from . import pyevermizer # as part of the source tree + +""" +In evermizer: + +Items are uniquely defined by a pair of (type, id). +For most items this is their vanilla location (i.e. CHECK_GOURD, number). + +Items have `provides`, which give the actual progression +instead of providing multiple events per item, we iterate through them in Logic.py + e.g. Found any weapon + +Locations have `requires` and `provides`. +Requirements have to be converted to (access) rules for AP + e.g. Chest locked behind having a weapon +Provides could be events, but instead we iterate through the entire logic in Logic.py + e.g. NPC available after fighting a Boss + +Rules are special locations that don't have a physical location +instead of implementing virtual locations and virtual items, we simply use them in Logic.py + e.g. 2DEs+Wheel+Gauge = Rocket + +Rules and Locations live on the same logic tree returned by pyevermizer.get_logic() + +TODO: for balancing we may want to generate Regions (with Entrances) for some +common rules, place the locations in those Regions and shorten the rules. +""" + +GAME_NAME = "Secret of Evermore" +ID_OFF_BASE = 64000 +ID_OFFS: typing.Dict[int,int] = { + pyevermizer.CHECK_ALCHEMY: ID_OFF_BASE + 0, # alchemy 64000..64049 + pyevermizer.CHECK_BOSS: ID_OFF_BASE + 50, # bosses 64050..6499 + pyevermizer.CHECK_GOURD: ID_OFF_BASE + 100, # gourds 64100..64399 + pyevermizer.CHECK_NPC: ID_OFF_BASE + 400, # npc 64400..64499 + # TODO: sniff 64500..64799 +} + + +def _get_locations(): + locs = pyevermizer.get_locations() + for loc in locs: + if loc.type == 3: # TODO: CHECK_GOURD + loc.name = f'{loc.name} #{loc.index}' + return locs + + +def _get_location_ids(): + m = {} + for loc in _get_locations(): + m[loc.name] = ID_OFFS[loc.type] + loc.index + m['Done'] = None + return m + + +def _get_items(): + return pyevermizer.get_items() + + +def _get_item_ids(): + m = {} + for item in _get_items(): + if item.name in m: continue + m[item.name] = ID_OFFS[item.type] + item.index + m['Victory'] = None + return m + + +class SoEWorld(World): + """ + TODO: insert game description here + """ + game: str = GAME_NAME + # options = soe_options + topology_present: bool = True + + item_name_to_id = _get_item_ids() + location_name_to_id = _get_location_ids() + + remote_items: bool = True # False # True only for testing + + def generate_basic(self): + print('SoE: generate_basic') + itempool = [item for item in map(lambda item: self.create_item(item), _get_items())] + self.world.itempool += itempool + self.world.get_location('Done', self.player).place_locked_item(self.create_event('Victory')) + + def create_regions(self): + # TODO: generate *some* regions from locations' requirements + r = Region('Menu', None, 'Menu', self.player, self.world) + r.exits = [Entrance(self.player, 'New Game', r)] + self.world.regions += [r] + + r = Region('Ingame', None, 'Ingame', self.player, self.world) + r.locations = [SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r) + for loc in _get_locations()] + r.locations.append(SoELocation(self.player, 'Done', None, r)) + self.world.regions += [r] + + self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player)) + + def create_event(self, event: str) -> Item: + progression = True + return SoEItem(event, progression, None, self.player) + + def create_item(self, item) -> Item: + # TODO: if item is string: look up item by name + return SoEItem(item.name, item.progression, self.item_name_to_id[item.name], self.player) + + def set_rules(self): + print('SoE: set_rules') + self.world.completion_condition[self.player] = lambda state: state.has('Victory', self.player) + # set Done from goal option once we have multiple goals + set_rule(self.world.get_location('Done', self.player), + lambda state: state._soe_has(pyevermizer.P_FINAL_BOSS, self.world, self.player)) + set_rule(self.world.get_entrance('New Game', self.player), lambda state: True) + for loc in _get_locations(): + set_rule(self.world.get_location(loc.name, self.player), self.make_rule(loc.requires)) + + def make_rule(self, requires): + def rule(state): + for count, progress in requires: + if not state._soe_has(progress, self.world, self.player, count): + return False + return True + + return rule + + +class SoEItem(Item): + game: str = GAME_NAME + + +class SoELocation(Location): + game: str = GAME_NAME + + def __init__(self, player: int, name: str, address: typing.Optional[int], parent): + super().__init__(player, name, address, parent) + self.event = not address