Zillion: validate rescue item links (#1140)

This commit is contained in:
Doug Hoskisson 2022-10-28 12:56:50 -07:00 committed by GitHub
parent 53974d568b
commit f298b8d6e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 48 additions and 8 deletions

16
Main.py
View File

@ -8,9 +8,9 @@ import concurrent.futures
import pickle import pickle
import tempfile import tempfile
import zipfile 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.Items import item_name_groups
from worlds.alttp.Regions import is_main_entrance from worlds.alttp.Regions import is_main_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned 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 # temporary home for item links, should be moved out of Main
for group_id, group in world.groups.items(): for group_id, group in world.groups.items():
def find_common_pool(players: Set[int], shared_pool: Set[str]): def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
classifications = collections.defaultdict(int) 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} counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in world.itempool: for item in world.itempool:
if item.player in counters and item.name in shared_pool: 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(): for player in players.copy():
if all([counters[player][item] == 0 for item in shared_pool]): if all([counters[player][item] == 0 for item in shared_pool]):
players.remove(player) players.remove(player)
del(counters[player]) del (counters[player])
if not players: if not players:
return None, None return None, None
@ -177,14 +179,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
counters[player][item] = count counters[player][item] = count
else: else:
for player in players: for player in players:
del(counters[player][item]) del (counters[player][item])
return counters, classifications return counters, classifications
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
if not common_item_count: if not common_item_count:
continue continue
new_itempool = [] new_itempool: List[Item] = []
for item_name, item_count in next(iter(common_item_count.values())).items(): for item_name, item_count in next(iter(common_item_count.values())).items():
for _ in range(item_count): for _ in range(item_count):
new_item = group["world"].create_item(item_name) new_item = group["world"].create_item(item_name)

View File

@ -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"Duplicate item reference of \"{item.name}\" in \"{world.worlds[player].game}\" "
f"of player \"{world.player_name[player]}\". Please make a copy instead.") 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: for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None) stage_callable = getattr(world_type, f"stage_{method_name}", None)
if stage_callable: if stage_callable:

View File

@ -11,7 +11,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \
from Options import AssembleOptions from Options import AssembleOptions
from .logic import cs_to_zz_locs from .logic import cs_to_zz_locs
from .region import ZillionLocation, ZillionRegion 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, \ 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, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \
zz_reg_name_to_reg_name, base_id zz_reg_name_to_reg_name, base_id
@ -242,6 +242,42 @@ class ZillionWorld(World):
self.world.completion_condition[self.player] = \ self.world.completion_condition[self.player] = \
lambda state: state.has("Win", 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: def post_fill(self) -> None:
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation. """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.""" This happens before progression balancing, so the items may not be in their final locations yet."""