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.

This commit is contained in:
Fabian Dill 2022-02-17 06:07:11 +01:00
parent 311fb04647
commit c525c80b49
5 changed files with 82 additions and 67 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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

72
Main.py
View File

@ -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")