Skip to content

Commit

Permalink
Merge branch 'main' into smw-main
Browse files Browse the repository at this point in the history
  • Loading branch information
PoryGone committed Dec 1, 2022
2 parents b92024e + ef66f64 commit a161083
Show file tree
Hide file tree
Showing 44 changed files with 570 additions and 379 deletions.
53 changes: 21 additions & 32 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class MultiWorld():
state: CollectionState

accessibility: Dict[int, Options.Accessibility]
early_items: Dict[int, Options.EarlyItems]
early_items: Dict[int, Dict[str, int]]
local_early_items: Dict[int, Dict[str, int]]
local_items: Dict[int, Options.LocalItems]
non_local_items: Dict[int, Options.NonLocalItems]
progression_balancing: Dict[int, Options.ProgressionBalancing]
Expand Down Expand Up @@ -94,6 +95,8 @@ def __init__(self, players: int):
self.customitemarray = []
self.shuffle_ganon = True
self.spoiler = Spoiler(self)
self.early_items = {player: {} for player in self.player_ids}
self.local_early_items = {player: {} for player in self.player_ids}
self.indirect_connections = {}
self.fix_trock_doors = self.AttributeProxy(
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
Expand Down Expand Up @@ -157,7 +160,7 @@ def set_player_attr(attr, val):
self.worlds = {}
self.slot_seeds = {}

def get_all_ids(self):
def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups)

def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
Expand Down Expand Up @@ -282,11 +285,11 @@ def secure(self):
self.is_race = True

@functools.cached_property
def player_ids(self):
def player_ids(self) -> Tuple[int, ...]:
return tuple(range(1, self.players + 1))

@functools.lru_cache()
def get_game_players(self, game_name: str):
def get_game_players(self, game_name: str) -> Tuple[int, ...]:
return tuple(player for player in self.player_ids if self.game[player] == game_name)

@functools.lru_cache()
Expand All @@ -305,10 +308,7 @@ def get_file_safe_player_name(self, player: int) -> str:

def get_out_file_name_base(self, player: int) -> str:
""" the base name (without file extension) for each player's output file for a seed """
return f"AP_{self.seed_name}_P{player}" \
+ (f"_{self.get_file_safe_player_name(player).replace(' ', '_')}"
if (self.player_name[player] != f"Player{player}")
else '')
return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}"

def initialize_regions(self, regions=None):
for region in regions if regions else self.regions:
Expand Down Expand Up @@ -424,46 +424,35 @@ def register_indirect_condition(self, region: Region, entrance: Entrance):
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
self.indirect_connections.setdefault(region, set()).add(entrance)

def get_locations(self) -> List[Location]:
def get_locations(self, player: Optional[int] = None) -> List[Location]:
if self._cached_locations is None:
self._cached_locations = [location for region in self.regions for location in region.locations]
if player is not None:
return [location for location in self._cached_locations if location.player == player]
return self._cached_locations

def clear_location_cache(self):
self._cached_locations = None

def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]:
if player is not None:
return [location for location in self.get_locations() if
location.player == player and not location.item]
return [location for location in self.get_locations() if not location.item]

def get_unfilled_dungeon_locations(self):
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
return [location for location in self.get_locations(player) if location.item is None]

def get_filled_locations(self, player: Optional[int] = None) -> List[Location]:
if player is not None:
return [location for location in self.get_locations() if
location.player == player and location.item is not None]
return [location for location in self.get_locations() if location.item is not None]
return [location for location in self.get_locations(player) if location.item is not None]

def get_reachable_locations(self, state: Optional[CollectionState] = None, player: Optional[int] = None) -> List[Location]:
if state is None:
state = self.state
return [location for location in self.get_locations() if
(player is None or location.player == player) and location.can_reach(state)]
state: CollectionState = state if state else self.state
return [location for location in self.get_locations(player) if location.can_reach(state)]

def get_placeable_locations(self, state=None, player=None) -> List[Location]:
if state is None:
state = self.state
return [location for location in self.get_locations() if
(player is None or location.player == player) and location.item is None and location.can_reach(state)]
state: CollectionState = state if state else self.state
return [location for location in self.get_locations(player) if location.item is None and location.can_reach(state)]

def get_unfilled_locations_for_players(self, locations: List[str], players: Iterable[int]):
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
for player in players:
if len(locations) == 0:
locations = [location.name for location in self.get_unfilled_locations(player)]
for location_name in locations:
if not location_names:
location_names = [location.name for location in self.get_unfilled_locations(player)]
for location_name in location_names:
location = self._location_cache.get((location_name, player), None)
if location is not None and location.item is None:
yield location
Expand Down
149 changes: 105 additions & 44 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]

def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None) -> None:
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
allow_partial: bool = False) -> None:
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []

Expand Down Expand Up @@ -132,7 +133,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
if on_place:
on_place(spot_to_fill)

if len(unplaced_items) > 0 and len(locations) > 0:
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
# There are leftover unplaceable items and locations that won't accept them
if world.can_beat_game():
logging.warning(
Expand Down Expand Up @@ -252,16 +253,20 @@ def distribute_early_items(world: MultiWorld,
fill_locations: typing.List[Location],
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
""" returns new fill_locations and itempool """
early_items_count: typing.Dict[typing.Tuple[str, int], int] = {}
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
for player in world.player_ids:
for item, count in world.early_items[player].value.items():
early_items_count[(item, player)] = count
items = itertools.chain(world.early_items[player], world.local_early_items[player])
for item in items:
early_items_count[item, player] = [world.early_items[player].get(item, 0),
world.local_early_items[player].get(item, 0)]
if early_items_count:
early_locations: typing.List[Location] = []
early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set()
base_state = world.state.copy()
base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations):
if loc.can_reach(world.state):
if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY:
early_priority_locations.append(loc)
else:
Expand All @@ -271,27 +276,56 @@ def distribute_early_items(world: MultiWorld,

early_prog_items: typing.List[Item] = []
early_rest_items: typing.List[Item] = []
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
item_indexes_to_remove: typing.Set[int] = set()
for i, item in enumerate(itempool):
if (item.name, item.player) in early_items_count:
if item.advancement:
early_prog_items.append(item)
if early_items_count[item.name, item.player][1]:
early_local_prog_items[item.player].append(item)
early_items_count[item.name, item.player][1] -= 1
else:
early_prog_items.append(item)
early_items_count[item.name, item.player][0] -= 1
else:
early_rest_items.append(item)
if early_items_count[item.name, item.player][1]:
early_local_rest_items[item.player].append(item)
early_items_count[item.name, item.player][1] -= 1
else:
early_rest_items.append(item)
early_items_count[item.name, item.player][0] -= 1
item_indexes_to_remove.add(i)
early_items_count[(item.name, item.player)] -= 1
if early_items_count[(item.name, item.player)] == 0:
del early_items_count[(item.name, item.player)]
if early_items_count[item.name, item.player] == [0, 0]:
del early_items_count[item.name, item.player]
if len(early_items_count) == 0:
break
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
fill_restrictive(world, world.state, early_locations, early_rest_items, lock=True)
for player in world.player_ids:
player_local = early_local_rest_items[player]
fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_rest_items.extend(early_local_rest_items[player])
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True)
early_locations += early_priority_locations
fill_restrictive(world, world.state, early_locations, early_prog_items, lock=True)
for player in world.player_ids:
player_local = early_local_prog_items[player]
fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_prog_items.extend(player_local)
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True)
unplaced_early_items = early_rest_items + early_prog_items
if unplaced_early_items:
logging.warning(f"Ran out of early locations for early items. Failed to place \
{len(unplaced_early_items)} items early.")
logging.warning("Ran out of early locations for early items. Failed to place "
f"{unplaced_early_items} early.")
itempool += unplaced_early_items

fill_locations.extend(early_locations)
Expand Down Expand Up @@ -659,6 +693,17 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
else:
warn(warning, force)

swept_state = world.state.copy()
swept_state.sweep_for_events()
reachable = frozenset(world.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
for loc in world.get_unfilled_locations():
if loc in reachable:
early_locations[loc.player].append(loc.name)
else: # not reachable with swept state
non_early_locations[loc.player].append(loc.name)

# TODO: remove. Preferably by implementing key drop
from worlds.alttp.Regions import key_drop_data
world_name_lookup = world.world_name_lookup
Expand All @@ -674,7 +719,39 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
if 'from_pool' not in block:
block['from_pool'] = True
if 'world' not in block:
block['world'] = False
target_world = False
else:
target_world = block['world']

if target_world is False or world.players == 1: # target own world
worlds: typing.Set[int] = {player}
elif target_world is True: # target any worlds besides own
worlds = set(world.player_ids) - {player}
elif target_world is None: # target all worlds
worlds = set(world.player_ids)
elif type(target_world) == list: # list of target worlds
worlds = set()
for listed_world in target_world:
if listed_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
block['force'])
continue
worlds.add(world_name_lookup[listed_world])
elif type(target_world) == int: # target world by slot number
if target_world not in range(1, world.players + 1):
failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
block['force'])
continue
worlds = {target_world}
else: # target world by slot name
if target_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
block['force'])
continue
worlds = {world_name_lookup[target_world]}
block['world'] = worlds

items: block_value = []
if "items" in block:
items = block["items"]
Expand Down Expand Up @@ -711,6 +788,17 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
for key, value in locations.items():
location_list += [key] * value
locations = location_list

if "early_locations" in locations:
locations.remove("early_locations")
for player in worlds:
locations += early_locations[player]
if "non_early_locations" in locations:
locations.remove("non_early_locations")
for player in worlds:
locations += non_early_locations[player]


block['locations'] = locations

if not block['count']:
Expand Down Expand Up @@ -746,38 +834,11 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
for placement in plando_blocks:
player = placement['player']
try:
target_world = placement['world']
worlds = placement['world']
locations = placement['locations']
items = placement['items']
maxcount = placement['count']['target']
from_pool = placement['from_pool']
if target_world is False or world.players == 1: # target own world
worlds: typing.Set[int] = {player}
elif target_world is True: # target any worlds besides own
worlds = set(world.player_ids) - {player}
elif target_world is None: # target all worlds
worlds = set(world.player_ids)
elif type(target_world) == list: # list of target worlds
worlds = set()
for listed_world in target_world:
if listed_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
placement['force'])
continue
worlds.add(world_name_lookup[listed_world])
elif type(target_world) == int: # target world by slot number
if target_world not in range(1, world.players + 1):
failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
placement['force'])
continue
worlds = {target_world}
else: # target world by slot name
if target_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
placement['force'])
continue
worlds = {world_name_lookup[target_world]}

candidates = list(location for location in world.get_unfilled_locations_for_players(locations,
worlds))
Expand Down
Loading

0 comments on commit a161083

Please sign in to comment.