diff --git a/Dungeons.py b/Dungeons.py index 16a8c4dd..711c17b9 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -1,5 +1,6 @@ from Items import ItemFactory from BaseClasses import Dungeon +from Fill import fill_restrictive import random @@ -103,7 +104,6 @@ def fill_dungeons(world): def fill_dungeons_restrictive(world): - from Main import fill_restrictive all_state_base = world.get_all_state() world.push_item(world.get_location('[dungeon-D3-B1] Skull Woods - South of Big Chest'), ItemFactory('Small Key (Skull Woods)'), False) @@ -111,15 +111,15 @@ def fill_dungeons_restrictive(world): shuffled_locations=world.get_unfilled_locations() random.shuffle(shuffled_locations) - + dungeon_items = [item for dungeon in world.dungeons for item in dungeon.all_items] - - #sort in the order Big Key, Small Key, Other before placing dungeon items - sort_order={"BigKey":3,"SmallKey":2}; - dungeon_items.sort(key=lambda item:sort_order.get(item.type, 1) ) - + + # sort in the order Big Key, Small Key, Other before placing dungeon items + sort_order = {"BigKey": 3, "SmallKey": 2} + dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) + fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items) - + world.state._clear_cache() diff --git a/Fill.py b/Fill.py new file mode 100644 index 00000000..e98f2799 --- /dev/null +++ b/Fill.py @@ -0,0 +1,299 @@ +import random +import logging + +def distribute_items_cutoff(world, cutoffrate=0.33): + # get list of locations to fill in + fill_locations = world.get_unfilled_locations() + random.shuffle(fill_locations) + + # get items to distribute + random.shuffle(world.itempool) + itempool = world.itempool + + total_advancement_items = len([item for item in itempool if item.advancement]) + placed_advancement_items = 0 + + progress_done = False + advancement_placed = False + + # sweep once to pick up preplaced items + world.state.sweep_for_events() + + while itempool and fill_locations: + candidate_item_to_place = None + item_to_place = None + for item in itempool: + if advancement_placed or (progress_done and (item.advancement or item.priority)): + item_to_place = item + break + if item.advancement: + candidate_item_to_place = item + if world.unlocks_new_location(item): + item_to_place = item + placed_advancement_items += 1 + break + + if item_to_place is None: + # check if we can reach all locations and that is why we find no new locations to place + if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): + progress_done = True + continue + # check if we have now placed all advancement items + if progress_done: + advancement_placed = True + continue + # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying + if candidate_item_to_place is not None: + item_to_place = candidate_item_to_place + placed_advancement_items += 1 + else: + # we placed all available progress items. Maybe the game can be beaten anyway? + if world.can_beat_game(): + logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') + progress_done = True + continue + raise RuntimeError('No more progress items left to place.') + + spot_to_fill = None + for location in (fill_locations if placed_advancement_items/total_advancement_items < cutoffrate else reversed(fill_locations)): + if world.state.can_reach(location) and location.can_fill(item_to_place): + 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(): + logging.getLogger('').warning('Not all items placed. Game beatable anyway.') + break + raise RuntimeError('No more spots to place %s' % item_to_place) + + world.push_item(spot_to_fill, item_to_place, True) + itempool.remove(item_to_place) + fill_locations.remove(spot_to_fill) + + logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in itempool], [location.name for location in fill_locations])) + + +def distribute_items_staleness(world): + # get list of locations to fill in + fill_locations = world.get_unfilled_locations() + random.shuffle(fill_locations) + + # get items to distribute + random.shuffle(world.itempool) + itempool = world.itempool + + progress_done = False + advancement_placed = False + + # sweep once to pick up preplaced items + world.state.sweep_for_events() + + while itempool and fill_locations: + candidate_item_to_place = None + item_to_place = None + for item in itempool: + if advancement_placed or (progress_done and (item.advancement or item.priority)): + item_to_place = item + break + if item.advancement: + candidate_item_to_place = item + if world.unlocks_new_location(item): + item_to_place = item + break + + if item_to_place is None: + # check if we can reach all locations and that is why we find no new locations to place + if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): + progress_done = True + continue + # check if we have now placed all advancement items + if progress_done: + advancement_placed = True + continue + # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying + if candidate_item_to_place is not None: + item_to_place = candidate_item_to_place + else: + # we placed all available progress items. Maybe the game can be beaten anyway? + if world.can_beat_game(): + logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') + progress_done = True + continue + raise RuntimeError('No more progress items left to place.') + + spot_to_fill = None + for location in fill_locations: + # increase likelyhood of skipping a location if it has been found stale + if not progress_done and random.randint(0, location.staleness_count) > 2: + continue + + if world.state.can_reach(location) and location.can_fill(item_to_place): + spot_to_fill = location + break + else: + location.staleness_count += 1 + + # might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate + if spot_to_fill is None: + for location in fill_locations: + if world.state.can_reach(location) and location.can_fill(item_to_place): + 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(): + logging.getLogger('').warning('Not all items placed. Game beatable anyway.') + break + raise RuntimeError('No more spots to place %s' % item_to_place) + + world.push_item(spot_to_fill, item_to_place, True) + itempool.remove(item_to_place) + fill_locations.remove(spot_to_fill) + + logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in itempool], [location.name for location in fill_locations])) + + +def fill_restrictive(world, base_state, locations, itempool): + def sweep_from_pool(): + new_state = base_state.copy() + for item in itempool: + new_state.collect(item, True) + new_state.sweep_for_events() + return new_state + + while itempool and locations: + item_to_place = itempool.pop() + maximum_exploration_state = sweep_from_pool() + + spot_to_fill = None + for location in locations: + if location.can_fill(item_to_place): + if world.check_beatable_only: + starting_state = base_state.copy() + for item in itempool: + starting_state.collect(item, True) + + if maximum_exploration_state.can_reach(location): + if world.check_beatable_only: + starting_state.collect(item_to_place, True) + else: + spot_to_fill = location + break + + if world.check_beatable_only and world.can_beat_game(starting_state): + 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.') + break + raise RuntimeError('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 + + +def distribute_items_restrictive(world, gftower_trash_count=0): + # get list of locations to fill in + fill_locations = world.get_unfilled_locations() + + # get items to distribute + random.shuffle(world.itempool) + progitempool = [item for item in world.itempool if item.advancement] + prioitempool = [item for item in world.itempool if not item.advancement and item.priority] + restitempool = [item for item in world.itempool if not item.advancement and not item.priority] + + # fill in gtower locations with trash first + 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 + + random.shuffle(fill_locations) + + fill_restrictive(world, world.state, fill_locations, progitempool) + + random.shuffle(fill_locations) + + while prioitempool and fill_locations: + spot_to_fill = fill_locations.pop() + item_to_place = prioitempool.pop() + world.push_item(spot_to_fill, item_to_place, False) + + while restitempool and fill_locations: + spot_to_fill = fill_locations.pop() + item_to_place = restitempool.pop() + world.push_item(spot_to_fill, item_to_place, False) + + logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in progitempool + prioitempool + restitempool], [location.name for location in fill_locations])) + + +def flood_items(world): + # get items to distribute + random.shuffle(world.itempool) + itempool = world.itempool + progress_done = False + + # sweep once to pick up preplaced items + world.state.sweep_for_events() + + # fill world from top of itempool while we can + while not progress_done: + location_list = world.get_unfilled_locations() + random.shuffle(location_list) + spot_to_fill = None + for location in location_list: + if world.state.can_reach(location) and location.can_fill(itempool[0]): + spot_to_fill = location + break + + if spot_to_fill: + item = itempool.pop(0) + world.push_item(spot_to_fill, item, True) + continue + + # ran out of spots, check if we need to step in and correct things + if len(world.get_reachable_locations()) == len(world.get_locations()): + progress_done = True + continue + + # need to place a progress item instead of an already placed item, find candidate + item_to_place = None + candidate_item_to_place = None + for item in itempool: + if item.advancement: + candidate_item_to_place = item + if world.unlocks_new_location(item): + item_to_place = item + break + + # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying + if item_to_place is None: + if candidate_item_to_place is not None: + item_to_place = candidate_item_to_place + else: + raise RuntimeError('No more progress items left to place.') + + # find item to replace with progress item + location_list = world.get_reachable_locations() + random.shuffle(location_list) + for location in location_list: + if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.key: + # safe to replace + replace_item = location.item + replace_item.location = None + itempool.append(replace_item) + world.push_item(location, item_to_place, True) + itempool.remove(item_to_place) + break diff --git a/Main.py b/Main.py index 778ef828..e716f0aa 100644 --- a/Main.py +++ b/Main.py @@ -5,6 +5,7 @@ from Rom import patch_rom, LocalRom, JsonRom from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Items import ItemFactory +from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, fill_restrictive, flood_items from collections import OrderedDict import random import time @@ -109,304 +110,6 @@ def main(args, seed=None): return world -def distribute_items_cutoff(world, cutoffrate=0.33): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) - - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - - total_advancement_items = len([item for item in itempool if item.advancement]) - placed_advancement_items = 0 - - progress_done = False - advancement_placed = False - - # sweep once to pick up preplaced items - world.state.sweep_for_events() - - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - placed_advancement_items += 1 - break - - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - placed_advancement_items += 1 - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise RuntimeError('No more progress items left to place.') - - spot_to_fill = None - for location in (fill_locations if placed_advancement_items/total_advancement_items < cutoffrate else reversed(fill_locations)): - if world.state.can_reach(location) and location.can_fill(item_to_place): - 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(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise RuntimeError('No more spots to place %s' % item_to_place) - - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) - - logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in itempool], [location.name for location in fill_locations])) - - -def distribute_items_staleness(world): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - random.shuffle(fill_locations) - - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - - progress_done = False - advancement_placed = False - - # sweep once to pick up preplaced items - world.state.sweep_for_events() - - while itempool and fill_locations: - candidate_item_to_place = None - item_to_place = None - for item in itempool: - if advancement_placed or (progress_done and (item.advancement or item.priority)): - item_to_place = item - break - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - - if item_to_place is None: - # check if we can reach all locations and that is why we find no new locations to place - if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - # check if we have now placed all advancement items - if progress_done: - advancement_placed = True - continue - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - # we placed all available progress items. Maybe the game can be beaten anyway? - if world.can_beat_game(): - logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') - progress_done = True - continue - raise RuntimeError('No more progress items left to place.') - - spot_to_fill = None - for location in fill_locations: - # increase likelyhood of skipping a location if it has been found stale - if not progress_done and random.randint(0, location.staleness_count) > 2: - continue - - if world.state.can_reach(location) and location.can_fill(item_to_place): - spot_to_fill = location - break - else: - location.staleness_count += 1 - - # might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate - if spot_to_fill is None: - for location in fill_locations: - if world.state.can_reach(location) and location.can_fill(item_to_place): - 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(): - logging.getLogger('').warning('Not all items placed. Game beatable anyway.') - break - raise RuntimeError('No more spots to place %s' % item_to_place) - - world.push_item(spot_to_fill, item_to_place, True) - itempool.remove(item_to_place) - fill_locations.remove(spot_to_fill) - - logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in itempool], [location.name for location in fill_locations])) - - -def fill_restrictive(world, base_state, locations, itempool): - def sweep_from_pool(): - new_state = base_state.copy() - for item in itempool: - new_state.collect(item, True) - new_state.sweep_for_events() - return new_state - - while itempool and locations: - item_to_place = itempool.pop() - maximum_exploration_state = sweep_from_pool() - - spot_to_fill = None - for location in locations: - if location.can_fill(item_to_place): - if world.check_beatable_only: - starting_state = base_state.copy() - for item in itempool: - starting_state.collect(item, True) - - if maximum_exploration_state.can_reach(location): - if world.check_beatable_only: - starting_state.collect(item_to_place, True) - else: - spot_to_fill = location - break - - if world.check_beatable_only and world.can_beat_game(starting_state): - 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.') - break - raise RuntimeError('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 - - -def distribute_items_restrictive(world, gftower_trash_count=0): - # get list of locations to fill in - fill_locations = world.get_unfilled_locations() - - # get items to distribute - random.shuffle(world.itempool) - progitempool = [item for item in world.itempool if item.advancement] - prioitempool = [item for item in world.itempool if not item.advancement and item.priority] - restitempool = [item for item in world.itempool if not item.advancement and not item.priority] - - # fill in gtower locations with trash first - 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 - - random.shuffle(fill_locations) - - fill_restrictive(world, world.state, fill_locations, progitempool) - - random.shuffle(fill_locations) - - while prioitempool and fill_locations: - spot_to_fill = fill_locations.pop() - item_to_place = prioitempool.pop() - world.push_item(spot_to_fill, item_to_place, False) - - while restitempool and fill_locations: - spot_to_fill = fill_locations.pop() - item_to_place = restitempool.pop() - world.push_item(spot_to_fill, item_to_place, False) - - logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in progitempool + prioitempool + restitempool], [location.name for location in fill_locations])) - - -def flood_items(world): - # get items to distribute - random.shuffle(world.itempool) - itempool = world.itempool - progress_done = False - - # sweep once to pick up preplaced items - world.state.sweep_for_events() - - # fill world from top of itempool while we can - while not progress_done: - location_list = world.get_unfilled_locations() - random.shuffle(location_list) - spot_to_fill = None - for location in location_list: - if world.state.can_reach(location) and location.can_fill(itempool[0]): - spot_to_fill = location - break - - if spot_to_fill: - item = itempool.pop(0) - world.push_item(spot_to_fill, item, True) - continue - - # ran out of spots, check if we need to step in and correct things - if len(world.get_reachable_locations()) == len(world.get_locations()): - progress_done = True - continue - - # need to place a progress item instead of an already placed item, find candidate - item_to_place = None - candidate_item_to_place = None - for item in itempool: - if item.advancement: - candidate_item_to_place = item - if world.unlocks_new_location(item): - item_to_place = item - break - - # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying - if item_to_place is None: - if candidate_item_to_place is not None: - item_to_place = candidate_item_to_place - else: - raise RuntimeError('No more progress items left to place.') - - # find item to replace with progress item - location_list = world.get_reachable_locations() - random.shuffle(location_list) - for location in location_list: - if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.key: - # safe to replace - replace_item = location.item - replace_item.location = None - itempool.append(replace_item) - world.push_item(location, item_to_place, True) - itempool.remove(item_to_place) - break - - def generate_itempool(world): if world.difficulty not in ['normal', 'timed', 'timed-ohko', 'timed-countdown'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'] or world.mode not in ['open', 'standard', 'swordless']: raise NotImplementedError('Not supported yet')