Strict and Partial key logic implementations with new test suite utility
This commit is contained in:
@@ -510,6 +510,7 @@ class CollectionState(object):
|
|||||||
self.world = parent
|
self.world = parent
|
||||||
if not skip_init:
|
if not skip_init:
|
||||||
self.prog_items = Counter()
|
self.prog_items = Counter()
|
||||||
|
self.forced_keys = Counter()
|
||||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||||
self.events = []
|
self.events = []
|
||||||
@@ -542,12 +543,13 @@ class CollectionState(object):
|
|||||||
queue = deque(self.blocked_connections[player].items())
|
queue = deque(self.blocked_connections[player].items())
|
||||||
|
|
||||||
self.traverse_world(queue, rrp, bc, player)
|
self.traverse_world(queue, rrp, bc, player)
|
||||||
unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations
|
if self.world.key_logic_algorithm[player] == 'default':
|
||||||
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
|
unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations
|
||||||
and x not in self.locations_checked and x.can_reach(self)]
|
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
|
||||||
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
|
and x not in self.locations_checked and x.can_reach(self)]
|
||||||
if len(unresolved_events) == 0:
|
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
|
||||||
self.check_key_doors_in_dungeons(rrp, player)
|
if len(unresolved_events) == 0:
|
||||||
|
self.check_key_doors_in_dungeons(rrp, player)
|
||||||
|
|
||||||
def traverse_world(self, queue, rrp, bc, player):
|
def traverse_world(self, queue, rrp, bc, player):
|
||||||
# run BFS on all connections, and keep track of those blocked by missing items
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
@@ -639,6 +641,7 @@ class CollectionState(object):
|
|||||||
|
|
||||||
def check_key_doors_in_dungeons(self, rrp, player):
|
def check_key_doors_in_dungeons(self, rrp, player):
|
||||||
for dungeon_name, checklist in self.dungeons_to_check[player].items():
|
for dungeon_name, checklist in self.dungeons_to_check[player].items():
|
||||||
|
# todo: optimization idea - abort exploration if there are unresolved events now
|
||||||
if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist):
|
if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist):
|
||||||
continue
|
continue
|
||||||
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
|
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
|
||||||
@@ -838,6 +841,7 @@ class CollectionState(object):
|
|||||||
def copy(self):
|
def copy(self):
|
||||||
ret = CollectionState(self.world, skip_init=True)
|
ret = CollectionState(self.world, skip_init=True)
|
||||||
ret.prog_items = self.prog_items.copy()
|
ret.prog_items = self.prog_items.copy()
|
||||||
|
ret.forced_keys = self.forced_keys.copy()
|
||||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
||||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
||||||
ret.events = copy.copy(self.events)
|
ret.events = copy.copy(self.events)
|
||||||
@@ -1045,6 +1049,14 @@ class CollectionState(object):
|
|||||||
return (item, player) in self.prog_items
|
return (item, player) in self.prog_items
|
||||||
return self.prog_items[item, player] >= count
|
return self.prog_items[item, player] >= count
|
||||||
|
|
||||||
|
def has_sm_key_strict(self, item, player, count=1):
|
||||||
|
if self.world.keyshuffle[player] == 'universal':
|
||||||
|
if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)':
|
||||||
|
return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool
|
||||||
|
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||||
|
obtained = self.prog_items[item, player] - self.forced_keys[item, player]
|
||||||
|
return obtained >= count
|
||||||
|
|
||||||
def can_buy_unlimited(self, item, player):
|
def can_buy_unlimited(self, item, player):
|
||||||
for shop in self.world.shops[player]:
|
for shop in self.world.shops[player]:
|
||||||
if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self):
|
if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self):
|
||||||
@@ -1241,6 +1253,8 @@ class CollectionState(object):
|
|||||||
def collect(self, item, event=False, location=None):
|
def collect(self, item, event=False, location=None):
|
||||||
if location:
|
if location:
|
||||||
self.locations_checked.add(location)
|
self.locations_checked.add(location)
|
||||||
|
if item and item.smallkey and location.forced_item is not None:
|
||||||
|
self.forced_keys[item.name, item.player] += 1
|
||||||
if not item:
|
if not item:
|
||||||
return
|
return
|
||||||
changed = False
|
changed = False
|
||||||
@@ -2949,7 +2963,7 @@ bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3}
|
|||||||
# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo)
|
# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo)
|
||||||
overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2}
|
overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2}
|
||||||
trap_door_mode = {'vanilla': 0, 'boss': 1, 'oneway': 2}
|
trap_door_mode = {'vanilla': 0, 'boss': 1, 'oneway': 2}
|
||||||
key_logic_algo = {'loose': 0, 'default': 1, 'partial': 2, 'strict': 4}
|
key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2}
|
||||||
|
|
||||||
# sfx_shuffle and other adjust items does not affect settings code
|
# sfx_shuffle and other adjust items does not affect settings code
|
||||||
|
|
||||||
|
|||||||
64
Fill.py
64
Fill.py
@@ -105,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
|
|||||||
spot_to_fill = None
|
spot_to_fill = None
|
||||||
|
|
||||||
item_locations = filter_locations(item_to_place, locations, world, vanilla)
|
item_locations = filter_locations(item_to_place, locations, world, vanilla)
|
||||||
|
verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement,
|
||||||
|
perform_access_check, key_pool, world)
|
||||||
for location in item_locations:
|
for location in item_locations:
|
||||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
|
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
|
||||||
single_player_placement, perform_access_check, key_pool, world)
|
single_player_placement, perform_access_check, key_pool, world)
|
||||||
@@ -128,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
|
|||||||
raise FillError('No more spots to place %s' % item_to_place)
|
raise FillError('No more spots to place %s' % item_to_place)
|
||||||
|
|
||||||
world.push_item(spot_to_fill, item_to_place, False)
|
world.push_item(spot_to_fill, item_to_place, False)
|
||||||
if item_to_place.smallkey:
|
|
||||||
with suppress(ValueError):
|
|
||||||
key_pool.remove(item_to_place)
|
|
||||||
track_outside_keys(item_to_place, spot_to_fill, world)
|
track_outside_keys(item_to_place, spot_to_fill, world)
|
||||||
track_dungeon_items(item_to_place, spot_to_fill, world)
|
track_dungeon_items(item_to_place, spot_to_fill, world)
|
||||||
locations.remove(spot_to_fill)
|
locations.remove(spot_to_fill)
|
||||||
@@ -144,6 +143,9 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
|
|||||||
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||||
location.item = item_to_place
|
location.item = item_to_place
|
||||||
location.event = True
|
location.event = True
|
||||||
|
if item_to_place.smallkey:
|
||||||
|
with suppress(ValueError):
|
||||||
|
key_pool.remove(item_to_place)
|
||||||
test_state = max_exp_state.copy()
|
test_state = max_exp_state.copy()
|
||||||
test_state.stale[item_to_place.player] = True
|
test_state.stale[item_to_place.player] = True
|
||||||
else:
|
else:
|
||||||
@@ -157,6 +159,8 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
|
|||||||
if item_to_place.smallkey or item_to_place.bigkey:
|
if item_to_place.smallkey or item_to_place.bigkey:
|
||||||
location.item = None
|
location.item = None
|
||||||
location.event = False
|
location.event = False
|
||||||
|
if item_to_place.smallkey:
|
||||||
|
key_pool.append(item_to_place)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -394,11 +398,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
|||||||
random.shuffle(fill_locations)
|
random.shuffle(fill_locations)
|
||||||
|
|
||||||
random.shuffle(world.itempool)
|
random.shuffle(world.itempool)
|
||||||
if world.item_pool_config.preferred:
|
config_sort(world)
|
||||||
pref = list(world.item_pool_config.preferred.keys())
|
|
||||||
pref_len = len(pref)
|
|
||||||
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
|
|
||||||
if (i.name, i.player) in world.item_pool_config.preferred else 0)
|
|
||||||
progitempool = [item for item in world.itempool if item.advancement]
|
progitempool = [item for item in world.itempool if item.advancement]
|
||||||
prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
|
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]
|
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
|
||||||
@@ -496,6 +496,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
|||||||
ensure_good_pots(world)
|
ensure_good_pots(world)
|
||||||
|
|
||||||
|
|
||||||
|
def config_sort(world):
|
||||||
|
if world.item_pool_config.verify:
|
||||||
|
config_sort_helper(world, world.item_pool_config.verify)
|
||||||
|
elif world.item_pool_config.preferred:
|
||||||
|
config_sort_helper(world, world.item_pool_config.preferred)
|
||||||
|
|
||||||
|
|
||||||
|
def config_sort_helper(world, sort_dict):
|
||||||
|
pref = list(sort_dict.keys())
|
||||||
|
pref_len = len(pref)
|
||||||
|
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
|
||||||
|
if (i.name, i.player) in sort_dict else 0)
|
||||||
|
|
||||||
|
|
||||||
def calc_trash_locations(world, player):
|
def calc_trash_locations(world, player):
|
||||||
total_count, gt_count = 0, 0
|
total_count, gt_count = 0, 0
|
||||||
for loc in world.get_locations():
|
for loc in world.get_locations():
|
||||||
@@ -667,6 +681,40 @@ def sell_keys(world, player):
|
|||||||
world.itempool.remove(universal_key)
|
world.itempool.remove(universal_key)
|
||||||
|
|
||||||
|
|
||||||
|
def verify(item_to_place, item_locations, state, spp, pac, key_pool, world):
|
||||||
|
if world.item_pool_config.verify:
|
||||||
|
logger = logging.getLogger('')
|
||||||
|
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
|
||||||
|
item_player = item_to_place.player
|
||||||
|
config = world.item_pool_config
|
||||||
|
if (item_name, item_player) in config.verify:
|
||||||
|
tests = config.verify[(item_name, item_player)]
|
||||||
|
issues = []
|
||||||
|
for location in item_locations:
|
||||||
|
if location.name in tests:
|
||||||
|
expected = tests[location.name]
|
||||||
|
spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world)
|
||||||
|
if spot and (item_to_place.smallkey or item_to_place.bigkey):
|
||||||
|
location.item = None
|
||||||
|
location.event = False
|
||||||
|
if item_to_place.smallkey:
|
||||||
|
key_pool.append(item_to_place)
|
||||||
|
if (expected and spot) or (not expected and spot is None):
|
||||||
|
logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}')
|
||||||
|
config.verify_count += 1
|
||||||
|
if config.verify_count >= config.verify_target:
|
||||||
|
exit()
|
||||||
|
else:
|
||||||
|
issues.append((item_name, item_player, location.name, expected))
|
||||||
|
if len(issues) > 0:
|
||||||
|
for name, player, loc, expected in issues:
|
||||||
|
if expected:
|
||||||
|
logger.error(f'Could not place {name} ({player}) at {loc}')
|
||||||
|
else:
|
||||||
|
logger.error(f'{name} ({player}) should not be allowed at {loc}')
|
||||||
|
raise Exception(f'Test failed placing {name}')
|
||||||
|
|
||||||
|
|
||||||
def balance_multiworld_progression(world):
|
def balance_multiworld_progression(world):
|
||||||
state = CollectionState(world)
|
state = CollectionState(world)
|
||||||
checked_locations = set()
|
checked_locations = set()
|
||||||
|
|||||||
@@ -1431,6 +1431,13 @@ def fill_specific_items(world):
|
|||||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||||
item_name = item_parts[0]
|
item_name = item_parts[0]
|
||||||
world.item_pool_config.preferred[(item_name, item_player)] = placement['locations']
|
world.item_pool_config.preferred[(item_name, item_player)] = placement['locations']
|
||||||
|
elif placement['type'] == 'Verification':
|
||||||
|
item = placement['item']
|
||||||
|
item_parts = item.split('#')
|
||||||
|
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||||
|
item_name = item_parts[0]
|
||||||
|
world.item_pool_config.verify[(item_name, item_player)] = placement['locations']
|
||||||
|
world.item_pool_config.verify_target += len(placement['locations'])
|
||||||
|
|
||||||
|
|
||||||
def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):
|
def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):
|
||||||
|
|||||||
@@ -158,12 +158,11 @@ CLI: `--trap_door_mode [vanilla|boss|oneway]`
|
|||||||
|
|
||||||
Determines how small key door logic works.
|
Determines how small key door logic works.
|
||||||
|
|
||||||
* Loose: Skips placement rules checks. Currently, experimental to see what kinds of problems can arise.
|
|
||||||
* Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable)
|
* Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable)
|
||||||
* Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches.
|
* Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches.
|
||||||
* Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic.
|
* Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic.
|
||||||
|
|
||||||
CLI: `--key_logic [loose|default|partial|strict]`
|
CLI: `--key_logic [default|partial|strict]`
|
||||||
|
|
||||||
### Decouple Doors
|
### Decouple Doors
|
||||||
|
|
||||||
|
|||||||
21
Rules.py
21
Rules.py
@@ -2076,13 +2076,16 @@ bunny_impassible_doors = {
|
|||||||
|
|
||||||
def add_key_logic_rules(world, player):
|
def add_key_logic_rules(world, player):
|
||||||
key_logic = world.key_logic[player]
|
key_logic = world.key_logic[player]
|
||||||
|
eval_func = eval_small_key_door
|
||||||
|
if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild':
|
||||||
|
eval_func = eval_small_key_door_strict
|
||||||
for d_name, d_logic in key_logic.items():
|
for d_name, d_logic in key_logic.items():
|
||||||
for door_name, rule in d_logic.door_rules.items():
|
for door_name, rule in d_logic.door_rules.items():
|
||||||
door_entrance = world.get_entrance(door_name, player)
|
door_entrance = world.get_entrance(door_name, player)
|
||||||
add_rule(door_entrance, eval_small_key_door(door_name, d_name, player))
|
add_rule(door_entrance, eval_func(door_name, d_name, player))
|
||||||
if door_entrance.door.dependents:
|
if door_entrance.door.dependents:
|
||||||
for dep in door_entrance.door.dependents:
|
for dep in door_entrance.door.dependents:
|
||||||
add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player))
|
add_rule(dep.entrance, eval_func(door_name, d_name, player))
|
||||||
for location in d_logic.bk_restricted:
|
for location in d_logic.bk_restricted:
|
||||||
if not location.forced_item:
|
if not location.forced_item:
|
||||||
forbid_item(location, d_logic.bk_name, player)
|
forbid_item(location, d_logic.bk_name, player)
|
||||||
@@ -2129,10 +2132,24 @@ def eval_small_key_door_main(state, door_name, dungeon, player):
|
|||||||
return door_openable
|
return door_openable
|
||||||
|
|
||||||
|
|
||||||
|
def eval_small_key_door_strict_main(state, door_name, dungeon, player):
|
||||||
|
if state.is_door_open(door_name, player):
|
||||||
|
return True
|
||||||
|
key_layout = state.world.key_layout[player][dungeon]
|
||||||
|
number = key_layout.max_chests
|
||||||
|
if number <= 0:
|
||||||
|
return True
|
||||||
|
return state.has_sm_key_strict(key_layout.key_logic.small_key_name, player, number)
|
||||||
|
|
||||||
|
|
||||||
def eval_small_key_door(door_name, dungeon, player):
|
def eval_small_key_door(door_name, dungeon, player):
|
||||||
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
|
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
|
||||||
|
|
||||||
|
|
||||||
|
def eval_small_key_door_strict(door_name, dungeon, player):
|
||||||
|
return lambda state: eval_small_key_door_strict_main(state, door_name, dungeon, player)
|
||||||
|
|
||||||
|
|
||||||
def allow_big_key_in_big_chest(bk_name, player):
|
def allow_big_key_in_big_chest(bk_name, player):
|
||||||
return lambda state, item: item.name == bk_name and item.player == player
|
return lambda state, item: item.name == bk_name and item.player == player
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
boss: 0
|
boss: 0
|
||||||
oneway: 0
|
oneway: 0
|
||||||
key_logic_algorithm:
|
key_logic_algorithm:
|
||||||
loose: 0
|
|
||||||
default: 1
|
default: 1
|
||||||
partial: 0
|
partial: 0
|
||||||
strict: 0
|
strict: 0
|
||||||
|
|||||||
@@ -202,7 +202,6 @@
|
|||||||
},
|
},
|
||||||
"key_logic_algorithm": {
|
"key_logic_algorithm": {
|
||||||
"choices": [
|
"choices": [
|
||||||
"loose",
|
|
||||||
"default",
|
"default",
|
||||||
"partial",
|
"partial",
|
||||||
"strict"
|
"strict"
|
||||||
|
|||||||
@@ -247,7 +247,6 @@
|
|||||||
],
|
],
|
||||||
"key_logic_algorithm": [
|
"key_logic_algorithm": [
|
||||||
"Key Logic Algorithm (default: %(default)s)",
|
"Key Logic Algorithm (default: %(default)s)",
|
||||||
"loose: Allow more randomization",
|
|
||||||
"default: Balance between safety and randomization",
|
"default: Balance between safety and randomization",
|
||||||
"partial: Partial protection when using certain minor glitches",
|
"partial: Partial protection when using certain minor glitches",
|
||||||
"strict: Ensure small keys are available"
|
"strict: Ensure small keys are available"
|
||||||
|
|||||||
@@ -97,7 +97,6 @@
|
|||||||
"randomizer.dungeon.trap_door_mode.oneway": "Remove Annoying Traps",
|
"randomizer.dungeon.trap_door_mode.oneway": "Remove Annoying Traps",
|
||||||
|
|
||||||
"randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm",
|
"randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm",
|
||||||
"randomizer.dungeon.key_logic_algorithm.loose": "Loose",
|
|
||||||
"randomizer.dungeon.key_logic_algorithm.default": "Default",
|
"randomizer.dungeon.key_logic_algorithm.default": "Default",
|
||||||
"randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection",
|
"randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection",
|
||||||
"randomizer.dungeon.key_logic_algorithm.strict": "Strict",
|
"randomizer.dungeon.key_logic_algorithm.strict": "Strict",
|
||||||
|
|||||||
@@ -57,7 +57,6 @@
|
|||||||
"type": "selectbox",
|
"type": "selectbox",
|
||||||
"default": "default",
|
"default": "default",
|
||||||
"options": [
|
"options": [
|
||||||
"loose",
|
|
||||||
"default",
|
"default",
|
||||||
"partial",
|
"partial",
|
||||||
"strict"
|
"strict"
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ class ItemPoolConfig(object):
|
|||||||
self.reserved_locations = defaultdict(set)
|
self.reserved_locations = defaultdict(set)
|
||||||
self.restricted = {}
|
self.restricted = {}
|
||||||
self.preferred = {}
|
self.preferred = {}
|
||||||
|
self.verify = {}
|
||||||
|
self.verify_count = 0
|
||||||
|
self.verify_target = 0
|
||||||
|
|
||||||
self.recorded_choices = []
|
self.recorded_choices = []
|
||||||
|
|
||||||
@@ -435,6 +438,9 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
|
|||||||
if (item_name, item_to_place.player) in config.preferred:
|
if (item_name, item_to_place.player) in config.preferred:
|
||||||
locs = config.preferred[(item_name, item_to_place.player)]
|
locs = config.preferred[(item_name, item_to_place.player)]
|
||||||
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
|
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
|
||||||
|
if (item_name, item_to_place.player) in config.verify:
|
||||||
|
locs = config.verify[(item_name, item_to_place.player)].keys()
|
||||||
|
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
126
test/NewTestSuite.py
Normal file
126
test/NewTestSuite.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import multiprocessing
|
||||||
|
import concurrent.futures
|
||||||
|
import argparse
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
cpu_threads = multiprocessing.cpu_count()
|
||||||
|
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
successes = []
|
||||||
|
errors = []
|
||||||
|
task_mapping = []
|
||||||
|
tests = OrderedDict()
|
||||||
|
|
||||||
|
successes.append(f"Testing DR (NewTestSuite)")
|
||||||
|
print(successes[0])
|
||||||
|
|
||||||
|
# max_attempts = args.count
|
||||||
|
pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads)
|
||||||
|
dead_or_alive = 0
|
||||||
|
alive = 0
|
||||||
|
|
||||||
|
def test(test_name: str, command: str, test_file: str):
|
||||||
|
tests[test_name] = [command]
|
||||||
|
|
||||||
|
base_command = f"python3.8 DungeonRandomizer.py --suppress_rom --suppress_spoiler"
|
||||||
|
|
||||||
|
def gen_seed():
|
||||||
|
task_command = base_command + " " + command
|
||||||
|
return subprocess.run(task_command, capture_output=True, shell=True, text=True)
|
||||||
|
|
||||||
|
task = pool.submit(gen_seed)
|
||||||
|
task.success = False
|
||||||
|
task.name = test_name
|
||||||
|
task.test_file = test_file
|
||||||
|
task.cmd = base_command + " " + command
|
||||||
|
task_mapping.append(task)
|
||||||
|
|
||||||
|
for test_suite, test_files in args.test_suite.items():
|
||||||
|
for test_file in test_files:
|
||||||
|
test(test_suite, f'--customizer {os.path.join(test_suite, test_file)}', test_file)
|
||||||
|
|
||||||
|
from tqdm import tqdm
|
||||||
|
with tqdm(concurrent.futures.as_completed(task_mapping),
|
||||||
|
total=len(task_mapping), unit="seed(s)",
|
||||||
|
desc=f"Success rate: 0.00%") as progressbar:
|
||||||
|
for task in progressbar:
|
||||||
|
dead_or_alive += 1
|
||||||
|
try:
|
||||||
|
result = task.result()
|
||||||
|
if result.returncode:
|
||||||
|
errors.append([task.name + ' ' + task.test_file, task.cmd, result.stderr])
|
||||||
|
else:
|
||||||
|
alive += 1
|
||||||
|
task.success = True
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}")
|
||||||
|
|
||||||
|
def get_results(testname: str):
|
||||||
|
result = ""
|
||||||
|
dead_or_alive = [task.success for task in task_mapping if task.name == testname]
|
||||||
|
alive = [x for x in dead_or_alive if x]
|
||||||
|
success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%"
|
||||||
|
successes.append(success)
|
||||||
|
print(success)
|
||||||
|
result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t"
|
||||||
|
return result.strip()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for t in tests.keys():
|
||||||
|
results.append(get_results(t))
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
print(result)
|
||||||
|
successes.append(result)
|
||||||
|
|
||||||
|
return successes, errors
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
successes = []
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(add_help=False)
|
||||||
|
# parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0))
|
||||||
|
parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1))
|
||||||
|
parser.add_argument('--help', default=False, action='store_true')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.help:
|
||||||
|
parser.print_help()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
cpu_threads = args.cpu_threads
|
||||||
|
|
||||||
|
test_suites = {}
|
||||||
|
# not sure if it supports subdirectories properly yet
|
||||||
|
for root, dirnames, filenames in os.walk('test/suite'):
|
||||||
|
test_suites[root] = fnmatch.filter(filenames, '*.yaml')
|
||||||
|
|
||||||
|
args = argparse.Namespace()
|
||||||
|
args.test_suite = test_suites
|
||||||
|
s, errors = main(args=args)
|
||||||
|
if successes:
|
||||||
|
successes += [""] * 2
|
||||||
|
successes += s
|
||||||
|
print()
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
with open(f"new-test-suite-errors.txt", 'w') as stream:
|
||||||
|
for error in errors:
|
||||||
|
stream.write(error[0] + "\n")
|
||||||
|
stream.write(error[1] + "\n")
|
||||||
|
stream.write(error[2] + "\n\n")
|
||||||
|
|
||||||
|
with open("new-test-suite-success.txt", "w") as stream:
|
||||||
|
stream.write(str.join("\n", successes))
|
||||||
|
|
||||||
|
input("Press enter to continue")
|
||||||
44
test/suite/default_key_logic.yaml
Normal file
44
test/suite/default_key_logic.yaml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Possible improvements: account for items that are possibly in logic
|
||||||
|
# Example: Mire Big Key in harmless means all 6 mire smalls required for fire-locked side,
|
||||||
|
# if you have access to harmless via:
|
||||||
|
# 2 pod smalls + bow, hammer or 3 pod small
|
||||||
|
meta:
|
||||||
|
players: 1
|
||||||
|
settings:
|
||||||
|
1:
|
||||||
|
key_logic_algorithm: default
|
||||||
|
keysanity: True
|
||||||
|
crystals_needed_for_gt: 0 # to skip trash fill
|
||||||
|
placements:
|
||||||
|
1:
|
||||||
|
Hobo: Big Key (Misery Mire)
|
||||||
|
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||||
|
Waterfall Fairy - Right: Small Key (Misery Mire)
|
||||||
|
Palace of Darkness - Big Chest: Hammer
|
||||||
|
advanced_placements:
|
||||||
|
1:
|
||||||
|
# Contrast with partial_2
|
||||||
|
- type: Verification
|
||||||
|
item: Flippers
|
||||||
|
locations:
|
||||||
|
Misery Mire - Map Chest: True
|
||||||
|
Misery Mire - Main Lobby: True
|
||||||
|
Misery Mire - Bridge Chest: True
|
||||||
|
Misery Mire - Spike Chest: True
|
||||||
|
Misery Mire - Compass Chest: False
|
||||||
|
Misery Mire - Big Key Chest: False
|
||||||
|
Misery Mire - Boss: False
|
||||||
|
# Contrast with partial_3
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Ganons Tower)
|
||||||
|
locations:
|
||||||
|
Ganons Tower - Big Key Chest: True
|
||||||
|
Ganons Tower - Big Key Room - Left: True
|
||||||
|
Ganons Tower - Big Key Room - Right: True
|
||||||
|
Ganons Tower - Bob's Chest: True
|
||||||
|
# Normal logic doesn't allow this placement
|
||||||
|
# unless hammer is placed before it - no algorithm does this in non-keysanity, but possible in keysanity
|
||||||
|
- type: Verification
|
||||||
|
item: Small Key (Palace of Darkness)
|
||||||
|
locations:
|
||||||
|
Palace of Darkness - Dark Maze - Bottom: True
|
||||||
24
test/suite/partial_key_logic.yaml
Normal file
24
test/suite/partial_key_logic.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Even though Lamp is Flipper-locked, this logic considers that a key could be wasted in the dark in mire
|
||||||
|
# Only fire locked mire is off limits
|
||||||
|
meta:
|
||||||
|
players: 1
|
||||||
|
settings:
|
||||||
|
1:
|
||||||
|
key_logic_algorithm: partial
|
||||||
|
keysanity: true
|
||||||
|
placements:
|
||||||
|
1:
|
||||||
|
Hobo: Lamp
|
||||||
|
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||||
|
advanced_placements:
|
||||||
|
1:
|
||||||
|
- type: Verification
|
||||||
|
item: Flippers
|
||||||
|
locations:
|
||||||
|
Misery Mire - Map Chest: True
|
||||||
|
Misery Mire - Main Lobby: True
|
||||||
|
Misery Mire - Bridge Chest: True
|
||||||
|
Misery Mire - Spike Chest: True
|
||||||
|
Misery Mire - Compass Chest: False
|
||||||
|
Misery Mire - Big Key Chest: False
|
||||||
|
Misery Mire - Boss: False
|
||||||
26
test/suite/partial_key_logic_2.yaml
Normal file
26
test/suite/partial_key_logic_2.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# For contrast with default logic
|
||||||
|
# This logic is not yet smart enough to allow the crystal blocked chests with two keys (Spike Pot and one other)
|
||||||
|
meta:
|
||||||
|
players: 1
|
||||||
|
settings:
|
||||||
|
1:
|
||||||
|
key_logic_algorithm: partial
|
||||||
|
keysanity: True
|
||||||
|
placements:
|
||||||
|
1:
|
||||||
|
Hobo: Lamp
|
||||||
|
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||||
|
Waterfall Fairy - Right: Small Key (Misery Mire)
|
||||||
|
Swamp Palace - Entrance: Big Key (Misery Mire)
|
||||||
|
advanced_placements:
|
||||||
|
1:
|
||||||
|
- type: Verification
|
||||||
|
item: Flippers
|
||||||
|
locations:
|
||||||
|
Misery Mire - Map Chest: False
|
||||||
|
Misery Mire - Main Lobby: False
|
||||||
|
Misery Mire - Bridge Chest: True
|
||||||
|
Misery Mire - Spike Chest: True
|
||||||
|
Misery Mire - Compass Chest: False
|
||||||
|
Misery Mire - Big Key Chest: False
|
||||||
|
Misery Mire - Boss: False
|
||||||
160
test/suite/partial_key_logic_3.yaml
Normal file
160
test/suite/partial_key_logic_3.yaml
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# For contrast with default logic
|
||||||
|
# Examples of valid big key placement that doesn't work with pure worst case scenarios
|
||||||
|
# Basically chests that are obtainable two ways+ of spending keys
|
||||||
|
# (Possible fix: access to the extra door grants access to the mini helma key too)
|
||||||
|
meta:
|
||||||
|
players: 1
|
||||||
|
settings:
|
||||||
|
1:
|
||||||
|
key_logic_algorithm: partial
|
||||||
|
crystals_needed_for_gt: 0 # to skip trash fill
|
||||||
|
advanced_placements:
|
||||||
|
1:
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Desert Palace)
|
||||||
|
locations:
|
||||||
|
Desert Palace - Big Chest: False
|
||||||
|
Desert Palace - Big Key Chest: True
|
||||||
|
Desert Palace - Boss: False
|
||||||
|
Desert Palace - Compass Chest: True
|
||||||
|
Desert Palace - Map Chest: True
|
||||||
|
Desert Palace - Torch: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Eastern Palace)
|
||||||
|
locations:
|
||||||
|
Eastern Palace - Big Chest: False
|
||||||
|
Eastern Palace - Big Key Chest: True
|
||||||
|
Eastern Palace - Boss: False
|
||||||
|
Eastern Palace - Cannonball Chest: True
|
||||||
|
Eastern Palace - Compass Chest: True
|
||||||
|
Eastern Palace - Map Chest: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Ganons Tower)
|
||||||
|
locations:
|
||||||
|
# These four require not wasting keys upstairs because the big key is down here
|
||||||
|
Ganons Tower - Big Key Chest: False
|
||||||
|
Ganons Tower - Big Key Room - Left: False
|
||||||
|
Ganons Tower - Big Key Room - Right: False
|
||||||
|
Ganons Tower - Bob's Chest: False
|
||||||
|
# These are normal
|
||||||
|
Ganons Tower - Big Chest: False
|
||||||
|
Ganons Tower - Bob's Torch: True
|
||||||
|
Ganons Tower - Compass Room - Bottom Left: True
|
||||||
|
Ganons Tower - Compass Room - Bottom Right: True
|
||||||
|
Ganons Tower - Compass Room - Top Left: True
|
||||||
|
Ganons Tower - Compass Room - Top Right: True
|
||||||
|
Ganons Tower - DMs Room - Bottom Left: True
|
||||||
|
Ganons Tower - DMs Room - Bottom Right: True
|
||||||
|
Ganons Tower - DMs Room - Top Left: True
|
||||||
|
Ganons Tower - DMs Room - Top Right: True
|
||||||
|
Ganons Tower - Firesnake Room: True
|
||||||
|
Ganons Tower - Hope Room - Left: True
|
||||||
|
Ganons Tower - Hope Room - Right: True
|
||||||
|
Ganons Tower - Map Chest: True
|
||||||
|
Ganons Tower - Mini Helmasaur Room - Left: False
|
||||||
|
Ganons Tower - Mini Helmasaur Room - Right: False
|
||||||
|
Ganons Tower - Pre-Moldorm Chest: False
|
||||||
|
Ganons Tower - Randomizer Room - Bottom Left: True
|
||||||
|
Ganons Tower - Randomizer Room - Bottom Right: True
|
||||||
|
Ganons Tower - Randomizer Room - Top Left: True
|
||||||
|
Ganons Tower - Randomizer Room - Top Right: True
|
||||||
|
Ganons Tower - Tile Room: True
|
||||||
|
Ganons Tower - Validation Chest: False
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Ice Palace)
|
||||||
|
locations:
|
||||||
|
Ice Palace - Big Chest: False
|
||||||
|
Ice Palace - Big Key Chest: True
|
||||||
|
Ice Palace - Boss: False
|
||||||
|
Ice Palace - Compass Chest: True
|
||||||
|
Ice Palace - Freezor Chest: True
|
||||||
|
Ice Palace - Iced T Room: True
|
||||||
|
Ice Palace - Map Chest: True
|
||||||
|
Ice Palace - Spike Room: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Misery Mire)
|
||||||
|
locations:
|
||||||
|
Misery Mire - Big Chest: False
|
||||||
|
Misery Mire - Big Key Chest: True
|
||||||
|
Misery Mire - Boss: False
|
||||||
|
Misery Mire - Bridge Chest: True
|
||||||
|
Misery Mire - Compass Chest: True
|
||||||
|
Misery Mire - Main Lobby: True
|
||||||
|
Misery Mire - Map Chest: True
|
||||||
|
Misery Mire - Spike Chest: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Palace of Darkness)
|
||||||
|
locations:
|
||||||
|
Palace of Darkness - Big Chest: False
|
||||||
|
Palace of Darkness - Big Key Chest: True
|
||||||
|
Palace of Darkness - Boss: False
|
||||||
|
Palace of Darkness - Compass Chest: True
|
||||||
|
Palace of Darkness - Dark Basement - Left: True
|
||||||
|
Palace of Darkness - Dark Basement - Right: True
|
||||||
|
Palace of Darkness - Dark Maze - Bottom: True
|
||||||
|
Palace of Darkness - Dark Maze - Top: True
|
||||||
|
Palace of Darkness - Harmless Hellway: True
|
||||||
|
Palace of Darkness - Map Chest: True
|
||||||
|
Palace of Darkness - Shooter Room: True
|
||||||
|
Palace of Darkness - Stalfos Basement: True
|
||||||
|
Palace of Darkness - The Arena - Bridge: True
|
||||||
|
Palace of Darkness - The Arena - Ledge: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Skull Woods)
|
||||||
|
locations:
|
||||||
|
Skull Woods - Big Chest: True
|
||||||
|
Skull Woods - Big Key Chest: True
|
||||||
|
Skull Woods - Boss: True
|
||||||
|
Skull Woods - Bridge Room: True
|
||||||
|
Skull Woods - Compass Chest: True
|
||||||
|
Skull Woods - Map Chest: True
|
||||||
|
Skull Woods - Pinball Room: True
|
||||||
|
Skull Woods - Pot Prison: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Swamp Palace)
|
||||||
|
locations:
|
||||||
|
Swamp Palace - Big Chest: True
|
||||||
|
Swamp Palace - Big Key Chest: True
|
||||||
|
Swamp Palace - Boss: True
|
||||||
|
Swamp Palace - Compass Chest: True
|
||||||
|
Swamp Palace - Entrance: False
|
||||||
|
Swamp Palace - Flooded Room - Left: True
|
||||||
|
Swamp Palace - Flooded Room - Right: True
|
||||||
|
Swamp Palace - Map Chest: True
|
||||||
|
Swamp Palace - Waterfall Room: True
|
||||||
|
Swamp Palace - West Chest: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Thieves Town)
|
||||||
|
locations:
|
||||||
|
Thieves' Town - Ambush Chest: True
|
||||||
|
Thieves' Town - Attic: False
|
||||||
|
Thieves' Town - Big Chest: False
|
||||||
|
Thieves' Town - Big Key Chest: True
|
||||||
|
Thieves' Town - Blind's Cell: False
|
||||||
|
Thieves' Town - Boss: False
|
||||||
|
Thieves' Town - Compass Chest: True
|
||||||
|
Thieves' Town - Map Chest: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Tower of Hera)
|
||||||
|
locations:
|
||||||
|
Tower of Hera - Basement Cage: True
|
||||||
|
Tower of Hera - Big Chest: False
|
||||||
|
Tower of Hera - Big Key Chest: True
|
||||||
|
Tower of Hera - Boss: False
|
||||||
|
Tower of Hera - Compass Chest: False
|
||||||
|
Tower of Hera - Map Chest: True
|
||||||
|
- type: Verification
|
||||||
|
item: Big Key (Turtle Rock)
|
||||||
|
locations:
|
||||||
|
Turtle Rock - Big Chest: False
|
||||||
|
Turtle Rock - Big Key Chest: True
|
||||||
|
Turtle Rock - Boss: False
|
||||||
|
Turtle Rock - Chain Chomps: True
|
||||||
|
Turtle Rock - Compass Chest: True
|
||||||
|
Turtle Rock - Crystaroller Room: False
|
||||||
|
Turtle Rock - Eye Bridge - Bottom Left: False
|
||||||
|
Turtle Rock - Eye Bridge - Bottom Right: False
|
||||||
|
Turtle Rock - Eye Bridge - Top Left: False
|
||||||
|
Turtle Rock - Eye Bridge - Top Right: False
|
||||||
|
Turtle Rock - Roller Room - Left: True
|
||||||
|
Turtle Rock - Roller Room - Right: True
|
||||||
22
test/suite/strict_key_logic.yaml
Normal file
22
test/suite/strict_key_logic.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
meta:
|
||||||
|
players: 1
|
||||||
|
settings:
|
||||||
|
1:
|
||||||
|
key_logic_algorithm: strict
|
||||||
|
keysanity: true
|
||||||
|
placements:
|
||||||
|
1:
|
||||||
|
Hobo: Big Key (Misery Mire)
|
||||||
|
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||||
|
advanced_placements:
|
||||||
|
1:
|
||||||
|
- type: Verification
|
||||||
|
item: Flippers
|
||||||
|
locations:
|
||||||
|
Misery Mire - Map Chest: False
|
||||||
|
Misery Mire - Main Lobby: False
|
||||||
|
Misery Mire - Bridge Chest: True
|
||||||
|
Misery Mire - Spike Chest: True
|
||||||
|
Misery Mire - Compass Chest: False
|
||||||
|
Misery Mire - Big Key Chest: False
|
||||||
|
Misery Mire - Boss: False
|
||||||
Reference in New Issue
Block a user