Files
Grinch-AP/worlds/generic/Rules.py
black-sliver 0ed3baabd4 Core: add generic handling of excluded locations
Currently there can be locations that are marked as excluded,
but don't have rules to enforce it, while fill has special handling
for excluded locations already.

This change removes special rules, and adds a generic rule instead.
2022-11-01 14:08:24 +01:00

157 lines
6.6 KiB
Python

import collections
import typing
from BaseClasses import LocationProgressType, MultiWorld
if typing.TYPE_CHECKING:
import BaseClasses
CollectionRule = typing.Callable[[BaseClasses.CollectionState], bool]
ItemRule = typing.Callable[[BaseClasses.Item], bool]
else:
CollectionRule = typing.Callable[[object], bool]
ItemRule = typing.Callable[[object], bool]
def locality_needed(world: MultiWorld) -> bool:
for player in world.player_ids:
if world.local_items[player].value:
return True
if world.non_local_items[player].value:
return True
# Group
for group_id, group in world.groups.items():
if set(world.player_ids) == set(group["players"]):
continue
if group["local_items"]:
return True
if group["non_local_items"]:
return True
def locality_rules(world: MultiWorld):
if locality_needed(world):
forbid_data: typing.Dict[int, typing.Dict[int, typing.Set[str]]] = \
collections.defaultdict(lambda: collections.defaultdict(set))
def forbid(sender: int, receiver: int, items: typing.Set[str]):
forbid_data[sender][receiver].update(items)
for receiving_player in world.player_ids:
local_items: typing.Set[str] = world.local_items[receiving_player].value
if local_items:
for sending_player in world.player_ids:
if receiving_player != sending_player:
forbid(sending_player, receiving_player, local_items)
non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value
if non_local_items:
forbid(receiving_player, receiving_player, non_local_items)
# Group
for receiving_group_id, receiving_group in world.groups.items():
if set(world.player_ids) == set(receiving_group["players"]):
continue
if receiving_group["local_items"]:
for sending_player in world.player_ids:
if sending_player not in receiving_group["players"]:
forbid(sending_player, receiving_group_id, receiving_group["local_items"])
if receiving_group["non_local_items"]:
for sending_player in world.player_ids:
if sending_player in receiving_group["players"]:
forbid(sending_player, receiving_group_id, receiving_group["non_local_items"])
# create fewer lambda's to save memory and cache misses
func_cache = {}
for location in world.get_locations():
if (location.player, location.item_rule) in func_cache:
location.item_rule = func_cache[location.player, location.item_rule]
# empty rule that just returns True, overwrite
elif location.item_rule is location.__class__.item_rule:
func_cache[location.player, location.item_rule] = location.item_rule = \
lambda i, sending_blockers = forbid_data[location.player], \
old_rule = location.item_rule: \
i.name not in sending_blockers[i.player]
# special rule, needs to also be fulfilled.
else:
func_cache[location.player, location.item_rule] = location.item_rule = \
lambda i, sending_blockers = forbid_data[location.player], \
old_rule = location.item_rule: \
i.name not in sending_blockers[i.player] and old_rule(i)
def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
for loc_name in exclude_locations:
try:
location = world.get_location(loc_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
if loc_name not in world.worlds[player].location_name_to_id:
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
else:
location.progress_type = LocationProgressType.EXCLUDED
def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):
spot.access_rule = rule
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"):
old_rule = spot.access_rule
# empty rule, replace instead of add
if old_rule is spot.__class__.access_rule:
spot.access_rule = rule if combine == "and" else old_rule
else:
if combine == "and":
spot.access_rule = lambda state: rule(state) and old_rule(state)
else:
spot.access_rule = lambda state: rule(state) or old_rule(state)
def forbid_item(location: "BaseClasses.Location", item: str, player: int):
old_rule = location.item_rule
# empty rule
if old_rule is location.__class__.item_rule:
location.item_rule = lambda i: i.name != item or i.player != player
else:
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int):
old_rule = location.item_rule
location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i)
def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]):
"""unused, but kept as a debugging tool."""
old_rule = location.item_rule
location.item_rule = lambda i: i.name not in items and old_rule(i)
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"):
old_rule = location.item_rule
# empty rule, replace instead of add
if old_rule is location.__class__.item_rule:
location.item_rule = rule if combine == "and" else old_rule
else:
if combine == "and":
location.item_rule = lambda item: rule(item) and old_rule(item)
else:
location.item_rule = lambda item: rule(item) or old_rule(item)
def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int,
locations: typing.Sequence["BaseClasses.Location"]) -> bool:
for location in locations:
if item_name(state, location[0], location[1]) == (item, player):
return True
return False
def item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \
typing.Optional[typing.Tuple[str, int]]:
location = state.multiworld.get_location(location, player)
if location.item is None:
return None
return location.item.name, location.item.player