Skip to content

Commit

Permalink
Merge branch 'ArchipelagoMW:main' into mmx3-main
Browse files Browse the repository at this point in the history
  • Loading branch information
TheLX5 authored May 31, 2024
2 parents 7e277e8 + 7058575 commit 5357701
Show file tree
Hide file tree
Showing 71 changed files with 1,404 additions and 680 deletions.
8 changes: 4 additions & 4 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
return True
return False

def has_from_list_exclusive(self, items: Iterable[str], player: int, count: int) -> bool:
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
found: int = 0
Expand All @@ -743,7 +743,7 @@ def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)

def count_from_list_exclusive(self, items: Iterable[str], player: int) -> int:
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)

Expand All @@ -758,7 +758,7 @@ def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
return True
return False

def has_group_exclusive(self, item_name_group: str, player: int, count: int = 1) -> bool:
def has_group_unique(self, item_name_group: str, player: int, count: int = 1) -> bool:
"""Returns True if the state contains at least `count` items present in a specified item group.
Ignores duplicates of the same item.
"""
Expand All @@ -778,7 +778,7 @@ def count_group(self, item_name_group: str, player: int) -> int:
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)

def count_group_exclusive(self, item_name_group: str, player: int) -> int:
def count_group_unique(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state.
Ignores duplicates of the same item."""
player_prog_items = self.prog_items[player]
Expand Down
64 changes: 49 additions & 15 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
"""
:param multiworld: Multiworld to be filled.
:param base_state: State assumed before fill.
:param locations: Locations to be filled with item_pool
:param item_pool: Items to fill into the locations
:param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled.
:param item_pool: Items to fill into the locations, gets mutated by removing items that get placed.
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
:param lock: locations are set to locked as they are filled
:param swap: if true, swaps of already place items are done in the event of a dead end
Expand Down Expand Up @@ -220,7 +220,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
def remaining_fill(multiworld: MultiWorld,
locations: typing.List[Location],
itempool: typing.List[Item],
name: str = "Remaining") -> None:
name: str = "Remaining",
move_unplaceable_to_start_inventory: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
Expand Down Expand Up @@ -284,13 +285,21 @@ def remaining_fill(multiworld: MultiWorld,

if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
f"Unplaced items:\n"
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
if move_unplaceable_to_start_inventory:
last_batch = []
for item in unplaced_items:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
last_batch.append(multiworld.worlds[item.player].create_filler())
remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry")
else:
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
f"Unplaced items:\n"
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")

itempool.extend(unplaced_items)

Expand Down Expand Up @@ -420,7 +429,8 @@ def distribute_early_items(multiworld: MultiWorld,
return fill_locations, itempool


def distribute_items_restrictive(multiworld: MultiWorld) -> None:
def distribute_items_restrictive(multiworld: MultiWorld,
panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
fill_locations = sorted(multiworld.get_unfilled_locations())
multiworld.random.shuffle(fill_locations)
# get items to distribute
Expand Down Expand Up @@ -470,8 +480,29 @@ def mark_for_locking(location: Location):

if progitempool:
# "advancement/progression fill"
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1,
name="Progression")
if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=True,
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False,
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, allow_partial=True,
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
if progitempool:
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
multiworld.push_precollected(item)
filleritempool.append(multiworld.worlds[item.player].create_filler())
logging.warning(f"{len(progitempool)} items moved to start inventory,"
f" due to failure in Progression fill step.")
progitempool[:] = []

else:
raise ValueError(f"Generator Panic Method {panic_method} not recognized.")
if progitempool:
raise FillError(
f"Not enough locations for progression items. "
Expand All @@ -486,7 +517,9 @@ def mark_for_locking(location: Location):

inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)

remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded",
move_unplaceable_to_start_inventory=panic_method=="start_inventory")

if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. "
Expand All @@ -495,7 +528,8 @@ def mark_for_locking(location: Location):

restitempool = filleritempool + usefulitempool

remaining_fill(multiworld, defaultlocations, restitempool)
remaining_fill(multiworld, defaultlocations, restitempool,
move_unplaceable_to_start_inventory=panic_method=="start_inventory")

unplaced = restitempool
unfilled = defaultlocations
Expand Down
39 changes: 29 additions & 10 deletions Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import urllib.request
from collections import Counter
from typing import Any, Dict, Tuple, Union
from itertools import chain

import ModuleUpdate

Expand Down Expand Up @@ -319,18 +320,34 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
logging.debug(f'Applying {new_weights}')
cleaned_weights = {}
for option in new_weights:
option_name = option.lstrip("+")
option_name = option.lstrip("+-")
if option.startswith("+") and option_name in weights:
cleaned_value = weights[option_name]
new_value = new_weights[option]
if isinstance(new_value, (set, dict)):
if isinstance(new_value, set):
cleaned_value.update(new_value)
elif isinstance(new_value, list):
cleaned_value.extend(new_value)
elif isinstance(new_value, dict):
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
else:
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
f" received {type(new_value).__name__}.")
cleaned_weights[option_name] = cleaned_value
elif option.startswith("-") and option_name in weights:
cleaned_value = weights[option_name]
new_value = new_weights[option]
if isinstance(new_value, set):
cleaned_value.difference_update(new_value)
elif isinstance(new_value, list):
for element in new_value:
cleaned_value.remove(element)
elif isinstance(new_value, dict):
cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
else:
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
f" received {type(new_value).__name__}.")
cleaned_weights[option_name] = cleaned_value
else:
cleaned_weights[option_name] = new_weights[option]
new_options = set(cleaned_weights) - set(weights)
Expand Down Expand Up @@ -415,7 +432,6 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
del game_weights[option_key]
else:
player_option = option.from_any(option.default) # call the from_any here to support default "random"
setattr(ret, option_key, player_option)
Expand All @@ -429,9 +445,9 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
if "linked_options" in weights:
weights = roll_linked_options(weights)

valid_trigger_names = set()
valid_keys = set()
if "triggers" in weights:
weights = roll_triggers(weights, weights["triggers"], valid_trigger_names)
weights = roll_triggers(weights, weights["triggers"], valid_keys)

requirements = weights.get("requires", {})
if requirements:
Expand Down Expand Up @@ -466,12 +482,14 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game]

if any(weight.startswith("+") for weight in game_weights) or \
any(weight.startswith("+") for weight in weights):
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
for weight in chain(game_weights, weights):
if weight.startswith("+"):
raise Exception(f"Merge tag cannot be used outside of trigger contexts. Found {weight}")
if weight.startswith("-"):
raise Exception(f"Remove tag cannot be used outside of trigger contexts. Found {weight}")

if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"], valid_trigger_names)
weights = roll_triggers(weights, game_weights["triggers"], valid_keys)
game_weights = weights[ret.game]

ret.name = get_choice('name', weights)
Expand All @@ -480,8 +498,9 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b

for option_key, option in world_type.options_dataclass.type_hints.items():
handle_option(ret, game_weights, option_key, option, plando_options)
valid_keys.add(option_key)
for option_key in game_weights:
if option_key in {"triggers", *valid_trigger_names}:
if option_key in {"triggers", *valid_keys}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
if PlandoOptions.items in plando_options:
Expand Down
16 changes: 14 additions & 2 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
from Options import StartInventoryPool
from Utils import __version__, output_path, version_tuple
from Utils import __version__, output_path, version_tuple, get_settings
from settings import get_settings
from worlds import AutoWorld
from worlds.generic.Rules import exclusion_rules, locality_rules
Expand Down Expand Up @@ -272,7 +272,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
if multiworld.algorithm == 'flood':
flood_items(multiworld) # different algo, biased towards early game progress items
elif multiworld.algorithm == 'balanced':
distribute_items_restrictive(multiworld)
distribute_items_restrictive(multiworld, get_settings().generator.panic_method)

AutoWorld.call_all(multiworld, 'post_fill')

Expand Down Expand Up @@ -372,6 +372,17 @@ def precollect_hint(location):

checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}

# get spheres -> filter address==None -> skip empty
spheres: List[Dict[int, Set[int]]] = []
for sphere in multiworld.get_spheres():
current_sphere: Dict[int, Set[int]] = collections.defaultdict(set)
for sphere_location in sphere:
if type(sphere_location.address) is int:
current_sphere[sphere_location.player].add(sphere_location.address)

if current_sphere:
spheres.append(dict(current_sphere))

multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
Expand All @@ -386,6 +397,7 @@ def precollect_hint(location):
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": multiworld.seed_name,
"spheres": spheres,
"datapackage": data_package,
}
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
Expand Down
22 changes: 21 additions & 1 deletion MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,11 @@ class Context:
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
non_hintable_names: typing.Dict[str, typing.Set[str]]
spheres: typing.List[typing.Dict[int, typing.Set[int]]]
""" each sphere is { player: { location_id, ... } } """
logger: logging.Logger


def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
Expand Down Expand Up @@ -238,6 +241,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo
self.stored_data = {}
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
self.read_data = {}
self.spheres = []

# init empty to satisfy linter, I suppose
self.gamespackage = {}
Expand Down Expand Up @@ -466,6 +470,9 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A
for game_name, data in self.location_name_groups.items():
self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame]

# sorted access spheres
self.spheres = decoded_obj.get("spheres", [])

# saving

def save(self, now=False) -> bool:
Expand Down Expand Up @@ -624,6 +631,16 @@ def get_rechecked_hints(self, team: int, slot: int):
self.recheck_hints(team, slot)
return self.hints[team, slot]

def get_sphere(self, player: int, location_id: int) -> int:
"""Get sphere of a location, -1 if spheres are not available."""
if self.spheres:
for i, sphere in enumerate(self.spheres):
if location_id in sphere.get(player, set()):
return i
raise KeyError(f"No Sphere found for location ID {location_id} belonging to player {player}. "
f"Location or player may not exist.")
return -1

def get_players_package(self):
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]

Expand Down Expand Up @@ -1549,6 +1566,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
self.ctx.random.shuffle(not_found_hints)
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
# By another popular vote, prefer early sphere
not_found_hints.sort(key=lambda hint: self.ctx.get_sphere(hint.finding_player, hint.location),
reverse=True)

hints = found_hints + old_hints
while can_pay > 0:
Expand All @@ -1558,10 +1578,10 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
hints.append(hint)
can_pay -= 1
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)

self.ctx.notify_hints(self.client.team, hints)
if not_found_hints:
points_available = get_client_points(self.ctx, self.client)
if hints and cost and int((points_available // cost) == 0):
self.output(
f"There may be more hintables, however, you cannot afford to pay for any more. "
Expand Down
Loading

0 comments on commit 5357701

Please sign in to comment.