Merge branch 'Ambrosia' into DoorDevVolatile

# Conflicts:
#	BaseClasses.py
#	CLI.py
#	Main.py
This commit is contained in:
aerinon
2021-10-29 15:26:51 -06:00
15 changed files with 200 additions and 73 deletions

View File

@@ -115,6 +115,7 @@ class World(object):
set_player_attr('compassshuffle', False) set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', False) set_player_attr('keyshuffle', False)
set_player_attr('bigkeyshuffle', False) set_player_attr('bigkeyshuffle', False)
set_player_attr('restrict_boss_items', 'none')
set_player_attr('bombbag', False) set_player_attr('bombbag', False)
set_player_attr('difficulty_requirements', None) set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none') set_player_attr('boss_shuffle', 'none')
@@ -326,7 +327,7 @@ class World(object):
elif item.name.startswith('Bottle'): elif item.name.startswith('Bottle'):
if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit: if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit:
ret.prog_items[item.name, item.player] += 1 ret.prog_items[item.name, item.player] += 1
elif item.advancement or item.smallkey or item.bigkey: elif item.advancement or item.smallkey or item.bigkey or item.compass or item.map:
ret.prog_items[item.name, item.player] += 1 ret.prog_items[item.name, item.player] += 1
for item in self.itempool: for item in self.itempool:
@@ -341,6 +342,8 @@ class World(object):
key_list += [dungeon.big_key.name] key_list += [dungeon.big_key.name]
if len(dungeon.small_keys) > 0: if len(dungeon.small_keys) > 0:
key_list += [x.name for x in dungeon.small_keys] key_list += [x.name for x in dungeon.small_keys]
# map/compass may be required now
key_list += [x.name for x in dungeon.dungeon_items]
from Items import ItemFactory from Items import ItemFactory
for item in ItemFactory(key_list, p): for item in ItemFactory(key_list, p):
soft_collect(item) soft_collect(item)
@@ -851,7 +854,7 @@ class CollectionState(object):
reduced = Counter() reduced = Counter()
for item, cnt in self.prog_items.items(): for item, cnt in self.prog_items.items():
item_name, item_player = item item_name, item_player = item
if item_player == player and self.check_if_progressive(item_name): if item_player == player and self.check_if_progressive(item_name, player):
if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles
bottle_count += cnt bottle_count += cnt
elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']:
@@ -867,8 +870,7 @@ class CollectionState(object):
reduced[('Heart Container', player)] = 1 reduced[('Heart Container', player)] = 1
return frozenset(reduced.items()) return frozenset(reduced.items())
@staticmethod def check_if_progressive(self, item_name, player):
def check_if_progressive(item_name):
return (item_name in return (item_name in
['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot',
'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel',
@@ -880,7 +882,8 @@ class CollectionState(object):
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna',
'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)',
'Magic Upgrade (1/4)'] 'Magic Upgrade (1/4)']
or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))
or (self.world.restrict_boss_items[player] != 'none' and item_name.startswith(('Map', 'Compass'))))
def can_reach(self, spot, resolution_hint=None, player=None): def can_reach(self, spot, resolution_hint=None, player=None):
try: try:
@@ -2192,6 +2195,12 @@ class Item(object):
item_dungeon = 'Hyrule Castle' item_dungeon = 'Hyrule Castle'
return item_dungeon return item_dungeon
def is_inside_dungeon_item(self, world):
return ((self.smallkey and not world.keyshuffle[self.player])
or (self.bigkey and not world.bigkeyshuffle[self.player])
or (self.compass and not world.compassshuffle[self.player])
or (self.map and not world.mapshuffle[self.player]))
def __str__(self): def __str__(self):
return str(self.__unicode__()) return str(self.__unicode__())
@@ -2411,6 +2420,7 @@ class Spoiler(object):
'ganon_crystals': self.world.crystals_needed_for_ganon, 'ganon_crystals': self.world.crystals_needed_for_ganon,
'open_pyramid': self.world.open_pyramid, 'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility, 'accessibility': self.world.accessibility,
'restricted_boss_items': self.world.restrict_boss_items,
'hints': self.world.hints, 'hints': self.world.hints,
'mapshuffle': self.world.mapshuffle, 'mapshuffle': self.world.mapshuffle,
'compassshuffle': self.world.compassshuffle, 'compassshuffle': self.world.compassshuffle,
@@ -2426,6 +2436,7 @@ class Spoiler(object):
'experimental': self.world.experimental, 'experimental': self.world.experimental,
'keydropshuffle': self.world.keydropshuffle, 'keydropshuffle': self.world.keydropshuffle,
'shopsanity': self.world.shopsanity, 'shopsanity': self.world.shopsanity,
'pseudoboots': self.world.pseudoboots,
'triforcegoal': self.world.treasure_hunt_count, 'triforcegoal': self.world.treasure_hunt_count,
'triforcepool': self.world.treasure_hunt_total, 'triforcepool': self.world.treasure_hunt_total,
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}
@@ -2453,6 +2464,9 @@ class Spoiler(object):
return json.dumps(out) return json.dumps(out)
def to_file(self, filename): def to_file(self, filename):
def yn(flag):
return 'Yes' if flag else 'No'
self.parse_data() self.parse_data()
with open(filename, 'w') as outfile: with open(filename, 'w') as outfile:
outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
@@ -2477,7 +2491,7 @@ class Spoiler(object):
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
outfile.write(f"Link's House Shuffled: {'Yes' if self.metadata['shufflelinks'][player] else 'No'}\n") outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n")
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) outfile.write('Intensity: %s\n' % self.metadata['intensity'][player])
addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else ''
@@ -2486,6 +2500,7 @@ class Spoiler(object):
outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition))
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n")
outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No'))
outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No'))
@@ -2494,12 +2509,13 @@ class Spoiler(object):
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player])
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write('Pot shuffle: %s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) outfile.write(f"Pot shuffle: {yn(self.metadata['potshuffle'][player])}\n")
outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n")
outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n")
outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) outfile.write(f"Key Drops shuffled: {yn(self.metadata['keydropshuffle'][player])}\n")
outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n")
outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n")
outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n")
if self.doors: if self.doors:
outfile.write('\n\nDoors:\n\n') outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join( outfile.write('\n'.join(
@@ -2681,6 +2697,16 @@ enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3}
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}
e_dmg = {"default": 0, "shuffled": 1, "random": 2} e_dmg = {"default": 0, "shuffled": 1, "random": 2}
# byte 8: RRAA A??? (restrict boss mode, algorithm, ? = unused)
rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2}
# algorithm: todo with "biased shuffles"
algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4}
# additions
# psuedoboots does not effect code
# sfx_shuffle and other adjust items does not effect settings code
class Settings(object): class Settings(object):
@staticmethod @staticmethod
@@ -2709,7 +2735,9 @@ class Settings(object):
| (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), | (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]),
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0)]) | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0),
(rb_mode[w.restrict_boss_items[p]] << 6)])
return base64.b64encode(code, "+-".encode()).decode() return base64.b64encode(code, "+-".encode()).decode()
@staticmethod @staticmethod
@@ -2755,6 +2783,8 @@ class Settings(object):
args.shufflepots[p] = True if settings[7] & 0x4 else False args.shufflepots[p] = True if settings[7] & 0x4 else False
args.bombbag[p] = True if settings[7] & 0x2 else False args.bombbag[p] = True if settings[7] & 0x2 else False
args.shufflelinks[p] = True if settings[7] & 0x1 else False args.shufflelinks[p] = True if settings[7] & 0x1 else False
if len(settings) > 8:
args.restrict_boss_items[p] = True if r(rb_mode)[(settings[8] & 0x80) >> 6] else False
class KeyRuleType(FastEnum): class KeyRuleType(FastEnum):

3
CLI.py
View File

@@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'bombbag', 'overworld_map', 'bombbag', 'overworld_map', 'restrict_boss_items',
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
@@ -140,6 +140,7 @@ def parse_settings():
"progressive": "on", "progressive": "on",
"accessibility": "items", "accessibility": "items",
"algorithm": "balanced", "algorithm": "balanced",
"restrict_boss_items": "none",
# Shuffle Ganon defaults to TRUE # Shuffle Ganon defaults to TRUE
"openpyramid": False, "openpyramid": False,

View File

@@ -218,7 +218,8 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro
return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS' return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS'
original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions, original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions,
valid_doors, bk_flag, world, player, exception) valid_doors, bk_flag, world, player, exception)
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception) dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception,
world, player)
either_crystal = True # if all hooks from the origin are either, explore all bits with either either_crystal = True # if all hooks from the origin are either, explore all bits with either
for hook, crystal in dungeon['Origin'].hooks.items(): for hook, crystal in dungeon['Origin'].hooks.items():
if crystal != CrystalBarrier.Either: if crystal != CrystalBarrier.Either:
@@ -239,7 +240,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro
o_state = extend_reachable_state_improved([parent], init_state, proposed_map, all_regions, o_state = extend_reachable_state_improved([parent], init_state, proposed_map, all_regions,
valid_doors, bk_flag, world, player, exception) valid_doors, bk_flag, world, player, exception)
o_state_cache[door.name] = o_state o_state_cache[door.name] = o_state
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception) piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception, world, player)
dungeon[door.name] = piece dungeon[door.name] = piece
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors, check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors,
group_flags, door_map, world, player, exception) group_flags, door_map, world, player, exception)
@@ -339,7 +340,7 @@ def explore_blue_state(door, dungeon, o_state, proposed_map, all_regions, valid_
blue_start.big_key_special = o_state.big_key_special blue_start.big_key_special = o_state.big_key_special
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, all_regions, valid_doors, bk_flag, b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, all_regions, valid_doors, bk_flag,
world, player, exception) world, player, exception)
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception) dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player)
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name): def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name):
@@ -603,7 +604,7 @@ def winnow_hangers(hangers, hooks):
hangers[hanger].remove(door) hangers[hanger].remove(door)
def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception): def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player):
# todo: info about dungeon events - not sure about that # todo: info about dungeon events - not sure about that
graph_piece = GraphPiece() graph_piece = GraphPiece()
all_unattached = {} all_unattached = {}
@@ -635,16 +636,15 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio
graph_piece.visited_regions.update(o_state.visited_orange) graph_piece.visited_regions.update(o_state.visited_orange)
graph_piece.visited_regions.update(b_state.visited_blue) graph_piece.visited_regions.update(b_state.visited_blue)
graph_piece.visited_regions.update(b_state.visited_orange) graph_piece.visited_regions.update(b_state.visited_orange)
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found)) graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found, world, player))
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found)) graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found, world, player))
graph_piece.pinball_used = o_state.pinball_used or b_state.pinball_used graph_piece.pinball_used = o_state.pinball_used or b_state.pinball_used
return graph_piece return graph_piece
def filter_for_potential_bk_locations(locations): def filter_for_potential_bk_locations(locations, world, player):
return [x for x in locations if return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and
'- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)]
and not x.forced_item and x.name not in ['Agahnim 1', 'Agahnim 2']]
type_map = { type_map = {
@@ -987,12 +987,8 @@ class ExplorationState(object):
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
return True return True
def count_locations_exclude_specials(self): def count_locations_exclude_specials(self, world, player):
cnt = 0 return count_locations_exclude_big_chest(self.found_locations, world, player)
for loc in self.found_locations:
if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and not loc.forced_item:
cnt += 1
return cnt
def validate(self, door, region, world, player): def validate(self, door, region, world, player):
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon, return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
@@ -1033,6 +1029,32 @@ class ExplorationState(object):
return 2 return 2
def count_locations_exclude_big_chest(locations, world, player):
cnt = 0
for loc in locations:
if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player)
and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)):
cnt += 1
return cnt
def prize_or_event(loc):
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
def reserved_location(loc, world, player):
return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player]
def blind_boss_unavail(loc, locations, world, player):
if loc.name == "Thieves' Town - Boss":
return (loc.parent_region.dungeon.boss.name == 'Blind' and
(not any(x for x in locations if x.name == 'Suspicious Maiden') or
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
not any(x for x in locations if x.name == 'Attic Cracked Floor'))))
return False
class ExplorableDoor(object): class ExplorableDoor(object):
def __init__(self, door, crystal, flag): def __init__(self, door, crystal, flag):
@@ -1056,7 +1078,8 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg
explorable_door = local_state.next_avail_door() explorable_door = local_state.next_avail_door()
if explorable_door.door.bigKey: if explorable_door.door.bigKey:
if bk_flag: if bk_flag:
big_not_found = not special_big_key_found(local_state) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0 big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special
else local_state.count_locations_exclude_specials(world, player) == 0)
if big_not_found: if big_not_found:
continue # we can't open this door continue # we can't open this door
if explorable_door.door in proposed_map: if explorable_door.door in proposed_map:

View File

@@ -117,6 +117,7 @@ def fill_dungeons(world):
def get_dungeon_item_pool(world): def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items] return [item for dungeon in world.dungeons for item in dungeon.all_items]
def fill_dungeons_restrictive(world, shuffled_locations): def fill_dungeons_restrictive(world, shuffled_locations):
all_state_base = world.get_all_state() all_state_base = world.get_all_state()
@@ -137,10 +138,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
item.priority = True item.priority = True
dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player]) dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)]
or (item.bigkey and not world.bigkeyshuffle[item.player])
or (item.map and not world.mapshuffle[item.player])
or (item.compass and not world.compassshuffle[item.player]))]
# sort in the order Big Key, Small Key, Other before placing dungeon items # sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2} sort_order = {"BigKey": 3, "SmallKey": 2}
@@ -441,7 +439,6 @@ dungeon_bigs = {
'Ganons Tower': 'Big Key (Ganons Tower)' 'Ganons Tower': 'Big Key (Ganons Tower)'
} }
dungeon_hints = { dungeon_hints = {
'Hyrule Castle': 'in Hyrule Castle', 'Hyrule Castle': 'in Hyrule Castle',
'Eastern Palace': 'in Eastern Palace', 'Eastern Palace': 'in Eastern Palace',

15
Fill.py
View File

@@ -237,7 +237,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
def valid_key_placement(item, location, itempool, world): def valid_key_placement(item, location, itempool, world):
if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player] or world.logic[item.player] == 'nologic': if not valid_reserved_placement(item, location, world):
return False
if ((not item.smallkey and not item.bigkey) or item.player != location.player
or world.retro[item.player] or world.logic[item.player] == 'nologic'):
return True return True
dungeon = location.parent_region.dungeon dungeon = location.parent_region.dungeon
if dungeon: if dungeon:
@@ -251,9 +254,13 @@ def valid_key_placement(item, location, itempool, world):
cr_count = world.crystals_needed_for_gt[location.player] cr_count = world.crystals_needed_for_gt[location.player]
return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count)
else: else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this
or (item.bigkey and not world.bigkeyshuffle[item.player]))
return not inside_dungeon_item
def valid_reserved_placement(item, location, world):
if item.player == location.player and item.is_inside_dungeon_item(world):
return location.name not in world.item_pool_config.reserved_locations[location.player]
return True
def track_outside_keys(item, location, world): def track_outside_keys(item, location, world):

View File

@@ -5,7 +5,8 @@ from collections import defaultdict, deque
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
from Regions import dungeon_events from Regions import dungeon_events
from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table
from DungeonGenerator import ExplorationState, special_big_key_doors from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event
from DungeonGenerator import reserved_location, blind_boss_unavail
class KeyLayout(object): class KeyLayout(object):
@@ -1100,40 +1101,30 @@ def location_is_bk_locked(loc, key_logic):
return loc in key_logic.bk_chests or loc in key_logic.bk_locked return loc in key_logic.bk_chests or loc in key_logic.bk_locked
def prize_or_event(loc): # todo: verfiy this code is defunct
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] # def prize_or_event(loc):
# return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
#
def boss_unavail(loc, world, player): #
# todo: ambrosia # def reserved_location(loc, world, player):
# return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name # return loc in world.item_pool.config.reserved_locations[player]
return False #
#
# def blind_boss_unavail(loc, state, world, player):
def blind_boss_unavail(loc, state, world, player): # if loc.name == "Thieves' Town - Boss":
if loc.name == "Thieves' Town - Boss": # return (loc.parent_region.dungeon.boss.name == 'Blind' and
# todo: check attic # (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or
return (loc.parent_region.dungeon.boss.name == 'Blind' and # (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
(not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or # not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor'))))
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and # return False
not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor'))))
return False
# counts free locations for keys - hence why reserved locations don't count
def count_free_locations(state, world, player): def count_free_locations(state, world, player):
cnt = 0 cnt = 0
for loc in state.found_locations: for loc in state.found_locations:
if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player) if (not prize_or_event(loc) and not loc.forced_item and not reserved_location(loc, world, player)
and not blind_boss_unavail(loc, state, world, player)): and not blind_boss_unavail(loc, state.found_locations, world, player)):
cnt += 1
return cnt
def count_locations_exclude_big_chest(state, world, player):
cnt = 0
for loc in state.found_locations:
if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player)
and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)):
cnt += 1 cnt += 1
return cnt return cnt
@@ -1437,7 +1428,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
if state.big_key_opened: if state.big_key_opened:
ttl_locations = count_free_locations(state, world, player) ttl_locations = count_free_locations(state, world, player)
else: else:
ttl_locations = count_locations_exclude_big_chest(state, world, player) ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
ttl_small_key_only = count_small_key_only_locations(state) ttl_small_key_only = count_small_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
@@ -1663,7 +1654,7 @@ def can_open_door(door, state, world, player):
if state.big_key_opened: if state.big_key_opened:
ttl_locations = count_free_locations(state, world, player) ttl_locations = count_free_locations(state, world, player)
else: else:
ttl_locations = count_locations_exclude_big_chest(state, world, player) ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
if door.smallKey: if door.smallKey:
ttl_small_key_only = count_small_key_only_locations(state) ttl_small_key_only = count_small_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)

View File

@@ -29,6 +29,8 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
from source.item.FillUtil import create_item_pool_config
__version__ = '1.0.1.0-v' __version__ = '1.0.1.0-v'
from source.classes.BabelFish import BabelFish from source.classes.BabelFish import BabelFish
@@ -105,6 +107,7 @@ def main(args, seed=None, fish=None):
world.shufflelinks = args.shufflelinks.copy() world.shufflelinks = args.shufflelinks.copy()
world.pseudoboots = args.pseudoboots.copy() world.pseudoboots = args.pseudoboots.copy()
world.overworld_map = args.overworld_map.copy() world.overworld_map = args.overworld_map.copy()
world.restrict_boss_items = args.restrict_boss_items.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@@ -149,6 +152,7 @@ def main(args, seed=None, fish=None):
create_dungeons(world, player) create_dungeons(world, player)
adjust_locations(world, player) adjust_locations(world, player)
place_bosses(world, player) place_bosses(world, player)
create_item_pool_config(world)
if any(world.potshuffle.values()): if any(world.potshuffle.values()):
logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))

View File

@@ -134,6 +134,7 @@ def roll_settings(weights):
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full'] ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
ret.accessibility = get_choice('accessibility') ret.accessibility = get_choice('accessibility')
ret.restrict_boss_items = get_choice('restrict_boss_items')
entrance_shuffle = get_choice('entrance_shuffle') entrance_shuffle = get_choice('entrance_shuffle')
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'

View File

@@ -4,6 +4,7 @@ from collections import deque
import OverworldGlitchRules import OverworldGlitchRules
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType
from Dungeons import dungeon_table
from RoomData import DoorKind from RoomData import DoorKind
from OverworldGlitchRules import overworld_glitches_rules from OverworldGlitchRules import overworld_glitches_rules
@@ -557,11 +558,29 @@ def global_rules(world, player):
add_key_logic_rules(world, player) add_key_logic_rules(world, player)
# End of door rando rules. # End of door rando rules.
if world.restrict_boss_items[player] != 'none':
def add_mc_rule(l):
boss_location = world.get_location(l, player)
d_name = boss_location.parent_region.dungeon.name
compass_name = f'Compass ({d_name})'
map_name = f'Map ({d_name})'
add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player))
for dungeon, info in dungeon_table.items():
if info.prize:
d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon
for loc in [info.prize, f'{d_name} - Boss']:
add_mc_rule(loc)
if world.doorShuffle[player] == 'crossed':
add_mc_rule('Agahnim 1')
add_mc_rule('Agahnim 2')
add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player) set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player)
and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop
def bomb_rules(world, player): def bomb_rules(world, player):
bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended. bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended.
bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave', bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave',

View File

@@ -254,6 +254,13 @@
"none" "none"
] ]
}, },
"restrict_boss_items": {
"choices": [
"none",
"mapcompass",
"dungeon"
]
},
"hints": { "hints": {
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"

View File

@@ -276,6 +276,13 @@
"Locations: You will be able to reach every location in the game.", "Locations: You will be able to reach every location in the game.",
"None: You will be able to reach enough locations to beat the game." "None: You will be able to reach enough locations to beat the game."
], ],
"restrict_boss_items": [
"Select which dungeon are not allowed on bosses (default: %(default)s)",
"None: All items allowed",
"Mapcompass: Map and Compass are required before you defeat the boss.",
"Dungeon: Same as above and keys too cannot be on the boss. Small key shuffle",
" and big key shuffle override this behavior"
],
"hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ], "hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ],
"shuffleganon": [ "shuffleganon": [
"Include the Ganon's Tower and Pyramid Hole in the", "Include the Ganon's Tower and Pyramid Hole in the",

View File

@@ -291,6 +291,10 @@
"randomizer.item.sortingalgo.vt26": "VT8.26", "randomizer.item.sortingalgo.vt26": "VT8.26",
"randomizer.item.sortingalgo.balanced": "Balanced", "randomizer.item.sortingalgo.balanced": "Balanced",
"randomizer.item.restrict_boss_items": "Forbidden Boss Items",
"randomizer.item.restrict_boss_items.none": "None",
"randomizer.item.restrict_boss_items.mapcompass": "Map & Compass",
"randomizer.item.restrict_boss_items.dungeon": "Map & Compass & Keys",
"bottom.content.worlds": "Worlds", "bottom.content.worlds": "Worlds",
"bottom.content.names": "Player names", "bottom.content.names": "Player names",

View File

@@ -124,6 +124,15 @@
"vt26", "vt26",
"balanced" "balanced"
] ]
},
"restrict_boss_items": {
"type": "selectbox",
"default": "none",
"options": [
"none",
"mapcompass",
"dungeon"
]
} }
} }
} }

View File

@@ -72,7 +72,8 @@ SETTINGSTOPROCESS = {
"progressives": "progressive", "progressives": "progressive",
"accessibility": "accessibility", "accessibility": "accessibility",
"sortingalgo": "algorithm", "sortingalgo": "algorithm",
"beemizer": "beemizer" "beemizer": "beemizer",
"restrict_boss_items": "restrict_boss_items"
}, },
"entrance": { "entrance": {
"openpyramid": "openpyramid", "openpyramid": "openpyramid",

26
source/item/FillUtil.py Normal file
View File

@@ -0,0 +1,26 @@
from collections import defaultdict
from Dungeons import dungeon_table
class ItemPoolConfig(object):
def __init__(self):
self.reserved_locations = defaultdict(set)
def create_item_pool_config(world):
world.item_pool_config = config = ItemPoolConfig()
player_set = set()
for player in range(1, world.players+1):
if world.restrict_boss_items[player] != 'none':
player_set.add(player)
if world.restrict_boss_items[player] == 'dungeon':
for dungeon, info in dungeon_table.items():
if info.prize:
d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon
config.reserved_locations[player].add(f'{d_name} - Boss')
for dungeon in world.dungeons:
if world.restrict_boss_items[dungeon.player] != 'none':
for item in dungeon.all_items:
if item.map or item.compass:
item.advancement = True