Merged in DR v0.5.1.0
This commit is contained in:
173
Fill.py
173
Fill.py
@@ -1,4 +1,6 @@
|
||||
import RaceRandom as random
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
@@ -211,7 +213,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||
spot_to_fill = location
|
||||
break
|
||||
elif item_to_place.smallkey or item_to_place.bigkey:
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = None
|
||||
|
||||
if spot_to_fill is None:
|
||||
@@ -221,7 +223,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
if world.accessibility[item_to_place.player] != 'none':
|
||||
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
|
||||
continue
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state,
|
||||
base_state, itempool, keys_in_itempool, single_player_placement)
|
||||
if spot_to_fill is None:
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
track_outside_keys(item_to_place, spot_to_fill, world)
|
||||
@@ -250,9 +255,7 @@ def valid_key_placement(item, location, itempool, world):
|
||||
def track_outside_keys(item, location, world):
|
||||
if not item.smallkey:
|
||||
return
|
||||
item_dungeon = item.name.split('(')[1][:-1]
|
||||
if item_dungeon == 'Escape':
|
||||
item_dungeon = 'Hyrule Castle'
|
||||
item_dungeon = item.dungeon
|
||||
if location.player == item.player:
|
||||
loc_dungeon = location.parent_region.dungeon
|
||||
if loc_dungeon and loc_dungeon.name == item_dungeon:
|
||||
@@ -260,6 +263,80 @@ def track_outside_keys(item, location, world):
|
||||
world.key_logic[item.player][item_dungeon].outside_keys += 1
|
||||
|
||||
|
||||
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
|
||||
keys_in_itempool=None, single_player_placement=False):
|
||||
def location_preference(loc):
|
||||
if not loc.item.advancement:
|
||||
return 1
|
||||
if loc.item.type and loc.item.type != 'Sword':
|
||||
if loc.item.type in ['Map', 'Compass']:
|
||||
return 2
|
||||
else:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
if item_to_place.type == 'Crystal':
|
||||
possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal']
|
||||
else:
|
||||
possible_swaps = [x for x in state.locations_checked
|
||||
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
|
||||
swap_locations = sorted(possible_swaps, key=location_preference)
|
||||
|
||||
for location in swap_locations:
|
||||
old_item = location.item
|
||||
new_pool = list(itempool) + [old_item]
|
||||
new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool,
|
||||
keys_in_itempool, single_player_placement)
|
||||
if new_spot:
|
||||
restore_item = new_spot.item
|
||||
new_spot.item = item_to_place
|
||||
swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool,
|
||||
keys_in_itempool, single_player_placement)
|
||||
if swap_spot:
|
||||
logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}')
|
||||
world.push_item(swap_spot, old_item, False)
|
||||
swap_spot.event = True
|
||||
locations.remove(swap_spot)
|
||||
locations.append(new_spot)
|
||||
return new_spot
|
||||
else:
|
||||
new_spot.item = restore_item
|
||||
else:
|
||||
location.item = old_item
|
||||
return None
|
||||
|
||||
|
||||
def find_spot_for_item(item_to_place, locations, world, base_state, pool,
|
||||
keys_in_itempool=None, single_player_placement=False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
for item in pool:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events()
|
||||
return new_state
|
||||
for location in locations:
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
perform_access_check = True
|
||||
old_item = None
|
||||
if world.accessibility[item_to_place.player] == 'none':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||
old_item = location.item
|
||||
location.item = item_to_place
|
||||
test_state = maximum_exploration_state.copy()
|
||||
test_state.stale[item_to_place.player] = True
|
||||
else:
|
||||
test_state = maximum_exploration_state
|
||||
if (not single_player_placement or location.player == item_to_place.player) \
|
||||
and location.can_fill(test_state, item_to_place, perform_access_check) \
|
||||
and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||
return location
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = old_item
|
||||
return None
|
||||
|
||||
|
||||
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
||||
# If not passed in, then get a shuffled list of locations to fill in
|
||||
if not fill_locations:
|
||||
@@ -420,9 +497,8 @@ def sell_keys(world, player):
|
||||
|
||||
def balance_multiworld_progression(world):
|
||||
state = CollectionState(world)
|
||||
checked_locations = []
|
||||
unchecked_locations = world.get_locations().copy()
|
||||
random.shuffle(unchecked_locations)
|
||||
checked_locations = set()
|
||||
unchecked_locations = set(world.get_locations())
|
||||
|
||||
reachable_locations_count = {}
|
||||
for player in range(1, world.players + 1):
|
||||
@@ -430,7 +506,7 @@ def balance_multiworld_progression(world):
|
||||
|
||||
def get_sphere_locations(sphere_state, locations):
|
||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
|
||||
return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)}
|
||||
|
||||
while True:
|
||||
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
||||
@@ -441,38 +517,42 @@ def balance_multiworld_progression(world):
|
||||
if checked_locations:
|
||||
threshold = max(reachable_locations_count.values()) - 20
|
||||
|
||||
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
|
||||
if balancing_players is not None and len(balancing_players) > 0:
|
||||
balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold}
|
||||
if balancing_players:
|
||||
balancing_state = state.copy()
|
||||
balancing_unchecked_locations = unchecked_locations.copy()
|
||||
balancing_reachables = reachable_locations_count.copy()
|
||||
balancing_sphere = sphere_locations.copy()
|
||||
candidate_items = []
|
||||
candidate_items = collections.defaultdict(set)
|
||||
while True:
|
||||
for location in balancing_sphere:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
balancing_state.collect(location.item, True, location)
|
||||
if location.item.player in balancing_players and not location.locked:
|
||||
candidate_items.append(location)
|
||||
player = location.item.player
|
||||
if player in balancing_players and not location.locked and location.player != player:
|
||||
candidate_items[player].add(location)
|
||||
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
|
||||
for location in balancing_sphere:
|
||||
balancing_unchecked_locations.remove(location)
|
||||
balancing_reachables[location.player] += 1
|
||||
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
|
||||
if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()):
|
||||
break
|
||||
elif not balancing_sphere:
|
||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
|
||||
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
|
||||
unlocked_locations = collections.defaultdict(set)
|
||||
for l in unchecked_locations:
|
||||
if l not in balancing_unchecked_locations:
|
||||
unlocked_locations[l.player].add(l)
|
||||
items_to_replace = []
|
||||
for player in balancing_players:
|
||||
locations_to_test = [l for l in unlocked_locations if l.player == player]
|
||||
# only replace items that end up in another player's world
|
||||
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
|
||||
locations_to_test = unlocked_locations[player]
|
||||
items_to_test = candidate_items[player]
|
||||
while items_to_test:
|
||||
testing = items_to_test.pop()
|
||||
reducing_state = state.copy()
|
||||
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]:
|
||||
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
|
||||
items_to_test):
|
||||
reducing_state.collect(location.item, True, location)
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
@@ -486,33 +566,44 @@ def balance_multiworld_progression(world):
|
||||
items_to_replace.append(testing)
|
||||
|
||||
replaced_items = False
|
||||
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
|
||||
# sort then shuffle to maintain deterministic behaviour,
|
||||
# while allowing use of set for better algorithm growth behaviour elsewhere
|
||||
replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked),
|
||||
key=lambda loc: (loc.name, loc.player))
|
||||
random.shuffle(replacement_locations)
|
||||
items_to_replace.sort(key=lambda item: (item.name, item.player))
|
||||
random.shuffle(items_to_replace)
|
||||
while replacement_locations and items_to_replace:
|
||||
new_location = replacement_locations.pop()
|
||||
old_location = items_to_replace.pop()
|
||||
for new_location in replacement_locations:
|
||||
if (new_location.can_fill(state, old_location.item, False) and
|
||||
old_location.can_fill(state, new_location.item, False)):
|
||||
replacement_locations.remove(new_location)
|
||||
new_location.item, old_location.item = old_location.item, new_location.item
|
||||
if world.shopsanity[new_location.player]:
|
||||
check_shop_swap(new_location)
|
||||
if world.shopsanity[old_location.player]:
|
||||
check_shop_swap(old_location)
|
||||
new_location.event, old_location.event = True, False
|
||||
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
||||
f"displacing {old_location.item} into {old_location}")
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not Progression Balance {old_location.item}")
|
||||
|
||||
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
|
||||
replacement_locations.insert(0, new_location)
|
||||
new_location = replacement_locations.pop()
|
||||
|
||||
new_location.item, old_location.item = old_location.item, new_location.item
|
||||
if world.shopsanity[new_location.player]:
|
||||
check_shop_swap(new_location)
|
||||
if world.shopsanity[old_location.player]:
|
||||
check_shop_swap(old_location)
|
||||
new_location.event, old_location.event = True, False
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
if replaced_items:
|
||||
for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]):
|
||||
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
|
||||
for location in get_sphere_locations(state, unlocked):
|
||||
unchecked_locations.remove(location)
|
||||
reachable_locations_count[location.player] += 1
|
||||
sphere_locations.append(location)
|
||||
sphere_locations.add(location)
|
||||
|
||||
for location in sphere_locations:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations.extend(sphere_locations)
|
||||
checked_locations |= sphere_locations
|
||||
|
||||
if world.has_beaten_game(state):
|
||||
break
|
||||
@@ -651,7 +742,7 @@ def balance_money_progression(world):
|
||||
if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]:
|
||||
wallet[player] += income
|
||||
rooms_visited[player].add(room)
|
||||
if checked_locations:
|
||||
if checked_locations or len(unchecked_locations) == 0:
|
||||
if world.has_beaten_game(state):
|
||||
done = True
|
||||
continue
|
||||
@@ -661,11 +752,11 @@ def balance_money_progression(world):
|
||||
solvent = set()
|
||||
insolvent = set()
|
||||
for player in range(1, world.players+1):
|
||||
if wallet[player] >= sphere_costs[player] > 0:
|
||||
if wallet[player] >= sphere_costs[player] >= 0:
|
||||
solvent.add(player)
|
||||
if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]:
|
||||
insolvent.add(player)
|
||||
if len(solvent) == 0:
|
||||
if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0:
|
||||
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p])
|
||||
difference = sphere_costs[target_player]-wallet[target_player]
|
||||
logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
|
||||
@@ -705,7 +796,7 @@ def balance_money_progression(world):
|
||||
for player in solvent:
|
||||
wallet[player] -= sphere_costs[player]
|
||||
for location in locked_by_money[player]:
|
||||
if location == 'Kiki':
|
||||
if isinstance(location, str) and location == 'Kiki':
|
||||
kiki_paid[player] = True
|
||||
else:
|
||||
state.collect(location.item, True, location)
|
||||
|
||||
Reference in New Issue
Block a user