From c525c80b4942839f4044c86cbade6e2c19863c90 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 17 Feb 2022 06:07:11 +0100 Subject: [PATCH] ItemLinks: move item links to events, mess up their logic in doing so and lock them behind plando option "item_links" until they're fixed. --- BaseClasses.py | 70 ++++++++++++++++++++++---------------- Fill.py | 2 +- Generate.py | 3 ++ Main.py | 72 ++++++++++++++++++++-------------------- worlds/alttp/__init__.py | 2 +- 5 files changed, 82 insertions(+), 67 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index e617f929..d9b3bf90 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -21,11 +21,13 @@ else: auto_world = object -class Group(TypedDict): +class Group(TypedDict, total=False): name: str game: str world: auto_world players: Set[int] + item_pool: Set[str] + replacement_items: Dict[int, Optional[str]] class MultiWorld(): @@ -43,6 +45,7 @@ class MultiWorld(): groups: Dict[int, Group] is_race: bool = False precollected_items: Dict[int, List[Item]] + state: CollectionState class AttributeProxy(): def __init__(self, rule): @@ -65,7 +68,6 @@ class MultiWorld(): self.seed = None self.seed_name: str = "Unavailable" self.precollected_items = {player: [] for player in self.player_ids} - self.state = CollectionState(self) self._cached_entrances = None self._cached_locations = None self._entrance_cache = {} @@ -145,6 +147,9 @@ class MultiWorld(): self.worlds = {} self.slot_seeds = {} + def get_all_ids(self): + return self.player_ids + tuple(self.groups) + def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]: """Create a group with name and return the assigned player ID and group. If a group of this name already exists, the set of players is extended instead of creating a new one.""" @@ -166,33 +171,11 @@ class MultiWorld(): getattr(self, option_key)[new_id] = option(option.default) self.worlds[new_id] = world_type(self, new_id) - self.player_name[new_id] = name - # TODO: remove when LttP are transitioned over - self.difficulty_requirements[new_id] = self.difficulty_requirements[next(iter(players))] new_group = self.groups[new_id] = Group(name=name, game=game, players=players, world=self.worlds[new_id]) - # instead of collect/remove overwrites, should encode sending as Events so they show up in spoiler log - def group_collect(state, item) -> bool: - changed = False - for player in new_group["players"]: - max(self.worlds[player].collect(state, item), changed) - return changed - - def group_remove(state, item) -> bool: - changed = False - for player in new_group["players"]: - max(self.worlds[player].remove(state, item), changed) - return changed - - new_world = new_group["world"] - new_world.collect = group_collect - new_world.remove = group_remove - - self.worlds[new_id] = new_world - return new_id, new_group def get_player_groups(self, player) -> Set[int]: @@ -221,6 +204,35 @@ class MultiWorld(): setattr(self, option_key, getattr(args, option_key, {})) self.worlds[player] = world_type(self, player) + item_links = {} + + for player in self.player_ids: + for item_link in self.item_links[player].value: + if item_link["name"] in item_links: + item_links[item_link["name"]]["players"][player] = item_link["replacement_item"] + item_links[item_link["name"]]["item_pool"] &= set(item_link["item_pool"]) + else: + if item_link["name"] in self.player_name.values(): + raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}).") + item_links[item_link["name"]] = { + "players": {player: item_link["replacement_item"]}, + "item_pool": set(item_link["item_pool"]), + "game": self.game[player] + } + + for name, item_link in item_links.items(): + current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups + pool = set() + for item in item_link["item_pool"]: + pool |= current_item_name_groups.get(item, {item}) + item_link["item_pool"] = pool + + for group_name, item_link in item_links.items(): + game = item_link["game"] + group_id, group = self.add_group(group_name, game, set(item_link["players"])) + group["item_pool"] = item_link["item_pool"] + group["replacement_items"] = item_link["players"] + # intended for unittests def set_default_common_options(self): for option_key, option in Options.common_options.items(): @@ -544,12 +556,12 @@ class CollectionState(object): def __init__(self, parent: MultiWorld): self.prog_items = Counter() self.world = parent - self.reachable_regions = {player: set() for player in range(1, parent.players + 1)} - self.blocked_connections = {player: set() for player in range(1, parent.players + 1)} + self.reachable_regions = {player: set() for player in parent.get_all_ids()} + self.blocked_connections = {player: set() for player in parent.get_all_ids()} self.events = set() self.path = {} self.locations_checked = set() - self.stale = {player: True for player in range(1, parent.players + 1)} + self.stale = {player: True for player in parent.get_all_ids()} for items in parent.precollected_items.values(): for item in items: self.collect(item, True) @@ -591,9 +603,9 @@ class CollectionState(object): ret = CollectionState(self.world) ret.prog_items = self.prog_items.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in - range(1, self.world.players + 1)} + self.reachable_regions} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in - range(1, self.world.players + 1)} + self.blocked_connections} ret.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) diff --git a/Fill.py b/Fill.py index 94a99380..812bf85f 100644 --- a/Fill.py +++ b/Fill.py @@ -310,7 +310,7 @@ def balance_multiworld_progression(world: MultiWorld): checked_locations = set() unchecked_locations = set(world.get_locations()) - reachable_locations_count = {player: 0 for player in world.player_ids} + reachable_locations_count = {player: 0 for player in world.get_all_ids()} def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) diff --git a/Generate.py b/Generate.py index 1ef8a067..0364bcde 100644 --- a/Generate.py +++ b/Generate.py @@ -502,6 +502,9 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b roll_alttp_settings(ret, game_weights, plando_options) else: raise Exception(f"Unsupported game {ret.game}") + # not meant to stay here, intended to be removed when itemlinks are stable + if not "item_links" in plando_options: + ret.item_links.value = [] return ret diff --git a/Main.py b/Main.py index 39f98ee8..42b72026 100644 --- a/Main.py +++ b/Main.py @@ -11,7 +11,7 @@ import tempfile import zipfile from typing import Dict, Tuple, Optional, Set -from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType +from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location from worlds.alttp.Items import item_name_groups from worlds.alttp.Regions import lookup_vanilla_location_to_entrance from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned @@ -71,12 +71,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.plando_connections = args.plando_connections.copy() world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() - world.set_options(args) world.player_name = args.name.copy() world.enemizer = args.enemizercli world.sprite = args.sprite.copy() world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. + world.set_options(args) + world.state = CollectionState(world) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) logger.info("Found World Types:") @@ -138,38 +139,18 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No AutoWorld.call_all(world, "generate_basic") # temporary home for item links, should be moved out of Main - item_links = {} - for player in world.player_ids: - for item_link in world.item_links[player].value: - if item_link["name"] in item_links: - item_links[item_link["name"]]["players"][player] = item_link["replacement_item"] - item_links[item_link["name"]]["item_pool"] &= set(item_link["item_pool"]) - else: - if item_link["name"] in world.player_name.values(): - raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}).") - item_links[item_link["name"]] = { - "players": {player: item_link["replacement_item"]}, - "item_pool": set(item_link["item_pool"]), - "game": world.game[player] - } + for group_id, group in world.groups.items(): + # TODO: remove when LttP options are transitioned over + world.difficulty_requirements[group_id] = world.difficulty_requirements[next(iter(group["players"]))] - for name, item_link in item_links.items(): - current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups - pool = set() - for item in item_link["item_pool"]: - pool |= current_item_name_groups.get(item, {item}) - item_link["item_pool"] = pool - - for group_name, item_link in item_links.items(): - game = item_link["game"] - group_id, group = world.add_group(group_name, game, set(item_link["players"])) - - def find_common_pool(players: Set[int], shared_pool: Set[int]) -> \ - Dict[int, Dict[str, int]]: + def find_common_pool(players: Set[int], shared_pool: Set[str]): + advancement = set() counters = {player: {name: 0 for name in shared_pool} for player in players} for item in world.itempool: if item.player in counters and item.name in shared_pool: counters[item.player][item.name] += 1 + if item.advancement: + advancement.add(item.name) for item in shared_pool: count = min(counters[player][item] for player in players) @@ -179,17 +160,32 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: for player in players: del(counters[player][item]) - return counters - - common_item_count = find_common_pool(group["players"], item_link["item_pool"]) + return counters, advancement + common_item_count, common_advancement_items = find_common_pool(group["players"], group["item_pool"]) + # TODO: fix logic + if common_advancement_items: + logger.warning(f"Logical requirements for {', '.join(common_advancement_items)} in group {group['name']} " + f"will be incorrect.") new_itempool = [] for item_name, item_count in next(iter(common_item_count.values())).items(): + advancement = item_name in common_advancement_items for _ in range(item_count): - new_itempool.append(group["world"].create_item(item_name)) + new_item = group["world"].create_item(item_name) + new_item.advancement = advancement + new_itempool.append(new_item) + region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world) + world.regions.append(region) + locations = region.locations = [] for item in world.itempool: - if common_item_count.get(item.player, {}).get(item.name, 0): + count = common_item_count.get(item.player, {}).get(item.name, 0) + if count: + loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}", + None, region) + loc.access_rule = lambda state: state.has(item.name, group_id, count) + locations.append(loc) + loc.place_locked_item(item) common_item_count[item.player][item.name] -= 1 else: new_itempool.append(item) @@ -197,13 +193,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No itemcount = len(world.itempool) world.itempool = new_itempool + # can produce more items than were removed while itemcount > len(world.itempool): for player in group["players"]: - if item_link["players"][player]: + if group["replacement_items"][player]: world.itempool.append(AutoWorld.call_single(world, "create_item", player, - item_link["players"][player])) + group["replacement_items"][player])) else: AutoWorld.call_single(world, "create_filler", player) + if any(world.item_links.values()): + world._recache() + world._all_state = None logger.info("Running Item Plando") diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index e4512ecb..e5082457 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -194,7 +194,7 @@ class ALTTPWorld(World): return elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3: return 'Mirror Shield' - elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2: + elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2: return 'Red Shield' elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1: return 'Blue Shield'