From 996bf8495c6373e1c36f830fa05dfab5403ed166 Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Sat, 10 Aug 2019 15:30:14 -0400 Subject: [PATCH] Implement new weapons modes This also includes some partial additional cleanup of the item pool. --- BaseClasses.py | 9 ++++- ItemList.py | 97 ++++++++++++++++++++++++++------------------------ Main.py | 1 + Rom.py | 12 +++++++ Rules.py | 1 + 5 files changed, 73 insertions(+), 47 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6e2335d4..cdee826a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3,7 +3,7 @@ from enum import Enum, unique import logging import json from collections import OrderedDict -from _vendor.collections_extended import bag, setlist +from _vendor.collections_extended import bag from Utils import int16_as_bytes class World(object): @@ -24,6 +24,7 @@ class World(object): self.shops = [] self.itempool = [] self.seed = None + self.precollected_items = [] self.state = CollectionState(self) self.required_medallions = dict([(player, ['Ether', 'Quake']) for player in range(1, players + 1)]) self._cached_entrances = None @@ -195,6 +196,10 @@ class World(object): def find_items(self, item, player): return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player] + def push_precollected(self, item): + self.precollected_items.append(item) + self.state.collect(item, True) + def push_item(self, location, item, collect=True): if not isinstance(location, Location): raise RuntimeError('Cannot assign item %s to location %s (player %d).' % (item, location, item.player)) @@ -302,6 +307,8 @@ class CollectionState(object): self.path = {} self.locations_checked = set() self.stale = {player: True for player in range(1, parent.players + 1)} + for item in parent.precollected_items: + self.collect(item, True) def update_reachable_regions(self, player): player_regions = [region for region in self.world.regions if region.player == player] diff --git a/ItemList.py b/ItemList.py index 205a41af..f72d2179 100644 --- a/ItemList.py +++ b/ItemList.py @@ -21,43 +21,24 @@ basicgloves = ['Power Glove', 'Titans Mitts'] normalbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)'] hardbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Bee)', 'Bottle (Good Bee)'] -normalbaseitems = (['Silver Arrows', 'Magic Upgrade (1/2)', 'Single Arrow', 'Sanctuary Heart Container', 'Arrows (10)', 'Bombs (3)'] + +normalbaseitems = (['Silver Arrows', 'Magic Upgrade (1/2)', 'Single Arrow', 'Sanctuary Heart Container', 'Arrows (10)', 'Bombs (10)'] + ['Rupees (300)'] * 4 + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 24) normalfirst15extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Arrows (10)'] * 6 + ['Bombs (3)'] * 6 -normalsecond15extra = ['Bombs (3)'] * 9 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)'] + ['Bombs (10)'] +normalsecond15extra = ['Bombs (3)'] * 10 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)'] normalthird10extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 3 + ['Arrows (10)', 'Rupee (1)', 'Rupees (5)'] normalfourth5extra = ['Arrows (10)'] * 2 + ['Rupees (20)'] * 2 + ['Rupees (5)'] normalfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2 -hardbaseitems = ['Silver Arrows', 'Single Arrow', 'Bombs (10)'] + ['Rupees (300)'] * 4 + ['Boss Heart Container'] * 6 + ['Piece of Heart'] * 20 + ['Rupees (5)'] * 7 + ['Bombs (3)'] * 4 -hardfirst20extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Bombs (3)'] * 5 + ['Rupees (5)'] * 10 + ['Arrows (10)', 'Rupee (1)'] -hardsecond10extra = ['Rupees (5)'] * 5 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)'] -hardthird10extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 3 + ['Rupees (5)'] * 3 -hardfourth10extra = ['Arrows (10)'] * 2 + ['Rupees (20)'] * 7 + ['Rupees (5)'] -hardfinal20extra = ['Rupees (20)'] * 18 + ['Rupees (5)'] * 2 - -expertbaseitems = (['Rupees (300)'] * 4 + ['Single Arrow', 'Silver Arrows', 'Boss Heart Container', 'Rupee (1)', 'Bombs (10)'] + ['Piece of Heart'] * 20 + ['Rupees (5)'] * 2 + - ['Bombs (3)'] * 9 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupees (20)'] * 2) -expertfirst15extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Rupees (5)'] * 12 -expertsecond15extra = ['Rupees (5)'] * 10 + ['Rupees (20)'] * 5 -expertthird10extra = ['Rupees (50)'] * 4 + ['Rupees (5)'] * 2 + ['Arrows (10)'] * 3 + ['Rupee (1)'] -expertfourth5extra = ['Rupees (5)'] * 5 -expertfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2 - Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'progressivesword', 'basicsword', 'timedohko', 'timedother', - 'triforcehunt', 'triforce_pieces_required', 'retro', 'conditional_extras', + 'triforcehunt', 'triforce_pieces_required', 'retro', 'extras', 'progressive_sword_limit', 'progressive_shield_limit', 'progressive_armor_limit', 'progressive_bottle_limit']) total_items_to_place = 153 -def no_conditional_extras(*_args): - return [] - - difficulties = { 'normal': Difficulty( baseitems = normalbaseitems, @@ -76,7 +57,6 @@ difficulties = { triforcehunt = ['Triforce Piece'] * 30, triforce_pieces_required = 20, retro = ['Small Key (Universal)'] * 17 + ['Rupees (20)'] * 10, - conditional_extras = no_conditional_extras, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 4, progressive_shield_limit = 3, @@ -84,7 +64,7 @@ difficulties = { progressive_bottle_limit = 4, ), 'hard': Difficulty( - baseitems = hardbaseitems, + baseitems = normalbaseitems, bottles = hardbottles, bottle_count = 4, same_bottle = False, @@ -95,27 +75,26 @@ difficulties = { swordless = ['Rupees (20)'] * 4, progressivesword = ['Progressive Sword'] * 3, basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'], - timedohko = ['Green Clock'] * 20, + timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, triforcehunt = ['Triforce Piece'] * 30, triforce_pieces_required = 20, retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15, - conditional_extras = no_conditional_extras, - extras = [hardfirst20extra, hardsecond10extra, hardthird10extra, hardfourth10extra, hardfinal20extra], + extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 3, progressive_shield_limit = 2, progressive_armor_limit = 1, progressive_bottle_limit = 4, ), 'expert': Difficulty( - baseitems = expertbaseitems, + baseitems = normalbaseitems, bottles = hardbottles, bottle_count = 4, same_bottle = False, progressiveshield = ['Progressive Shield'] * 3, basicshield = ['Progressive Shield'] * 3, #only the first one will upgrade, making this equivalent to two blue shields - progressivearmor = [], - basicarmor = [], + progressivearmor = ['Progressive Armor'] * 2, # neither will count + basicarmor = ['Progressive Armor'] * 2, # neither will count swordless = ['Rupees (20)'] * 4, progressivesword = ['Progressive Sword'] * 3, basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'], @@ -124,8 +103,7 @@ difficulties = { triforcehunt = ['Triforce Piece'] * 30, triforce_pieces_required = 20, retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15, - conditional_extras = no_conditional_extras, - extras = [expertfirst15extra, expertsecond15extra, expertthird10extra, expertfourth5extra, expertfinal25extra], + extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 2, progressive_shield_limit = 1, progressive_armor_limit = 0, @@ -169,11 +147,13 @@ def generate_itempool(world, player): # set up item pool if world.custom: - (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro, world.customitemarray) + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro, world.customitemarray) world.rupoor_cost = min(world.customitemarray[67], 9999) else: - (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro) + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro) world.itempool += ItemFactory(pool, player) + for item in precollected_items: + world.push_precollected(ItemFactory(item, player)) for (location, item) in placed_items: world.push_item(world.get_location(location, player), ItemFactory(item, player), False) world.get_location(location, player).event = True @@ -301,7 +281,7 @@ def fill_prizes(world, attempts=15): random.shuffle(prize_locs) fill_restrictive(world, all_state, prize_locs, prizepool) except FillError as e: - logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times" % (e, attempts)) + logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts) for location in empty_crystal_locations: location.item = None continue @@ -335,6 +315,7 @@ def set_up_shops(world, player): def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro): pool = [] placed_items = [] + precollected_items = [] clock_mode = None treasure_hunt_count = None treasure_hunt_icon = None @@ -387,6 +368,31 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r if swords == 'swordless': pool.extend(diff.swordless) + elif swords == 'assured': + precollected_items.append('Fighter Sword') + if want_progressives(): + pool.extend(diff.progressivesword) + pool.extend(['Rupees (100)']) + else: + pool.extend(diff.basicsword) + pool.extend(['Rupees (100)']) + elif swords == 'vanilla': + swords_to_use = [] + if want_progressives(): + swords_to_use.extend(diff.progressivesword) + swords_to_use.extend(['Progressive Sword']) + else: + swords_to_use.extend(diff.basicsword) + swords_to_use.extend(['Fighter Sword']) + random.shuffle(swords_to_use) + + placed_items.append(('Link\'s Uncle', swords_to_use.pop())) + placed_items.append(('Blacksmith', swords_to_use.pop())) + placed_items.append(('Pyramid Fairy - Left', swords_to_use.pop())) + if goal != 'pedestal': + placed_items.append(('Master Sword Pedestal', swords_to_use.pop())) + else: + placed_items.append(('Master Sword Pedestal', 'Triforce')) else: if want_progressives(): pool.extend(diff.progressivesword) @@ -411,16 +417,12 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r treasure_hunt_count = diff.triforce_pieces_required treasure_hunt_icon = 'Triforce Piece' - cond_extras = diff.conditional_extras(timer, goal, mode, pool, placed_items) - pool.extend(cond_extras) - extraitems -= len(cond_extras) - for extra in diff.extras: if extraitems > 0: pool.extend(extra) extraitems -= len(extra) - if goal == 'pedestal': + if goal == 'pedestal' and swords != 'vanilla': placed_items.append(('Master Sword Pedestal', 'Triforce')) if retro: pool = [item.replace('Single Arrow','Rupees (5)') for item in pool] @@ -433,11 +435,12 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r placed_items.append((key_location, 'Small Key (Universal)')) else: pool.extend(['Small Key (Universal)']) - return (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) + return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray): pool = [] placed_items = [] + precollected_items = [] clock_mode = None treasure_hunt_count = None treasure_hunt_icon = None @@ -561,7 +564,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s pool.extend(['Fighter Sword'] * customitemarray[32]) pool.extend(['Progressive Sword'] * customitemarray[36]) - + if shuffle == 'insanity_legacy': placed_items.append(('Link\'s House', 'Magic Mirror')) placed_items.append(('Sanctuary', 'Moon Pearl')) @@ -576,7 +579,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s if itemtotal < total_items_to_place: pool.extend(['Nothing'] * (total_items_to_place - itemtotal)) - return (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) + return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) # A quick test to ensure all combinations generate the correct amount of items. def test(): @@ -592,13 +595,15 @@ def test(): count = len(out[0]) + len(out[1]) correct_count = total_items_to_place - if goal in ['pedestal']: + if goal == 'pedestal' and swords != 'vanilla': # pedestal goals generate one extra item correct_count += 1 if retro: correct_count += 28 - - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, retro)) + try: + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro)) + except AssertionError as e: + print(e) if __name__ == '__main__': test() diff --git a/Main.py b/Main.py index 24a4ff9c..81096152 100644 --- a/Main.py +++ b/Main.py @@ -247,6 +247,7 @@ def copy_world(world): # copy progress items in state ret.state.prog_items = world.state.prog_items.copy() + ret.precollected_items = world.precollected_items.copy() ret.state.stale = {player: True for player in range(1, world.players + 1)} for player in range(1, world.players + 1): diff --git a/Rom.py b/Rom.py index 3d88033d..95f22fb1 100644 --- a/Rom.py +++ b/Rom.py @@ -858,6 +858,18 @@ def patch_rom(world, player, rom): rom.write_byte(0x18302C, 0x18) # starting max health rom.write_byte(0x18302D, 0x18) # starting current health rom.write_byte(0x183039, 0x68) # starting abilities, bit array + + for item in world.precollected_items: + if item.player != player: + continue + + if item.name == 'Fighter Sword': + rom.write_byte(0x183000+0x19, 0x01) + rom.write_byte(0x0271A6+0x19, 0x01) + rom.write_byte(0x180043, 0x01) # special starting sword byte + else: + raise RuntimeError("Unsupported pre-collected item: {}".format(item)) + rom.write_byte(0x18004A, 0x00 if world.mode != 'inverted' else 0x01) # Inverted mode rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier rom.write_byte(0x2AF79, 0xD0 if world.mode != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both) diff --git a/Rules.py b/Rules.py index 116c4148..b193f4d9 100644 --- a/Rules.py +++ b/Rules.py @@ -957,6 +957,7 @@ def standard_rules(world, player): def uncle_item_rule(item): copy_state = CollectionState(world) copy_state.collect(item) + copy_state.sweep_for_events() return copy_state.can_reach('Sanctuary', 'Region', player) add_item_rule(world.get_location('Link\'s Uncle', player), uncle_item_rule)