Implement new weapons modes

This also includes some partial additional cleanup of the item pool.
This commit is contained in:
Kevin Cathcart 2019-08-10 15:30:14 -04:00
parent b8ea2eb4b1
commit 996bf8495c
5 changed files with 73 additions and 47 deletions

View File

@ -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]

View File

@ -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()

View File

@ -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):

12
Rom.py
View File

@ -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)

View File

@ -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)