Allow locations to be prioritized for progress item placement (#189)

This commit is contained in:
Brad Humphrey 2022-01-19 20:19:07 -07:00 committed by GitHub
parent ab4fb6e69c
commit ec95ce8329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 371 additions and 45 deletions

View File

@ -787,7 +787,7 @@ class Region(object):
self.type = type_ self.type = type_
self.entrances = [] self.entrances = []
self.exits = [] self.exits = []
self.locations = [] self.locations: List[Location] = []
self.dungeon = None self.dungeon = None
self.shop = None self.shop = None
self.world = world self.world = world
@ -908,6 +908,11 @@ class Boss():
return f"Boss({self.name})" return f"Boss({self.name})"
class LocationProgressType(Enum):
DEFAULT = 1
PRIORITY = 2
EXCLUDED = 3
class Location(): class Location():
# If given as integer, then this is the shop's inventory index # If given as integer, then this is the shop's inventory index
shop_slot: Optional[int] = None shop_slot: Optional[int] = None
@ -919,6 +924,7 @@ class Location():
show_in_spoiler: bool = True show_in_spoiler: bool = True
excluded: bool = False excluded: bool = False
crystal: bool = False crystal: bool = False
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda item, state: False) always_allow = staticmethod(lambda item, state: False)
access_rule = staticmethod(lambda state: True) access_rule = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True) item_rule = staticmethod(lambda item: True)

78
Fill.py
View File

@ -5,7 +5,7 @@ import itertools
from collections import Counter, deque from collections import Counter, deque
from BaseClasses import CollectionState, Location, MultiWorld, Item from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item
from worlds.generic import PlandoItem from worlds.generic import PlandoItem
from worlds.AutoWorld import call_all from worlds.AutoWorld import call_all
@ -106,8 +106,19 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
def distribute_items_restrictive(world: MultiWorld, fill_locations=None): def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in # If not passed in, then get a shuffled list of locations to fill in
if not fill_locations: if not fill_locations:
fill_locations = world.get_unfilled_locations() fill_locations = world.get_locations()
world.random.shuffle(fill_locations)
world.random.shuffle(fill_locations)
locations: dict[LocationProgressType, list[Location]] = {
type: [] for type in LocationProgressType}
for loc in fill_locations:
locations[loc.progress_type].append(loc)
prioritylocations = locations[LocationProgressType.PRIORITY]
defaultlocations = locations[LocationProgressType.DEFAULT]
excludedlocations = locations[LocationProgressType.EXCLUDED]
# get items to distribute # get items to distribute
world.random.shuffle(world.itempool) world.random.shuffle(world.itempool)
@ -129,21 +140,42 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
else: else:
restitempool.append(item) restitempool.append(item)
world.random.shuffle(fill_locations) call_all(world, "fill_hook", progitempool, nonexcludeditempool,
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations) localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
fill_restrictive(world, world.state, fill_locations, progitempool) locationDeficit = len(progitempool) - len(prioritylocations)
if locationDeficit > 0:
if locationDeficit > len(defaultlocations):
raise FillError(
f'Not enough locations for advancement items. There are {len(progitempool)} advancement items with {len(prioritylocations)} priority locations and {len(defaultlocations)} default locations')
prioritylocations += defaultlocations[:locationDeficit]
defaultlocations = defaultlocations[locationDeficit:]
fill_restrictive(world, world.state, prioritylocations, progitempool)
if prioritylocations:
defaultlocations = prioritylocations + defaultlocations
if progitempool:
fill_restrictive(world, world.state, defaultlocations, progitempool)
if nonexcludeditempool: if nonexcludeditempool:
world.random.shuffle(fill_locations) world.random.shuffle(defaultlocations)
fill_restrictive(world, world.state, fill_locations, nonexcludeditempool) # needs logical fill to not conflict with local items # needs logical fill to not conflict with local items
nonexcludeditempool, defaultlocations = fast_fill(
world, nonexcludeditempool, defaultlocations)
if(len(nonexcludeditempool) > 0):
raise FillError(
f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations')
defaultlocations = defaultlocations + excludedlocations
world.random.shuffle(defaultlocations)
if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
local_locations = {player: [] for player in world.player_ids} local_locations = {player: [] for player in world.player_ids}
for location in fill_locations: for location in defaultlocations:
local_locations[location.player].append(location) local_locations[location.player].append(location)
for locations in local_locations.values(): for player_locations in local_locations.values():
world.random.shuffle(locations) world.random.shuffle(player_locations)
for player, items in localrestitempool.items(): # items already shuffled for player, items in localrestitempool.items(): # items already shuffled
player_local_locations = local_locations[player] player_local_locations = local_locations[player]
@ -154,24 +186,27 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
break break
spot_to_fill = player_local_locations.pop() spot_to_fill = player_local_locations.pop()
world.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill) defaultlocations.remove(spot_to_fill)
for item_to_place in nonlocalrestitempool: for item_to_place in nonlocalrestitempool:
for i, location in enumerate(fill_locations): for i, location in enumerate(defaultlocations):
if location.player != item_to_place.player: if location.player != item_to_place.player:
world.push_item(fill_locations.pop(i), item_to_place, False) world.push_item(defaultlocations.pop(i), item_to_place, False)
break break
else: else:
logging.warning(f"Could not place non_local_item {item_to_place} among {fill_locations}, tossing.") logging.warning(
f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.")
world.random.shuffle(fill_locations) world.random.shuffle(defaultlocations)
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations) restitempool, defaultlocations = fast_fill(
world, restitempool, defaultlocations)
unplaced = progitempool + restitempool unplaced = progitempool + restitempool
unfilled = [location.name for location in fill_locations] unfilled = [location.name for location in defaultlocations]
if unplaced or unfilled: if unplaced or unfilled:
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') logging.warning(
f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]: def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
@ -279,7 +314,10 @@ def balance_multiworld_progression(world: MultiWorld):
balancing_state.collect(location.item, True, location) balancing_state.collect(location.item, True, location)
player = location.item.player player = location.item.player
# only replace items that end up in another player's world # only replace items that end up in another player's world
if not location.locked and player in balancing_players and location.player != player: if(not location.locked and
player in balancing_players and
location.player != player and
location.progress_type != LocationProgressType.PRIORITY):
candidate_items[player].add(location) candidate_items[player].add(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere: for location in balancing_sphere:

View File

@ -1,9 +1,9 @@
from typing import NamedTuple, List from typing import List
import unittest import unittest
from worlds.AutoWorld import World from worlds.AutoWorld import World
from Fill import FillError, fill_restrictive from Fill import FillError, balance_multiworld_progression, fill_restrictive, distribute_items_restrictive
from BaseClasses import MultiWorld, Region, RegionType, Item, Location from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location
from worlds.generic.Rules import set_rule from worlds.generic.Rules import CollectionRule, set_rule
def generate_multi_world(players: int = 1) -> MultiWorld: def generate_multi_world(players: int = 1) -> MultiWorld:
@ -19,31 +19,87 @@ def generate_multi_world(players: int = 1) -> MultiWorld:
"Menu Region Hint", player_id, multi_world) "Menu Region Hint", player_id, multi_world)
multi_world.regions.append(region) multi_world.regions.append(region)
multi_world.set_seed() multi_world.set_seed(0)
multi_world.set_default_common_options() multi_world.set_default_common_options()
return multi_world return multi_world
class PlayerDefinition(NamedTuple): class PlayerDefinition(object):
world: MultiWorld
id: int id: int
menu: Region menu: Region
locations: List[Location] locations: List[Location]
prog_items: List[Item] prog_items: List[Item]
basic_items: List[Item]
regions: List[Region]
def __init__(self, world: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
self.world = world
self.id = id
self.menu = menu
self.locations = locations
self.prog_items = prog_items
self.basic_items = basic_items
self.regions = [menu]
def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region:
region_tag = "_region" + str(len(self.regions))
region_name = "player" + str(self.id) + region_tag
region = Region("player" + str(self.id) + region_tag, RegionType.Generic,
"Region Hint", self.id, self.world)
self.locations += generate_locations(size,
self.id, None, region, region_tag)
entrance = Entrance(self.id, region_name + "_entrance", parent)
parent.exits.append(entrance)
entrance.connect(region)
entrance.access_rule = access_rule
self.regions.append(region)
self.world.regions.append(region)
return region
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition: def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
items = items.copy()
while len(items) > 0:
location = region.locations.pop(0)
region.locations.append(location)
if location.item:
return items
item = items.pop(0)
world.push_item(location, item, False)
location.event = item.advancement
return items
def regionContains(region: Region, item: Item) -> bool:
for location in region.locations:
if location.item == item:
return True
return False
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
menu = multi_world.get_region("Menu", player_id) menu = multi_world.get_region("Menu", player_id)
locations = generate_locations(location_count, player_id, None, menu) locations = generate_locations(location_count, player_id, None, menu)
prog_items = generate_items(prog_item_count, player_id, True) prog_items = generate_items(prog_item_count, player_id, True)
multi_world.itempool += prog_items
basic_items = generate_items(basic_item_count, player_id, False)
multi_world.itempool += basic_items
return PlayerDefinition(player_id, menu, locations, prog_items) return PlayerDefinition(multi_world, player_id, menu, locations, prog_items, basic_items)
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> List[Location]: def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]:
locations = [] locations = []
prefix = "player" + str(player_id) + tag + "_location"
for i in range(count): for i in range(count):
name = "player" + str(player_id) + "_location" + str(i) name = prefix + str(i)
location = Location(player_id, name, address, region) location = Location(player_id, name, address, region)
locations.append(location) locations.append(location)
region.locations.append(location) region.locations.append(location)
@ -52,14 +108,15 @@ def generate_locations(count: int, player_id: int, address: int = None, region:
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]: def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
items = [] items = []
type = "prog" if advancement else ""
for i in range(count): for i in range(count):
name = "player" + str(player_id) + "_item" + str(i) name = "player" + str(player_id) + "_" + type + "item" + str(i)
items.append(Item(name, advancement, code, player_id)) items.append(Item(name, advancement, code, player_id))
return items return items
class TestBase(unittest.TestCase): class TestFillRestrictive(unittest.TestCase):
def test_basic_fill_restrictive(self): def test_basic_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
@ -76,7 +133,7 @@ class TestBase(unittest.TestCase):
self.assertEqual([], player1.locations) self.assertEqual([], player1.locations)
self.assertEqual([], player1.prog_items) self.assertEqual([], player1.prog_items)
def test_ordered_fill_restrictive(self): def test_ordered_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
items = player1.prog_items items = player1.prog_items
@ -92,7 +149,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(locations[0].item, items[0]) self.assertEqual(locations[0].item, items[0])
self.assertEqual(locations[1].item, items[1]) self.assertEqual(locations[1].item, items[1])
def test_fill_restrictive_remaining_locations(self): def test_partial_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 3, 2) player1 = generate_player_data(multi_world, 1, 3, 2)
@ -106,7 +163,7 @@ class TestBase(unittest.TestCase):
item0.name, player1.id) and state.has(item1.name, player1.id) item0.name, player1.id) and state.has(item1.name, player1.id)
set_rule(loc1, lambda state: state.has( set_rule(loc1, lambda state: state.has(
item0.name, player1.id)) item0.name, player1.id))
#forces a swap # forces a swap
set_rule(loc2, lambda state: state.has( set_rule(loc2, lambda state: state.has(
item0.name, player1.id)) item0.name, player1.id))
fill_restrictive(multi_world, multi_world.state, fill_restrictive(multi_world, multi_world.state,
@ -117,7 +174,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(1, len(player1.locations)) self.assertEqual(1, len(player1.locations))
self.assertEqual(player1.locations[0], loc2) self.assertEqual(player1.locations[0], loc2)
def test_minimal_fill_restrictive(self): def test_minimal_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
@ -137,7 +194,7 @@ class TestBase(unittest.TestCase):
# Unnecessary unreachable Item # Unnecessary unreachable Item
self.assertEqual(locations[1].item, items[0]) self.assertEqual(locations[1].item, items[0])
def test_reversed_fill_restrictive(self): def test_reversed_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
@ -155,7 +212,7 @@ 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_multi_step_fill_restrictive(self): def test_multi_step_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 4, 4) player1 = generate_player_data(multi_world, 1, 4, 4)
@ -179,7 +236,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(locations[2].item, items[0]) self.assertEqual(locations[2].item, items[0])
self.assertEqual(locations[3].item, items[3]) self.assertEqual(locations[3].item, items[3])
def test_impossible_fill_restrictive(self): def test_impossible_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
items = player1.prog_items items = player1.prog_items
@ -195,7 +252,7 @@ class TestBase(unittest.TestCase):
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
player1.locations.copy(), player1.prog_items.copy()) player1.locations.copy(), player1.prog_items.copy())
def test_circular_fill_restrictive(self): def test_circular_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 3, 3) player1 = generate_player_data(multi_world, 1, 3, 3)
@ -215,7 +272,7 @@ class TestBase(unittest.TestCase):
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
player1.locations.copy(), player1.prog_items.copy()) player1.locations.copy(), player1.prog_items.copy())
def test_competing_fill_restrictive(self): def test_competing_fill(self):
multi_world = generate_multi_world() multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
@ -231,7 +288,7 @@ class TestBase(unittest.TestCase):
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
player1.locations.copy(), player1.prog_items.copy()) player1.locations.copy(), player1.prog_items.copy())
def test_multiplayer_fill_restrictive(self): def test_multiplayer_fill(self):
multi_world = generate_multi_world(2) multi_world = generate_multi_world(2)
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
player2 = generate_player_data(multi_world, 2, 2, 2) player2 = generate_player_data(multi_world, 2, 2, 2)
@ -251,7 +308,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(player2.locations[0].item, player1.prog_items[0]) self.assertEqual(player2.locations[0].item, player1.prog_items[0])
self.assertEqual(player2.locations[1].item, player2.prog_items[0]) self.assertEqual(player2.locations[1].item, player2.prog_items[0])
def test_multiplayer_rules_fill_restrictive(self): def test_multiplayer_rules_fill(self):
multi_world = generate_multi_world(2) multi_world = generate_multi_world(2)
player1 = generate_player_data(multi_world, 1, 2, 2) player1 = generate_player_data(multi_world, 1, 2, 2)
player2 = generate_player_data(multi_world, 2, 2, 2) player2 = generate_player_data(multi_world, 2, 2, 2)
@ -273,3 +330,225 @@ class TestBase(unittest.TestCase):
self.assertEqual(player1.locations[1].item, player2.prog_items[1]) self.assertEqual(player1.locations[1].item, player2.prog_items[1])
self.assertEqual(player2.locations[0].item, player1.prog_items[0]) self.assertEqual(player2.locations[0].item, player1.prog_items[0])
self.assertEqual(player2.locations[1].item, player1.prog_items[1]) self.assertEqual(player2.locations[1].item, player1.prog_items[1])
class TestDistributeItemsRestrictive(unittest.TestCase):
def test_basic_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
prog_items = player1.prog_items
basic_items = player1.basic_items
distribute_items_restrictive(multi_world)
self.assertEqual(locations[0].item, basic_items[0])
self.assertEqual(locations[1].item, prog_items[0])
self.assertEqual(locations[2].item, prog_items[1])
self.assertEqual(locations[3].item, basic_items[1])
def test_excluded_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[1].progress_type = LocationProgressType.EXCLUDED
locations[2].progress_type = LocationProgressType.EXCLUDED
distribute_items_restrictive(multi_world)
self.assertFalse(locations[1].item.advancement)
self.assertFalse(locations[2].item.advancement)
def test_non_excluded_item_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
basic_items = player1.basic_items
locations[1].progress_type = LocationProgressType.EXCLUDED
basic_items[1].never_exclude = True
distribute_items_restrictive(multi_world)
self.assertEqual(locations[1].item, basic_items[0])
def test_too_many_excluded_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[0].progress_type = LocationProgressType.EXCLUDED
locations[1].progress_type = LocationProgressType.EXCLUDED
locations[2].progress_type = LocationProgressType.EXCLUDED
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
def test_non_excluded_item_must_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
basic_items = player1.basic_items
locations[1].progress_type = LocationProgressType.EXCLUDED
locations[2].progress_type = LocationProgressType.EXCLUDED
basic_items[0].never_exclude = True
basic_items[1].never_exclude = True
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
def test_priority_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[0].progress_type = LocationProgressType.PRIORITY
locations[3].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multi_world)
self.assertTrue(locations[0].item.advancement)
self.assertTrue(locations[3].item.advancement)
def test_excess_priority_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[0].progress_type = LocationProgressType.PRIORITY
locations[1].progress_type = LocationProgressType.PRIORITY
locations[2].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multi_world)
self.assertFalse(locations[3].item.advancement)
def test_multiple_world_distribute(self):
multi_world = generate_multi_world(3)
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
player2 = generate_player_data(
multi_world, 2, 4, prog_item_count=1, basic_item_count=3)
player3 = generate_player_data(
multi_world, 3, 6, prog_item_count=4, basic_item_count=2)
distribute_items_restrictive(multi_world)
self.assertEqual(player1.locations[0].item, player1.prog_items[1])
self.assertEqual(player1.locations[1].item, player3.prog_items[2])
self.assertEqual(player1.locations[2].item, player3.prog_items[1])
self.assertEqual(player1.locations[3].item, player2.prog_items[0])
self.assertEqual(player2.locations[0].item, player1.basic_items[0])
self.assertEqual(player2.locations[1].item, player2.basic_items[1])
self.assertEqual(player2.locations[2].item, player3.basic_items[1])
self.assertEqual(player2.locations[3].item, player2.basic_items[0])
self.assertEqual(player3.locations[0].item, player1.basic_items[1])
self.assertEqual(player3.locations[1].item, player3.prog_items[3])
self.assertEqual(player3.locations[2].item, player1.prog_items[0])
self.assertEqual(player3.locations[3].item, player3.basic_items[0])
self.assertEqual(player3.locations[4].item, player3.prog_items[0])
self.assertEqual(player3.locations[5].item, player2.basic_items[2])
def test_multiple_world_priority_distribute(self):
multi_world = generate_multi_world(3)
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
player2 = generate_player_data(
multi_world, 2, 4, prog_item_count=1, basic_item_count=3)
player3 = generate_player_data(
multi_world, 3, 6, prog_item_count=4, basic_item_count=2)
player1.locations[2].progress_type = LocationProgressType.PRIORITY
player1.locations[3].progress_type = LocationProgressType.PRIORITY
player2.locations[1].progress_type = LocationProgressType.PRIORITY
player3.locations[0].progress_type = LocationProgressType.PRIORITY
player3.locations[1].progress_type = LocationProgressType.PRIORITY
player3.locations[2].progress_type = LocationProgressType.PRIORITY
player3.locations[3].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multi_world)
self.assertTrue(player1.locations[2].item.advancement)
self.assertTrue(player1.locations[3].item.advancement)
self.assertTrue(player2.locations[1].item.advancement)
self.assertTrue(player3.locations[0].item.advancement)
self.assertTrue(player3.locations[1].item.advancement)
self.assertTrue(player3.locations[2].item.advancement)
self.assertTrue(player3.locations[3].item.advancement)
class TestBalanceMultiworldProgression(unittest.TestCase):
def assertRegionContains(self, region: Region, item: Item):
for location in region.locations:
if location.item and location.item == item:
return True
self.fail("Expected " + region.name + " to contain " + item.name +
"\n Contains" + str(list(map(lambda location: location.item, region.locations))))
def setUp(self):
multi_world = generate_multi_world(2)
self.multi_world = multi_world
player1 = generate_player_data(
multi_world, 1, prog_item_count=2, basic_item_count=40)
self.player1 = player1
player2 = generate_player_data(
multi_world, 2, prog_item_count=2, basic_item_count=40)
self.player2 = player2
multi_world.completion_condition[player1.id] = lambda state: state.has(
player1.prog_items[0].name, player1.id) and state.has(
player1.prog_items[1].name, player1.id)
multi_world.completion_condition[player2.id] = lambda state: state.has(
player2.prog_items[0].name, player2.id) and state.has(
player2.prog_items[1].name, player2.id)
items = player1.basic_items + player2.basic_items
# Sphere 1
region = player1.generate_region(player1.menu, 20)
items = fillRegion(multi_world, region, [
player1.prog_items[0]] + items)
# Sphere 2
region = player1.generate_region(
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
items = fillRegion(
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
# Sphere 3
region = player2.generate_region(
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
items = fillRegion(multi_world, region, [
player2.prog_items[1]] + items)
multi_world.progression_balancing[player1.id] = True
multi_world.progression_balancing[player2.id] = True
def test_balances_progression(self):
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])
balance_multiworld_progression(self.multi_world)
self.assertRegionContains(
self.player1.regions[1], self.player2.prog_items[0])
def test_ignores_priority_locations(self):
self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
balance_multiworld_progression(self.multi_world)
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])

View File

@ -1,5 +1,7 @@
import typing import typing
from BaseClasses import LocationProgressType
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import BaseClasses import BaseClasses
@ -31,6 +33,7 @@ def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]):
else: else:
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude)) add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
location.excluded = True location.excluded = True
location.progress_type = LocationProgressType.EXCLUDED
def set_rule(spot, rule: CollectionRule): def set_rule(spot, rule: CollectionRule):
spot.access_rule = rule spot.access_rule = rule