shopsanity

This commit is contained in:
Fabian Dill 2020-08-23 21:38:21 +02:00
parent 26ab3dd69a
commit 9cabd41d3b
8 changed files with 175 additions and 80 deletions

View File

@ -5,7 +5,7 @@ from enum import Enum, unique
import logging
import json
from collections import OrderedDict, Counter, deque
from typing import Union, Optional
from typing import Union, Optional, List
import secrets
import random
@ -521,6 +521,10 @@ class CollectionState(object):
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
shop in self.world.shops)
def can_buy(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has(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]
@ -598,7 +602,7 @@ class CollectionState(object):
def can_shoot_arrows(self, player: int) -> bool:
if self.world.retro[player]:
# TODO: Progressive and Non-Progressive silvers work differently (progressive is not usable until the shop arrow is bought)
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy_unlimited('Single Arrow', player)
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player)
return self.has('Bow', player) or self.has('Silver Bow', player)
def can_get_good_bee(self, player: int) -> bool:
@ -1054,11 +1058,12 @@ class ShopType(Enum):
class Shop(object):
slots = 3
def __init__(self, region, room_id, type, shopkeeper_config, custom, locked):
def __init__(self, region, room_id, type, shopkeeper_config, custom, locked: bool):
self.region = region
self.room_id = room_id
self.type = type
self.inventory = [None, None, None]
self.inventory: List[Union[None, dict]] = [None, None, None]
self.shopkeeper_config = shopkeeper_config
self.custom = custom
self.locked = locked
@ -1085,20 +1090,31 @@ class Shop(object):
config |= 0x10 # Alt. VRAM
return [0x00]+int16_as_bytes(self.room_id)+[door_id, 0x00, config, self.shopkeeper_config, 0x00]
def has_unlimited(self, item):
def has_unlimited(self, item: str) -> bool:
for inv in self.inventory:
if inv is None:
continue
if inv['item'] == item:
return True
if inv['max'] != 0 and inv['replacement'] is not None and inv['replacement'] == item:
return True
elif inv['item'] is not None and inv['item'] == item:
return False
def has(self, item: str) -> bool:
for inv in self.inventory:
if inv is None:
continue
if inv['item'] == item:
return True
if inv['max'] != 0 and inv['replacement'] == item:
return True
return False
def clear_inventory(self):
self.inventory = [None, None, None]
def add_inventory(self, slot, item, price, max=0, replacement=None, replacement_price=0, create_location=False):
def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False):
self.inventory[slot] = {
'item': item,
'price': price,
@ -1108,6 +1124,18 @@ class Shop(object):
'create_location': create_location
}
def push_inventory(self, slot: int, item: str, price: int, max: int = 1):
if not self.inventory[slot]:
raise ValueError("Inventory can't be pushed back if it doesn't exist")
self.inventory[slot] = {
'item': item,
'price': price,
'max': max,
'replacement': self.inventory[slot]["item"],
'replacement_price': self.inventory[slot]["price"],
'create_location': self.inventory[slot]["create_location"]
}
class Spoiler(object):
world: World
@ -1243,6 +1271,7 @@ class Spoiler(object):
'progression_balancing': self.world.progression_balancing,
'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required,
'shop_shuffle': self.world.shop_shuffle
}
def to_json(self):
@ -1290,15 +1319,15 @@ class Spoiler(object):
outfile.write('Progression Balanced: %s\n' % (
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write(
'Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if "triforce" in self.metadata["goal"][player]: # triforce hunt
outfile.write(
"Pieces available for Triforce: %s\n" % self.metadata['triforce_pieces_available'][player])
outfile.write(
"Pieces required for Triforce: %s\n" % self.metadata["triforce_pieces_required"][player])
outfile.write("Pieces available for Triforce: %s\n" %
self.metadata['triforce_pieces_available'][player])
outfile.write("Pieces required for Triforce: %s\n" %
self.metadata["triforce_pieces_required"][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 Progression: %s\n' % self.metadata['progressive'][player])
@ -1308,14 +1337,20 @@ class Spoiler(object):
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(
'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('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(
'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('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop price shuffle: %s\n' %
bool_to_text("p" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop upgrade shuffle: %s\n' %
bool_to_text("u" in self.metadata["shop_shuffle"][player]))
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write(
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
@ -1331,7 +1366,11 @@ class Spoiler(object):
'Pot shuffle %s\n' % ('Yes' if self.metadata['shufflepots'][player] else 'No'))
if self.entrances:
outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()]))
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: '
if self.world.players > 1 else '', entry['entrance'],
'<=>' if entry['direction'] == 'both' else
'<=' if entry['direction'] == 'exit' else '=>',
entry['exit']) for entry in self.entrances.values()]))
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')

View File

@ -312,7 +312,12 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--shop_shuffle', default='off', choices=['off', 'inventory', 'price', 'inventory_price'])
parser.add_argument('--shop_shuffle', default='', help='''\
combine letters for options:
i: shuffle the inventories of the shops around
p: randomize the prices of the items in shop inventories
u: shuffle capacity upgrades into the item pool
''')
parser.add_argument('--remote_items', default=defval(False), action='store_true')
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(''))

View File

@ -1061,7 +1061,8 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
else:
raise NotImplementedError(f'{world.shuffle[player]} Shuffling not supported yet. Player {player}')
raise NotImplementedError(
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}')
# check for swamp palace fix
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':

30
Gui.py
View File

@ -75,14 +75,14 @@ def guiMain(args=None):
compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar)
bigkeyshuffleVar = IntVar()
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Big Keys", 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 = Label(keyshuffleFrame, text='Small Key Shuffle')
modeLabel.pack(side=LEFT)
retroVar = IntVar()
@ -447,6 +447,20 @@ def guiMain(args=None):
killable_thievesShuffleButton = Checkbutton(enemizerFrame, text="Killable Thieves", variable=killableThievesVar)
killable_thievesShuffleButton.grid(row=2, column=3, sticky=W)
shopframe = LabelFrame(randomizerWindow, text="Shops", padx=5, pady=2)
shopPriceShuffleVar = IntVar()
shopPriceShuffleButton = Checkbutton(shopframe, text="Random Prices", variable=shopPriceShuffleVar)
shopPriceShuffleButton.grid(row=0, column=0, sticky=W)
shopShuffleVar = IntVar()
shopShuffleButton = Checkbutton(shopframe, text="Shuffle Inventories", variable=shopShuffleVar)
shopShuffleButton.grid(row=0, column=1, sticky=W)
shopUpgradeShuffleVar = IntVar()
shopUpgradeShuffleButton = Checkbutton(shopframe, text="Lootable Upgrades", variable=shopUpgradeShuffleVar)
shopUpgradeShuffleButton.grid(row=0, column=2, sticky=W)
multiworldframe = LabelFrame(randomizerWindow, text="Multiworld", padx=5, pady=2)
worldLabel = Label(multiworldframe, text='Worlds')
@ -521,6 +535,13 @@ def guiMain(args=None):
guiargs.custom = bool(customVar.get())
guiargs.triforce_pieces_required = min(90, int(triforcecountVar.get()))
guiargs.triforce_pieces_available = min(90, int(triforcepieceVar.get()))
guiargs.shop_shuffle = ""
if shopShuffleVar.get():
guiargs.shop_shuffle += "i"
if shopPriceShuffleVar.get():
guiargs.shop_shuffle += "p"
if shopUpgradeShuffleVar.get():
guiargs.shop_shuffle += "u"
guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()),
int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()),
int(magicpowderVar.get()), int(firerodVar.get()),
@ -567,9 +588,9 @@ def guiMain(args=None):
main(seed=guiargs.seed, args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
messagebox.showerror(title="Error while creating multiworld", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
messagebox.showinfo(title="Success", message="Multiworld created successfully")
generateButton = Button(farBottomFrame, text='Generate Patched Rom', command=generateRom)
@ -590,6 +611,7 @@ def guiMain(args=None):
topFrame.pack(side=TOP)
multiworldframe.pack(side=BOTTOM, expand=True, fill=X)
enemizerFrame.pack(side=BOTTOM, fill=BOTH)
shopframe.pack(side=BOTTOM, expand=True, fill=X)
# Adjuster Controls

View File

@ -311,8 +311,6 @@ def generate_itempool(world, player: int):
progressionitems += [ItemFactory("Triforce Piece", player)] * (triforce_pieces - 30)
nonprogressionitems = nonprogressionitems[(triforce_pieces - 30):]
world.itempool += progressionitems + nonprogressionitems
# shuffle medallions
mm_medallion = ['Ether', 'Quake', 'Bombos'][world.random.randint(0, 2)]
tr_medallion = ['Ether', 'Quake', 'Bombos'][world.random.randint(0, 2)]
@ -323,43 +321,64 @@ def generate_itempool(world, player: int):
if world.retro[player]:
set_up_take_anys(world, player)
if world.shop_shuffle[player] != 'off':
shuffle_shops(world, player)
if world.shop_shuffle[player]:
shuffle_shops(world, nonprogressionitems, player)
create_dynamic_shop_locations(world, player)
world.itempool += progressionitems + nonprogressionitems
def shuffle_shops(world, player: int):
def shuffle_shops(world, items, player: int):
option = world.shop_shuffle[player]
shops = []
total_inventory = []
for shop in world.shops:
if shop.type == ShopType.Shop and shop.region.player == player:
shops.append(shop)
total_inventory.extend(shop.inventory)
if 'price' in option:
def price_adjust(price: int) -> int:
# it is important that a base price of 0 always returns 0 as new price!
return int(price * (0.5 + world.random.random() * 2))
for item in total_inventory:
if item:
item["price"] = price_adjust(item["price"])
item['replacement_price'] = price_adjust(item["price"])
if 'u' in option:
if world.retro[player]:
new_items = ["Bomb Upgrade (+5)"] * 7
else:
new_items = ["Bomb Upgrade (+5)", "Arrow Upgrade (+5)"] * 7
for shop in world.shops:
if shop.type == ShopType.UpgradeShop and shop.region.player == player:
for item in shop.inventory:
if item:
item['price'] = price_adjust(item["price"])
item['replacement_price'] = price_adjust(item["price"])
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
shop.region.name == "Capacity Upgrade":
shop.clear_inventory()
if 'inventory' in option:
world.random.shuffle(total_inventory)
i = 0
for shop in shops:
slots = shop.slots
shop.inventory = total_inventory[i:i + slots]
i += slots
for i, item in enumerate(items):
if item.name.startswith(("Bombs", "Arrows", "Rupees")):
items[i] = ItemFactory(new_items.pop(), player)
if not new_items:
break
else:
logging.warning(f"Not all upgrades put into Player{player}' item pool. Still missing: {new_items}")
if 'p' in option or 'i' in option:
shops = []
total_inventory = []
for shop in world.shops:
if shop.type == ShopType.Shop and shop.region.player == player:
shops.append(shop)
total_inventory.extend(shop.inventory)
if 'p' in option:
def price_adjust(price: int) -> int:
# it is important that a base price of 0 always returns 0 as new price!
return int(price * (0.5 + world.random.random() * 2))
for item in total_inventory:
if item:
item["price"] = price_adjust(item["price"])
item['replacement_price'] = price_adjust(item["price"])
for shop in world.shops:
if shop.type == ShopType.UpgradeShop and shop.region.player == player:
for item in shop.inventory:
if item:
item['price'] = price_adjust(item["price"])
item['replacement_price'] = price_adjust(item["price"])
if 'i' in option:
world.random.shuffle(total_inventory)
i = 0
for shop in shops:
slots = shop.slots
shop.inventory = total_inventory[i:i + slots]
i += slots
take_any_locations = [
@ -466,21 +485,21 @@ def set_up_shops(world, player: int):
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.add_inventory(2, 'Single Arrow', 80)
rss.locked = True
if world.keyshuffle[player] == "universal":
if world.keyshuffle[player] == "universal" or world.retro[player]:
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
slots = [0, 1, 2]
world.random.shuffle(slots)
slots = iter(slots)
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)
shop.push_inventory(next(slots), 'Single Arrow', 80)
if world.keyshuffle[player] == "universal":
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
def get_pool_core(world, player: int):

View File

@ -312,7 +312,7 @@ def roll_settings(weights):
ret.shop_shuffle = get_choice('shop_shuffle', weights, False)
if not ret.shop_shuffle:
ret.shop_shuffle = 'off'
ret.shop_shuffle = ''
ret.mode = get_choice('world_state', weights, None) # legacy support
if ret.mode == 'retro':

25
Rom.py
View File

@ -1157,20 +1157,27 @@ def patch_rom(world, rom, player, team, enemized):
assert equip[:0x340] == [0] * 0x340
rom.write_bytes(0x183000, equip[0x340:])
rom.write_bytes(0x271A6, equip[0x340:0x340+60])
rom.write_bytes(0x271A6, equip[0x340:0x340 + 60])
rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
rom.write_byte(0x2AF79, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both)
rom.write_byte(0x3A943, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
rom.write_byte(0x3A96D, 0xF0 if world.mode[player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader))
rom.write_bytes(0x180080, [50, 50, 70, 70]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
rom.write_byte(0x2AF79, 0xD0 if world.mode[
player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both)
rom.write_byte(0x3A943, 0xD0 if world.mode[
player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
rom.write_byte(0x3A96D, 0xF0 if world.mode[
player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader))
if 'u' in world.shop_shuffle[player]:
rom.write_bytes(0x180080,
[5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
else:
rom.write_bytes(0x180080,
[50, 50, 70, 70]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
rom.write_byte(0x18004D, ((0x01 if 'arrows' in world.escape_assist[player] else 0x00) |
(0x02 if 'bombs' in world.escape_assist[player] else 0x00) |
(0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist
(0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt']:
rom.write_byte(0x18003E, 0x01) # make ganon invincible

View File

@ -190,10 +190,13 @@ beemizer: # Remove items from the global item pool and replace them with single
3: 0 # 100% of the non-essential item pool is replaced with bee traps, of which 50% could be single bees
4: 0 # 100% of the non-essential item pool is replaced with bee traps
shop_shuffle:
off: 1
inventory: 0 # shuffle the inventories of the shops around
price: 0 # randomize the prices of the items in shop inventories
inventory_price: 0 # shuffle inventories and randomize prices
none: 1
i: 0 # shuffle the inventories of the shops around
p: 0 # randomize the prices of the items in shop inventories
u: 0 # shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
ip: 0 # shuffle inventories and randomize prices
uip: 0 # shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
# you can add more combos
timer:
none: 1
timed: 0
@ -245,7 +248,6 @@ linked_options:
full: 1
random: 1
singularity: 1
duality: 1
enemy_damage:
shuffled: 1
random: 1