Skip to content

Commit

Permalink
Merge branch 'refs/heads/main' into fork/migrate-descriptions
Browse files Browse the repository at this point in the history
# Conflicts:
#	WebHostLib/options.py
#	docs/world api.md
#	worlds/AutoWorld.py
  • Loading branch information
ThePhar committed May 23, 2024
2 parents f883157 + 8b6eae0 commit ce4e253
Show file tree
Hide file tree
Showing 285 changed files with 34,518 additions and 5,286 deletions.
8 changes: 8 additions & 0 deletions AHITClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from worlds.ahit.Client import launch
import Utils
import ModuleUpdate
ModuleUpdate.update()

if __name__ == "__main__":
Utils.init_logging("AHITClient", exception_logger="Client")
launch()
76 changes: 66 additions & 10 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
TypedDict, Union, Type, ClassVar

import NetUtils
import Options
Expand Down Expand Up @@ -707,15 +707,49 @@ def has_any(self, items: Iterable[str], player: int) -> bool:
"""Returns True if at least one item name of items is in state at least once."""
return any(self.prog_items[player][item] for item in items)

def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified."""
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())

def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified."""
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())

def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]

def item_count(self, item: str, player: int) -> int:
Utils.deprecate("Use count instead.")
return self.count(item, player)
def has_from_list(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."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name]
if found >= count:
return True
return False

def has_from_list_exclusive(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
player_prog_items = self.prog_items[player]
for item_name in items:
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False

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

# item name group related
def has_group(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."""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
Expand All @@ -724,12 +758,34 @@ def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:
return True
return False

def count_group(self, item_name_group: str, player: int) -> int:
def has_group_exclusive(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.
"""
found: int = 0
player_prog_items = self.prog_items[player]
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]:
found += player_prog_items[item_name]
return found
found += player_prog_items[item_name] > 0
if found >= count:
return True
return False

def count_group(self, item_name_group: str, player: int) -> int:
"""Returns the cumulative count of items from an item group present in state."""
player_prog_items = self.prog_items[player]
return sum(
player_prog_items[item_name]
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:
"""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]
return sum(
player_prog_items[item_name] > 0
for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]
)

# Item related
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
Expand Down Expand Up @@ -990,7 +1046,7 @@ def __init__(self, player: int, name: str = '', address: Optional[int] = None, p
self.parent_region = parent

def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player])
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))))
Expand Down Expand Up @@ -1186,7 +1242,7 @@ def create_playthrough(self, create_paths: bool = True) -> None:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([multiworld.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
Expand Down
2 changes: 2 additions & 0 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ class CommonContext:

finished_game: bool
ready: bool
team: typing.Optional[int]
slot: typing.Optional[int]
auth: typing.Optional[str]
seed_name: typing.Optional[str]

Expand Down
85 changes: 63 additions & 22 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ def _log_fill_progress(name: str, placed: int, total_items: int) -> None:
logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.")


def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState:
def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple(),
locations: typing.Optional[typing.List[Location]] = None) -> CollectionState:
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.sweep_for_events()
new_state.sweep_for_events(locations=locations)
return new_state


Expand All @@ -34,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 @@ -66,7 +67,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
item_pool.pop(p)
break
maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items)
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
if single_player_placement else None)

has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)

Expand Down Expand Up @@ -112,7 +114,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati

location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool)
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool,
multiworld.get_filled_locations(item.player)
if single_player_placement else None)
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
# to clean that up later, so there is a chance generation fails.
Expand Down Expand Up @@ -170,7 +174,9 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati

if cleanup_required:
# validate all placements and remove invalid ones
state = sweep_from_pool(base_state, [])
state = sweep_from_pool(
base_state, [], multiworld.get_filled_locations(item.player)
if single_player_placement else None)
for placement in placements:
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
placement.item.location = None
Expand Down Expand Up @@ -214,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 @@ -278,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 @@ -414,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 @@ -456,14 +472,37 @@ def mark_for_locking(location: Location):

if prioritylocations:
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

if progitempool:
# "advancement/progression fill"
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, 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 @@ -478,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 @@ -487,18 +528,18 @@ 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

if unplaced or unfilled:
logging.warning(
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}")
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
items_counter = Counter(location.item.player for location in multiworld.get_filled_locations())
locations_counter = Counter(location.player for location in multiworld.get_locations())
items_counter.update(item.player for item in unplaced)
locations_counter.update(location.player for location in unfilled)
print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f"Per-Player counts: {print_data})")

Expand Down
Loading

0 comments on commit ce4e253

Please sign in to comment.