make "universal" small key shuffle a thing and split it out of retro

also make retro usable independently from the other world modes in mystery
This commit is contained in:
Fabian Dill 2020-08-20 20:13:00 +02:00
parent bea54d91de
commit 685ff49711
10 changed files with 101 additions and 53 deletions

View File

@ -516,17 +516,13 @@ class CollectionState(object):
def has_key(self, item, player, count: int = 1):
if self.world.logic[player] == 'nologic':
return True
if self.world.retro[player]:
if self.world.keyshuffle[player] == "universal":
return self.can_buy_unlimited('Small Key (Universal)', player)
if count == 1:
return (item, player) in self.prog_items
return self.prog_items[item, player] >= count
def can_buy_unlimited(self, item: str, player: int) -> bool:
for shop in self.world.shops:
if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self):
return True
return False
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
shop in self.world.shops)
def item_count(self, item, player: int) -> int:
return self.prog_items[item, player]
@ -619,9 +615,10 @@ class CollectionState(object):
)
def has_sword(self, player: int) -> bool:
return self.has('Fighter Sword', player) or self.has('Master Sword', player) or self.has('Tempered Sword',
player) or self.has(
'Golden Sword', player)
return self.has('Fighter Sword', player) \
or self.has('Master Sword', player) \
or self.has('Tempered Sword', player) \
or self.has('Golden Sword', player)
def has_beam_sword(self, player: int) -> bool:
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
@ -1267,7 +1264,9 @@ class Spoiler(object):
def to_file(self, filename):
self.parse_data()
def bool_to_text(variable: bool) -> str:
def bool_to_text(variable: Union[bool, str]) -> str:
if type(variable) == str:
return variable
return 'Yes' if variable else 'No'
with open(filename, 'w', encoding="utf-8-sig") as outfile:
@ -1312,7 +1311,7 @@ class Spoiler(object):
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'))
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
outfile.write('Big Key shuffle: %s\n' % (
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])

View File

@ -6,7 +6,8 @@ from Items import ItemFactory
def create_dungeons(world, player):
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys, dungeon_items, player)
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == "universal" else small_keys,
dungeon_items, player)
dungeon.boss = BossFactory(default_boss, player)
for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon

View File

@ -231,17 +231,27 @@ def parse_arguments(argv, no_defaults=False):
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--mapshuffle', default=defval(False), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--mapshuffle', default=defval(False),
help='Maps are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--compassshuffle', default=defval(False),
help='Compasses are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--keyshuffle', default=defval("off"), help='\
on: Small Keys are no longer restricted to their dungeons, but can be anywhere.\
universal: Makes all Small Keys usable in any dungeon and places shops to buy more keys.',
choices=["on", "universal", "off"])
parser.add_argument('--bigkeyshuffle', default=defval(False),
help='Big Keys are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(False), help='''\
Keys are universal, shooting arrows costs rupees,
@ -326,9 +336,13 @@ def parse_arguments(argv, no_defaults=False):
ret.dungeon_counters = True
elif ret.dungeon_counters == 'off':
ret.dungeon_counters = False
if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
if ret.keysanity:
ret.mapshuffle = ret.compassshuffle = ret.keyshuffle = ret.bigkeyshuffle = True
elif ret.keyshuffle == "on":
ret.keyshuffle = True
elif ret.keyshuffle == "off":
ret.keyshuffle = False
if multiargs.multi:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):

View File

@ -203,7 +203,13 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
continue
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid')
placements = []
for region in world.regions:
for location in region.locations:
if location.item and not location.event:
placements.append(location)
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
f'\nAlready placed {len(placements)}: {", ".join(placements)}')
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)

21
Gui.py
View File

@ -67,16 +67,27 @@ def guiMain(args=None):
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
mcsbshuffleFrame = Frame(checkBoxFrame)
mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ")
mapshuffleVar = IntVar()
mapshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Maps", variable=mapshuffleVar)
compassshuffleVar = IntVar()
compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar)
keyshuffleVar = IntVar()
keyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Keys", variable=keyshuffleVar)
bigkeyshuffleVar = IntVar()
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
keyshuffleFrame = Frame(checkBoxFrame)
keyshuffleVar = StringVar()
keyshuffleVar.set('off')
modeOptionMenu = OptionMenu(keyshuffleFrame, keyshuffleVar, 'off', 'universal', 'on')
modeOptionMenu.pack(side=LEFT)
modeLabel = Label(keyshuffleFrame, text='Key Shuffle')
modeLabel.pack(side=LEFT)
retroVar = IntVar()
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode", variable=retroVar)
shuffleGanonVar = IntVar()
shuffleGanonVar.set(1) # set default
shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool",
@ -99,8 +110,8 @@ def guiMain(args=None):
mcsbLabel.grid(row=0, column=0)
mapshuffleCheckbutton.grid(row=0, column=1)
compassshuffleCheckbutton.grid(row=0, column=2)
keyshuffleCheckbutton.grid(row=0, column=3)
bigkeyshuffleCheckbutton.grid(row=0, column=4)
keyshuffleFrame.pack(expand=True, anchor=W)
retroCheckbutton.pack(expand=True, anchor=W)
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
hintsCheckbutton.pack(expand=True, anchor=W)
@ -476,7 +487,7 @@ def guiMain(args=None):
guiargs.openpyramid = bool(openpyramidVar.get())
guiargs.mapshuffle = bool(mapshuffleVar.get())
guiargs.compassshuffle = bool(compassshuffleVar.get())
guiargs.keyshuffle = bool(keyshuffleVar.get())
guiargs.keyshuffle = {"on": True, "universal": "universal", "off": False}[keyshuffleVar.get()]
guiargs.bigkeyshuffle = bool(bigkeyshuffleVar.get())
guiargs.retro = bool(retroVar.get())
guiargs.quickswap = bool(quickSwapVar.get())

View File

@ -43,7 +43,7 @@ Difficulty = namedtuple('Difficulty',
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'progressivemagic', 'basicmagic',
'progressivesword', 'basicsword', 'progressivebow', 'basicbow', 'timedohko', 'timedother',
'triforcehunt', 'retro',
'triforcehunt', 'universal_keys',
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
'progressive_armor_limit', 'progressive_bottle_limit',
'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit'])
@ -70,7 +70,7 @@ difficulties = {
timedohko=['Green Clock'] * 25,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
triforcehunt=['Triforce Piece'] * 30,
retro=['Small Key (Universal)'] * 28,
universal_keys=['Small Key (Universal)'] * 28,
extras=[easyfirst15extra, easysecond15extra, easythird10extra, easyfourth5extra, easyfinal25extra],
progressive_sword_limit=8,
progressive_shield_limit=6,
@ -99,7 +99,7 @@ difficulties = {
timedohko=['Green Clock'] * 25,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
triforcehunt=['Triforce Piece'] * 30,
retro=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
universal_keys=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=4,
progressive_shield_limit=3,
@ -128,7 +128,7 @@ difficulties = {
timedohko=['Green Clock'] * 25,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
triforcehunt=['Triforce Piece'] * 30,
retro=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=3,
progressive_shield_limit=2,
@ -158,7 +158,7 @@ difficulties = {
timedohko=['Green Clock'] * 20 + ['Red Clock'] * 5,
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
triforcehunt=['Triforce Piece'] * 30,
retro=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit=2,
progressive_shield_limit=1,
@ -425,21 +425,26 @@ def fill_prizes(world, attempts=15):
raise FillError('Unable to place dungeon prizes')
def set_up_shops(world, player):
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops
def set_up_shops(world, player: int):
# TODO: move hard+ mode changes for shields here, utilizing the new shops
if world.retro[player]:
rss = world.get_region('Red Shield Shop', player).shop
if not rss.locked:
rss.add_inventory(2, 'Single Arrow', 80)
rss.locked = True
if world.keyshuffle[player] == "universal":
for shop in world.random.sample([s for s in world.shops if
s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player],
5):
shop.locked = True
if world.retro[player]:
shop.add_inventory(0, 'Single Arrow', 80)
else:
shop.add_inventory(0, "Red Potion", 150)
shop.add_inventory(1, 'Small Key (Universal)', 100)
shop.add_inventory(2, 'Bombs (10)', 50)
rss.locked = True
def get_pool_core(world, player: int):
@ -592,7 +597,8 @@ def get_pool_core(world, player: int):
pool = [item.replace('Arrows (10)', 'Rupees (5)') for item in pool]
pool = [item.replace('Arrow Upgrade (+5)', 'Rupees (5)') for item in pool]
pool = [item.replace('Arrow Upgrade (+10)', 'Rupees (5)') for item in pool]
pool.extend(diff.retro)
if world.keyshuffle[player] == "universal":
pool.extend(diff.universal_keys)
if mode == 'standard':
key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
@ -609,7 +615,6 @@ def make_custom_item_pool(world, player):
timer = world.timer[player]
goal = world.goal[player]
mode = world.mode[player]
retro = world.retro[player]
customitemarray = world.customitemarray[player]
pool = []
@ -726,7 +731,7 @@ def make_custom_item_pool(world, player):
itemtotal = itemtotal + 1
if mode == 'standard':
if retro:
if world.keyshuffle == "universal":
key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
@ -749,9 +754,10 @@ def make_custom_item_pool(world, player):
pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28])
if retro:
if world.keyshuffle == "universal":
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
logging.warning(f"Pool was filled up with {total_items_to_place - itemtotal} Nothing's for player {player}")
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon)

View File

@ -278,7 +278,8 @@ def roll_settings(weights):
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
ret.keyshuffle = get_choice('smallkey_shuffle', weights, 's' in dungeon_items)
ret.keyshuffle = get_choice('smallkey_shuffle', weights,
'universal' if 'u' in dungeon_items else 's' in dungeon_items)
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
ret.accessibility = get_choice('accessibility', weights)
@ -309,10 +310,13 @@ def roll_settings(weights):
ret.triforce_pieces_required = get_choice('triforce_pieces_required', weights, 20)
ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 90)
ret.mode = get_choice('world_state', weights)
ret.mode = get_choice('world_state', weights, None) # legacy support
if ret.mode == 'retro':
ret.mode = 'open'
ret.retro = True
elif ret.mode is None:
ret.mode = get_choice("mode", weights)
ret.retro = get_choice("retro", weights)
ret.hints = get_choice('hints', weights)

2
Rom.py
View File

@ -1237,7 +1237,7 @@ def patch_rom(world, rom, player, team, enemized):
write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal
write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle[player] else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.retro[player] else 0x00) # universal keys
rom.write_byte(0x180172, int(world.keyshuffle == "universal")) # universal keys
rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost

View File

@ -206,8 +206,9 @@ def global_rules(world, player):
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Sewers Door', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player) or (world.retro[player] and world.mode[
player] == 'standard')) # standard retro cannot access the shop
lambda state: state.has_key('Small Key (Hyrule Castle)', player) or (
world.keyshuffle[player] == "universal" and world.mode[
player] == 'standard')) # standard universal small keys cannot access the shop
set_rule(world.get_entrance('Sewers Back Door', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
set_rule(world.get_entrance('Agahnim 1', player),
@ -896,7 +897,7 @@ def set_trock_key_rules(world, player):
return 4
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not world.keyshuffle[player] and not world.retro[player]:
if not can_reach_front and not world.keyshuffle[player]:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest:
@ -905,7 +906,8 @@ def set_trock_key_rules(world, player):
if world.accessibility[player] == 'locations':
if world.bigkeyshuffle[player] and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
else:
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works

View File

@ -38,6 +38,7 @@ compass_shuffle: # Shuffle compasses into the world and other dungeons, includin
off: 1
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
on: 0
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
off: 1
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
on: 0
@ -50,6 +51,8 @@ dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does n
none: 1 # Shuffle none of the 4
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
ub: 0 # universal small keys and shuffled big keys
# you can add more combos of these letters here
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 1 # Show when compass is picked up
@ -119,11 +122,13 @@ ganon_open: # Crystals required to hurt Ganon
'6': 2
'7': 1
random: 0
world_state:
mode:
standard: 1 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
open: 1 # Begin the game from your choice of Link's House or the Sanctuary
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
retro: 0 # Small keys are universal, you must buy a quiver, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
retro:
on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
off: 1
hints:
'on': 1 # Hint tiles sometimes give item location hints
'off': 0 # Hint tiles provide gameplay tips