Boss item restriction logic added

Reserved location logic started
This commit is contained in:
aerinon
2021-08-20 14:32:54 -06:00
parent b2a6e4a7d4
commit f259e8bdc8
15 changed files with 181 additions and 68 deletions

View File

@@ -114,6 +114,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')
@@ -324,7 +325,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:
@@ -339,6 +340,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)
@@ -2178,6 +2181,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__())
@@ -2388,6 +2397,7 @@ class Spoiler(object):
'weapons': self.world.swords, 'weapons': self.world.swords,
'goal': self.world.goal, 'goal': self.world.goal,
'shuffle': self.world.shuffle, 'shuffle': self.world.shuffle,
'linkshuffle': self.world.shufflelinks,
'door_shuffle': self.world.doorShuffle, 'door_shuffle': self.world.doorShuffle,
'intensity': self.world.intensity, 'intensity': self.world.intensity,
'item_pool': self.world.difficulty, 'item_pool': self.world.difficulty,
@@ -2396,6 +2406,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,
@@ -2411,6 +2422,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,
'psuedoboots': 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)}
@@ -2438,6 +2450,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))
@@ -2462,6 +2477,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"Links House Shuffled: {self.metadata['linkshuffle'][player]}\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 ''
@@ -2470,6 +2486,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'))
@@ -2478,12 +2495,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['bombbag'][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(
@@ -2665,6 +2683,12 @@ 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}
# additions
# shuffle links: 1 bit
# restrict_boss_mode: 2 bits
# psuedoboots does not effect code
# sfx_shuffle and other adjust items does not effect settings code
class Settings(object): class Settings(object):
@staticmethod @staticmethod
@@ -2738,7 +2762,6 @@ class Settings(object):
args.shufflepots[p] = True if settings[7] & 0x4 else False args.shufflepots[p] = True if settings[7] & 0x4 else False
@unique
class KeyRuleType(FastEnum): class KeyRuleType(FastEnum):
WorstCase = 0 WorstCase = 0
AllowSmall = 1 AllowSmall = 1

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', 'bombbag', '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

@@ -226,7 +226,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:
@@ -247,7 +248,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)
@@ -347,7 +348,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):
@@ -639,7 +640,7 @@ def stonewall_valid(stonewall):
return True return True
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 = {}
@@ -671,16 +672,14 @@ 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 count_locations_exclude_big_chest(locations, world, player)
'- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events
and not x.forced_item and x.name not in ['Agahnim 1', 'Agahnim 2']]
type_map = { type_map = {
@@ -1023,12 +1022,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,
@@ -1069,6 +1064,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 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):
@@ -1092,7 +1113,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}
@@ -414,7 +412,7 @@ dungeon_prize = {
'Palace of Darkness': 'Palace of Darkness - Prize', 'Palace of Darkness': 'Palace of Darkness - Prize',
'Swamp Palace': 'Swamp Palace - Prize', 'Swamp Palace': 'Swamp Palace - Prize',
'Skull Woods': 'Skull Woods - Prize', 'Skull Woods': 'Skull Woods - Prize',
'Thieves Town': 'Thieves Town - Prize', 'Thieves Town': "Thieves' Town - Prize",
'Ice Palace': 'Ice Palace - Prize', 'Ice Palace': 'Ice Palace - Prize',
'Misery Mire': 'Misery Mire - Prize', 'Misery Mire': 'Misery Mire - Prize',
'Turtle Rock': 'Turtle Rock - Prize', 'Turtle Rock': 'Turtle Rock - Prize',

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:
@@ -247,9 +250,13 @@ def valid_key_placement(item, location, itempool, world):
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player])
return key_logic.check_placement(unplaced_keys, location if item.bigkey else None) return key_logic.check_placement(unplaced_keys, location if item.bigkey else None)
else: else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) return item.is_inside_dungeon_item(world)
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_prize from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize
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):
@@ -1078,40 +1079,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
@@ -1407,7 +1398,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)
@@ -1596,7 +1587,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

@@ -28,6 +28,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__ = '0.5.1.0-u' __version__ = '0.5.1.0-u'
from source.classes.BabelFish import BabelFish from source.classes.BabelFish import BabelFish
@@ -103,6 +105,7 @@ def main(args, seed=None, fish=None):
world.treasure_hunt_total = args.triforce_pool.copy() world.treasure_hunt_total = args.triforce_pool.copy()
world.shufflelinks = args.shufflelinks.copy() world.shufflelinks = args.shufflelinks.copy()
world.pseudoboots = args.pseudoboots.copy() world.pseudoboots = args.pseudoboots.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)}
@@ -146,6 +149,7 @@ def main(args, seed=None, fish=None):
create_rooms(world, player) create_rooms(world, player)
create_dungeons(world, player) create_dungeons(world, player)
adjust_locations(world, player) adjust_locations(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

@@ -132,6 +132,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_regions, dungeon_prize
from RoomData import DoorKind from RoomData import DoorKind
from OverworldGlitchRules import overworld_glitches_rules from OverworldGlitchRules import overworld_glitches_rules
@@ -553,11 +554,28 @@ 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 in dungeon_prize.keys():
d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon
for loc in [dungeon_prize[dungeon], 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

@@ -287,6 +287,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",

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

@@ -0,0 +1,20 @@
from collections import defaultdict
from Dungeons import dungeon_prize
class ItemPoolConfig(object):
def __init__(self):
self.reserved_locations = defaultdict(set)
def create_item_pool_config(world):
config = ItemPoolConfig()
if world.algorithm in ['balanced']:
for player in range(1, world.players+1):
if world.restrict_boss_items[player]:
for dungeon in dungeon_prize:
if dungeon.startswith('Thieves'):
dungeon = "Thieves' Town"
config.reserved_locations[player].add(f'{dungeon} - Boss')
world.item_pool_config = config