From f298b8d6e71e010572735717ed57fed487aa2705 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 28 Oct 2022 12:56:50 -0700 Subject: [PATCH] Zillion: validate rescue item links (#1140) --- Main.py | 16 +++++++++------- worlds/AutoWorld.py | 2 ++ worlds/zillion/__init__.py | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Main.py b/Main.py index 38100bd0..d4df1b18 100644 --- a/Main.py +++ b/Main.py @@ -8,9 +8,9 @@ import concurrent.futures import pickle import tempfile import zipfile -from typing import Dict, Tuple, Optional, Set +from typing import Dict, List, Tuple, Optional, Set -from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location +from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location from worlds.alttp.Items import item_name_groups from worlds.alttp.Regions import is_main_entrance from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned @@ -154,8 +154,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # temporary home for item links, should be moved out of Main for group_id, group in world.groups.items(): - def find_common_pool(players: Set[int], shared_pool: Set[str]): - classifications = collections.defaultdict(int) + def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ + Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] + ]: + classifications: Dict[str, int] = collections.defaultdict(int) 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: @@ -165,7 +167,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in players.copy(): if all([counters[player][item] == 0 for item in shared_pool]): players.remove(player) - del(counters[player]) + del (counters[player]) if not players: return None, None @@ -177,14 +179,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No counters[player][item] = count else: for player in players: - del(counters[player][item]) + del (counters[player][item]) return counters, classifications common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) if not common_item_count: continue - new_itempool = [] + new_itempool: List[Item] = [] for item_name, item_count in next(iter(common_item_count.values())).items(): for _ in range(item_count): new_item = group["world"].create_item(item_name) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 8d3fab64..37cf5300 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -90,6 +90,8 @@ def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None: f"Duplicate item reference of \"{item.name}\" in \"{world.worlds[player].game}\" " f"of player \"{world.player_name[player]}\". Please make a copy instead.") + # TODO: investigate: Iterating through a set is not a deterministic order. + # If any random is used, this could make unreproducible seed. for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) if stage_callable: diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index d9827828..918b91ec 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -11,7 +11,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \ from Options import AssembleOptions from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import zillion_options, validate +from .options import ZillionStartChar, zillion_options, 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 @@ -242,6 +242,42 @@ class ZillionWorld(World): self.world.completion_condition[self.player] = \ lambda state: state.has("Win", self.player) + @staticmethod + def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: + # item link pools are about to be created in main + # JJ can't be an item link unless all the players share the same start_char + # (The reason for this is that the JJ ZillionItem will have a different ZzItem depending + # on whether the start char is Apple or Champ, and the logic depends on that ZzItem.) + for group in multiworld.groups.values(): + # TODO: remove asserts on group when we can specify which members of TypedDict are optional + assert "game" in group + if group["game"] == "Zillion": + assert "item_pool" in group + item_pool = group["item_pool"] + to_stay = "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].get_current_option_name()) + for player in group_players + ] + start_char_counts = Counter(sc for _, sc in players_start_chars) + # majority rules + if start_char_counts["Apple"] > start_char_counts["Champ"]: + to_stay = "Apple" + elif start_char_counts["Champ"] > start_char_counts["Apple"]: + to_stay = "Champ" + else: # equal + to_stay = multiworld.random.choice(("Apple", "Champ")) + + for p, sc in players_start_chars: + if sc != to_stay: + group_players.remove(p) + assert "world" in group + cast(ZillionWorld, group["world"])._make_item_maps(to_stay) + def post_fill(self) -> None: """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. This happens before progression balancing, so the items may not be in their final locations yet."""