Multiworld core implementation By Bonta0

Does not include the server/client code or the rom writes
specific to it. Indeed it cannot write multiworld roms at
all right now, pending addition future updates to support
the official ALTTPR Multiworld client.

Includes some GUI changes by Alaszun

Co-authored-by: Alaszun <koelze@google.com>
This commit is contained in:
Bonta-kun
2019-04-18 11:23:24 +02:00
committed by Kevin Cathcart
parent d44d194de7
commit 1a62b1da28
16 changed files with 1821 additions and 1611 deletions

162
Fill.py
View File

@@ -1,6 +1,9 @@
import random
import logging
from BaseClasses import CollectionState
class FillError(RuntimeError):
pass
@@ -167,31 +170,41 @@ def fill_restrictive(world, base_state, locations, itempool):
return new_state
while itempool and locations:
item_to_place = itempool.pop()
items_to_place = []
nextpool = []
placing_players = set()
for item in reversed(itempool):
if item.player not in placing_players:
placing_players.add(item.player)
items_to_place.append(item)
else:
nextpool.insert(0, item)
itempool = nextpool
maximum_exploration_state = sweep_from_pool()
perform_access_check = True
if world.check_beatable_only:
perform_access_check = not world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
spot_to_fill = None
for location in locations:
if location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
spot_to_fill = location
break
spot_to_fill = None
for location in locations:
if location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
spot_to_fill = location
break
if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
if world.can_beat_game():
if not world.check_beatable_only:
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)
if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
if world.can_beat_game():
if not world.check_beatable_only:
logging.getLogger('').warning('Not all items placed. Game beatable anyway.')
break
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)
spot_to_fill.event = True
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)
spot_to_fill.event = True
def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=None):
@@ -207,20 +220,25 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
# fill in gtower locations with trash first
if world.ganonstower_vanilla:
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name]
random.shuffle(gtower_locations)
trashcnt = 0
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = restitempool.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
for player in range(1, world.players + 1):
if world.ganonstower_vanilla[player]:
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player]
random.shuffle(gtower_locations)
trashcnt = 0
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = restitempool.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
random.shuffle(fill_locations)
fill_locations.reverse()
# Make sure the escape small key is placed first in standard keysanity to prevent running out of spots
if world.keysanity and world.mode == 'standard':
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0)
fill_restrictive(world, world.state, fill_locations, progitempool)
random.shuffle(fill_locations)
@@ -297,3 +315,93 @@ def flood_items(world):
world.push_item(location, item_to_place, True)
itempool.remove(item_to_place)
break
def balance_multiworld_progression(world):
state = CollectionState(world)
checked_locations = []
unchecked_locations = world.get_locations().copy()
random.shuffle(unchecked_locations)
reachable_locations_count = {}
for player in range(1, world.players + 1):
reachable_locations_count[player] = 0
def get_sphere_locations(sphere_state, locations):
if not world.keysanity:
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc)]
while True:
sphere_locations = get_sphere_locations(state, unchecked_locations)
for location in sphere_locations:
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
if checked_locations:
average_reachable_locations = sum(reachable_locations_count.values()) / world.players
threshold = ((average_reachable_locations + max(reachable_locations_count.values())) / 2) * 0.8 #todo: probably needs some tweaking
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 = []
while True:
for location in balancing_sphere:
if location.event:
balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players:
candidate_items.append(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()]):
break
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
items_to_replace = []
for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player]
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != 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]:
reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test)
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
if reachable_locations_count[player] + len(reduced_sphere) < threshold:
items_to_replace.append(testing)
replaced_items = False
locations_for_replacing = [l for l in checked_locations if not l.event]
while locations_for_replacing and items_to_replace:
new_location = locations_for_replacing.pop()
old_location = items_to_replace.pop()
new_location.item, old_location.item = old_location.item, new_location.item
new_location.event = True
old_location.event = 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]):
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
sphere_locations.append(location)
for location in sphere_locations:
if location.event:
state.collect(location.item, True, location)
checked_locations.extend(sphere_locations)
if world.has_beaten_game(state):
break