Add fallback item swap for unreachable items

This commit is contained in:
Brad Humphrey 2021-12-20 17:47:04 -07:00 committed by Fabian Dill
parent 461961c3be
commit 6a34fe5184
2 changed files with 92 additions and 22 deletions

42
Fill.py
View File

@ -3,7 +3,7 @@ import typing
import collections import collections
import itertools import itertools
from BaseClasses import CollectionState, Location, MultiWorld from BaseClasses import CollectionState, Location, MultiWorld, Item
from worlds.generic import PlandoItem from worlds.generic import PlandoItem
from worlds.AutoWorld import call_all from worlds.AutoWorld import call_all
@ -12,15 +12,16 @@ class FillError(RuntimeError):
pass pass
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool, single_player_placement=False, def sweep_from_pool(base_state: CollectionState, itempool: list[Item]):
lock=False):
def sweep_from_pool():
new_state = base_state.copy() new_state = base_state.copy()
for item in itempool: for item in itempool:
new_state.collect(item, True) new_state.collect(item, True)
new_state.sweep_for_events() new_state.sweep_for_events()
return new_state return new_state
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool: list[Item], single_player_placement=False,
lock=False):
unplaced_items = [] unplaced_items = []
placements = [] placements = []
@ -29,13 +30,16 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
reachable_items.setdefault(item.player, []).append(item) reachable_items.setdefault(item.player, []).append(item)
while any(reachable_items.values()) and locations: while any(reachable_items.values()) and locations:
items_to_place = [items.pop() for items in reachable_items.values() if items] # grab one item per player # grab one item per player
items_to_place = [items.pop()
for items in reachable_items.values() if items]
for item in items_to_place: for item in items_to_place:
itempool.remove(item) itempool.remove(item)
maximum_exploration_state = sweep_from_pool() maximum_exploration_state = sweep_from_pool(base_state, itempool)
has_beaten_game = world.has_beaten_game(maximum_exploration_state) has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place: for item_to_place in items_to_place:
spot_to_fill: Location = None
if world.accessibility[item_to_place.player] == 'minimal': if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) if single_player_placement else not has_beaten_game item_to_place.player) if single_player_placement else not has_beaten_game
@ -45,12 +49,34 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
for i, location in enumerate(locations): for i, location in enumerate(locations):
if (not single_player_placement or location.player == item_to_place.player) \ if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check): and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
spot_to_fill = locations.pop(i) # poping by index is faster than removing by content, # poping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element # skipping a scan for the element
break break
else: else:
# we filled all reachable spots. Maybe the game can be beaten anyway? # we filled all reachable spots.
# try swaping this item with previously placed items
for(i, location) in enumerate(placements):
placed_item = location.item
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, itempool)
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(swap_state, item_to_place, perform_access_check):
# Add this item to the exisiting placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
reachable_items.setdefault(placed_item.player, []).append(placed_item)
itempool.append(placed_item)
break
else:
# Item can't be placed here, restore original item
location.item = placed_item
placed_item.location = location
if spot_to_fill == None:
# Maybe the game can be beaten anyway?
unplaced_items.append(item_to_place) unplaced_items.append(item_to_place)
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game(): if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
logging.warning( logging.warning(

View File

@ -1,6 +1,7 @@
import unittest import unittest
import pytest
from worlds.AutoWorld import World from worlds.AutoWorld import World
from Fill import fill_restrictive from Fill import FillError, fill_restrictive
from BaseClasses import MultiWorld, Region, RegionType, Item, Location from BaseClasses import MultiWorld, Region, RegionType, Item, Location
from worlds.generic.Rules import set_rule from worlds.generic.Rules import set_rule
@ -106,3 +107,46 @@ class TestBase(unittest.TestCase):
self.assertEqual(loc0.item, item1) self.assertEqual(loc0.item, item1)
self.assertEqual(loc1.item, item0) self.assertEqual(loc1.item, item0)
def test_impossible_fill_restrictive(self):
multi_world = generate_multi_world()
player1_id = 1
player1_menu = multi_world.get_region("Menu", player1_id)
locations = generate_locations(2, player1_id, None, player1_menu)
items = generate_items(2, player1_id, True)
item0 = items[0]
item1 = items[1]
loc0 = locations[0]
loc1 = locations[1]
multi_world.completion_condition[player1_id] = lambda state: state.has(
item0.name, player1_id) and state.has(item1.name, player1_id)
set_rule(loc1, lambda state: state.has(item1.name, player1_id))
set_rule(loc0, lambda state: state.has(item0.name, player1_id))
with pytest.raises(FillError):
fill_restrictive(multi_world, multi_world.state, locations, items)
def test_circular_fill_restrictive(self):
multi_world = generate_multi_world()
player1_id = 1
player1_menu = multi_world.get_region("Menu", player1_id)
locations = generate_locations(3, player1_id, None, player1_menu)
items = generate_items(3, player1_id, True)
item0 = items[0]
item1 = items[1]
item2 = items[2]
loc0 = locations[0]
loc1 = locations[1]
loc2 = locations[2]
multi_world.completion_condition[player1_id] = lambda state: state.has(
item0.name, player1_id) and state.has(item1.name, player1_id) and state.has(item2.name, player1_id)
set_rule(loc1, lambda state: state.has(item0.name, player1_id))
set_rule(loc2, lambda state: state.has(item1.name, player1_id))
set_rule(loc0, lambda state: state.has(item2.name, player1_id))
with pytest.raises(FillError):
fill_restrictive(multi_world, multi_world.state, locations, items)