Merge branch 'master' into pull/58

# Conflicts:
#	AdjusterMain.py
#	BaseClasses.py
#	EntranceShuffle.py
#	Gui.py
#	InvertedRegions.py
#	ItemList.py
#	Main.py
#	Plando.py
#	Rom.py
#	Rules.py
This commit is contained in:
Fabian Dill 2020-04-04 09:16:39 +02:00
commit f89c28d5c2
79 changed files with 5886 additions and 5077 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: Berserker55 # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

9
.gitignore vendored
View File

@ -1,6 +1,7 @@
.idea
.vscode
*_Spoiler.txt
*.bmbp
*.pyc
*.sfc
*.wixobj
@ -12,4 +13,10 @@ README.html
*multidata
*multisave
EnemizerCLI/
.mypy_cache/
.mypy_cache/
RaceRom.py
weights/
/MultiMystery/
/Players/
/QUsb2Snes/
/options.yaml

View File

@ -6,6 +6,7 @@ import textwrap
import sys
from AdjusterMain import adjust
from Rom import get_sprite_from_name
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@ -15,7 +16,8 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def main():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttPR rom to adjust.')
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
@ -31,6 +33,8 @@ def main():
''')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
@ -38,14 +42,15 @@ def main():
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--names', default='', type=str)
args = parser.parse_args()
# ToDo: Validate files further than mere existance
if not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
input('Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
# set up logger

View File

@ -3,32 +3,25 @@ import time
import logging
from Utils import output_path
from Rom import LocalRom, Sprite, apply_rom_settings
from Rom import LocalRom, apply_rom_settings
def adjust(args):
start = time.perf_counter()
logger = logging.getLogger('')
logger = logging.getLogger('Adjuster')
logger.info('Patching ROM.')
if args.sprite is not None:
if isinstance(args.sprite, Sprite):
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted'
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
rom = LocalRom(args.rom, False)
rom = LocalRom(args.rom, patch=False)
if os.path.isfile(args.baserom):
baserom = LocalRom(args.baserom, patch=True)
rom.orig_buffer = baserom.orig_buffer
else:
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite)
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
rom.write_to_file(output_path('%s.sfc' % outfilebase))
rom.write_to_file(output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc'))
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.perf_counter() - start)

File diff suppressed because it is too large Load Diff

View File

@ -76,7 +76,7 @@ def KholdstareDefeatRule(state, player):
(
state.has('Bombos', player) and
# FIXME: the following only actually works for the vanilla location for swordless
(state.has_sword(player) or state.world.swords == 'swordless')
(state.has_sword(player) or state.world.swords[player] == 'swordless')
)
) and
(
@ -86,7 +86,7 @@ def KholdstareDefeatRule(state, player):
(
state.has('Fire Rod', player) and
state.has('Bombos', player) and
state.world.swords == 'swordless' and
state.world.swords[player] == 'swordless' and
state.can_extend_magic(player, 16)
)
)
@ -120,8 +120,8 @@ boss_table = {
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
}
def can_place_boss(world, boss, dungeon_name, level=None):
if world.swords in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
def can_place_boss(world, player, boss, dungeon_name, level=None):
if world.swords[player] in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
return False
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'top':
@ -143,10 +143,10 @@ def can_place_boss(world, boss, dungeon_name, level=None):
return True
def place_bosses(world, player):
if world.boss_shuffle == 'none':
if world.boss_shuffle[player] == 'none':
return
# Most to least restrictive order
if world.mode != 'inverted':
if world.mode[player] != 'inverted':
boss_locations = [
['Ganons Tower', 'top'],
['Tower of Hera', None],
@ -182,15 +182,15 @@ def place_bosses(world, player):
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
if world.boss_shuffle in ["basic", "normal"]:
if world.boss_shuffle[player] in ["basic", "normal"]:
# temporary hack for swordless kholdstare:
if world.swords == 'swordless':
if world.swords[player] == 'swordless':
world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player)
logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace')
boss_locations.remove(['Ice Palace', None])
placeable_bosses.remove('Kholdstare')
if world.boss_shuffle == "basic": # vanilla bosses shuffled
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)]
@ -200,18 +200,18 @@ def place_bosses(world, player):
random.shuffle(bosses)
for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '')
boss = next((b for b in bosses if can_place_boss(world, b, loc, level)), None)
boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None)
if not boss:
raise FillError('Could not place boss for location %s' % loc_text)
bosses.remove(boss)
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
elif world.boss_shuffle == "chaos": #all bosses chosen at random
elif world.boss_shuffle[player] == "chaos": #all bosses chosen at random
for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '')
try:
boss = random.choice([b for b in placeable_bosses if can_place_boss(world, b, loc, level)])
boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)])
except IndexError:
raise FillError('Could not place boss for location %s' % loc_text)

View File

@ -8,7 +8,7 @@ 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 else small_keys, dungeon_items, player)
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys, dungeon_items, player)
dungeon.boss = BossFactory(default_boss, player)
for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon
@ -27,7 +27,7 @@ def create_dungeons(world, player):
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 'Trinexx', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
if world.mode != 'inverted':
if world.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
else:
@ -47,7 +47,7 @@ def fill_dungeons(world):
for player in range(1, world.players + 1):
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
if world.retro:
if world.retro[player]:
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
else:
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
@ -113,21 +113,20 @@ def fill_dungeons(world):
continue
# next place dungeon items
if world.place_dungeon_items:
for dungeon_item in dungeon_items:
di_location = dungeon_locations.pop()
world.push_item(di_location, dungeon_item, False)
for dungeon_item in dungeon_items:
di_location = dungeon_locations.pop()
world.push_item(di_location, dungeon_item, False)
def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.key or world.place_dungeon_items]
return [item for dungeon in world.dungeons for item in dungeon.all_items]
def fill_dungeons_restrictive(world, shuffled_locations):
all_state_base = world.get_all_state()
for player in range(1, world.players + 1):
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
if world.retro:
if world.retro[player]:
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
else:
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
@ -135,22 +134,24 @@ def fill_dungeons_restrictive(world, shuffled_locations):
pinball_room.locked = True
shuffled_locations.remove(pinball_room)
if world.keysanity:
#in keysanity dungeon items are distributed as part of the normal item pool
for item in world.get_items():
if item.key:
item.advancement = True
elif item.map or item.compass:
item.priority = True
return
# with shuffled dungeon items they are distributed as part of the normal item pool
for item in world.get_items():
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
all_state_base.collect(item, True)
item.advancement = True
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
item.priority = True
dungeon_items = get_dungeon_item_pool(world)
dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player])
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_order = {"BigKey": 3, "SmallKey": 2}
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items)
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, True)

View File

@ -8,6 +8,21 @@ Hints will appear in the following ratios across the 15 telepathic tiles that ha
5 hints for valuable items.
4 junk hints.
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead:
5 hints for inconvenient item locations.
8 hints for valuable items.
7 junk hints.
In the simple, restricted, and restricted legacy shuffles, these are the ratios:
2 hints for inconvenient entrances.
1 hint for an inconvenient dungeon entrance.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
5 junk hints.
These hints will use the following format:
Entrance hints go "[Entrance on overworld] leads to [interior]".
@ -65,7 +80,12 @@ Spike Cave
Magic Bat
Sahasrahla (Green Pendant)
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If keysanity is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
Graveyard Cave
Mimic Cave
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:
@ -103,8 +123,8 @@ Death Mountain Return Cave (East): The westmost cave on west DM
Spectacle Rock Cave Peak: The highest cave on west DM
Spectacle Rock Cave: The right ledge on west DM
Spectacle Rock Cave (Bottom): The left ledge on west DM
Paradox Cave (Bottom): The southmost cave on east DM
Paradox Cave (Middle): The right paired cave on east DM
Paradox Cave (Bottom): The right paired cave on east DM
Paradox Cave (Middle): The southmost cave on east DM
Paradox Cave (Top): The east DM summit cave
Fairy Ascension Cave (Bottom): The east DM cave behind rocks
Fairy Ascension Cave (Top): The central ledge on east DM

View File

@ -1,13 +1,16 @@
#!/usr/bin/env python3
import argparse
import copy
import os
import logging
import random
import textwrap
import shlex
import sys
from Main import main
from Utils import is_bundled, close_console, output_path
from Rom import get_sprite_from_name
from Utils import is_bundled, close_console
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@ -15,11 +18,18 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action):
return textwrap.dedent(action.help)
def parse_arguments(argv, no_defaults=False):
def defval(value):
return value if not no_defaults else None
# we need to know how many players we have first
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args(argv)
def start():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
parser.add_argument('--logic', default='noglitches', const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
help='''\
Select Enforcement of Item Requirements. (default: %(default)s)
No Glitches:
@ -28,7 +38,7 @@ def start():
No Logic: Distribute items without regard for
item requirements.
''')
parser.add_argument('--mode', default='open', const='open', nargs='?', choices=['standard', 'open', 'inverted'],
parser.add_argument('--mode', default=defval('open'), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
help='''\
Select game mode. (default: %(default)s)
Open: World starts with Zelda rescued.
@ -41,7 +51,7 @@ def start():
Requires the moon pearl to be Link in the Light World
instead of a bunny.
''')
parser.add_argument('--swords', default='random', const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
parser.add_argument('--swords', default=defval('random'), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
help='''\
Select sword placement. (default: %(default)s)
Random: All swords placed randomly.
@ -55,7 +65,7 @@ def start():
Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations.
''')
parser.add_argument('--goal', default='ganon', const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
help='''\
Select completion goal. (default: %(default)s)
Ganon: Collect all crystals, beat Agahnim 2 then
@ -67,21 +77,21 @@ def start():
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
20 of them to beat the game.
''')
parser.add_argument('--difficulty', default='normal', const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select game difficulty. Affects available itempool. (default: %(default)s)
Normal: Normal difficulty.
Hard: A harder setting with less equipment and reduced health.
Expert: A harder yet setting with minimum equipment and health.
''')
parser.add_argument('--item_functionality', default='normal', const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
parser.add_argument('--item_functionality', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select limits on item functionality to increase difficulty. (default: %(default)s)
Normal: Normal functionality.
Hard: Reduced functionality.
Expert: Greatly reduced functionality.
''')
parser.add_argument('--timer', default='none', const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
help='''\
Select game timer setting. Affects available itempool. (default: %(default)s)
None: No timer.
@ -101,7 +111,7 @@ def start():
Timed mode. If time runs out, you lose (but can
still keep playing).
''')
parser.add_argument('--progressive', default='on', const='normal', nargs='?', choices=['on', 'off', 'random'],
parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'],
help='''\
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
On: Swords, Shields, Armor, and Gloves will
@ -115,7 +125,7 @@ def start():
category, be randomly progressive or not.
Link will die in one hit.
''')
parser.add_argument('--algorithm', default='balanced', const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
help='''\
Select item filling algorithm. (default: %(default)s
balanced: vt26 derivitive that aims to strike a balance between
@ -138,7 +148,7 @@ def start():
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default='full', const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
parser.add_argument('--shuffle', default=defval('full'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
@ -162,7 +172,7 @@ def start():
The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla.
''')
parser.add_argument('--crystals_ganon', default='7', const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to defeat ganon. Any other
requirements for ganon for the selected goal still apply.
@ -171,16 +181,18 @@ def start():
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--crystals_gt', default='7', const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
parser.add_argument('--crystals_gt', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to open GT. For inverted mode
this applies to the castle tower door instead. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--rom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--openpyramid', default=defval(False), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
''', action='store_true')
parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'), help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--count', help='''\
Use to batch generate multiple seeds with same settings.
@ -189,50 +201,51 @@ def start():
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
parser.add_argument('--fastmenu', default='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('--keysanity', help='''\
Keys (and other dungeon items) are no longer restricted to
their dungeons, but can be anywhere
''', action='store_true')
parser.add_argument('--retro', help='''\
parser.add_argument('--extendedmsu', help='Use v31 Extended msu', 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('--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,
and a few other little things make this more like Zelda-1.
''', action='store_true')
parser.add_argument('--custom', default=False, help='Not supported.')
parser.add_argument('--customitemarray', default=False, help='Not supported.')
parser.add_argument('--nodungeonitems', help='''\
Remove Maps and Compasses from Itempool, replacing them by
empty slots.
''', action='store_true')
parser.add_argument('--accessibility', default='items', const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
parser.add_argument('--startinventory', default=defval(''), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--custom', default=defval(False), help='Not supported.')
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about
reaching all locations or all keys.
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.
''')
parser.add_argument('--hints', help='''\
parser.add_argument('--hints', default=defval(False), help='''\
Make telepathic tiles and storytellers give helpful hints.
''', action='store_true')
# included for backwards compatibility
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=True)
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
parser.add_argument('--no-shuffleganon', help='''\
If set, the Pyramid Hole and Ganon's Tower are not
included entrance shuffle pool.
''', action='store_false', dest='shuffleganon')
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
parser.add_argument('--heartbeep', default=defval('normal'), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\
Select the rate at which the heart beep sound is played at
low health. (default: %(default)s)
''')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
@ -246,21 +259,58 @@ def start():
Output .json patch to stdout instead of a patched rom. Used
for VT site integration, do not use otherwise.
''')
parser.add_argument('--skip_playthrough', action='store_true', default=False)
parser.add_argument('--enemizercli', default='')
parser.add_argument('--shufflebosses', default='none', choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=False, action='store_true')
parser.add_argument('--enemy_health', default='default', choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default='default', choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepalette', default=False, action='store_true')
parser.add_argument('--shufflepots', default=False, action='store_true')
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
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('--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(''))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath')
args = parser.parse_args()
parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')
parser.add_argument('--create_diff', default=defval(False), action='store_true', help='''\
create a binary patch file from which the randomized rom can be recreated using MultiClient.
Does not work with jsonout.''')
if args.outputpath and os.path.isdir(args.outputpath):
output_path.cached_path = args.outputpath
if multiargs.multi:
for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
ret = parser.parse_args(argv)
if ret.timer == "none":
ret.timer = False
if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
if multiargs.multi:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'timer',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'heartbeep',
'remote_items', 'progressive', 'extendedmsu']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
else:
getattr(ret, name)[player] = value
return ret
def start():
args = parse_arguments(None)
if is_bundled() and len(sys.argv) == 1:
# for the bundled builds, if we have no arguments, the user
@ -276,9 +326,9 @@ def start():
if not args.jsonout and not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite):
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in args.sprite.values()]):
if not args.jsonout:
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
else:
raise IOError('Cannot find sprite file at %s' % args.sprite)

View File

@ -19,17 +19,17 @@ def link_entrances(world, player):
connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections
if world.shuffle == 'vanilla':
if world.shuffle[player] == 'vanilla':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
for exitname, regionname in default_dungeon_connections:
connect_simple(world, exitname, regionname, player)
elif world.shuffle == 'dungeonssimple':
elif world.shuffle[player] == 'dungeonssimple':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player)
elif world.shuffle == 'dungeonsfull':
elif world.shuffle[player] == 'dungeonsfull':
for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player)
@ -39,7 +39,7 @@ def link_entrances(world, player):
lw_entrances = list(LW_Dungeon_Entrances)
dw_entrances = list(DW_Dungeon_Entrances)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@ -52,14 +52,14 @@ def link_entrances(world, player):
dw_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit')
if world.mode == 'standard':
if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
connect_mandatory_exits(world, lw_entrances, [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], list(LW_Dungeon_Entrances_Must_Exit), player)
else:
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.shuffle == 'simple':
elif world.shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player)
old_man_entrances = list(Old_Man_Entrances)
@ -130,7 +130,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'restricted':
elif world.shuffle[player] == 'restricted':
simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@ -201,7 +201,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'restricted_legacy':
elif world.shuffle[player] == 'restricted_legacy':
simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances)
@ -256,7 +256,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'full':
elif world.shuffle[player] == 'full':
skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@ -273,7 +273,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@ -309,7 +309,7 @@ def link_entrances(world, player):
pass
else: #if the cave wasn't placed we get here
connect_caves(world, lw_entrances, [], old_man_house, player)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@ -361,7 +361,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'crossed':
elif world.shuffle[player] == 'crossed':
skull_woods_shuffle(world, player)
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
@ -376,7 +376,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@ -392,7 +392,7 @@ def link_entrances(world, player):
#place must-exit caves
connect_mandatory_exits(world, entrances, caves, must_exits, player)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# rest of hyrule castle must be dealt with
connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@ -437,7 +437,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, entrances, door_targets, player)
elif world.shuffle == 'full_legacy':
elif world.shuffle[player] == 'full_legacy':
skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
@ -451,7 +451,7 @@ def link_entrances(world, player):
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Single_Cave_Targets)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
@ -471,7 +471,7 @@ def link_entrances(world, player):
else:
connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@ -513,7 +513,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'madness_legacy':
elif world.shuffle[player] == 'madness_legacy':
# here lie dragons, connections are no longer two way
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
@ -552,7 +552,7 @@ def link_entrances(world, player):
('Lumberjack Tree Exit', 'Lumberjack Tree (top)'),
(('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')]
if world.mode == 'standard':
if world.mode[player] == 'standard':
# cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@ -606,7 +606,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, target, player)
# hyrule castle handling
if world.mode == 'standard':
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@ -755,7 +755,7 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'insanity':
elif world.shuffle[player] == 'insanity':
# beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@ -792,7 +792,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode == 'standard':
if world.mode[player] == 'standard':
# cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@ -825,7 +825,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, hole_targets.pop(), player)
# hyrule castle handling
if world.mode == 'standard':
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@ -902,8 +902,8 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'insanity_legacy':
world.fix_fake_world = False
elif world.shuffle[player] == 'insanity_legacy':
world.fix_fake_world[player] = False
# beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@ -927,7 +927,7 @@ def link_entrances(world, player):
hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)',
'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)']
if world.mode == 'standard':
if world.mode[player] == 'standard':
# cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@ -960,7 +960,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, hole_targets.pop(), player)
# hyrule castle handling
if world.mode == 'standard':
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@ -1079,17 +1079,17 @@ def link_inverted_entrances(world, player):
connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections
if world.shuffle == 'vanilla':
if world.shuffle[player] == 'vanilla':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
for exitname, regionname in inverted_default_dungeon_connections:
connect_simple(world, exitname, regionname, player)
elif world.shuffle == 'dungeonssimple':
elif world.shuffle[player] == 'dungeonssimple':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player)
elif world.shuffle == 'dungeonsfull':
elif world.shuffle[player] == 'dungeonsfull':
for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player)
@ -1151,7 +1151,7 @@ def link_inverted_entrances(world, player):
remaining_lw_entrances = [i for i in all_dungeon_entrances if i in lw_entrances]
connect_caves(world, remaining_lw_entrances, remaining_dw_entrances, dungeon_exits, player)
elif world.shuffle == 'simple':
elif world.shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player)
old_man_entrances = list(Inverted_Old_Man_Entrances)
@ -1160,7 +1160,7 @@ def link_inverted_entrances(world, player):
single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets)
# we shuffle all 2 entrance caves as pairs as a start
@ -1191,6 +1191,8 @@ def link_inverted_entrances(world, player):
bomb_shop_doors.remove(links_house)
if links_house in blacksmith_doors:
blacksmith_doors.remove(links_house)
if links_house in old_man_entrances:
old_man_entrances.remove(links_house)
# place dark sanc
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors]
@ -1243,7 +1245,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'restricted':
elif world.shuffle[player] == 'restricted':
simple_shuffle_dungeons(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
@ -1253,7 +1255,7 @@ def link_inverted_entrances(world, player):
caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House)
single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets)
# place links house
@ -1326,7 +1328,7 @@ def link_inverted_entrances(world, player):
doors = lw_entrances + dw_entrances
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'full':
elif world.shuffle[player] == 'full':
skull_woods_shuffle(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
@ -1335,7 +1337,7 @@ def link_inverted_entrances(world, player):
old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera'])
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets)
old_man_house = list(Old_Man_House)
@ -1477,7 +1479,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'crossed':
elif world.shuffle[player] == 'crossed':
skull_woods_shuffle(world, player)
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
@ -1486,7 +1488,7 @@ def link_inverted_entrances(world, player):
old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera'])
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) # don't need to consider three exit caves, have one exit caves to avoid parity issues
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets)
# randomize which desert ledge door is a must-exit
@ -1587,7 +1589,7 @@ def link_inverted_entrances(world, player):
# place remaining doors
connect_doors(world, entrances, door_targets, player)
elif world.shuffle == 'insanity':
elif world.shuffle[player] == 'insanity':
# beware ye who enter here
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
@ -1610,7 +1612,7 @@ def link_inverted_entrances(world, player):
# bomb shop logic for.
# Specifically we could potentially add: 'Dark Death Mountain Ledge (East)' and doors associated with pits
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors + ['Turtle Rock Isolated Ledge Entrance', 'Hookshot Cave Back Entrance'])
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets)
random.shuffle(doors)
@ -1764,7 +1766,7 @@ def connect_simple(world, exitname, regionname, player):
world.get_entrance(exitname, player).connect(world.get_region(regionname, player))
def connect_entrance(world, entrancename, exitname, player):
def connect_entrance(world, entrancename: str, exitname: str, player: int):
entrance = world.get_entrance(entrancename, player)
# check if we got an entrance or a region to connect to
try:
@ -1831,7 +1833,7 @@ def scramble_holes(world, player):
else:
hole_targets.append(('Pyramid Exit', 'Pyramid'))
if world.mode == 'standard':
if world.mode[player] == 'standard':
# cannot move uncle cave
connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
@ -1840,14 +1842,14 @@ def scramble_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.shuffle == 'crossed':
if world.shuffle[player] == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon:
random.shuffle(hole_targets)
exit, target = hole_targets.pop()
connect_two_way(world, 'Pyramid Entrance', exit, player)
connect_entrance(world, 'Pyramid Hole', target, player)
if world.shuffle != 'crossed':
if world.shuffle[player] != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
random.shuffle(hole_targets)
@ -1882,14 +1884,14 @@ def scramble_inverted_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.shuffle == 'crossed':
if world.shuffle[player] == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon:
random.shuffle(hole_targets)
exit, target = hole_targets.pop()
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
connect_entrance(world, 'Inverted Pyramid Hole', target, player)
if world.shuffle != 'crossed':
if world.shuffle[player] != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
random.shuffle(hole_targets)
@ -1931,11 +1933,11 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, dp_m
if len(cave) == 2:
entrance = entrances.pop()
# ToDo Better solution, this is a hot fix. Do not connect both sides of trock/desert ledge only to each other
if world.mode != 'inverted' and entrance == 'Dark Death Mountain Ledge (West)':
if world.mode[player] != 'inverted' and entrance == 'Dark Death Mountain Ledge (West)':
new_entrance = entrances.pop()
entrances.append(entrance)
entrance = new_entrance
if world.mode == 'inverted' and entrance == dp_must_exit:
if world.mode[player] == 'inverted' and entrance == dp_must_exit:
new_entrance = entrances.pop()
entrances.append(entrance)
entrance = new_entrance
@ -2006,7 +2008,7 @@ def simple_shuffle_dungeons(world, player):
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
if world.mode != 'inverted':
if world.mode[player] != 'inverted':
if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
@ -2021,13 +2023,13 @@ def simple_shuffle_dungeons(world, player):
# mix up 4 door dungeons
multi_dungeons = ['Desert', 'Turtle Rock']
if world.mode == 'open' or (world.mode == 'inverted' and world.shuffle_ganon):
if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon):
multi_dungeons.append('Hyrule Castle')
random.shuffle(multi_dungeons)
dp_target = multi_dungeons[0]
tr_target = multi_dungeons[1]
if world.mode not in ['open', 'inverted'] or (world.mode == 'inverted' and world.shuffle_ganon is False):
if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False):
# place hyrule castle as intended
hc_target = 'Hyrule Castle'
else:
@ -2035,7 +2037,7 @@ def simple_shuffle_dungeons(world, player):
# ToDo improve this?
if world.mode != 'inverted':
if world.mode[player] != 'inverted':
if hc_target == 'Hyrule Castle':
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
@ -2646,8 +2648,6 @@ Inverted_Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)',
'Desert Palace Entrance (West)',
'Desert Palace Entrance (North)']
Inverted_Blacksmith_Multi_Cave_Doors = [] # same as non-inverted
Inverted_LW_Single_Cave_Doors = LW_Single_Cave_Doors + ['Inverted Big Bomb Shop']
Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
@ -2715,39 +2715,8 @@ Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing',
'Bumper Cave (Top)',
'Mimic Cave',
'Dark Lake Hylia Shop',
'Inverted Links House']
Inverted_Blacksmith_Single_Cave_Doors = ['Blinds Hideout',
'Lake Hylia Fairy',
'Light Hype Fairy',
'Desert Fairy',
'Chicken House',
'Aginahs Cave',
'Sahasrahlas Hut',
'Cave Shop (Lake Hylia)',
'Blacksmiths Hut',
'Sick Kids House',
'Lost Woods Gamble',
'Fortune Teller (Light)',
'Snitch Lady (East)',
'Snitch Lady (West)',
'Bush Covered House',
'Tavern (Front)',
'Light World Bomb Hut',
'Kakariko Shop',
'Mini Moldorm Cave',
'Long Fairy Cave',
'Good Bee Cave',
'20 Rupee Cave',
'50 Rupee Cave',
'Ice Rod Cave',
'Library',
'Potion Shop',
'Dam',
'Lumberjack House',
'Lake Hylia Fortune Teller',
'Kakariko Gamble Game',
'Inverted Big Bomb Shop']
'Inverted Links House',
'Inverted Big Bomb Shop']
Inverted_Single_Cave_Targets = ['Blinds Hideout',
@ -3020,7 +2989,7 @@ inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia
('Lake Hylia Island Pier', 'Lake Hylia Island'),
('Lake Hylia Warp', 'Northeast Light World'),
('Northeast Light World Warp', 'Light World'),
('Zoras River', 'Zoras River'),
('Zoras River', 'Zoras River'),
('Kings Grave Outer Rocks', 'Kings Grave Area'),
('Kings Grave Inner Rocks', 'Light World'),
('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'),

137
Fill.py
View File

@ -161,7 +161,7 @@ def distribute_items_staleness(world):
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s', [item.name for item in itempool], [location.name for location in fill_locations])
def fill_restrictive(world, base_state, locations, itempool):
def fill_restrictive(world, base_state, locations, itempool, single_player_placement = False):
def sweep_from_pool():
new_state = base_state.copy()
for item in itempool:
@ -169,45 +169,51 @@ def fill_restrictive(world, base_state, locations, itempool):
new_state.sweep_for_events()
return new_state
while itempool and locations:
items_to_place = []
nextpool = []
placing_players = set()
for item in reversed(itempool):
if item.player not in placing_players:
placing_players.add(item.player)
items_to_place.append(item)
else:
nextpool.insert(0, item)
itempool = nextpool
unplaced_items = []
maximum_exploration_state = sweep_from_pool()
no_access_checks = {}
reachable_items = {}
for item in itempool:
if world.accessibility[item.player] == 'none':
no_access_checks.setdefault(item.player, []).append(item)
else:
reachable_items.setdefault(item.player, []).append(item)
perform_access_check = True
if world.accessibility == 'none':
perform_access_check = not world.has_beaten_game(maximum_exploration_state)
for player_items in [no_access_checks, reachable_items]:
while any(player_items.values()) and locations:
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
for item_to_place in items_to_place:
spot_to_fill = None
for location in locations:
if location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
spot_to_fill = location
break
maximum_exploration_state = sweep_from_pool()
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
if world.can_beat_game():
if world.accessibility != 'none':
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
continue
raise FillError('No more spots to place %s' % item_to_place)
for item_to_place in items_to_place:
perform_access_check = True
if world.accessibility[item_to_place.player] == 'none':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)
spot_to_fill.event = True
spot_to_fill = None
for location in locations:
if (not single_player_placement or location.player == item_to_place.player)\
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
spot_to_fill = location
break
if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.insert(0, item_to_place)
if world.can_beat_game():
if world.accessibility[item_to_place.player] != 'none':
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
continue
raise FillError('No more spots to place %s' % item_to_place)
def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=None):
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)
spot_to_fill.event = True
itempool.extend(unplaced_items)
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in
if not fill_locations:
fill_locations = world.get_unfilled_locations()
@ -221,23 +227,26 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
# fill in gtower locations with trash first
for player in range(1, world.players + 1):
if world.ganonstower_vanilla[player]:
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player]
random.shuffle(gtower_locations)
trashcnt = 0
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = restitempool.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
if not gftower_trash or not world.ganonstower_vanilla[player]:
continue
gftower_trash_count = (random.randint(15, 50) if world.goal[player] == 'triforcehunt' else random.randint(0, 15))
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player]
random.shuffle(gtower_locations)
trashcnt = 0
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = restitempool.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
random.shuffle(fill_locations)
fill_locations.reverse()
# Make sure the escape small key is placed first in standard keysanity to prevent running out of spots
if world.keysanity and world.mode == 'standard':
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0)
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.mode[item.player] == 'standard' and world.keyshuffle[item.player] else 0)
fill_restrictive(world, world.state, fill_locations, progitempool)
@ -307,7 +316,7 @@ def flood_items(world):
location_list = world.get_reachable_locations()
random.shuffle(location_list)
for location in location_list:
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.key:
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey:
# safe to replace
replace_item = location.item
replace_item.location = None
@ -327,8 +336,7 @@ def balance_multiworld_progression(world):
reachable_locations_count[player] = 0
def get_sphere_locations(sphere_state, locations):
if not world.keysanity:
sphere_state.sweep_for_events(key_only=True, locations=locations)
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc)]
while True:
@ -338,8 +346,7 @@ def balance_multiworld_progression(world):
reachable_locations_count[location.player] += 1
if checked_locations:
average_reachable_locations = sum(reachable_locations_count.values()) / world.players
threshold = ((average_reachable_locations + max(reachable_locations_count.values())) / 2) * 0.8 #todo: probably needs some tweaking
threshold = max(reachable_locations_count.values()) - 20
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
if balancing_players:
@ -350,9 +357,9 @@ def balance_multiworld_progression(world):
candidate_items = []
while True:
for location in balancing_sphere:
if location.event:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players:
if location.item.player in balancing_players and not location.locked:
candidate_items.append(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere:
@ -360,11 +367,14 @@ def balance_multiworld_progression(world):
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
items_to_replace = []
for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player]
# only replace items that end up in another player's world
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test:
testing = items_to_test.pop()
@ -374,9 +384,6 @@ def balance_multiworld_progression(world):
reducing_state.sweep_for_events(locations=locations_to_test)
if testing.locked:
continue
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
@ -386,13 +393,17 @@ def balance_multiworld_progression(world):
items_to_replace.append(testing)
replaced_items = False
locations_for_replacing = [l for l in checked_locations if not l.event and not l.locked]
while locations_for_replacing and items_to_replace:
new_location = locations_for_replacing.pop()
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
while replacement_locations and items_to_replace:
new_location = replacement_locations.pop()
old_location = items_to_replace.pop()
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
replacement_locations.insert(0, new_location)
new_location = replacement_locations.pop()
new_location.item, old_location.item = old_location.item, new_location.item
new_location.event = True
old_location.event = False
new_location.event, old_location.event = True, False
state.collect(new_location.item, True, new_location)
replaced_items = True
if replaced_items:
@ -402,9 +413,11 @@ def balance_multiworld_progression(world):
sphere_locations.append(location)
for location in sphere_locations:
if location.event:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations.extend(sphere_locations)
if world.has_beaten_game(state):
break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')

333
Gui.py
View File

@ -2,6 +2,7 @@
from argparse import Namespace
from glob import glob
import json
import logging
import random
import os
import shutil
@ -9,7 +10,11 @@ from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, T
from urllib.parse import urlparse
from urllib.request import urlopen
import ModuleUpdate
ModuleUpdate.update()
from AdjusterMain import adjust
from EntranceRandomizer import parse_arguments
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from Main import main, __version__ as ESVersion
from Rom import Sprite
@ -58,16 +63,20 @@ def guiMain(args=None):
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
suppressRomVar = IntVar()
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
quickSwapVar = IntVar()
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
keysanityVar = IntVar()
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar)
openpyramidVar = IntVar()
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)
retroVar = IntVar()
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
dungeonItemsVar = IntVar()
dungeonItemsCheckbutton = Checkbutton(checkBoxFrame, text="Place Dungeon Items (Compasses/Maps)", onvalue=0, offvalue=1, variable=dungeonItemsVar)
disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
shuffleGanonVar = IntVar()
shuffleGanonVar.set(1) #set default
shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar)
@ -79,61 +88,31 @@ def guiMain(args=None):
createSpoilerCheckbutton.pack(expand=True, anchor=W)
suppressRomCheckbutton.pack(expand=True, anchor=W)
quickSwapCheckbutton.pack(expand=True, anchor=W)
keysanityCheckbutton.pack(expand=True, anchor=W)
openpyramidCheckbutton.pack(expand=True, anchor=W)
mcsbshuffleFrame.pack(expand=True, anchor=W)
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)
retroCheckbutton.pack(expand=True, anchor=W)
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
disableMusicCheckbutton.pack(expand=True, anchor=W)
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
hintsCheckbutton.pack(expand=True, anchor=W)
customCheckbutton.pack(expand=True, anchor=W)
fileDialogFrame = Frame(rightHalfFrame)
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1)
for i in range(5):
romOptionsFrame.rowconfigure(i, weight=1)
heartbeepFrame = Frame(fileDialogFrame)
heartbeepVar = StringVar()
heartbeepVar.set('normal')
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=RIGHT)
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep sound rate')
heartbeepLabel.pack(side=LEFT, padx=(0,52))
disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar)
disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
heartcolorFrame = Frame(fileDialogFrame)
heartcolorVar = StringVar()
heartcolorVar.set('red')
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=RIGHT)
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT, padx=(0,127))
fastMenuFrame = Frame(fileDialogFrame)
fastMenuVar = StringVar()
fastMenuVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=RIGHT)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT, padx=(0,100))
heartbeepFrame.pack(expand=True, anchor=E)
heartcolorFrame.pack(expand=True, anchor=E)
fastMenuFrame.pack(expand=True, anchor=E)
romDialogFrame = Frame(fileDialogFrame)
baseRomLabel = Label(romDialogFrame, text='Base Rom')
romVar = StringVar()
romEntry = Entry(romDialogFrame, textvariable=romVar)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
romVar.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT)
romSelectButton.pack(side=LEFT)
spriteDialogFrame = Frame(fileDialogFrame)
baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite:')
spriteDialogFrame = Frame(romOptionsFrame)
spriteDialogFrame.grid(row=0, column=1)
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
spriteNameVar = StringVar()
sprite = None
@ -153,17 +132,88 @@ def guiMain(args=None):
def SpriteSelect():
SpriteSelector(mainWindow, set_sprite)
spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect)
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
romDialogFrame.pack()
spriteDialogFrame.pack()
quickSwapVar = IntVar()
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
checkBoxFrame.pack()
fileDialogFrame.pack()
fastMenuFrame = Frame(romOptionsFrame)
fastMenuFrame.grid(row=1, column=1, sticky=E)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT)
fastMenuVar = StringVar()
fastMenuVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame)
heartcolorFrame.grid(row=2, column=0, sticky=E)
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT)
heartcolorVar = StringVar()
heartcolorVar.set('red')
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=LEFT)
heartbeepFrame = Frame(romOptionsFrame)
heartbeepFrame.grid(row=2, column=1, sticky=E)
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep')
heartbeepLabel.pack(side=LEFT)
heartbeepVar = StringVar()
heartbeepVar.set('normal')
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=LEFT)
owPalettesFrame = Frame(romOptionsFrame)
owPalettesFrame.grid(row=3, column=0, sticky=E)
owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes')
owPalettesLabel.pack(side=LEFT)
owPalettesVar = StringVar()
owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout')
owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame)
uwPalettesFrame.grid(row=3, column=1, sticky=E)
uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes')
uwPalettesLabel.pack(side=LEFT)
uwPalettesVar = StringVar()
uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout')
uwPalettesOptionMenu.pack(side=LEFT)
romDialogFrame = Frame(romOptionsFrame)
romDialogFrame.grid(row=4, column=0, columnspan=2, sticky=W+E)
baseRomLabel = Label(romDialogFrame, text='Base Rom: ')
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
romEntry = Entry(romDialogFrame, textvariable=romVar)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
import Patch
try:
Patch.get_base_rom_bytes(rom) # throws error on checksum fail
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while reading ROM", message=str(e))
else:
romVar.set(rom)
romSelectButton['state'] = "disabled"
romSelectButton["text"] = "ROM verified"
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, expand=True, fill=X)
romSelectButton.pack(side=LEFT)
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
drowDownFrame = Frame(topFrame)
@ -285,18 +335,19 @@ def guiMain(args=None):
algorithmFrame.pack(expand=True, anchor=E)
shuffleFrame.pack(expand=True, anchor=E)
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=5)
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=2)
enemizerFrame.columnconfigure(0, weight=1)
enemizerFrame.columnconfigure(1, weight=1)
enemizerFrame.columnconfigure(2, weight=1)
enemizerFrame.columnconfigure(3, weight=1)
enemizerPathFrame = Frame(enemizerFrame)
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W)
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W+E, padx=3)
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
enemizerCLIlabel.pack(side=LEFT)
enemizerCLIpathVar = StringVar()
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80)
enemizerCLIpathEntry.pack(side=LEFT)
enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core")
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT, expand=True, fill=X)
def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")])
if path:
@ -304,18 +355,21 @@ def guiMain(args=None):
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
enemizerCLIbrowseButton.pack(side=LEFT)
enemyShuffleVar = IntVar()
enemyShuffleButton = Checkbutton(enemizerFrame, text="Enemy shuffle", variable=enemyShuffleVar)
enemyShuffleButton.grid(row=1, column=0)
paletteShuffleVar = IntVar()
paletteShuffleButton = Checkbutton(enemizerFrame, text="Palette shuffle", variable=paletteShuffleVar)
paletteShuffleButton.grid(row=1, column=1)
potShuffleVar = IntVar()
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
potShuffleButton.grid(row=1, column=2)
potShuffleButton.grid(row=0, column=3)
enemizerEnemyFrame = Frame(enemizerFrame)
enemizerEnemyFrame.grid(row=1, column=0, pady=5)
enemizerEnemyLabel = Label(enemizerEnemyFrame, text='Enemy shuffle')
enemizerEnemyLabel.pack(side=LEFT)
enemyShuffleVar = StringVar()
enemyShuffleVar.set('none')
enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos')
enemizerEnemyOption.pack(side=LEFT)
enemizerBossFrame = Frame(enemizerFrame)
enemizerBossFrame.grid(row=2, column=0)
enemizerBossFrame.grid(row=1, column=1)
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
enemizerBossLabel.pack(side=LEFT)
enemizerBossVar = StringVar()
@ -324,7 +378,7 @@ def guiMain(args=None):
enemizerBossOption.pack(side=LEFT)
enemizerDamageFrame = Frame(enemizerFrame)
enemizerDamageFrame.grid(row=2, column=1)
enemizerDamageFrame.grid(row=1, column=2)
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
enemizerDamageLabel.pack(side=LEFT)
enemizerDamageVar = StringVar()
@ -333,7 +387,7 @@ def guiMain(args=None):
enemizerDamageOption.pack(side=LEFT)
enemizerHealthFrame = Frame(enemizerFrame)
enemizerHealthFrame.grid(row=2, column=2)
enemizerHealthFrame.grid(row=1, column=3)
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
enemizerHealthLabel.pack(side=LEFT)
enemizerHealthVar = StringVar()
@ -346,6 +400,9 @@ def guiMain(args=None):
worldLabel = Label(bottomFrame, text='Worlds')
worldVar = StringVar()
worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar)
namesLabel = Label(bottomFrame, text='Player names')
namesVar = StringVar()
namesEntry = Entry(bottomFrame, textvariable=namesVar)
seedLabel = Label(bottomFrame, text='Seed #')
seedVar = StringVar()
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
@ -354,8 +411,9 @@ def guiMain(args=None):
countSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=countVar)
def generateRom():
guiargs = Namespace
guiargs = Namespace()
guiargs.multi = int(worldVar.get())
guiargs.names = namesVar.get()
guiargs.seed = int(seedVar.get()) if seedVar.get() else None
guiargs.count = int(countVar.get()) if countVar.get() != '1' else None
guiargs.mode = modeVar.get()
@ -367,6 +425,8 @@ def guiMain(args=None):
guiargs.difficulty = difficultyVar.get()
guiargs.item_functionality = itemfunctionVar.get()
guiargs.timer = timerVar.get()
if guiargs.timer == "none":
guiargs.timer = False
guiargs.progressive = progressiveVar.get()
guiargs.accessibility = accessibilityVar.get()
guiargs.algorithm = algorithmVar.get()
@ -376,19 +436,23 @@ def guiMain(args=None):
guiargs.fastmenu = fastMenuVar.get()
guiargs.create_spoiler = bool(createSpoilerVar.get())
guiargs.suppress_rom = bool(suppressRomVar.get())
guiargs.keysanity = bool(keysanityVar.get())
guiargs.openpyramid = bool(openpyramidVar.get())
guiargs.mapshuffle = bool(mapshuffleVar.get())
guiargs.compassshuffle = bool(compassshuffleVar.get())
guiargs.keyshuffle = bool(keyshuffleVar.get())
guiargs.bigkeyshuffle = bool(bigkeyshuffleVar.get())
guiargs.retro = bool(retroVar.get())
guiargs.nodungeonitems = bool(dungeonItemsVar.get())
guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.shuffleganon = bool(shuffleGanonVar.get())
guiargs.hints = bool(hintsVar.get())
guiargs.enemizercli = enemizerCLIpathVar.get()
guiargs.shufflebosses = enemizerBossVar.get()
guiargs.shuffleenemies = bool(enemyShuffleVar.get())
guiargs.shuffleenemies = enemyShuffleVar.get()
guiargs.enemy_health = enemizerHealthVar.get()
guiargs.enemy_damage = enemizerDamageVar.get()
guiargs.shufflepalette = bool(paletteShuffleVar.get())
guiargs.shufflepots = bool(potShuffleVar.get())
guiargs.custom = bool(customVar.get())
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()),
@ -398,14 +462,19 @@ def guiMain(args=None):
int(sword3Var.get()), int(sword4Var.get()), int(progswordVar.get()), int(shield1Var.get()), int(shield2Var.get()), int(shield3Var.get()), int(progshieldVar.get()), int(bluemailVar.get()),
int(redmailVar.get()), int(progmailVar.get()), int(halfmagicVar.get()), int(quartermagicVar.get()), int(bcap5Var.get()), int(bcap10Var.get()), int(acap5Var.get()), int(acap10Var.get()),
int(arrow1Var.get()), int(arrow10Var.get()), int(bomb1Var.get()), int(bomb3Var.get()), int(rupee1Var.get()), int(rupee5Var.get()), int(rupee20Var.get()), int(rupee50Var.get()), int(rupee100Var.get()),
int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(triforcepieceVar.get()), int(triforcecountVar.get()),
int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(progbowVar.get()), int(bomb10Var.get()), int(triforcepieceVar.get()),
int(triforcecountVar.get()), int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
guiargs.rom = romVar.get()
guiargs.jsonout = None
guiargs.sprite = sprite
guiargs.skip_playthrough = False
guiargs.outputpath = None
# get default values for missing parameters
for k,v in vars(parse_arguments(['--multi', str(guiargs.multi)])).items():
if k not in vars(guiargs):
setattr(guiargs, k, v)
elif type(v) is dict: # use same settings for every player
setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)})
try:
if not guiargs.suppress_rom and not os.path.exists(guiargs.rom):
raise FileNotFoundError(f"Could not find specified rom file {guiargs.rom}")
if guiargs.count is not None:
seed = guiargs.seed
for _ in range(guiargs.count):
@ -414,6 +483,7 @@ def guiMain(args=None):
else:
main(seed=guiargs.seed, args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
@ -422,6 +492,8 @@ def guiMain(args=None):
worldLabel.pack(side=LEFT)
worldSpinbox.pack(side=LEFT)
namesLabel.pack(side=LEFT)
namesEntry.pack(side=LEFT)
seedLabel.pack(side=LEFT, padx=(5, 0))
seedEntry.pack(side=LEFT)
countLabel.pack(side=LEFT, padx=(5, 0))
@ -502,25 +574,42 @@ def guiMain(args=None):
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
fastMenuLabel2.pack(side=LEFT)
owPalettesFrame2 = Frame(drowDownFrame2)
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout')
owPalettesOptionMenu2.pack(side=RIGHT)
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
owPalettesLabel2.pack(side=LEFT)
uwPalettesFrame2 = Frame(drowDownFrame2)
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout')
uwPalettesOptionMenu2.pack(side=RIGHT)
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
uwPalettesLabel2.pack(side=LEFT)
heartbeepFrame2.pack(expand=True, anchor=E)
heartcolorFrame2.pack(expand=True, anchor=E)
fastMenuFrame2.pack(expand=True, anchor=E)
owPalettesFrame2.pack(expand=True, anchor=E)
uwPalettesFrame2.pack(expand=True, anchor=E)
bottomFrame2 = Frame(topFrame2)
def adjustRom():
guiargs = Namespace
guiargs = Namespace()
guiargs.heartbeep = heartbeepVar.get()
guiargs.heartcolor = heartcolorVar.get()
guiargs.fastmenu = fastMenuVar.get()
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = sprite
try:
adjust(args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
@ -553,19 +642,19 @@ def guiMain(args=None):
bowFrame = Frame(itemList1)
bowLabel = Label(bowFrame, text='Bow')
bowVar = StringVar(value='1')
bowVar = StringVar(value='0')
bowEntry = Entry(bowFrame, textvariable=bowVar, width=3, validate='all', vcmd=vcmd)
bowFrame.pack()
bowLabel.pack(anchor=W, side=LEFT, padx=(0,53))
bowEntry.pack(anchor=E)
silverarrowFrame = Frame(itemList1)
silverarrowLabel = Label(silverarrowFrame, text='Silver Arrow')
silverarrowVar = StringVar(value='1')
silverarrowEntry = Entry(silverarrowFrame, textvariable=silverarrowVar, width=3, validate='all', vcmd=vcmd)
silverarrowFrame.pack()
silverarrowLabel.pack(anchor=W, side=LEFT, padx=(0,13))
silverarrowEntry.pack(anchor=E)
progbowFrame = Frame(itemList1)
progbowLabel = Label(progbowFrame, text='Prog.Bow')
progbowVar = StringVar(value='2')
progbowEntry = Entry(progbowFrame, textvariable=progbowVar, width=3, validate='all', vcmd=vcmd)
progbowFrame.pack()
progbowLabel.pack(anchor=W, side=LEFT, padx=(0,25))
progbowEntry.pack(anchor=E)
boomerangFrame = Frame(itemList1)
boomerangLabel = Label(boomerangFrame, text='Boomerang')
@ -921,7 +1010,7 @@ def guiMain(args=None):
bcap5Frame = Frame(itemList3)
bcap5Label = Label(bcap5Frame, text='Bomb C.+5')
bcap5Var = StringVar(value='6')
bcap5Var = StringVar(value='0')
bcap5Entry = Entry(bcap5Frame, textvariable=bcap5Var, width=3, validate='all', vcmd=vcmd)
bcap5Frame.pack()
bcap5Label.pack(anchor=W, side=LEFT, padx=(0,16))
@ -929,7 +1018,7 @@ def guiMain(args=None):
bcap10Frame = Frame(itemList3)
bcap10Label = Label(bcap10Frame, text='Bomb C.+10')
bcap10Var = StringVar(value='1')
bcap10Var = StringVar(value='0')
bcap10Entry = Entry(bcap10Frame, textvariable=bcap10Var, width=3, validate='all', vcmd=vcmd)
bcap10Frame.pack()
bcap10Label.pack(anchor=W, side=LEFT, padx=(0,10))
@ -937,7 +1026,7 @@ def guiMain(args=None):
acap5Frame = Frame(itemList4)
acap5Label = Label(acap5Frame, text='Arrow C.+5')
acap5Var = StringVar(value='6')
acap5Var = StringVar(value='0')
acap5Entry = Entry(acap5Frame, textvariable=acap5Var, width=3, validate='all', vcmd=vcmd)
acap5Frame.pack()
acap5Label.pack(anchor=W, side=LEFT, padx=(0,7))
@ -945,7 +1034,7 @@ def guiMain(args=None):
acap10Frame = Frame(itemList4)
acap10Label = Label(acap10Frame, text='Arrow C.+10')
acap10Var = StringVar(value='1')
acap10Var = StringVar(value='0')
acap10Entry = Entry(acap10Frame, textvariable=acap10Var, width=3, validate='all', vcmd=vcmd)
acap10Frame.pack()
acap10Label.pack(anchor=W, side=LEFT, padx=(0,1))
@ -961,7 +1050,7 @@ def guiMain(args=None):
arrow10Frame = Frame(itemList4)
arrow10Label = Label(arrow10Frame, text='Arrows (10)')
arrow10Var = StringVar(value='5')
arrow10Var = StringVar(value='12')
arrow10Entry = Entry(arrow10Frame, textvariable=arrow10Var, width=3, validate='all', vcmd=vcmd)
arrow10Frame.pack()
arrow10Label.pack(anchor=W, side=LEFT, padx=(0,7))
@ -977,12 +1066,20 @@ def guiMain(args=None):
bomb3Frame = Frame(itemList4)
bomb3Label = Label(bomb3Frame, text='Bombs (3)')
bomb3Var = StringVar(value='10')
bomb3Var = StringVar(value='16')
bomb3Entry = Entry(bomb3Frame, textvariable=bomb3Var, width=3, validate='all', vcmd=vcmd)
bomb3Frame.pack()
bomb3Label.pack(anchor=W, side=LEFT, padx=(0,13))
bomb3Entry.pack(anchor=E)
bomb10Frame = Frame(itemList4)
bomb10Label = Label(bomb10Frame, text='Bombs (10)')
bomb10Var = StringVar(value='1')
bomb10Entry = Entry(bomb10Frame, textvariable=bomb10Var, width=3, validate='all', vcmd=vcmd)
bomb10Frame.pack()
bomb10Label.pack(anchor=W, side=LEFT, padx=(0,7))
bomb10Entry.pack(anchor=E)
rupee1Frame = Frame(itemList4)
rupee1Label = Label(rupee1Frame, text='Rupee (1)')
rupee1Var = StringVar(value='2')
@ -1031,14 +1128,6 @@ def guiMain(args=None):
rupee300Label.pack(anchor=W, side=LEFT, padx=(0,0))
rupee300Entry.pack(anchor=E)
rupoorFrame = Frame(itemList4)
rupoorLabel = Label(rupoorFrame, text='Rupoor')
rupoorVar = StringVar(value='0')
rupoorEntry = Entry(rupoorFrame, textvariable=rupoorVar, width=3, validate='all', vcmd=vcmd)
rupoorFrame.pack()
rupoorLabel.pack(anchor=W, side=LEFT, padx=(0,28))
rupoorEntry.pack(anchor=E)
blueclockFrame = Frame(itemList4)
blueclockLabel = Label(blueclockFrame, text='Blue Clock')
blueclockVar = StringVar(value='0')
@ -1063,6 +1152,14 @@ def guiMain(args=None):
redclockLabel.pack(anchor=W, side=LEFT, padx=(0,14))
redclockEntry.pack(anchor=E)
silverarrowFrame = Frame(itemList5)
silverarrowLabel = Label(silverarrowFrame, text='Silver Arrow')
silverarrowVar = StringVar(value='0')
silverarrowEntry = Entry(silverarrowFrame, textvariable=silverarrowVar, width=3, validate='all', vcmd=vcmd)
silverarrowFrame.pack()
silverarrowLabel.pack(anchor=W, side=LEFT, padx=(0,64))
silverarrowEntry.pack(anchor=E)
universalkeyFrame = Frame(itemList5)
universalkeyLabel = Label(universalkeyFrame, text='Universal Key')
universalkeyVar = StringVar(value='0')
@ -1095,6 +1192,14 @@ def guiMain(args=None):
triforceLabel.pack(anchor=W, side=LEFT, padx=(0,23))
triforceEntry.pack(anchor=E)
rupoorFrame = Frame(itemList5)
rupoorLabel = Label(rupoorFrame, text='Rupoor')
rupoorVar = StringVar(value='0')
rupoorEntry = Entry(rupoorFrame, textvariable=rupoorVar, width=3, validate='all', vcmd=vcmd)
rupoorFrame.pack()
rupoorLabel.pack(anchor=W, side=LEFT, padx=(0,87))
rupoorEntry.pack(anchor=E)
rupoorcostFrame = Frame(itemList5)
rupoorcostLabel = Label(rupoorcostFrame, text='Rupoor Cost')
rupoorcostVar = StringVar(value='10')
@ -1111,13 +1216,17 @@ def guiMain(args=None):
topFrame3.pack(side=TOP, pady=(17,0))
if args is not None:
for k,v in vars(args).items():
if type(v) is dict:
setattr(args, k, v[1]) # only get values for player 1 for now
# load values from commandline args
createSpoilerVar.set(int(args.create_spoiler))
suppressRomVar.set(int(args.suppress_rom))
keysanityVar.set(args.keysanity)
mapshuffleVar.set(args.mapshuffle)
compassshuffleVar.set(args.compassshuffle)
keyshuffleVar.set(args.keyshuffle)
bigkeyshuffleVar.set(args.bigkeyshuffle)
retroVar.set(args.retro)
if args.nodungeonitems:
dungeonItemsVar.set(int(not args.nodungeonitems))
quickSwapVar.set(int(args.quickswap))
disableMusicVar.set(int(args.disablemusic))
if args.count:

View File

@ -1,5 +1,6 @@
import collections
from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
from BaseClasses import RegionType
from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
def create_inverted_regions(world, player):
@ -302,52 +303,13 @@ def create_inverted_regions(world, player):
create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing'])
]
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
region = world.get_region(region_name, player)
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
region.shop = shop
world.shops.append(shop)
for index, (item, price) in enumerate(default_shop_contents[region_name]):
shop.add_inventory(index, item, price)
world.initialize_regions()
region = world.get_region('Capacity Upgrade', player)
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
region.shop = shop
world.shops.append(shop)
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
world.intialize_regions()
def create_lw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
def create_dw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)
def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None):
ret = Region(name, type, hint, player)
if locations is None:
locations = []
if exits is None:
exits = []
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
for location in locations:
address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret))
return ret
def mark_dark_world_regions(world):
def mark_dark_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in world.regions if region.type == RegionType.DarkWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@ -360,7 +322,7 @@ def mark_dark_world_regions(world):
seen.add(exit.connected_region)
queue.append(exit.connected_region)
queue = collections.deque(region for region in world.regions if region.type == RegionType.LightWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@ -372,270 +334,3 @@ def mark_dark_world_regions(world):
if exit.connected_region not in seen:
seen.add(exit.connected_region)
queue.append(exit.connected_region)
# (room_id, shopkeeper, replaceable)
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
'Red Shield Shop': (0x0110, 0xC1, True),
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
'Village of Outcasts Shop': (0x010F, 0xC1, True),
'Dark World Potion Shop': (0x010F, 0xC1, True),
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
'Kakariko Shop': (0x011F, 0xA0, True),
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
'Potion Shop': (0x0109, 0xFF, False),
# Bomb Shop not currently modeled as a shop, due to special nature of items
}
# region, [item]
# slot, item, price, max=0, replacement=None, replacement_price=0
# item = (item, price)
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
default_shop_contents = {
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
'Village of Outcasts Shop': _dark_world_shop_defaults,
'Dark World Potion Shop': _dark_world_shop_defaults,
'Light World Death Mountain Shop': _basic_shop_defaults,
'Kakariko Shop': _basic_shop_defaults,
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
}
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
'Bottle Merchant': (0x2EB18, False, 'with a merchant'),
'Flute Spot': (0x18014A, False, 'underground'),
'Sunken Treasure': (0x180145, False, 'underwater'),
'Purple Chest': (0x33D68, False, 'from a box'),
'Blind\'s Hideout - Top': (0xEB0F, False, 'in a basement'),
'Blind\'s Hideout - Left': (0xEB12, False, 'in a basement'),
'Blind\'s Hideout - Right': (0xEB15, False, 'in a basement'),
'Blind\'s Hideout - Far Left': (0xEB18, False, 'in a basement'),
'Blind\'s Hideout - Far Right': (0xEB1B, False, 'in a basement'),
'Link\'s Uncle': (0x2DF45, False, 'with your uncle'),
'Secret Passage': (0xE971, False, 'near your uncle'),
'King Zora': (0xEE1C3, False, 'at a high price'),
'Zora\'s Ledge': (0x180149, False, 'near Zora'),
'Waterfall Fairy - Left': (0xE9B0, False, 'near a fairy'),
'Waterfall Fairy - Right': (0xE9D1, False, 'near a fairy'),
'King\'s Tomb': (0xE97A, False, 'alone in a cave'),
'Floodgate Chest': (0xE98C, False, 'in the dam'),
'Link\'s House': (0xE9BC, False, 'in your home'),
'Kakariko Tavern': (0xE9CE, False, 'in the bar'),
'Chicken House': (0xE9E9, False, 'near poultry'),
'Aginah\'s Cave': (0xE9F2, False, 'with Aginah'),
'Sahasrahla\'s Hut - Left': (0xEA82, False, 'near the elder'),
'Sahasrahla\'s Hut - Middle': (0xEA85, False, 'near the elder'),
'Sahasrahla\'s Hut - Right': (0xEA88, False, 'near the elder'),
'Sahasrahla': (0x2F1FC, False, 'with the elder'),
'Kakariko Well - Top': (0xEA8E, False, 'in a well'),
'Kakariko Well - Left': (0xEA91, False, 'in a well'),
'Kakariko Well - Middle': (0xEA94, False, 'in a well'),
'Kakariko Well - Right': (0xEA97, False, 'in a well'),
'Kakariko Well - Bottom': (0xEA9A, False, 'in a well'),
'Blacksmith': (0x18002A, False, 'with the smith'),
'Magic Bat': (0x180015, False, 'with the bat'),
'Sick Kid': (0x339CF, False, 'with the sick'),
'Hobo': (0x33E7D, False, 'with the hobo'),
'Lost Woods Hideout': (0x180000, False, 'near a thief'),
'Lumberjack Tree': (0x180001, False, 'in a hole'),
'Cave 45': (0x180003, False, 'alone in a cave'),
'Graveyard Cave': (0x180004, False, 'alone in a cave'),
'Checkerboard Cave': (0x180005, False, 'alone in a cave'),
'Mini Moldorm Cave - Far Left': (0xEB42, False, 'near Moldorms'),
'Mini Moldorm Cave - Left': (0xEB45, False, 'near Moldorms'),
'Mini Moldorm Cave - Right': (0xEB48, False, 'near Moldorms'),
'Mini Moldorm Cave - Far Right': (0xEB4B, False, 'near Moldorms'),
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'),
'Ice Rod Cave': (0xEB4E, False, 'in a frozen cave'),
'Bonk Rock Cave': (0xEB3F, False, 'alone in a cave'),
'Library': (0x180012, False, 'near books'),
'Potion Shop': (0x180014, False, 'near potions'),
'Lake Hylia Island': (0x180144, False, 'on an island'),
'Maze Race': (0x180142, False, 'at the race'),
'Desert Ledge': (0x180143, False, 'in the desert'),
'Desert Palace - Big Chest': (0xE98F, False, 'in Desert Palace'),
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'),
'Desert Palace - Map Chest': (0xE9B6, False, 'in Desert Palace'),
'Desert Palace - Compass Chest': (0xE9CB, False, 'in Desert Palace'),
'Desert Palace - Big Key Chest': (0xE9C2, False, 'in Desert Palace'),
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'),
'Eastern Palace - Compass Chest': (0xE977, False, 'in Eastern Palace'),
'Eastern Palace - Big Chest': (0xE97D, False, 'in Eastern Palace'),
'Eastern Palace - Cannonball Chest': (0xE9B3, False, 'in Eastern Palace'),
'Eastern Palace - Big Key Chest': (0xE9B9, False, 'in Eastern Palace'),
'Eastern Palace - Map Chest': (0xE9F5, False, 'in Eastern Palace'),
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'),
'Master Sword Pedestal': (0x289B0, False, 'at the pedestal'),
'Hyrule Castle - Boomerang Chest': (0xE974, False, 'in Hyrule Castle'),
'Hyrule Castle - Map Chest': (0xEB0C, False, 'in Hyrule Castle'),
'Hyrule Castle - Zelda\'s Chest': (0xEB09, False, 'in Hyrule Castle'),
'Sewers - Dark Cross': (0xE96E, False, 'in the sewers'),
'Sewers - Secret Room - Left': (0xEB5D, False, 'in the sewers'),
'Sewers - Secret Room - Middle': (0xEB60, False, 'in the sewers'),
'Sewers - Secret Room - Right': (0xEB63, False, 'in the sewers'),
'Sanctuary': (0xEA79, False, 'in Sanctuary'),
'Castle Tower - Room 03': (0xEAB5, False, 'in Castle Tower'),
'Castle Tower - Dark Maze': (0xEAB2, False, 'in Castle Tower'),
'Old Man': (0xF69FA, False, 'with the old man'),
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'),
'Paradox Cave Lower - Far Left': (0xEB2A, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Left': (0xEB2D, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Right': (0xEB30, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Far Right': (0xEB33, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Middle': (0xEB36, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Left': (0xEB39, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Right': (0xEB3C, False, 'in a cave with seven chests'),
'Spiral Cave': (0xE9BF, False, 'in spiral cave'),
'Ether Tablet': (0x180016, False, 'at a monolith'),
'Spectacle Rock': (0x180140, False, 'atop a rock'),
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'),
'Tower of Hera - Map Chest': (0xE9AD, False, 'in Tower of Hera'),
'Tower of Hera - Big Key Chest': (0xE9E6, False, 'in Tower of Hera'),
'Tower of Hera - Compass Chest': (0xE9FB, False, 'in Tower of Hera'),
'Tower of Hera - Big Chest': (0xE9F8, False, 'in Tower of Hera'),
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'),
'Pyramid': (0x180147, False, 'on the pyramid'),
'Catfish': (0xEE185, False, 'with a catfish'),
'Stumpy': (0x330C7, False, 'with tree boy'),
'Digging Game': (0x180148, False, 'underground'),
'Bombos Tablet': (0x180017, False, 'at a monolith'),
'Hype Cave - Top': (0xEB1E, False, 'near a bat-like man'),
'Hype Cave - Middle Right': (0xEB21, False, 'near a bat-like man'),
'Hype Cave - Middle Left': (0xEB24, False, 'near a bat-like man'),
'Hype Cave - Bottom': (0xEB27, False, 'near a bat-like man'),
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'),
'Peg Cave': (0x180006, False, 'alone in a cave'),
'Pyramid Fairy - Left': (0xE980, False, 'near a fairy'),
'Pyramid Fairy - Right': (0xE983, False, 'near a fairy'),
'Brewery': (0xE9EC, False, 'alone in a home'),
'C-Shaped House': (0xE9EF, False, 'alone in a home'),
'Chest Game': (0xEDA8, False, 'as a prize'),
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'),
'Mire Shed - Left': (0xEA73, False, 'near sparks'),
'Mire Shed - Right': (0xEA76, False, 'near sparks'),
'Superbunny Cave - Top': (0xEA7C, False, 'in a connection'),
'Superbunny Cave - Bottom': (0xEA7F, False, 'in a connection'),
'Spike Cave': (0xEA8B, False, 'beyond spikes'),
'Hookshot Cave - Top Right': (0xEB51, False, 'across pits'),
'Hookshot Cave - Top Left': (0xEB54, False, 'across pits'),
'Hookshot Cave - Bottom Right': (0xEB5A, False, 'across pits'),
'Hookshot Cave - Bottom Left': (0xEB57, False, 'across pits'),
'Floating Island': (0x180141, False, 'on an island'),
'Mimic Cave': (0xE9C5, False, 'in a cave of mimicry'),
'Swamp Palace - Entrance': (0xEA9D, False, 'in Swamp Palace'),
'Swamp Palace - Map Chest': (0xE986, False, 'in Swamp Palace'),
'Swamp Palace - Big Chest': (0xE989, False, 'in Swamp Palace'),
'Swamp Palace - Compass Chest': (0xEAA0, False, 'in Swamp Palace'),
'Swamp Palace - Big Key Chest': (0xEAA6, False, 'in Swamp Palace'),
'Swamp Palace - West Chest': (0xEAA3, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Left': (0xEAA9, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Right': (0xEAAC, False, 'in Swamp Palace'),
'Swamp Palace - Waterfall Room': (0xEAAF, False, 'in Swamp Palace'),
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'),
'Thieves\' Town - Big Key Chest': (0xEA04, False, 'in Thieves\' Town'),
'Thieves\' Town - Map Chest': (0xEA01, False, 'in Thieves\' Town'),
'Thieves\' Town - Compass Chest': (0xEA07, False, 'in Thieves\' Town'),
'Thieves\' Town - Ambush Chest': (0xEA0A, False, 'in Thieves\' Town'),
'Thieves\' Town - Attic': (0xEA0D, False, 'in Thieves\' Town'),
'Thieves\' Town - Big Chest': (0xEA10, False, 'in Thieves\' Town'),
'Thieves\' Town - Blind\'s Cell': (0xEA13, False, 'in Thieves\' Town'),
'Thieves\' Town - Boss': (0x180156, False, 'with Blind'),
'Skull Woods - Compass Chest': (0xE992, False, 'in Skull Woods'),
'Skull Woods - Map Chest': (0xE99B, False, 'in Skull Woods'),
'Skull Woods - Big Chest': (0xE998, False, 'in Skull Woods'),
'Skull Woods - Pot Prison': (0xE9A1, False, 'in Skull Woods'),
'Skull Woods - Pinball Room': (0xE9C8, False, 'in Skull Woods'),
'Skull Woods - Big Key Chest': (0xE99E, False, 'in Skull Woods'),
'Skull Woods - Bridge Room': (0xE9FE, False, 'near Mothula'),
'Skull Woods - Boss': (0x180155, False, 'with Mothula'),
'Ice Palace - Compass Chest': (0xE9D4, False, 'in Ice Palace'),
'Ice Palace - Freezor Chest': (0xE995, False, 'in Ice Palace'),
'Ice Palace - Big Chest': (0xE9AA, False, 'in Ice Palace'),
'Ice Palace - Iced T Room': (0xE9E3, False, 'in Ice Palace'),
'Ice Palace - Spike Room': (0xE9E0, False, 'in Ice Palace'),
'Ice Palace - Big Key Chest': (0xE9A4, False, 'in Ice Palace'),
'Ice Palace - Map Chest': (0xE9DD, False, 'in Ice Palace'),
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'),
'Misery Mire - Big Chest': (0xEA67, False, 'in Misery Mire'),
'Misery Mire - Map Chest': (0xEA6A, False, 'in Misery Mire'),
'Misery Mire - Main Lobby': (0xEA5E, False, 'in Misery Mire'),
'Misery Mire - Bridge Chest': (0xEA61, False, 'in Misery Mire'),
'Misery Mire - Spike Chest': (0xE9DA, False, 'in Misery Mire'),
'Misery Mire - Compass Chest': (0xEA64, False, 'in Misery Mire'),
'Misery Mire - Big Key Chest': (0xEA6D, False, 'in Misery Mire'),
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'),
'Turtle Rock - Compass Chest': (0xEA22, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Left': (0xEA1C, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Right': (0xEA1F, False, 'in Turtle Rock'),
'Turtle Rock - Chain Chomps': (0xEA16, False, 'in Turtle Rock'),
'Turtle Rock - Big Key Chest': (0xEA25, False, 'in Turtle Rock'),
'Turtle Rock - Big Chest': (0xEA19, False, 'in Turtle Rock'),
'Turtle Rock - Crystaroller Room': (0xEA34, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Left': (0xEA31, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Right': (0xEA2E, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Left': (0xEA2B, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Right': (0xEA28, False, 'in Turtle Rock'),
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'),
'Palace of Darkness - Shooter Room': (0xEA5B, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Bridge': (0xEA3D, False, 'in Palace of Darkness'),
'Palace of Darkness - Stalfos Basement': (0xEA49, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Key Chest': (0xEA37, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Ledge': (0xEA3A, False, 'in Palace of Darkness'),
'Palace of Darkness - Map Chest': (0xEA52, False, 'in Palace of Darkness'),
'Palace of Darkness - Compass Chest': (0xEA43, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Left': (0xEA4C, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Right': (0xEA4F, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Top': (0xEA55, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Bottom': (0xEA58, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Chest': (0xEA40, False, 'in Palace of Darkness'),
'Palace of Darkness - Harmless Hellway': (0xEA46, False, 'in Palace of Darkness'),
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'),
'Ganons Tower - Bob\'s Torch': (0x180161, False, 'in Ganon\'s Tower'),
'Ganons Tower - Hope Room - Left': (0xEAD9, False, 'in Ganon\'s Tower'),
'Ganons Tower - Hope Room - Right': (0xEADC, False, 'in Ganon\'s Tower'),
'Ganons Tower - Tile Room': (0xEAE2, False, 'in Ganon\'s Tower'),
'Ganons Tower - Compass Room - Top Left': (0xEAE5, False, 'in Ganon\'s Tower'),
'Ganons Tower - Compass Room - Top Right': (0xEAE8, False, 'in Ganon\'s Tower'),
'Ganons Tower - Compass Room - Bottom Left': (0xEAEB, False, 'in Ganon\'s Tower'),
'Ganons Tower - Compass Room - Bottom Right': (0xEAEE, False, 'in Ganon\'s Tower'),
'Ganons Tower - DMs Room - Top Left': (0xEAB8, False, 'in Ganon\'s Tower'),
'Ganons Tower - DMs Room - Top Right': (0xEABB, False, 'in Ganon\'s Tower'),
'Ganons Tower - DMs Room - Bottom Left': (0xEABE, False, 'in Ganon\'s Tower'),
'Ganons Tower - DMs Room - Bottom Right': (0xEAC1, False, 'in Ganon\'s Tower'),
'Ganons Tower - Map Chest': (0xEAD3, False, 'in Ganon\'s Tower'),
'Ganons Tower - Firesnake Room': (0xEAD0, False, 'in Ganon\'s Tower'),
'Ganons Tower - Randomizer Room - Top Left': (0xEAC4, False, 'in Ganon\'s Tower'),
'Ganons Tower - Randomizer Room - Top Right': (0xEAC7, False, 'in Ganon\'s Tower'),
'Ganons Tower - Randomizer Room - Bottom Left': (0xEACA, False, 'in Ganon\'s Tower'),
'Ganons Tower - Randomizer Room - Bottom Right': (0xEACD, False, 'in Ganon\'s Tower'),
'Ganons Tower - Bob\'s Chest': (0xEADF, False, 'in Ganon\'s Tower'),
'Ganons Tower - Big Chest': (0xEAD6, False, 'in Ganon\'s Tower'),
'Ganons Tower - Big Key Room - Left': (0xEAF4, False, 'in Ganon\'s Tower'),
'Ganons Tower - Big Key Room - Right': (0xEAF7, False, 'in Ganon\'s Tower'),
'Ganons Tower - Big Key Chest': (0xEAF1, False, 'in Ganon\'s Tower'),
'Ganons Tower - Mini Helmasaur Room - Left': (0xEAFD, False, 'atop Ganon\'s Tower'),
'Ganons Tower - Mini Helmasaur Room - Right': (0xEB00, False, 'atop Ganon\'s Tower'),
'Ganons Tower - Pre-Moldorm Chest': (0xEB03, False, 'atop Ganon\'s Tower'),
'Ganons Tower - Validation Chest': (0xEB06, False, 'atop Ganon\'s Tower'),
'Ganon': (None, False, 'from me'),
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'),
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'),
'Floodgate': (None, False, None),
'Frog': (None, False, None),
'Missing Smith': (None, False, None),
'Dark Blacksmith Ruins': (None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'),
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'),
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'),
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'),
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'),
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'),
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'),
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'),
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')}

View File

@ -13,7 +13,7 @@ from Items import ItemFactory
#This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
#Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp',
alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Flute', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp',
'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang']
progressivegloves = ['Progressive Glove'] * 2
basicgloves = ['Power Glove', 'Titans Mitts']
@ -35,7 +35,7 @@ Difficulty = namedtuple('Difficulty',
'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother',
'triforcehunt', 'triforce_pieces_required', 'retro',
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
'progressive_armor_limit', 'progressive_bottle_limit',
'progressive_armor_limit', 'progressive_bottle_limit',
'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit'])
total_items_to_place = 153
@ -51,8 +51,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2,
basicarmor = ['Blue Mail', 'Red Mail'],
swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3,
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'],
progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
basicbow = ['Bow', 'Silver Arrows'],
timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@ -78,8 +78,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2,
basicarmor = ['Progressive Armor'] * 2, # neither will count
swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3,
basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'],
progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@ -105,8 +105,8 @@ difficulties = {
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'],
progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@ -125,26 +125,30 @@ difficulties = {
}
def generate_itempool(world, player):
if (world.difficulty not in ['normal', 'hard', 'expert'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals']
or world.mode not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal',
'dungeons',
'triforcehunt',
'crystals']
or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer[player] not in [False,
'display',
'timed',
'timed-ohko',
'ohko',
'timed-countdown']):
raise NotImplementedError('Not supported yet')
if world.timer[player] in ['ohko', 'timed-ohko']:
world.can_take_damage[player] = False
if world.timer in ['ohko', 'timed-ohko']:
world.can_take_damage = False
if world.goal in ['pedestal', 'triforcehunt']:
if world.goal[player] in ['pedestal', 'triforcehunt']:
world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False)
else:
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
if world.goal in ['triforcehunt']:
if world.mode == 'inverted':
region = world.get_region('Light World',player)
else:
region = world.get_region('Hyrule Castle Courtyard', player)
if world.goal[player] in ['triforcehunt']:
region = world.get_region('Light World',player)
loc = Location(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) > state.world.treasure_hunt_count
loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) > state.world.treasure_hunt_count[player]
region.locations.append(loc)
world.dynamic_locations.append(loc)
@ -153,7 +157,7 @@ def generate_itempool(world, player):
world.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True
loc.locked = True
world.get_location('Ganon', player).event = True
world.get_location('Ganon', player).locked = True
world.push_item(world.get_location('Agahnim 1', player), ItemFactory('Beat Agahnim 1', player), False)
@ -177,38 +181,89 @@ def generate_itempool(world, player):
# set up item pool
if world.custom:
(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)
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive[player], world.shuffle[player],
world.difficulty[player], world.timer[player], world.goal[player],
world.mode[player], world.swords[player],
world.retro[player], world.customitemarray)
world.rupoor_cost = min(world.customitemarray[69], 9999)
else:
(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)
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
lamps_needed_for_dark_rooms) = get_pool_core(world.progressive[player], world.shuffle[player],
world.difficulty[player], world.timer[player], world.goal[player],
world.mode[player], world.swords[player], world.retro[player])
for item in precollected_items:
world.push_precollected(ItemFactory(item, player))
for (location, item) in placed_items:
if world.mode[player] == 'standard' and not world.state.has_blunt_weapon(player):
if "Link's Uncle" not in placed_items:
found_sword = False
found_bow = False
possible_weapons = []
for item in pool:
if item in ['Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']:
if not found_sword and world.swords[player] != 'swordless':
found_sword = True
possible_weapons.append(item)
if item in ['Progressive Bow', 'Bow'] and not found_bow:
found_bow = True
possible_weapons.append(item)
if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
if item not in possible_weapons:
possible_weapons.append(item)
starting_weapon = random.choice(possible_weapons)
placed_items["Link's Uncle"] = starting_weapon
pool.remove(starting_weapon)
if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and world.enemy_health[player] not in ['default', 'easy']:
world.escape_assist[player].append('bombs')
for (location, item) in placed_items.items():
world.push_item(world.get_location(location, player), ItemFactory(item, player), False)
world.get_location(location, player).event = True
world.get_location(location, player).locked = True
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
if clock_mode is not None:
world.clock_mode = clock_mode
if treasure_hunt_count is not None:
world.treasure_hunt_count = treasure_hunt_count
if treasure_hunt_icon is not None:
world.treasure_hunt_icon = treasure_hunt_icon
if world.keysanity:
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player])
items = ItemFactory(pool, player)
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
if clock_mode is not None:
world.clock_mode[player] = clock_mode
if treasure_hunt_count is not None:
world.treasure_hunt_count[player] = treasure_hunt_count
if treasure_hunt_icon is not None:
world.treasure_hunt_icon[player] = treasure_hunt_icon
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
and ((item.smallkey and world.keyshuffle[player])
or (item.bigkey and world.bigkeyshuffle[player])
or (item.map and world.mapshuffle[player])
or (item.compass and world.compassshuffle[player]))])
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if world.difficulty in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
[item for item in world.itempool if item.name == 'Boss Heart Container' and item.player == player][0].advancement = True
elif world.difficulty in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
adv_heart_pieces = [item for item in world.itempool if item.name == 'Piece of Heart' and item.player == player][0:4]
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
[item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
for hp in adv_heart_pieces:
hp.advancement = True
beeweights = {0: {None: 100},
1: {None: 75, 'trap': 25},
2: {None: 40, 'trap': 40, 'bee': 20},
3: {'trap': 50, 'bee': 50},
4: {'trap': 100}}
def beemizer(item):
if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type:
choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0]
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
return item
world.itempool += [beemizer(item) for item in items]
# shuffle medallions
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
@ -217,7 +272,7 @@ def generate_itempool(world, player):
place_bosses(world, player)
set_up_shops(world, player)
if world.retro:
if world.retro[player]:
set_up_take_anys(world, player)
create_dynamic_shop_locations(world, player)
@ -233,9 +288,9 @@ take_any_locations = [
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
def set_up_take_anys(world, player):
if world.mode == 'inverted' and 'Dark Sanctuary Hint' in take_any_locations:
if world.mode[player] == 'inverted' and 'Dark Sanctuary Hint' in take_any_locations:
take_any_locations.remove('Dark Sanctuary Hint')
regions = random.sample(take_any_locations, 5)
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player)
@ -244,11 +299,10 @@ def set_up_take_anys(world, player):
reg = regions.pop()
entrance = world.get_region(reg, player).entrances[0]
connect_entrance(world, entrance, old_man_take_any, player)
connect_entrance(world, entrance.name, old_man_take_any.name, player)
entrance.target = 0x58
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True)
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True, True)
world.shops.append(old_man_take_any.shop)
old_man_take_any.shop.active = True
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
if swords:
@ -267,15 +321,14 @@ def set_up_take_anys(world, player):
target, room_id = random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
reg = regions.pop()
entrance = world.get_region(reg, player).entrances[0]
connect_entrance(world, entrance, take_any, player)
connect_entrance(world, entrance.name, take_any.name, player)
entrance.target = target
take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True)
take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True, True)
world.shops.append(take_any.shop)
take_any.shop.active = True
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
world.intialize_regions()
world.initialize_regions()
def create_dynamic_shop_locations(world, player):
for shop in world.shops:
@ -312,9 +365,9 @@ def fill_prizes(world, attempts=15):
prize_locs = list(empty_crystal_locations)
random.shuffle(prizepool)
random.shuffle(prize_locs)
fill_restrictive(world, all_state, prize_locs, prizepool)
fill_restrictive(world, all_state, prize_locs, prizepool, True)
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 - attempt - 1)
for location in empty_crystal_locations:
location.item = None
continue
@ -324,30 +377,23 @@ def fill_prizes(world, attempts=15):
def set_up_shops(world, player):
# Changes to basic Shops
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops
for shop in world.shops:
shop.active = True
if world.retro:
if world.retro[player]:
rss = world.get_region('Red Shield Shop', player).shop
rss.active = True
rss.add_inventory(2, 'Single Arrow', 80)
# Randomized changes to Shops
if world.retro:
for shop in random.sample([s for s in world.shops if s.replaceable and s.type == ShopType.Shop and s.region.player == player], 5):
shop.active = True
if not rss.locked:
rss.add_inventory(2, 'Single Arrow', 80)
for shop in 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
shop.add_inventory(0, 'Single Arrow', 80)
shop.add_inventory(1, 'Small Key (Universal)', 100)
shop.add_inventory(2, 'Bombs (10)', 50)
rss.locked = True
#special shop types
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro):
pool = []
placed_items = []
placed_items = {}
precollected_items = []
clock_mode = None
treasure_hunt_count = None
@ -355,6 +401,10 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
pool.extend(alwaysitems)
def place_item(loc, item):
assert loc not in placed_items
placed_items[loc] = item
def want_progressives():
return random.choice([True, False]) if progressive == 'random' else progressive == 'on'
@ -366,9 +416,9 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
lamps_needed_for_dark_rooms = 1
# insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
if shuffle == 'insanity_legacy':
placed_items.append(('Link\'s House', 'Magic Mirror'))
placed_items.append(('Sanctuary', 'Moon Pearl'))
if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
else:
pool.extend(['Magic Mirror', 'Moon Pearl'])
@ -399,50 +449,37 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
else:
pool.extend(diff.basicarmor)
if swords != 'swordless':
if want_progressives():
pool.extend(['Progressive Bow'] * 2)
else:
pool.extend(diff.basicbow)
if want_progressives():
pool.extend(['Progressive Bow'] * 2)
elif swords != 'swordless':
pool.extend(diff.basicbow)
else:
pool.extend(['Bow', 'Silver Arrows'])
if swords == 'swordless':
pool.extend(diff.swordless)
if want_progressives():
pool.extend(['Progressive Bow'] * 2)
else:
pool.extend(['Bow', 'Silver Arrows'])
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'])
swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
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()))
place_item('Link\'s Uncle', swords_to_use.pop())
place_item('Blacksmith', swords_to_use.pop())
place_item('Pyramid Fairy - Left', swords_to_use.pop())
if goal != 'pedestal':
placed_items.append(('Master Sword Pedestal', swords_to_use.pop()))
place_item('Master Sword Pedestal', swords_to_use.pop())
else:
placed_items.append(('Master Sword Pedestal', 'Triforce'))
place_item('Master Sword Pedestal', 'Triforce')
else:
if want_progressives():
pool.extend(diff.progressivesword)
pool.extend(['Progressive Sword'])
else:
pool.extend(diff.basicsword)
pool.extend(['Fighter Sword'])
progressive_swords = want_progressives()
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
if swords == 'assured':
if progressive_swords:
precollected_items.append('Progressive Sword')
pool.remove('Progressive Sword')
else:
precollected_items.append('Fighter Sword')
pool.remove('Fighter Sword')
pool.extend(['Rupees (50)'])
extraitems = total_items_to_place - len(pool) - len(placed_items)
@ -466,7 +503,7 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
extraitems -= len(extra)
if goal == 'pedestal' and swords != 'vanilla':
placed_items.append(('Master Sword Pedestal', 'Triforce'))
place_item('Master Sword Pedestal', 'Triforce')
if retro:
pool = [item.replace('Single Arrow','Rupees (5)') for item in pool]
pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool]
@ -475,30 +512,34 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
pool.extend(diff.retro)
if mode == 'standard':
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
placed_items.append((key_location, 'Small Key (Universal)'))
place_item(key_location, 'Small Key (Universal)')
else:
pool.extend(['Small Key (Universal)'])
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 = []
placed_items = {}
precollected_items = []
clock_mode = None
treasure_hunt_count = None
treasure_hunt_icon = None
def place_item(loc, item):
assert loc not in placed_items
placed_items[loc] = item
# Correct for insanely oversized item counts and take initial steps to handle undersized pools.
for x in range(0, 64):
for x in range(0, 66):
if customitemarray[x] > total_items_to_place:
customitemarray[x] = total_items_to_place
if customitemarray[66] > total_items_to_place:
customitemarray[66] = total_items_to_place
if customitemarray[68] > total_items_to_place:
customitemarray[68] = total_items_to_place
itemtotal = 0
for x in range(0, 65):
for x in range(0, 66):
itemtotal = itemtotal + customitemarray[x]
itemtotal = itemtotal + customitemarray[66]
itemtotal = itemtotal + customitemarray[68]
itemtotal = itemtotal + customitemarray[70]
pool.extend(['Bow'] * customitemarray[0])
pool.extend(['Silver Arrows']* customitemarray[1])
@ -515,7 +556,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
pool.extend(['Lamp'] * customitemarray[12])
pool.extend(['Hammer'] * customitemarray[13])
pool.extend(['Shovel'] * customitemarray[14])
pool.extend(['Ocarina'] * customitemarray[15])
pool.extend(['Flute'] * customitemarray[15])
pool.extend(['Bug Catching Net'] * customitemarray[16])
pool.extend(['Book of Mudora'] * customitemarray[17])
pool.extend(['Cane of Somaria'] * customitemarray[19])
@ -559,8 +600,10 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
pool.extend(['Blue Clock'] * customitemarray[61])
pool.extend(['Green Clock'] * customitemarray[62])
pool.extend(['Red Clock'] * customitemarray[63])
pool.extend(['Triforce Piece'] * customitemarray[64])
pool.extend(['Triforce'] * customitemarray[66])
pool.extend(['Progressive Bow'] * customitemarray[64])
pool.extend(['Bombs (10)'] * customitemarray[65])
pool.extend(['Triforce Piece'] * customitemarray[66])
pool.extend(['Triforce'] * customitemarray[68])
diff = difficulties[difficulty]
@ -575,12 +618,12 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
thisbottle = random.choice(diff.bottles)
pool.append(thisbottle)
if customitemarray[64] > 0 or customitemarray[65] > 0:
treasure_hunt_count = max(min(customitemarray[65], 99), 1) #To display, count must be between 1 and 99.
if customitemarray[66] > 0 or customitemarray[67] > 0:
treasure_hunt_count = max(min(customitemarray[67], 99), 1) #To display, count must be between 1 and 99.
treasure_hunt_icon = 'Triforce Piece'
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
if (customitemarray[64] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[66] == 0):
extrapieces = treasure_hunt_count - customitemarray[64]
if (customitemarray[66] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[68] == 0):
extrapieces = treasure_hunt_count - customitemarray[66]
pool.extend(['Triforce Piece'] * extrapieces)
itemtotal = itemtotal + extrapieces
@ -592,25 +635,25 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
clock_mode = 'ohko'
if goal == 'pedestal':
placed_items.append(('Master Sword Pedestal', 'Triforce'))
place_item('Master Sword Pedestal', 'Triforce')
itemtotal = itemtotal + 1
if mode == 'standard':
if retro:
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
placed_items.append((key_location, 'Small Key (Universal)'))
pool.extend(['Small Key (Universal)'] * max((customitemarray[68] - 1), 0))
place_item(key_location, 'Small Key (Universal)')
pool.extend(['Small Key (Universal)'] * max((customitemarray[70] - 1), 0))
else:
pool.extend(['Small Key (Universal)'] * customitemarray[68])
pool.extend(['Small Key (Universal)'] * customitemarray[70])
else:
pool.extend(['Small Key (Universal)'] * customitemarray[68])
pool.extend(['Small Key (Universal)'] * customitemarray[70])
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'))
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
pool.extend(['Magic Mirror'] * max((customitemarray[22] -1 ), 0))
pool.extend(['Moon Pearl'] * max((customitemarray[28] - 1), 0))
else:
@ -628,13 +671,14 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
def test():
for difficulty in ['normal', 'hard', 'expert']:
for goal in ['ganon', 'triforcehunt', 'pedestal']:
for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
for timer in [False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
for mode in ['open', 'standard', 'inverted']:
for swords in ['random', 'assured', 'swordless', 'vanilla']:
for progressive in ['on', 'off']:
for shuffle in ['full', 'insanity_legacy']:
for retro in [True, False]:
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro)
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords,
retro)
count = len(out[0]) + len(out[1])
correct_count = total_items_to_place

View File

@ -25,11 +25,12 @@ def ItemFactory(items, player):
# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Progressive Bow (Alt)': (True, False, None, 0x65, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'),
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'),
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'),
'Magic Mirror': (True, False, None, 0x1A, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'),
'Ocarina': (True, False, None, 0x14, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'),
'Flute': (True, False, None, 0x14, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'flute boy plays again', 'the Flute'),
'Pegasus Boots': (True, False, None, 0x4B, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the Boots'),
'Power Glove': (True, False, None, 0x1B, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the glove'),
'Cape': (True, False, None, 0x19, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'),
@ -43,14 +44,14 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'),
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'),
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'),
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'),
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'),
'Bottle (Green Potion)': (True, False, None, 0x2C, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a Bottle'),
'Bottle (Blue Potion)': (True, False, None, 0x2D, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a Bottle'),
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a Bottle'),
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a Bottle'),
'Bottle (Bee)': (True, False, None, 0x3C, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a Bottle'),
'Bottle (Good Bee)': (True, False, None, 0x48, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a Bottle'),
'Master Sword': (True, False, 'Sword', 0x50, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'),
@ -60,19 +61,19 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Progressive Sword': (True, False, 'Sword', 0x5E, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a sword'),
'Progressive Glove': (True, False, None, 0x61, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a glove'),
'Silver Arrows': (True, False, None, 0x58, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the silver arrows'),
'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01], None, None, None, None, None, None, None),
'Red Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02], None, None, None, None, None, None, None),
'Blue Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03], None, None, None, None, None, None, None),
'Green Pendant': (True, False, 'Crystal', (0x04, 0x38, 0x62, 0x00, 0x69, 0x01), None, None, None, None, None, None, None),
'Red Pendant': (True, False, 'Crystal', (0x02, 0x34, 0x60, 0x00, 0x69, 0x02), None, None, None, None, None, None, None),
'Blue Pendant': (True, False, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, None),
'Triforce': (True, False, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
'Power Star': (True, False, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
'Triforce Piece': (True, False, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06], None, None, None, None, None, None, None),
'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06], None, None, None, None, None, None, None),
'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06], None, None, None, None, None, None, None),
'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x06], None, None, None, None, None, None, None),
'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06], None, None, None, None, None, None, None),
'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06], None, None, None, None, None, None, None),
'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06], None, None, None, None, None, None, None),
'Crystal 1': (True, False, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, None),
'Crystal 2': (True, False, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, None),
'Crystal 3': (True, False, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, None),
'Crystal 4': (True, False, 'Crystal', (0x20, 0x34, 0x64, 0x40, 0x6D, 0x06), None, None, None, None, None, None, None),
'Crystal 5': (True, False, 'Crystal', (0x04, 0x32, 0x64, 0x40, 0x6E, 0x06), None, None, None, None, None, None, None),
'Crystal 6': (True, False, 'Crystal', (0x01, 0x32, 0x64, 0x40, 0x6F, 0x06), None, None, None, None, None, None, None),
'Crystal 7': (True, False, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, None),
'Single Arrow': (False, False, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
'Arrows (10)': (False, False, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'),
'Arrow Upgrade (+10)': (False, False, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
@ -161,15 +162,20 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
'Red Potion': (False, False, None, 0x2E, None, None, None, None, None, None, None),
'Green Potion': (False, False, None, 0x2F, None, None, None, None, None, None, None),
'Blue Potion': (False, False, None, 0x30, None, None, None, None, None, None, None),
'Bee': (False, False, None, 0x0E, None, None, None, None, None, None, None),
'Small Heart': (False, False, None, 0x42, None, None, None, None, None, None, None),
'Bee Trap': (False, False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'friendship'),
'Red Potion': (False, False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
'Green Potion': (False, False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
'Blue Potion': (False, False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
'Bee': (False, False, None, 0x0E, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee'),
'Small Heart': (False, False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'),
'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None, None),
'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None, None),
'Get Frog': (True, False, 'Event', None, None, None, None, None, None, None, None),
'Return Smith': (True, False, 'Event', None, None, None, None, None, None, None, None),
'Pick Up Purple Chest': (True, False, 'Event', None, None, None, None, None, None, None, None),
'Open Floodgate': (True, False, 'Event', None, None, None, None, None, None, None, None),
}
}
lookup_id_to_name = {data[3]: name for name, data in item_table.items()}
hint_blacklist = {"Triforce"}

View File

@ -1,6 +1,7 @@
MIT License
Copyright (c) 2017 LLCoolDave
Copyright (c) 2020 Berserker66
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

320
Main.py
View File

@ -6,25 +6,33 @@ import logging
import os
import random
import time
import zlib
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
from Regions import create_regions, mark_light_world_regions
from Items import ItemFactory
from Regions import create_regions, create_shops, mark_light_world_regions
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances
from Rom import patch_rom, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path
from Utils import output_path, parse_player_names
__version__ = '0.6.3-pre'
def main(args, seed=None):
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
start = time.perf_counter()
# initialize the world
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints)
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty,
args.item_functionality, args.timer, args.progressive.copy(), args.goal, args.algorithm,
args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
logger = logging.getLogger('')
if seed is None:
random.seed(None)
@ -33,36 +41,65 @@ def main(args, seed=None):
world.seed = int(seed)
random.seed(world.seed)
world.crystals_needed_for_ganon = random.randint(0, 7) if args.crystals_ganon == 'random' else int(args.crystals_ganon)
world.crystals_needed_for_gt = random.randint(0, 7) if args.crystals_gt == 'random' else int(args.crystals_gt)
world.remote_items = args.remote_items.copy()
world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
world.open_pyramid = args.openpyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.shuffleenemies.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.beemizer = args.beemizer.copy()
world.timer = args.timer.copy()
world.shufflepots = args.shufflepots.copy()
world.progressive = args.progressive.copy()
world.extendedmsu = args.extendedmsu.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n', __version__, world.seed)
world.difficulty_requirements = difficulties[world.difficulty]
parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names)
for i, team in enumerate(parsed_names, 1):
if world.players > 1:
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
for player, name in enumerate(team, 1):
world.player_names[player].append(name)
if world.mode != 'inverted':
for player in range(1, world.players + 1):
logger.info('')
for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none':
world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player)
if item:
world.push_precollected(item)
if world.mode[player] != 'inverted':
create_regions(world, player)
create_dungeons(world, player)
else:
for player in range(1, world.players + 1):
else:
create_inverted_regions(world, player)
create_dungeons(world, player)
create_shops(world, player)
create_dungeons(world, player)
logger.info('Shuffling the World about.')
if world.mode != 'inverted':
for player in range(1, world.players + 1):
for player in range(1, world.players + 1):
if world.mode[player] != 'inverted':
link_entrances(world, player)
mark_light_world_regions(world)
else:
for player in range(1, world.players + 1):
mark_light_world_regions(world, player)
else:
link_inverted_entrances(world, player)
mark_dark_world_regions(world)
mark_dark_world_regions(world, player)
logger.info('Generating Item Pool.')
@ -81,7 +118,8 @@ def main(args, seed=None):
logger.info('Placing Dungeon Items.')
shuffled_locations = None
if args.algorithm in ['balanced', 'vt26'] or args.keysanity:
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
shuffled_locations = world.get_unfilled_locations()
random.shuffle(shuffled_locations)
fill_dungeons_restrictive(world, shuffled_locations)
@ -99,12 +137,12 @@ def main(args, seed=None):
elif args.algorithm == 'freshness':
distribute_items_staleness(world)
elif args.algorithm == 'vt25':
distribute_items_restrictive(world, 0)
distribute_items_restrictive(world, False)
elif args.algorithm == 'vt26':
distribute_items_restrictive(world, gt_filler(world), shuffled_locations)
distribute_items_restrictive(world, True, shuffled_locations)
elif args.algorithm == 'balanced':
distribute_items_restrictive(world, gt_filler(world))
distribute_items_restrictive(world, True)
if world.players > 1:
logger.info('Balancing multiworld progression.')
@ -112,55 +150,105 @@ def main(args, seed=None):
logger.info('Patching ROM.')
if args.sprite is not None:
if isinstance(args.sprite, Sprite):
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
outfilebase = 'ER_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "", world.seed)
use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots)
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
rom_names = []
jsonout = {}
if not args.suppress_rom:
if world.players > 1:
raise NotImplementedError("Multiworld rom writes have not been implemented")
def _gen_rom(team: int, player: int):
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or args.shufflepots[player] or sprite_random_on_hit)
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom, extendedmsu=args.extendedmsu[player])
patch_rom(world, rom, player, team, use_enemizer)
if use_enemizer and (args.enemizercli or not args.jsonout):
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player],
sprite_random_on_hit, extendedmsu=args.extendedmsu[player])
if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000, args.extendedmsu[player])
if args.race:
patch_race_rom(rom)
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
args.ow_palettes[player], args.uw_palettes[player])
if args.jsonout:
jsonout[f'patch_t{team}_p{player}'] = rom.patches
else:
player = 1
mcsb_name = ''
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-keysanity'
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]].count(True) == 1:
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[
player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-%s%s%s%sshuffle' % (
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
local_rom = None
if args.jsonout:
rom = JsonRom()
else:
if use_enemizer:
local_rom = LocalRom(args.rom)
rom = JsonRom()
else:
rom = LocalRom(args.rom)
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
if world.players > 1:
outfilepname += f'_P{player}'
if world.players > 1 or world.teams > 1:
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][
team] != 'Player %d' % player else ''
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player],
world.difficulty_adjustments[player],
world.mode[player], world.goal[player],
"" if world.timer[player] in [False,
'display'] else "-" +
world.timer[
player],
world.shuffle[player], world.algorithm,
mcsb_name,
"-retro" if world.retro[player] else "",
"-prog_" + world.progressive[player] if
world.progressive[player] in ['off',
'random'] else "",
"-nohints" if not world.hints[
player] else "")) if not args.outputname else ''
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
rom.write_to_file(rompath)
if args.create_diff:
import Patch
Patch.create_patch_file(rompath)
return (player, team, list(rom.name))
patch_rom(world, player, rom)
enemizer_patch = []
if use_enemizer:
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shuffleenemies, args.enemy_health, args.enemy_damage, args.shufflepalette, args.shufflepots)
if args.jsonout:
jsonout['patch'] = rom.patches
if use_enemizer:
jsonout['enemizer' % player] = enemizer_patch
else:
if use_enemizer:
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch)
rom = local_rom
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite)
rom.write_to_file(output_path('%s.sfc' % outfilebase))
if args.create_spoiler and not args.jsonout:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
if not args.suppress_rom:
import concurrent.futures
futures = []
with concurrent.futures.ThreadPoolExecutor() as pool:
for team in range(world.teams):
for player in range(1, world.players + 1):
futures.append(pool.submit(_gen_rom, team, player))
for future in futures:
rom_name = future.result()
rom_names.append(rom_name)
multidata = zlib.compress(json.dumps({"names": parsed_names,
"roms": rom_names,
"remote_items": [player for player in range(1, world.players + 1) if
world.remote_items[player]],
"locations": [((location.address, location.player),
(location.item.code, location.item.player))
for location in world.get_filled_locations() if
type(location.address) is int]
}).encode("utf-8"))
if args.jsonout:
jsonout["multidata"] = list(multidata)
else:
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
f.write(multidata)
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
@ -176,44 +264,54 @@ def main(args, seed=None):
return world
def gt_filler(world):
if world.goal == 'triforcehunt':
return random.randint(15, 50)
return random.randint(0, 15)
def copy_world(world):
# ToDo: Not good yet
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints)
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
ret.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
ret.powder_patch_required = world.powder_patch_required.copy()
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
ret.treasure_hunt_count = world.treasure_hunt_count
ret.treasure_hunt_icon = world.treasure_hunt_icon
ret.sewer_light_cone = world.sewer_light_cone
ret.treasure_hunt_count = world.treasure_hunt_count.copy()
ret.treasure_hunt_icon = world.treasure_hunt_icon.copy()
ret.sewer_light_cone = world.sewer_light_cone.copy()
ret.light_world_light_cone = world.light_world_light_cone
ret.dark_world_light_cone = world.dark_world_light_cone
ret.seed = world.seed
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge
ret.can_access_trock_front = world.can_access_trock_front
ret.can_access_trock_big_chest = world.can_access_trock_big_chest
ret.can_access_trock_middle = world.can_access_trock_middle
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy()
ret.can_access_trock_front = world.can_access_trock_front.copy()
ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy()
ret.can_access_trock_middle = world.can_access_trock_middle.copy()
ret.can_take_damage = world.can_take_damage
ret.difficulty_requirements = world.difficulty_requirements
ret.fix_fake_world = world.fix_fake_world
ret.difficulty_requirements = world.difficulty_requirements.copy()
ret.fix_fake_world = world.fix_fake_world.copy()
ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon
ret.crystals_needed_for_gt = world.crystals_needed_for_gt
ret.mapshuffle = world.mapshuffle.copy()
ret.compassshuffle = world.compassshuffle.copy()
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.open_pyramid = world.open_pyramid.copy()
ret.boss_shuffle = world.boss_shuffle.copy()
ret.enemy_shuffle = world.enemy_shuffle.copy()
ret.enemy_health = world.enemy_health.copy()
ret.enemy_damage = world.enemy_damage.copy()
ret.beemizer = world.beemizer.copy()
ret.timer = world.timer.copy()
ret.shufflepots = world.shufflepots.copy()
ret.extendedmsu = world.extendedmsu.copy()
if world.mode != 'inverted':
for player in range(1, world.players + 1):
for player in range(1, world.players + 1):
if world.mode[player] != 'inverted':
create_regions(ret, player)
create_dungeons(ret, player)
else:
for player in range(1, world.players + 1):
else:
create_inverted_regions(ret, player)
create_dungeons(ret, player)
create_shops(ret, player)
create_dungeons(ret, player)
copy_dynamic_regions_and_locations(world, ret)
@ -224,7 +322,6 @@ def copy_world(world):
for shop in world.shops:
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
copied_shop.active = shop.active
copied_shop.inventory = copy.copy(shop.inventory)
# connect copied world
@ -241,6 +338,7 @@ def copy_world(world):
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
ret.get_location(location.name, location.player).item = item
item.location = ret.get_location(location.name, location.player)
item.world = ret
if location.event:
ret.get_location(location.name, location.player).event = True
if location.locked:
@ -250,9 +348,11 @@ def copy_world(world):
for item in world.itempool:
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type, player = item.player))
for item in world.precollected_items:
ret.push_precollected(ItemFactory(item.name, item.player))
# 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):
@ -263,14 +363,14 @@ def copy_world(world):
def copy_dynamic_regions_and_locations(world, ret):
for region in world.dynamic_regions:
new_reg = Region(region.name, region.type, region.hint_text, region.player)
new_reg.world = ret
ret.regions.append(new_reg)
ret.initialize_regions([new_reg])
ret.dynamic_regions.append(new_reg)
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
if region.shop:
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.replaceable)
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.custom, region.shop.locked)
ret.shops.append(new_reg.shop)
for location in world.dynamic_locations:
@ -292,7 +392,7 @@ def create_playthrough(world):
world = copy_world(world)
# if we only check for beatable, we can do this sanity check first before writing down spheres
if world.accessibility == 'none' and not world.can_beat_game():
if not world.can_beat_game():
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
# get locations containing progress items
@ -303,8 +403,7 @@ def create_playthrough(world):
sphere_candidates = list(prog_locations)
logging.getLogger('').debug('Building up collection spheres.')
while sphere_candidates:
if not world.keysanity:
state.sweep_for_events(key_only=True)
state.sweep_for_events(key_only=True)
sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
@ -323,9 +422,10 @@ def create_playthrough(world):
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations))
if not sphere:
logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates])
if not world.accessibility == 'none':
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.')
else:
old_world.spoiler.unreachables = sphere_candidates.copy()
break
# in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
@ -336,7 +436,6 @@ def create_playthrough(world):
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item
location.item = None
state.remove(old_item)
if world.can_beat_game(state_cache[num]):
to_delete.append(location)
else:
@ -347,6 +446,14 @@ def create_playthrough(world):
for location in to_delete:
sphere.remove(location)
# second phase, sphere 0
for item in [i for i in world.precollected_items if i.advancement]:
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items.remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally
@ -357,8 +464,7 @@ def create_playthrough(world):
state = CollectionState(world)
collection_spheres = []
while required_locations:
if not world.keysanity:
state.sweep_for_events(key_only=True)
state.sweep_for_events(key_only=True)
sphere = list(filter(state.can_reach, required_locations))
@ -393,10 +499,12 @@ def create_playthrough(world):
old_world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
for _, path in dict(old_world.spoiler.paths).items():
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode != 'inverted':
if world.mode[player] != 'inverted':
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
else:
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)])
old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
for i, sphere in enumerate(collection_spheres):
old_world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sphere}

50
ModuleUpdate.py Normal file
View File

@ -0,0 +1,50 @@
import os
import sys
import subprocess
import importlib
update_ran = hasattr(sys, "frozen") and getattr(sys, "frozen") # don't run update if environment is frozen/compiled
def update_command():
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt', '--upgrade'])
naming_specialties = {"PyYAML": "yaml"} # PyYAML is imported as the name yaml
def update():
global update_ran
if not update_ran:
update_ran = True
path = os.path.join(os.path.dirname(sys.argv[0]), 'requirements.txt')
if not os.path.exists(path):
os.path.join(os.path.dirname(__file__), 'requirements.txt')
with open(path) as requirementsfile:
for line in requirementsfile.readlines():
module, remote_version = line.split(">=")
module = naming_specialties.get(module, module)
try:
module = importlib.import_module(module)
except:
import traceback
traceback.print_exc()
input(f'Required python module {module} not found, press enter to install it')
update_command()
return
else:
if hasattr(module, "__version__"):
module_version = module.__version__
module = module.__name__ # also unloads the module to make it writable
if type(module_version) == str:
module_version = tuple(int(part.strip()) for part in module_version.split("."))
remote_version = tuple(int(part.strip()) for part in remote_version.split("."))
if module_version < remote_version:
input(f'Required python module {module} is outdated ({module_version}<{remote_version}),'
' press enter to upgrade it')
update_command()
return
if __name__ == "__main__":
update()

1041
MultiClient.py Normal file

File diff suppressed because it is too large Load Diff

182
MultiMystery.py Normal file
View File

@ -0,0 +1,182 @@
__author__ = "Berserker55" # you can find me on the ALTTP Randomizer Discord
"""
This script launches a Multiplayer "Multiworld" Mystery Game
.yaml files for all participating players should be placed in a /Players folder.
For every player a mystery game is rolled and a ROM created.
After generation the server is automatically launched.
It is still up to the host to forward the correct port (38281 by default) and distribute the roms to the players.
Regular Mystery has to work for this first, such as a ALTTP Base ROM and Enemizer Setup.
A guide can be found here: https://docs.google.com/document/d/19FoqUkuyStMqhOq8uGiocskMo1KMjOW4nEeG81xrKoI/edit
Configuration can be found in host.yaml
"""
import os
import subprocess
import sys
def feedback(text:str):
print(text)
input("Press Enter to ignore and probably crash.")
if __name__ == "__main__":
try:
print(f"{__author__}'s MultiMystery Launcher")
import ModuleUpdate
ModuleUpdate.update()
from Utils import get_public_ipv4, get_options
from Patch import create_patch_file
options = get_options()
multi_mystery_options = options["multi_mystery_options"]
output_path = multi_mystery_options["output_path"]
enemizer_path = multi_mystery_options["enemizer_path"]
player_files_path = multi_mystery_options["player_files_path"]
race = multi_mystery_options["race"]
create_spoiler = multi_mystery_options["create_spoiler"]
zip_roms = multi_mystery_options["zip_roms"]
zip_diffs = multi_mystery_options["zip_diffs"]
zip_spoiler = multi_mystery_options["zip_spoiler"]
zip_multidata = multi_mystery_options["zip_multidata"]
zip_format = multi_mystery_options["zip_format"]
#zip_password = multi_mystery_options["zip_password"] not at this time
player_name = multi_mystery_options["player_name"]
meta_file_path = multi_mystery_options["meta_file_path"]
teams = multi_mystery_options["teams"]
rom_file = options["general_options"]["rom_file"]
host = options["server_options"]["host"]
port = options["server_options"]["port"]
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
if not os.path.exists(enemizer_path):
feedback(f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
if not os.path.exists(rom_file):
feedback(f"Base rom is expected as {rom_file} in the Multiworld root folder please place/rename it there.")
player_files = []
os.makedirs(player_files_path, exist_ok=True)
for file in os.listdir(player_files_path):
lfile = file.lower()
if lfile.endswith(".yaml") and lfile != meta_file_path.lower():
player_files.append(file)
print(f"Found player's file {file}.")
player_count = len(player_files)
if player_count == 0:
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
else:
print(player_count, "Players found.")
player_string = ""
for i, file in enumerate(player_files, 1):
player_string += f"--p{i} {os.path.join(player_files_path, file)} "
if os.path.exists("BerserkerMultiServer.exe"):
basemysterycommand = "BerserkerMystery.exe" #compiled windows
elif os.path.exists("BerserkerMultiServer"):
basemysterycommand = "BerserkerMystery" # compiled linux
else:
basemysterycommand = f"py -{py_version} Mystery.py" #source
command = f"{basemysterycommand} --multi {len(player_files)} {player_string} " \
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
f"--outputpath \"{output_path}\" --teams {teams}"
if create_spoiler:
command += " --create_spoiler"
if race:
command += " --race"
if os.path.exists(os.path.join(player_files_path, meta_file_path)):
command += f" --meta {os.path.join(player_files_path, meta_file_path)}"
print(command)
import time
start = time.perf_counter()
text = subprocess.check_output(command, shell=True).decode()
print(f"Took {time.perf_counter()-start:.3f} seconds to generate rom(s).")
seedname = ""
for segment in text.split():
if segment.startswith("M"):
seedname = segment
break
multidataname = f"ER_{seedname}_multidata"
spoilername = f"ER_{seedname}_Spoiler.txt"
romfilename = ""
if player_name:
for file in os.listdir(output_path):
if player_name in file:
import webbrowser
romfilename = os.path.join(output_path, file)
print(f"Launching ROM file {romfilename}")
webbrowser.open(romfilename)
break
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs)):
import zipfile
compression = {1 : zipfile.ZIP_DEFLATED,
2 : zipfile.ZIP_LZMA,
3 : zipfile.ZIP_BZIP2}[zip_format]
typical_zip_ending = {1: "zip",
2: "7z",
3: "bz2"}[zip_format]
def pack_file(file: str):
zf.write(os.path.join(output_path, file), file)
print(f"Packed {file} into zipfile {zipname}")
def remove_zipped_file(file: str):
os.remove(os.path.join(output_path, file))
print(f"Removed {file} which is now present in the zipfile")
zipname = os.path.join(output_path, f"ER_{seedname}.{typical_zip_ending}")
print(f"Creating zipfile {zipname}")
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
for file in os.listdir(output_path):
if file.endswith(".sfc") and seedname in file:
if zip_diffs:
diff = os.path.split(create_patch_file(os.path.join(output_path, file), ipv4))[1]
pack_file(diff)
if zip_diffs == 2:
remove_zipped_file(diff)
if zip_roms:
pack_file(file)
if zip_roms == 2 and player_name.lower() not in file.lower():
remove_zipped_file(file)
if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):
pack_file(multidataname)
if zip_multidata == 2:
remove_zipped_file(multidataname)
if zip_spoiler and create_spoiler:
pack_file(spoilername)
if zip_spoiler == 2:
remove_zipped_file(spoilername)
if os.path.exists(os.path.join(output_path, multidataname)):
if os.path.exists("BerserkerMultiServer.exe"):
baseservercommand = "BerserkerMultiServer.exe" # compiled windows
elif os.path.exists("BerserkerMultiServer"):
baseservercommand = "BerserkerMultiServer" # compiled linux
else:
baseservercommand = f"py -{py_version} MultiServer.py" # source
#don't have a mac to test that. If you try to run compiled on mac, good luck.
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
except:
import traceback
traceback.print_exc()
input("Press enter to close")

705
MultiServer.py Normal file
View File

@ -0,0 +1,705 @@
import argparse
import asyncio
import functools
import json
import logging
import zlib
import collections
import typing
import ModuleUpdate
ModuleUpdate.update()
import websockets
import prompt_toolkit
from prompt_toolkit.patch_stdout import patch_stdout
from fuzzywuzzy import process as fuzzy_process
import Items
import Regions
import Utils
from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address
console_names = frozenset(set(Items.item_table) | set(Regions.location_table))
class Client:
version: typing.List[int] = [0, 0, 0]
tags: typing.List[str] = []
def __init__(self, socket: websockets.server.WebSocketServerProtocol):
self.socket = socket
self.auth = False
self.name = None
self.team = None
self.slot = None
self.send_index = 0
self.tags = []
self.version = [0, 0, 0]
@property
def wants_item_notification(self):
return self.auth and "FoundItems" in self.tags
class Context:
def __init__(self, host: str, port: int, password: str, location_check_points: int, hint_cost: int,
item_cheat: bool):
self.data_filename = None
self.save_filename = None
self.disable_save = False
self.player_names = {}
self.rom_names = {}
self.remote_items = set()
self.locations = {}
self.host = host
self.port = port
self.password = password
self.server = None
self.countdown_timer = 0
self.clients = []
self.received_items = {}
self.location_checks = collections.defaultdict(set)
self.hint_cost = hint_cost
self.location_check_points = location_check_points
self.hints_used = collections.defaultdict(int)
self.hints_sent = collections.defaultdict(set)
self.item_cheat = item_cheat
def get_save(self) -> dict:
return {
"rom_names": list(self.rom_names.items()),
"received_items": tuple((k, v) for k, v in self.received_items.items()),
"hints_used" : tuple((key,value) for key, value in self.hints_used.items()),
"hints_sent" : tuple((key,tuple(value)) for key, value in self.hints_sent.items()),
"location_checks" : tuple((key,tuple(value)) for key, value in self.location_checks.items())
}
def set_save(self, savedata: dict):
rom_names = savedata["rom_names"]
received_items = {tuple(k): [ReceivedItem(*i) for i in v] for k, v in savedata["received_items"]}
if not all([self.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
raise Exception('Save file mismatch, will start a new game')
self.received_items = received_items
self.hints_used.update({tuple(key): value for key, value in savedata["hints_used"]})
self.hints_sent.update({tuple(key): set(value) for key, value in savedata["hints_sent"]})
self.location_checks.update({tuple(key): set(value) for key, value in savedata["location_checks"]})
logging.info(f'Loaded save file with {sum([len(p) for p in received_items.values()])} received items '
f'for {len(received_items)} players')
async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed:
return
try:
await websocket.send(json.dumps(msgs))
except websockets.ConnectionClosed:
pass
def broadcast_all(ctx : Context, msgs):
for client in ctx.clients:
if client.auth:
asyncio.create_task(send_msgs(client.socket, msgs))
def broadcast_team(ctx : Context, team, msgs):
for client in ctx.clients:
if client.auth and client.team == team:
asyncio.create_task(send_msgs(client.socket, msgs))
def notify_all(ctx : Context, text):
logging.info("Notice (all): %s" % text)
broadcast_all(ctx, [['Print', text]])
def notify_team(ctx: Context, team: int, text: str):
logging.info("Notice (Team #%d): %s" % (team + 1, text))
broadcast_team(ctx, team, [['Print', text]])
def notify_client(client: Client, text: str):
if not client.auth:
return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
# separated out, due to compatibilty between client's
def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
cmd = [["Hint", hints]]
texts = [['Print', format_hint(ctx, team, hint)] for hint in hints]
for _, text in texts:
logging.info("Notice (Team #%d): %s" % (team + 1, text))
for client in ctx.clients:
if client.auth and client.team == team:
if "Berserker" in client.tags:
payload = cmd
else:
payload = texts
asyncio.create_task(send_msgs(client.socket, payload))
async def server(websocket, path, ctx: Context):
client = Client(websocket)
ctx.clients.append(client)
try:
await on_client_connected(ctx, client)
async for data in websocket:
for msg in json.loads(data):
if len(msg) == 1:
cmd = msg
args = None
else:
cmd = msg[0]
args = msg[1]
await process_client_cmd(ctx, client, cmd, args)
except Exception as e:
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
finally:
await on_client_disconnected(ctx, client)
ctx.clients.remove(client)
async def on_client_connected(ctx: Context, client: Client):
await send_msgs(client.socket, [['RoomInfo', {
'password': ctx.password is not None,
'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth],
# tags are for additional features in the communication.
# Name them by feature or fork, as you feel is appropriate.
'tags': ['Berserker'],
'version': [1, 2, 0]
}]])
async def on_client_disconnected(ctx: Context, client: Client):
if client.auth:
await on_client_left(ctx, client)
async def on_client_joined(ctx: Context, client: Client):
notify_all(ctx, "%s (Team #%d) has joined the game. Client(%s, %s)." % (client.name, client.team + 1,
".".join(str(x) for x in client.version),
client.tags))
async def on_client_left(ctx: Context, client: Client):
notify_all(ctx, "%s (Team #%d) has left the game" % (client.name, client.team + 1))
async def countdown(ctx: Context, timer):
notify_all(ctx, f'[Server]: Starting countdown of {timer}s')
if ctx.countdown_timer:
ctx.countdown_timer = timer # timer is already running, set it to a different time
else:
ctx.countdown_timer = timer
while ctx.countdown_timer > 0:
notify_all(ctx, f'[Server]: {ctx.countdown_timer}')
ctx.countdown_timer -= 1
await asyncio.sleep(1)
notify_all(ctx, f'[Server]: GO')
def get_connected_players_string(ctx: Context):
auth_clients = {(c.team, c.slot) for c in ctx.clients if c.auth}
player_names = sorted(ctx.player_names.keys())
current_team = -1
text = ''
for team, slot in player_names:
player_name = ctx.player_names[team, slot]
if team != current_team:
text += f':: Team #{team + 1}: '
current_team = team
if (team, slot) in auth_clients:
text += f'{player_name} '
else:
text += f'({player_name}) '
return f'{len(auth_clients)} players of {len(ctx.player_names)} connected ' + text[:-1]
def get_received_items(ctx: Context, team: int, player: int) -> typing.List[ReceivedItem]:
return ctx.received_items.setdefault((team, player), [])
def tuplize_received_items(items):
return [(item.item, item.location, item.player) for item in items]
def send_new_items(ctx: Context):
for client in ctx.clients:
if not client.auth:
continue
items = get_received_items(ctx, client.team, client.slot)
if len(items) > client.send_index:
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
client.send_index = len(items)
def forfeit_player(ctx: Context, team: int, slot: int):
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int}
notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations)
def register_location_checks(ctx: Context, team: int, slot: int, locations):
found_items = False
for location in locations:
if (location, slot) in ctx.locations:
target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot or slot in ctx.remote_items:
found = False
recvd_items = get_received_items(ctx, team, target_player)
for recvd_item in recvd_items:
if recvd_item.location == location and recvd_item.player == slot:
found = True
break
if not found:
new_item = ReceivedItem(target_item, location, slot)
recvd_items.append(new_item)
if slot != target_player:
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item),
ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
found_items = True
elif target_player == slot: # local pickup, notify clients of the pickup
if location not in ctx.location_checks[team, slot]:
for client in ctx.clients:
if client.team == team and client.wants_item_notification:
asyncio.create_task(
send_msgs(client.socket, [['ItemFound', (target_item, location, slot)]]))
ctx.location_checks[team, slot] |= set(locations)
send_new_items(ctx)
if found_items:
save(ctx)
def save(ctx: Context):
if not ctx.disable_save:
try:
jsonstr = json.dumps(ctx.get_save())
with open(ctx.save_filename, "wb") as f:
f.write(zlib.compress(jsonstr.encode("utf-8")))
except Exception as e:
logging.exception(e)
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[Utils.Hint]:
hints = []
seeked_item_id = Items.item_table[item][3]
for check, result in ctx.locations.items():
item_id, receiving_player = result
if receiving_player == slot and item_id == seeked_item_id:
location_id, finding_player = check
found = location_id in ctx.location_checks[team, finding_player]
hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found))
return hints
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[Utils.Hint]:
hints = []
seeked_location = Regions.location_table[location][0]
for check, result in ctx.locations.items():
location_id, finding_player = check
if finding_player == slot and location_id == seeked_location:
item_id, receiving_player = result
found = location_id in ctx.location_checks[team, finding_player]
hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found))
break # each location has 1 item
return hints
def format_hint(ctx: Context, team: int, hint: Utils.Hint) -> str:
return f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
f"{Items.lookup_id_to_name[hint.item]} can be found " \
f"at {get_location_name_from_address(hint.location)} " \
f"in {ctx.player_names[team, hint.finding_player]}'s World." \
+ (" (found)" if hint.found else "")
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str]= console_names) -> typing.Tuple[str, bool, str]:
picks = fuzzy_process.extract(input_text, possible_answers, limit=2)
dif = picks[0][1] - picks[1][1]
if picks[0][1] == 100:
return picks[0][0], True, "Perfect Match"
elif picks[0][1] < 75:
return picks[0][0], False, f"Didn't find something that closely matches, did you mean {picks[0][0]}?"
elif dif > 5:
return picks[0][0], True, "Close Match"
else:
return picks[0][0], False, f"Too many close matches, did you mean {picks[0][0]}?"
async def process_client_cmd(ctx: Context, client: Client, cmd, args):
if type(cmd) is not str:
await send_msgs(client.socket, [['InvalidCmd']])
return
if cmd == 'Connect':
if not args or type(args) is not dict or \
'password' not in args or type(args['password']) not in [str, type(None)] or \
'rom' not in args or type(args['rom']) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
return
errors = set()
if ctx.password is not None and args['password'] != ctx.password:
errors.add('InvalidPassword')
if tuple(args['rom']) not in ctx.rom_names:
errors.add('InvalidRom')
else:
team, slot = ctx.rom_names[tuple(args['rom'])]
if any([c.slot == slot and c.team == team for c in ctx.clients if c.auth]):
errors.add('SlotAlreadyTaken')
else:
client.name = ctx.player_names[(team, slot)]
client.team = team
client.slot = slot
if errors:
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
else:
client.auth = True
client.version = args.get('version', Client.version)
client.tags = args.get('tags', Client.tags)
reply = [['Connected', [(client.team, client.slot),
[(p, n) for (t, p), n in ctx.player_names.items() if t == client.team]]]]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
client.send_index = len(items)
await send_msgs(client.socket, reply)
await on_client_joined(ctx, client)
if not client.auth:
return
if cmd == 'Sync':
items = get_received_items(ctx, client.team, client.slot)
if items:
client.send_index = len(items)
await send_msgs(client.socket, [['ReceivedItems', (0, tuplize_received_items(items))]])
if cmd == 'LocationChecks':
if type(args) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
return
register_location_checks(ctx, client.team, client.slot, args)
if cmd == 'LocationScouts':
if type(args) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
return
locs = []
for location in args:
if type(location) is not int or 0 >= location > len(Regions.location_table):
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
return
loc_name = list(Regions.location_table.keys())[location - 1]
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item]
if item_type:
target_item = replacements.get(item_type[0], target_item)
locs.append([loc_name, location, target_item, target_player])
logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
await send_msgs(client.socket, [['LocationInfo', [l[1:] for l in locs]]])
if cmd == 'UpdateTags':
if not args or type(args) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'UpdateTags']])
return
client.tags = args
if cmd == 'Say':
if type(args) is not str or not args.isprintable():
await send_msgs(client.socket, [['InvalidArguments', 'Say']])
return
notify_all(ctx, client.name + ': ' + args)
if args.startswith('!players'):
notify_all(ctx, get_connected_players_string(ctx))
elif args.startswith('!forfeit'):
forfeit_player(ctx, client.team, client.slot)
elif args.startswith('!countdown'):
try:
timer = int(args.split()[1])
except (IndexError, ValueError):
timer = 10
asyncio.create_task(countdown(ctx, timer))
elif args.startswith('!getitem') and ctx.item_cheat:
item_name = args[9:].lower()
item_name, usable, response = get_intended_text(item_name, Items.item_table.keys())
if usable:
new_item = ReceivedItem(Items.item_table[item_name][3], -1, client.slot)
get_received_items(ctx, client.team, client.slot).append(new_item)
notify_all(ctx, 'Cheat console: sending "' + item_name + '" to ' + client.name)
send_new_items(ctx)
else:
notify_client(client, response)
elif args.startswith("!hint"):
points_available = ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - \
ctx.hint_cost * ctx.hints_used[client.team, client.slot]
item_name = args[6:]
if not item_name:
notify_client(client, "Use !hint {item_name/location_name}, "
"for example !hint Lamp or !hint Link's House. "
f"A hint costs {ctx.hint_cost} points. "
f"You have {points_available} points.")
for item_name in ctx.hints_sent[client.team, client.slot]:
if item_name in Items.item_table: # item name
hints = collect_hints(ctx, client.team, client.slot, item_name)
else: # location name
hints = collect_hints_location(ctx, client.team, client.slot, item_name)
notify_hints(ctx, client.team, hints)
else:
item_name, usable, response = get_intended_text(item_name)
if usable:
if item_name in Items.hint_blacklist:
notify_client(client, f"Sorry, \"{item_name}\" is marked as non-hintable.")
hints = []
elif item_name in Items.item_table: # item name
hints = collect_hints(ctx, client.team, client.slot, item_name)
else: # location name
hints = collect_hints_location(ctx, client.team, client.slot, item_name)
if hints:
if item_name in ctx.hints_sent[client.team, client.slot]:
notify_hints(ctx, client.team, hints)
notify_client(client, "Hint was previously used, no points deducted.")
else:
found = 0
for hint in hints:
found += 1 - hint.found
if not found:
notify_hints(ctx, client.team, hints)
notify_client(client, "No new items found, no points deducted.")
else:
if ctx.hint_cost:
can_pay = points_available // (ctx.hint_cost * found) >= 1
else:
can_pay = True
if can_pay:
ctx.hints_used[client.team, client.slot] += found
ctx.hints_sent[client.team, client.slot].add(item_name)
notify_hints(ctx, client.team, hints)
save(ctx)
else:
notify_client(client, f"You can't afford the hint. "
f"You have {points_available} points and need at least {ctx.hint_cost}, "
f"more if multiple items are still to be found.")
else:
notify_client(client, "Nothing found. Item/Location may not exist.")
else:
notify_client(client, response)
def set_password(ctx : Context, password):
ctx.password = password
logging.warning('Password set to ' + password if password else 'Password disabled')
async def console(ctx: Context):
session = prompt_toolkit.PromptSession()
running = True
while running:
with patch_stdout():
input_text = await session.prompt_async()
try:
command = input_text.split()
if not command:
continue
if command[0] == '/exit':
await ctx.server.ws_server._close()
running = False
if command[0] == '/players':
logging.info(get_connected_players_string(ctx))
if command[0] == '/password':
set_password(ctx, command[1] if len(command) > 1 else None)
if command[0] == '/kick' and len(command) > 1:
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
for client in ctx.clients:
if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team):
if client.socket and not client.socket.closed:
await client.socket.close()
if command[0] == '/forfeitslot' and len(command) > 1 and command[1].isdigit():
if len(command) > 2 and command[2].isdigit():
team = int(command[1]) - 1
slot = int(command[2])
else:
team = 0
slot = int(command[1])
forfeit_player(ctx, team, slot)
if command[0] == '/forfeitplayer' and len(command) > 1:
seeked_player = command[1].lower()
for (team, slot), name in ctx.player_names.items():
if name.lower() == seeked_player:
forfeit_player(ctx, team, slot)
if command[0] == '/senditem':
if len(command) <= 2:
logging.info("Use /senditem {Playername} {itemname}\nFor example /senditem Berserker Lamp")
else:
seeked_player, usable, response = get_intended_text(command[1], ctx.player_names.values())
if usable:
item = " ".join(command[2:])
item, usable, response = get_intended_text(item, Items.item_table.keys())
if usable:
for client in ctx.clients:
if client.name == seeked_player:
new_item = ReceivedItem(Items.item_table[item][3], -1, client.slot)
get_received_items(ctx, client.team, client.slot).append(new_item)
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
send_new_items(ctx)
else:
logging.warning(response)
else:
logging.warning(response)
if command[0] == '/hint':
if len(command) <= 2:
logging.info("Use /hint {Playername} {itemname/locationname}\nFor example /hint Berserker Lamp")
else:
seeked_player, usable, response = get_intended_text(command[1], ctx.player_names.values())
if usable:
for (team, slot), name in ctx.player_names.items():
if name == seeked_player:
item = " ".join(command[2:])
item, usable, response = get_intended_text(item)
if usable:
if item in Items.item_table: #item name
hints = collect_hints(ctx, team, slot, item)
notify_hints(ctx, team, hints)
else: #location name
hints = collect_hints_location(ctx, team, slot, item)
notify_hints(ctx, team, hints)
else:
logging.warning(response)
else:
logging.warning(response)
if command[0][0] != '/':
notify_all(ctx, '[Server]: ' + input_text)
except:
import traceback
traceback.print_exc()
async def forward_port(port: int):
import upnpy
import socket
upnp = upnpy.UPnP()
upnp.discover()
device = upnp.get_igd()
service = device['WANPPPConnection.1']
# get own lan IP
ip = socket.gethostbyname(socket.gethostname())
# This specific action returns an empty dict: {}
service.AddPortMapping(
NewRemoteHost='',
NewExternalPort=port,
NewProtocol='TCP',
NewInternalPort=port,
NewInternalClient=ip,
NewEnabled=1,
NewPortMappingDescription='Berserker\'s Multiworld',
NewLeaseDuration=60 * 60 * 24 # 24 hours
)
logging.info(f"Attempted to forward port {port} to {ip}, your local ip address.")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
defaults = Utils.get_options()["server_options"]
parser.add_argument('--host', default=defaults["host"])
parser.add_argument('--port', default=defaults["port"], type=int)
parser.add_argument('--password', default=defaults["password"])
parser.add_argument('--multidata', default=defaults["multidata"])
parser.add_argument('--savefile', default=defaults["savefile"])
parser.add_argument('--disable_save', default=defaults["disable_save"], action='store_true')
parser.add_argument('--loglevel', default=defaults["loglevel"],
choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int)
parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true')
parser.add_argument('--port_forward', default=defaults["port_forward"], action='store_true')
args = parser.parse_args()
return args
async def main(args: argparse.Namespace):
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
portforwardtask = None
if args.port_forward:
portforwardtask = asyncio.create_task(forward_port(args.port))
ctx = Context(args.host, args.port, args.password, args.location_check_points, args.hint_cost,
not args.disable_item_cheat)
ctx.data_filename = args.multidata
try:
if not ctx.data_filename:
import tkinter
import tkinter.filedialog
root = tkinter.Tk()
root.withdraw()
ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),))
with open(ctx.data_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
for team, names in enumerate(jsonobj['names']):
for player, name in enumerate(names, 1):
ctx.player_names[(team, player)] = name
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj['roms']}
ctx.remote_items = set(jsonobj['remote_items'])
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']}
except Exception as e:
logging.error('Failed to read multiworld data (%s)' % e)
return
ip = args.host if args.host else Utils.get_public_ipv4()
ctx.disable_save = args.disable_save
if not ctx.disable_save:
if not ctx.save_filename:
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (
ctx.data_filename + '_')) + 'multisave'
try:
with open(ctx.save_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
ctx.set_save(jsonobj)
except FileNotFoundError:
logging.error('No save data found, starting a new game')
except Exception as e:
logging.exception(e)
if portforwardtask:
try:
await portforwardtask
except:
logging.exception("Automatic port forwarding failed with:")
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
ping_interval=None)
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
'No password' if not ctx.password else 'Password: %s' % ctx.password))
await ctx.server
await console(ctx)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main(parse_args()))
loop.close()

304
Mystery.py Normal file
View File

@ -0,0 +1,304 @@
import argparse
import logging
import random
import urllib.request
import urllib.parse
import typing
import os
import ModuleUpdate
ModuleUpdate.update()
from Utils import parse_yaml
from Rom import get_sprite_from_name
from EntranceRandomizer import parse_arguments
from Main import main as ERmain
def main():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args()
parser = argparse.ArgumentParser()
parser.add_argument('--weights',
help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
action='store_true')
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1))
parser.add_argument('--create_spoiler', action='store_true')
parser.add_argument('--rom')
parser.add_argument('--enemizercli')
parser.add_argument('--outputpath')
parser.add_argument('--race', action='store_true')
parser.add_argument('--meta', default=None)
for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
args = parser.parse_args()
if args.seed is None:
random.seed(None)
seed = random.randint(0, 999999999)
else:
seed = args.seed
random.seed(seed)
seedname = "M"+(f"{random.randint(0, 999999999)}".zfill(9))
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
weights_cache = {}
if args.weights:
try:
weights_cache[args.weights] = get_weights(args.weights)
except Exception as e:
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights} >> {weights_cache[args.weights]['description']}")
if args.meta:
try:
weights_cache[args.meta] = get_weights(args.meta)
except Exception as e:
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
print(f"Meta: {args.meta} >> {weights_cache[args.meta]['meta_description']}")
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
for player in range(1, args.multi + 1):
path = getattr(args, f'p{player}')
if path:
try:
if path not in weights_cache:
weights_cache[path] = get_weights(path)
print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}")
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
erargs.create_spoiler = args.create_spoiler
erargs.race = args.race
erargs.outputname = seedname
erargs.outputpath = args.outputpath
erargs.teams = args.teams
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[erargs.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
if args.rom:
erargs.rom = args.rom
if args.enemizercli:
erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
if args.meta:
for player, path in player_path_cache.items():
weights_cache[path].setdefault("meta_ignore", [])
meta_weights = weights_cache[args.meta]
for key in meta_weights:
option = get_choice(key, meta_weights)
if option is not None:
for player, path in player_path_cache.items():
players_meta = weights_cache[path]["meta_ignore"]
if key not in players_meta:
weights_cache[path][key] = option
elif type(players_meta) == dict and option not in players_meta[key]:
weights_cache[path][key] = option
for player in range(1, args.multi + 1):
path = player_path_cache[player]
if path:
try:
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
if settings.sprite is not None and not os.path.isfile(settings.sprite) and not get_sprite_from_name(settings.sprite):
logging.warning(f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.")
for k, v in vars(settings).items():
if v is not None:
getattr(erargs, k)[player] = v
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
if not erargs.name[player]:
erargs.name[player] = os.path.split(path)[-1].split(".")[0]
erargs.names = ",".join(erargs.name[i] for i in range(1, args.multi + 1))
del(erargs.name)
ERmain(erargs, seed)
def get_weights(path):
try:
if urllib.parse.urlparse(path).scheme:
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
else:
with open(path, 'rb') as f:
yaml = str(f.read(), "utf-8")
except Exception as e:
print('Failed to read weights (%s)' % e)
return
return parse_yaml(yaml)
def interpret_on_off(value):
return {"on": True, "off": False}.get(value, value)
def convert_to_on_off(value):
return {True: "on", False: "off"}.get(value, value)
def get_choice(option, root) -> typing.Any:
if option not in root:
return None
if type(root[option]) is not dict:
return interpret_on_off(root[option])
if not root[option]:
return None
return interpret_on_off(
random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0])
def handle_name(name: str):
return name.strip().replace(' ', '_')
def roll_settings(weights):
ret = argparse.Namespace()
ret.name = get_choice('name', weights)
if ret.name:
ret.name = handle_name(ret.name)
glitches_required = get_choice('glitches_required', weights)
if glitches_required not in ['none', 'no_logic']:
logging.warning("Only NMG and No Logic supported")
glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
# item_placement = get_choice('item_placement')
# not supported in ER
dungeon_items = get_choice('dungeon_items', weights)
if dungeon_items == 'full' or dungeon_items == True:
dungeon_items = 'mcsb'
elif dungeon_items == 'standard':
dungeon_items = ""
elif not dungeon_items:
dungeon_items = ""
ret.mapshuffle = get_choice('map_shuffle', weights) if 'map_shuffle' in weights else 'm' in dungeon_items
ret.compassshuffle = get_choice('compass_shuffle', weights) if 'compass_shuffle' in weights else 'c' in dungeon_items
ret.keyshuffle = get_choice('smallkey_shuffle', weights) if 'smallkey_shuffle' in weights else 's' in dungeon_items
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights) if 'bigkey_shuffle' in weights else 'b' in dungeon_items
ret.accessibility = get_choice('accessibility', weights)
entrance_shuffle = get_choice('entrance_shuffle', weights)
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals', weights)
ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals',
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt'
}[goal]
ret.openpyramid = goal == 'fast_ganon'
ret.crystals_gt = get_choice('tower_open', weights)
ret.crystals_ganon = get_choice('ganon_open', weights)
ret.mode = get_choice('world_state', weights)
if ret.mode == 'retro':
ret.mode = 'open'
ret.retro = True
ret.hints = get_choice('hints', weights)
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[get_choice('weapons', weights)]
ret.difficulty = get_choice('item_pool', weights)
ret.item_functionality = get_choice('item_functionality', weights)
ret.shufflebosses = {'none': 'none',
'simple': 'basic',
'full': 'normal',
'random': 'chaos'
}[get_choice('boss_shuffle', weights)]
ret.shuffleenemies = {'none': 'none',
'shuffled': 'shuffled',
'random': 'chaos'
}[get_choice('enemy_shuffle', weights)]
ret.enemy_damage = {'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos'
}[get_choice('enemy_damage', weights)]
ret.enemy_health = get_choice('enemy_health', weights)
ret.shufflepots = get_choice('pot_shuffle', weights)
ret.beemizer = int(get_choice('beemizer', weights)) if 'beemizer' in weights else 0
ret.timer = {'none': False,
None: False,
False: False,
'timed': 'timed',
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice('timer', weights)] if 'timer' in weights.keys() else False
ret.progressive = convert_to_on_off(get_choice('progressive', weights)) if "progressive" in weights else 'on'
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights)
if item.startswith(('Progressive ', 'Small Key ', 'Rupee', 'Piece of Heart', 'Boss Heart Container',
'Sanctuary Heart Container', 'Arrow', 'Bombs ', 'Bomb ', 'Bottle')) and isinstance(
itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
elif itemvalue:
startitems.append(item)
if glitches_required in ['no_logic'] and 'Pegasus Boots' not in startitems:
startitems.append('Pegasus Boots')
ret.startinventory = ','.join(startitems)
ret.remote_items = get_choice('remote_items', weights) if 'remote_items' in weights else False
if 'rom' in weights:
romweights = weights['rom']
ret.sprite = get_choice('sprite', romweights)
ret.disablemusic = get_choice('disablemusic', romweights)
ret.extendedmsu = get_choice('extendedmsu', romweights)
ret.quickswap = get_choice('quickswap', romweights)
ret.fastmenu = get_choice('menuspeed', romweights)
ret.heartcolor = get_choice('heartcolor', romweights)
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights))
ret.ow_palettes = get_choice('ow_palettes', romweights)
ret.uw_palettes = get_choice('uw_palettes', romweights)
return ret
if __name__ == '__main__':
main()

104
Options.py Normal file
View File

@ -0,0 +1,104 @@
from __future__ import annotations
from enum import IntEnum, auto, Enum
class Toggle(IntEnum):
off = 0
on = 1
@classmethod
def from_text(cls, text: str) -> Toggle:
if text.lower() in {"off", "0", "false", "none", "null", "no"}:
return Toggle.off
else:
return Toggle.on
class Choice(IntEnum):
@classmethod
def from_text(cls, text: str) -> Choice:
for option in cls:
if option.name == text.lower():
return option
raise KeyError(
f'Could not find option "{text}" for "{cls.__name__}", known options are {", ".join(f"{option.name}" for option in cls)}')
class Logic(Choice):
no_glitches = auto()
no_logic = auto()
class Goal(Choice):
ganon = auto()
fast_ganon = auto()
all_dungeons = auto()
pedestal = auto()
triforce_hunt = auto()
class Accessibility(Choice):
locations = auto()
items = auto()
beatable = auto()
class Crystals(Enum):
# can't use IntEnum since there's also random
C0 = 0
C1 = 1
C2 = 2
C3 = 3
C4 = 4
C5 = 5
C6 = 6
C7 = 7
Random = -1
@staticmethod
def from_text(text: str) -> Crystals:
for option in Crystals:
if str(option.value) == text.lower():
return option
return Crystals.Random
class WorldState(Choice):
standard = auto()
open = auto()
retro = auto()
inverted = auto()
class Bosses(Choice):
vanilla = auto()
simple = auto()
full = auto()
chaos = auto()
class Enemies(Choice):
vanilla = auto()
shuffled = auto()
chaos = auto()
mapshuffle = Toggle
compassshuffle = Toggle
keyshuffle = Toggle
bigkeyshuffle = Toggle
hints = Toggle
if __name__ == "__main__":
import argparse
test = argparse.Namespace()
test.logic = Logic.from_text("no_logic")
test.mapshuffle = mapshuffle.from_text("ON")
try:
test.logic = Logic.from_text("owg")
except KeyError as e:
print(e)
if test.mapshuffle:
print("Mapshuffle is on")
print(test)

76
Patch.py Normal file
View File

@ -0,0 +1,76 @@
import bsdiff4
import yaml
import os
import lzma
import hashlib
from typing import Tuple, Optional
import Utils
from Rom import JAP10HASH, read_rom
base_rom_bytes = None
def get_base_rom_bytes(file_name: str = None) -> bytes:
global base_rom_bytes
if not base_rom_bytes:
options = Utils.get_options()
if not file_name:
file_name = options["general_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if JAP10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
'Get the correct game and version, then dump it')
return base_rom_bytes
def generate_patch(rom: bytes, metadata=None) -> bytes:
if metadata is None:
metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
patch = yaml.dump({"meta": metadata,
"patch": patch})
return patch.encode(encoding="utf-8-sig")
def create_patch_file(rom_file_to_patch: str, server: str = "") -> str:
bytes = generate_patch(load_bytes(rom_file_to_patch),
{
"server": server}) # allow immediate connection to server in multiworld. Empty string otherwise
target = os.path.splitext(rom_file_to_patch)[0] + ".bmbp"
write_lzma(bytes, target)
return target
def create_rom_file(patch_file) -> Tuple[dict, str]:
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
target = os.path.splitext(patch_file)[0] + ".sfc"
with open(target, "wb") as f:
f.write(patched_data)
return data["meta"], target
def load_bytes(path: str):
with open(path, "rb") as f:
return f.read()
def write_lzma(data: bytes, path: str):
with lzma.LZMAFile(path, 'wb') as f:
f.write(data)
if __name__ == "__main__":
ipv4 = Utils.get_public_ipv4()
import sys
for rom in sys.argv:
if rom.endswith(".sfc"):
print(f"Creating patch for {rom}")
result = create_patch_file(rom, ipv4)
print(f"Created patch {result}")

View File

@ -10,7 +10,7 @@ import sys
from BaseClasses import World
from Regions import create_regions
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom
from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name
from Rules import set_rules
from Dungeons import create_dungeons
from Items import ItemFactory
@ -23,7 +23,8 @@ def main(args):
start_time = time.perf_counter()
# initialize the world
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, 'none', False)
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
world.player_names[1].append("Player 1")
logger = logging.getLogger('')
hasher = hashlib.md5()
@ -36,7 +37,7 @@ def main(args):
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
world.difficulty_requirements = difficulties[world.difficulty]
world.difficulty_requirements[1] = difficulties[world.difficulty[1]]
create_regions(world, 1)
create_dungeons(world, 1)
@ -68,13 +69,10 @@ def main(args):
logger.info('Patching ROM.')
if args.sprite is not None:
sprite = Sprite(args.sprite)
else:
sprite = None
rom = LocalRom(args.rom)
patch_rom(world, 1, rom, args.heartbeep, args.heartcolor, sprite)
patch_rom(world, rom, 1, 1, False)
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
for textname, texttype, text in text_patches:
if texttype == 'text':
@ -114,16 +112,16 @@ def fill_world(world, plando, text_patches):
tr_medallion = medallionstr.strip()
elif line.startswith('!mode'):
_, modestr = line.split(':', 1)
world.mode = modestr.strip()
world.mode = {1: modestr.strip()}
elif line.startswith('!logic'):
_, logicstr = line.split(':', 1)
world.logic = logicstr.strip()
world.logic = {1: logicstr.strip()}
elif line.startswith('!goal'):
_, goalstr = line.split(':', 1)
world.goal = goalstr.strip()
world.goal = {1: goalstr.strip()}
elif line.startswith('!light_cone_sewers'):
_, sewerstr = line.split(':', 1)
world.sewer_light_cone = sewerstr.strip().lower() == 'true'
world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'}
elif line.startswith('!light_cone_lw'):
_, lwconestr = line.split(':', 1)
world.light_world_light_cone = lwconestr.strip().lower() == 'true'
@ -132,19 +130,19 @@ def fill_world(world, plando, text_patches):
world.dark_world_light_cone = dwconestr.strip().lower() == 'true'
elif line.startswith('!fix_trock_doors'):
_, trdstr = line.split(':', 1)
world.fix_trock_doors = trdstr.strip().lower() == 'true'
world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'}
elif line.startswith('!fix_trock_exit'):
_, trfstr = line.split(':', 1)
world.fix_trock_exit = trfstr.strip().lower() == 'true'
world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'}
elif line.startswith('!fix_gtower_exit'):
_, gtfstr = line.split(':', 1)
world.fix_gtower_exit = gtfstr.strip().lower() == 'true'
elif line.startswith('!fix_pod_exit'):
_, podestr = line.split(':', 1)
world.fix_palaceofdarkness_exit = podestr.strip().lower() == 'true'
world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'}
elif line.startswith('!fix_skullwoods_exit'):
_, swestr = line.split(':', 1)
world.fix_skullwoods_exit = swestr.strip().lower() == 'true'
world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'}
elif line.startswith('!check_beatable_only'):
_, chkbtstr = line.split(':', 1)
world.check_beatable_only = chkbtstr.strip().lower() == 'true'
@ -172,7 +170,7 @@ def fill_world(world, plando, text_patches):
item = ItemFactory(itemstr.strip(), 1)
if item is not None:
world.push_item(location, item)
if item.key:
if item.smallkey or item.bigkey:
location.event = True
elif '<=>' in line:
entrance, exit = line.split('<=>', 1)
@ -211,6 +209,8 @@ def start():
help='Select the rate at which the heart beep sound is played at low health.')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.')
parser.add_argument('--plando', help='Filled out template to use for setting up the rom.')
args = parser.parse_args()
@ -222,8 +222,8 @@ def start():
if not os.path.isfile(args.plando):
input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando)
sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.rom):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
# set up logger

View File

@ -18,7 +18,7 @@
Mushroom: Mushroom
Bottle Merchant: Bottle
Flute Spot: Ocarina
Flute Spot: Flute
Sunken Treasure: Nothing
Purple Chest: Nothing
Blind's Hideout - Top: Nothing

549
README.md
View File

@ -1,489 +1,60 @@
# ALttPEntranceRandomizer
This is a entrance randomizer for _The Legend of Zelda: A Link to the Past_ for the SNES.
See https://alttpr.com/ for more details on the normal randomizer.
# Installation
Clone this repository and then run ```EntranceRandomizer.py``` (requires Python 3).
Alternatively, run ```Gui.py``` for a simple graphical user interface.
For releases, a Windows standalone executable is available for users without Python 3.
# Settings
## Game Mode
### Standard
Fixes Hyrule Castle Secret Entrance and Front Door, but may lead to weird rain state issues if you exit through the Hyrule Castle side exits before rescuing Zelda in a full shuffle.
Gives lightcone in Hyrule Castle Sewers even without the Lamp.
### Open
This mode starts with the option to start in your house or the sanctuary, you are free to explore.
Special notes:
- Uncle already in sewers and most likely does not have a sword.
- Sewers do not get a free light cone.
- It may be a while before you find a sword, think of other ways to do damage to enemies. (bombs are a great tool, as well as picking up bushes in over world).
### Swordless
This mode removes all swords from the itempool. Otherwise just like open.
Special notes:
- The Medallions to open Misery Mire and Turtle Rock can be used without a sword if you stand on the symbol.
- The curtains in Skull Woods and Hyrule Castle Tower that normally require a sword to cut have been removed.
- Ganon takes damage from the Hammer.
- The magic barrier to Hyrule Castle Tower can be broken with a Hammer.
- The Hammer can be used to activate the Ether and Bombos tablets.
### Inverted
This mode is similar to Open but requires the Moon Pearl in order to not transform into a bunny in the Light World and the Sanctuary spawn point is moved to the Dark Sanctuary
Special Notes:
- Link's House is shuffled freely. The Dark Sanctuary is shuffled within West Dark World.
- There are a number of overworld changes to account for the inability to mirror from the Light World to the Dark World.
- Legacy shuffles are not implemente for this mode.
## Game Logic
This determines the Item Requirements for each location.
### No Glitches
The game can be completed without knowing how to perform glitches of any kind.
### Minor Glitches
May require Fake Flippers, Bunny Revival.
### No Logic
Items are placed without regard for progression or the seed being possible. Major glitches are likely required.
## Game Goal
### Ganon
Standard game completion requiring you to collect the 7 crystals, defeat Agahnim 2 and then beat Ganon.
### Pedestal
Places the Triforce at the Master Sword Pedestal. Ganon cannot be damaged.
### All Dungeons
Ganon cannot be damaged until all dungeons (including Hyrule Castle Tower and Ganons Tower) are cleared.
### Triforce Hunt
Triforce Pieces are added to the item pool, and some number of them being found will trigger game completion. Ganon cannot be damaged.
By default 30 Triforce Pieces are placed while 20 are needed to beat the game. Both values can be adjusted with the custom item pool feature.
### Crystals
Standard game completion requiring you to collect the 7 crystals and then beat Ganon.
This is only noticeably different if the the Ganon shuffle option is enabled.
## Game Difficulty
### Normal
This is the default setting that has an item pool most similar to the original
The Legend of Zelda: A Link to the Past.
### Hard
This setting reduces the availability of a variety of minor helpful items, most notably
limiting the player to two bottles, a Tempered Sword, and Blue Mail. Several minor game
mechanics are adjusted to increase difficulty, most notably weakening potions and preventing
the player from having fairies in bottles.
### Expert
This setting is a more extreme version of the Hard setting. Potions are further nerfed, the item
pool is less helpful, and the player can find no armor, only a Master Sword, and only a single bottle.
## Timer Setting
### None
Does not invoke a timer.
### Display
Displays a timer on-screen but does not alter the item pool.
This will prevent the dungeon item count feature in Easy and Keysanity from working.
### Timed
Displays a count-up timer on screen that can be reduced with Green Clocks and Blue Clocks or
increased with Red Clocks found in chests that will be added to the itempool.
### Timed-OHKO
Displays a countdown timer on screen that, when it hits zero, will put the player into a one hit
knockout state until more time is added to the clock via some of the Green Clocks that will be added
to the itempool.
### OHKO
The player will be in a one hit knockout state the entire game. This is the same as Timed-OHKO except
without the Clock items and the timer permanently at zero.
### Timed-countdown
Displays a countdown timer on screen that can be increased with Green Clocks and Blue Clocks or
decreased with Red Clocks found in chests that will be added to the itempool. The goal of this mode
is to finish the game without the timer reaching zero, but the game will continue uninterrupted if
the player runs out of time.
## Progressive equipment
Determines if Sword, Shield, and gloves are progressive (upgrading in sequence) or not.
### On (Default)
This setting makes swords, shields, armor, and gloves progressive. The first of any type of equipment found
by the player will be the lowest level item, and each subsequent find of a category will upgrade that type of
equipment.
### Off
This setting makes swords, shields, armor, and gloves non-progressive. All of the items of these types will be
randomly placed in chests, and the player could find them in any order and thus instantly receive high level equipment.
Downgrades are not possible; finding a lower level piece of equipment than what is already in the player's possession
will simply do nothing.
### Random
This setting makes swords, shields, armor, and gloves randomly either progressive or not. Each category is independently
randomized.
## Item Distribution Algorithm
Determines how the items are shuffled.
### Balanced
This is a variation of VT26 that aims to strike a balance between the overworld heavy VT25 and the dungeon heavy VT26 algorithm.
It does this by reshuffling the remaining locations after placing dungeon items.
### VT26
Items and locations are shuffled like in VT25, and dungeon items are now placed using the same algorithm. When Ganon is not
shuffled it includes a slight deliberate bias against having too many desireable items in Ganon's Tower to help counterbalance
the sheer number of chests in that single location.
### VT25
Items and locations are shuffled and placed from the top of the lists. The only thing preventing an item from being placed into a spot
is if is absolutely impossible to be there given the previous made placement choices. Leads to very uniform but guaranteed solvable distributions.
### VT22
The ordinary VT v8.22 algorithm. Fixes issues in placement in VT21 by discarding all previously skipped and unfilled locations
after 2/3 of the progression items were placed to prevent stale late game locations from soaking up the same items all the time.
### VT21
The ordinary VT v8.21 algorithm. Unbiased placement of items into unlocked locations, placing items that unlock new locations first.
May lead to distributions that seem a bit wonky (high likelyhood of ice rod in Turtle Rock, for instance)
### Flood
Pushes out items starting from Link's House and is slightly biased to placing progression items with less restrictions. Use for relatively simple distributions.
### Freshness
Alternative approach to VT22 to improve on VT21 flaws. Locations that are skipped because they are currently unreachable increase in
staleness, decreasing the likelihood of receiving a progress item.
## Entrance Shuffle Algorithm
Determines how locations are shuffled. In all modes other than Insanity and the similar legacy versions, holes shuffle as a pair with the connecting cave and the front
two sections of Skull Woods remain confined to the general Skull Woods area. Link's house is never shuffled as a design decision.
### Vanilla
Places entrances in the same locations they were in the original The Legend of Zelda: A Link to the Past.
### Simple
Shuffles dungeon entrances between each other and keeps all 4-entrance dungeons confined to one location such that dungeons will one to one swap with each other.
Other than on Light World Death Mountain, interiors are shuffled but still connect the same points on the overworld. On Death Mountain, entrances are connected more freely.
### Restricted
Uses dungeon shuffling from Simple but freely connects remaining entrances. Caves and dungeons with multiple entrances will be confined to one world.
### Full
Mixes cave and dungeon entrances freely. Caves and dungeons with multiple entrances will be confined to one world.
### Crossed
Mixes cave and dungeon entrances freely, but now connector caves and dungeons can link Light World and Dark World.
### Insanity
Decouples entrances and exits from each other and shuffles them freely. Caves that were single entrance in vanilla still can only exit to the same location from which they were entered.
### Legacy Variants
Similar to the base shuffles, but the distinction between single entrance and multi-entrance caves from older versions of the randomizer is maintained.
Madness_Legacy is the more similar to the modern Insanity. Insanity_Legacy has fake worlds and guaranteed Moon Pearl and Magic Mirror for a very different experience.
### Dungeon Variants
The dungeon variants only mix up dungeons and keep the rest of the overworld vanilla.
## Heartbeep Sound Rate
Select frequency of beeps when on low health. Can completely disable them.
## Heart Color
Select the color of Link's hearts.
## Menu Speed
A setting that lets the player set the rate at which the menu opens and closes.
## Create Spoiler Log
Output a Spoiler File.
## Do not Create Patched Rom
If set, will not produce a patched rom as output. Useful in conjunction with the spoiler log option to batch
generate spoilers for statistical analysis.
## Enable L/R button quickswapping
Use to enable quick item swap with L/R buttons. Press L and R together to switch the state of items like the Mushroom/Powder pair.
## Keysanity
This setting allows dungeon specific items (Small Key, Big Key, Map, Compass) to be distributed anywhere in the world and not just
in their native dungeon. Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that
is traditionally a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but
the rest of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell
for which dungeons contain pendants and crystals; finding a Map for a dungeon will allow the overworld map to display its prize.
## Retro
This setting turns all Small Keys into universal Small Keys that can be used in any dungeon and are distributed across the world.
The Bow now consumed rupees to shoot; the cost is 10 rupees per Wood Arrow and 50 per Silver Arrow. Shooting Wood Arrows requires
the purchase of an arrow item from shops, and to account for this and the dynamic use of keys, both Wood Arrows and Small Keys will
be added to several shops around the world. Four "take any" caves are added that allow the player to choose between an extra Heart
Container and a Bottle being filled with Blue Potion, and one of the four swords from the item pool is placed into a special cave as
well. The five caves that are removed for these will be randomly selected single entrance caves that did not contain any items or any shops.
In further concert with the Bow changes, all arrows under pots, in chests, and elsewhere in the seed will be replaced with rupees.
## Place Dungeon Items
If not set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world.
This may lead to different amount of itempool items being placed in a dungeon than you are used to.
## Include Ganon's Tower and Pyramid Hole in Shuffle pool
If set, Ganon's Tower is included in the dungeon shuffle pool and the Pyramid Hole/Exit pair is included in the Holes shuffle pool. Ganon can not be defeated until the primary goal is fulfilled.
This setting removes any bias against Ganon's Tower that some algorithms may have.
## Include Helpful Hints
If set, the 15 telepathic tiles and 5 storytellers scattered about Hyrule will give helpful hints about various items and entrances. An exact breakdown of the hint
distribution is provided as an included text file.
## Use Custom Item Pool
If set, the item pool normally associated with your difficulty setting is replaced by the item pool specified in the custom tab. This feature is only supported when the randomizer is run
via the GUI; attempting to set this via the command line does nothing.
## Seed
Can be used to set a seed number to generate. Using the same seed with same settings on the same version of the entrance randomizer will always yield an identical output.
## Count
Use to batch generate multiple seeds with same settings. If a seed number is provided, it will be used for the first seed, then used to derive the next seed (i.e. generating 10 seeds with the same seed number given will produce the same 10 (different) roms each time).
# Command Line Options
```
-h, --help
```
Show the help message and exit.
```
--create_spoiler
```
Output a Spoiler File (default: False)
```
--logic [{noglitches,minorglitches,nologic}]
```
Select the game logic (default: noglitches)
```
--mode [{standard,open,swordless,inverted}]
```
Select the game mode. (default: open)
```
--goal [{ganon,pedestal,dungeons,triforcehunt,crystals}]
```
Select the game completion goal. (default: ganon)
```
--difficulty [{normal,hard,expert}]
```
Select the game difficulty. Affects available itempool. (default: normal)
```
--item_functionality [{normal,hard,expert}]
```
Select limits on item functionality to increase difficulty. (default: normal)
```
--timer [{none,display,timed,timed-ohko,ohko,timed-countdown}]
```
Select the timer setting. (default: none)
```
--progressive [{on,off,random}]
```
Select the setting for progressive equipment. (default: on)
```
--algorithm [{freshness,flood,vt21,vt22,vt25,vt26,balanced}]
```
Select item distribution algorithm. (default: balanced)
```
--shuffle [{default,simple,restricted,full,crossed,insanity,restricted_legacy,full_legacy,madness_legacy,insanity_legacy,dungeonsfull,dungeonssimple}]
```
Select entrance shuffle algorithm. (default: full)
```
--rom ROM
```
Path to a Japanese 1.0 A Link to the Past Rom. (default: Zelda no Densetsu - Kamigami no Triforce (Japan).sfc)
```
--loglevel [{error,info,warning,debug}]
```
Select level of logging for output. (default: info)
```
--seed SEED
```
Define seed number to generate. (default: None)
```
--count COUNT
```
Set the count option (default: None)
```
--quickswap
```
Use to enable quick item swap with L/R buttons. (default: False)
```
--fastmenu [{normal,instant,double,triple,quadruple,half}]
```
Alters the rate at which the menu opens and closes. (default: normal)
```
--disablemusic
```
Disables game music, resulting in the game sound being just the SFX. (default: False)
```
--keysanity
```
Enable Keysanity (default: False)
```
--retro
```
Enable Retro mode (default: False)
```
--nodungeonitems
```
If set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world.
This may lead to different amount of itempool items being placed in a dungeon than you are used to. (default: False)
```
--heartbeep [{normal,half,quarter,off}]
```
Select frequency of beeps when on low health. (default: normal)
```
--heartcolor [{red,blue,green,yellow,random}]
```
Select the color of Link\'s heart meter. (default: red)
```
--sprite SPRITE
```
Use to select a different sprite sheet to use for Link. Path to a binary file of length 0x7000 containing the sprite data stored at address 0x80000 in the rom. (default: None)
```
--accessibility [{items,locations,none}]
```
Sets the item/location accessibility rules. (default: items)
```
--hints
```
Enables helpful hints from storytellers and telepathic tiles (default: False)
```
--no-shuffleganon
```
Disables the "Include Ganon's Tower and Pyramid Hole in Shuffle pool" option. (default: Enabled)
```
--suppress_rom
```
Enables the "Do not Create Patched Rom" option. (default: False)
```
--gui
```
Open the graphical user interface. Preloads selections with set command line parameters.
Berserker's Multiworld Utilities for Bonta's Multiworld
=======================================================
This is a complete fork of Bonta's Multiworld V31, which assumes you already know how to setup and use that project. Instructions here are only for the additions.
This is a drop-in replacement with everything from Bonta's Multiworld included.
You can find a guide here: https://docs.google.com/document/d/1r7qs1-MK7YbFf2d-mEUeTy2wHykIf1ALG9pLtVvUbSw/edit#
Additions/Changes
-----------------
Project
* Available in precompiled form for Windows 64Bit on [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases) page.
* Compatible with Python 3.7 and 3.8. Potentially future versions as well.
* Update modules if they are too old, preventing a crash when trying to connect among potential other issues
* Autoinstall missing modules
* Allow newer versions of modules than specified, as they will *usually* not break compatibility
* Support for V31 extendedmsu
* Has support for binary patching to allow legal distribution of multiworld rom files
* Various performance improvements
* Various crash fixes
MultiMystery.py
* Allows you to generate a Multiworld with individual player mystery weights. Since weights can also be set to 100%, this also allows for individual settings for each player in a regular multiworld.
Basis is a .yaml file that sets these weights. You can find an [easy.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/easy.yaml) in this project folder to get started.
* Additional instructions are at the start of the file. Open with a text editor.
* Configuration options in the host.yaml file.
MultiServer.py
* Added a try/except to prevent malformed console commands from crashing the entire server
* Supports automatic port-forwarding, can be enabled in host.yaml
* improved `!players` command, mentioning how many players are currently connected of how many expected and who's missing
* /forfeitplayer Playername now works when the player is not currently connected
* various commands, like /senditem and /hint use "fuzzy text matching", no longer requiring you to enter a location, player name or item name perfectly
* Added /hint command on the server (use just /hint for help on command)
can be used as /hint Playername Itemname
All Itemnames can be found in Items.py starting at line 25
example:
/hint Berserker Progressive Sword
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Hype Cave - Top in ahhdurr's World
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Blind's Hideout - Far Right in Schulzer's World
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Palace of Darkness - Map Chest in Thorus's World
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Ganons Tower - Map Chest in Will's World
* A player-side hint command "!hint" also exists. It needs to be turned on in the host.yaml and is based on points.
Mystery.py
* Defaults to generating a non-race ROM (Bonta's only makes race ROMs at this time)
If a race ROM is desired, pass --create-race as argument to it
* When an error is generated due to a broken .yaml file, it now mentions in the error trace which file, line and character is the culprit
* Option for progressive items, allowing you to turn them off (see easy.yaml for more info)
* Rom-Option for extendedmsu (see easy.yaml for more info)
* Option for "timer"
* Supports new Meta-Mystery mode. Read [meta.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/meta.yaml) for details.
* Added `dungeonssimple` and `dungeonsfull` ER modes
MultiClient.py
* Awaits a Qusb2snes connection when started, latching on when available
* Terminal improvements
* Running it with a patch file will patch out the multiworld rom and then automatically connect to the host that created the multiworld
* Cheating is now controlled by the server and can be disabled through host.yaml

View File

@ -293,35 +293,26 @@ def create_regions(world, player):
create_dw_region(player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop'])
]
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
region = world.get_region(region_name, player)
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
region.shop = shop
world.shops.append(shop)
for index, (item, price) in enumerate(default_shop_contents[region_name]):
shop.add_inventory(index, item, price)
world.initialize_regions()
region = world.get_region('Capacity Upgrade', player)
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
region.shop = shop
world.shops.append(shop)
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
world.intialize_regions()
def create_lw_region(player, name, locations=None, exits=None):
def create_lw_region(player: int, name: str, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
def create_dw_region(player, name, locations=None, exits=None):
def create_dw_region(player: int, name: str, locations=None, exits=None):
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
def create_cave_region(player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
def create_dungeon_region(player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)
def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None):
def _create_region(player: int, name: str, type: RegionType, hint: str, locations=None, exits=None):
ret = Region(name, type, hint, player)
if locations is None:
locations = []
@ -331,14 +322,15 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
for location in locations:
address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret))
address, player_address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
return ret
def mark_light_world_regions(world):
def mark_light_world_regions(world, player: int):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in world.regions if region.type == RegionType.LightWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@ -351,7 +343,7 @@ def mark_light_world_regions(world):
seen.add(exit.connected_region)
queue.append(exit.connected_region)
queue = collections.deque(region for region in world.regions if region.type == RegionType.DarkWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@ -364,269 +356,278 @@ def mark_light_world_regions(world):
seen.add(exit.connected_region)
queue.append(exit.connected_region)
# (room_id, shopkeeper, replaceable)
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
'Red Shield Shop': (0x0110, 0xC1, True),
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
'Village of Outcasts Shop': (0x010F, 0xC1, True),
'Dark World Potion Shop': (0x010F, 0xC1, True),
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
'Kakariko Shop': (0x011F, 0xA0, True),
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
'Potion Shop': (0x0109, 0xFF, False),
# Bomb Shop not currently modeled as a shop, due to special nature of items
}
# region, [item]
# slot, item, price, max=0, replacement=None, replacement_price=0
# item = (item, price)
def create_shops(world, player: int):
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items():
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
locked = True
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
region = world.get_region(region_name, player)
shop = Shop(region, room_id, type, shopkeeper, custom, locked)
region.shop = shop
world.shops.append(shop)
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
# (type, room_id, shopkeeper, custom, locked, [items])
# item = (item, price, max=0, replacement=None, replacement_price=0)
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
default_shop_contents = {
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
'Village of Outcasts Shop': _dark_world_shop_defaults,
'Dark World Potion Shop': _dark_world_shop_defaults,
'Light World Death Mountain Shop': _basic_shop_defaults,
'Kakariko Shop': _basic_shop_defaults,
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
}
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
'Bottle Merchant': (0x2eb18, False, 'with a merchant'),
'Flute Spot': (0x18014a, False, 'underground'),
'Sunken Treasure': (0x180145, False, 'underwater'),
'Purple Chest': (0x33d68, False, 'from a box'),
"Blind's Hideout - Top": (0xeb0f, False, 'in a basement'),
"Blind's Hideout - Left": (0xeb12, False, 'in a basement'),
"Blind's Hideout - Right": (0xeb15, False, 'in a basement'),
"Blind's Hideout - Far Left": (0xeb18, False, 'in a basement'),
"Blind's Hideout - Far Right": (0xeb1b, False, 'in a basement'),
"Link's Uncle": (0x2df45, False, 'with your uncle'),
'Secret Passage': (0xe971, False, 'near your uncle'),
'King Zora': (0xee1c3, False, 'at a high price'),
"Zora's Ledge": (0x180149, False, 'near Zora'),
'Waterfall Fairy - Left': (0xe9b0, False, 'near a fairy'),
'Waterfall Fairy - Right': (0xe9d1, False, 'near a fairy'),
"King's Tomb": (0xe97a, False, 'alone in a cave'),
'Floodgate Chest': (0xe98c, False, 'in the dam'),
"Link's House": (0xe9bc, False, 'in your home'),
'Kakariko Tavern': (0xe9ce, False, 'in the bar'),
'Chicken House': (0xe9e9, False, 'near poultry'),
"Aginah's Cave": (0xe9f2, False, 'with Aginah'),
"Sahasrahla's Hut - Left": (0xea82, False, 'near the elder'),
"Sahasrahla's Hut - Middle": (0xea85, False, 'near the elder'),
"Sahasrahla's Hut - Right": (0xea88, False, 'near the elder'),
'Sahasrahla': (0x2f1fc, False, 'with the elder'),
'Kakariko Well - Top': (0xea8e, False, 'in a well'),
'Kakariko Well - Left': (0xea91, False, 'in a well'),
'Kakariko Well - Middle': (0xea94, False, 'in a well'),
'Kakariko Well - Right': (0xea97, False, 'in a well'),
'Kakariko Well - Bottom': (0xea9a, False, 'in a well'),
'Blacksmith': (0x18002a, False, 'with the smith'),
'Magic Bat': (0x180015, False, 'with the bat'),
'Sick Kid': (0x339cf, False, 'with the sick'),
'Hobo': (0x33e7d, False, 'with the hobo'),
'Lost Woods Hideout': (0x180000, False, 'near a thief'),
'Lumberjack Tree': (0x180001, False, 'in a hole'),
'Cave 45': (0x180003, False, 'alone in a cave'),
'Graveyard Cave': (0x180004, False, 'alone in a cave'),
'Checkerboard Cave': (0x180005, False, 'alone in a cave'),
'Mini Moldorm Cave - Far Left': (0xeb42, False, 'near Moldorms'),
'Mini Moldorm Cave - Left': (0xeb45, False, 'near Moldorms'),
'Mini Moldorm Cave - Right': (0xeb48, False, 'near Moldorms'),
'Mini Moldorm Cave - Far Right': (0xeb4b, False, 'near Moldorms'),
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'),
'Ice Rod Cave': (0xeb4e, False, 'in a frozen cave'),
'Bonk Rock Cave': (0xeb3f, False, 'alone in a cave'),
'Library': (0x180012, False, 'near books'),
'Potion Shop': (0x180014, False, 'near potions'),
'Lake Hylia Island': (0x180144, False, 'on an island'),
'Maze Race': (0x180142, False, 'at the race'),
'Desert Ledge': (0x180143, False, 'in the desert'),
'Desert Palace - Big Chest': (0xe98f, False, 'in Desert Palace'),
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'),
'Desert Palace - Map Chest': (0xe9b6, False, 'in Desert Palace'),
'Desert Palace - Compass Chest': (0xe9cb, False, 'in Desert Palace'),
'Desert Palace - Big Key Chest': (0xe9c2, False, 'in Desert Palace'),
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'),
'Eastern Palace - Compass Chest': (0xe977, False, 'in Eastern Palace'),
'Eastern Palace - Big Chest': (0xe97d, False, 'in Eastern Palace'),
'Eastern Palace - Cannonball Chest': (0xe9b3, False, 'in Eastern Palace'),
'Eastern Palace - Big Key Chest': (0xe9b9, False, 'in Eastern Palace'),
'Eastern Palace - Map Chest': (0xe9f5, False, 'in Eastern Palace'),
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'),
'Master Sword Pedestal': (0x289b0, False, 'at the pedestal'),
'Hyrule Castle - Boomerang Chest': (0xe974, False, 'in Hyrule Castle'),
'Hyrule Castle - Map Chest': (0xeb0c, False, 'in Hyrule Castle'),
"Hyrule Castle - Zelda's Chest": (0xeb09, False, 'in Hyrule Castle'),
'Sewers - Dark Cross': (0xe96e, False, 'in the sewers'),
'Sewers - Secret Room - Left': (0xeb5d, False, 'in the sewers'),
'Sewers - Secret Room - Middle': (0xeb60, False, 'in the sewers'),
'Sewers - Secret Room - Right': (0xeb63, False, 'in the sewers'),
'Sanctuary': (0xea79, False, 'in Sanctuary'),
'Castle Tower - Room 03': (0xeab5, False, 'in Castle Tower'),
'Castle Tower - Dark Maze': (0xeab2, False, 'in Castle Tower'),
'Old Man': (0xf69fa, False, 'with the old man'),
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'),
'Paradox Cave Lower - Far Left': (0xeb2a, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Left': (0xeb2d, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Right': (0xeb30, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Far Right': (0xeb33, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Middle': (0xeb36, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Left': (0xeb39, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Right': (0xeb3c, False, 'in a cave with seven chests'),
'Spiral Cave': (0xe9bf, False, 'in spiral cave'),
'Ether Tablet': (0x180016, False, 'at a monolith'),
'Spectacle Rock': (0x180140, False, 'atop a rock'),
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'),
'Tower of Hera - Map Chest': (0xe9ad, False, 'in Tower of Hera'),
'Tower of Hera - Big Key Chest': (0xe9e6, False, 'in Tower of Hera'),
'Tower of Hera - Compass Chest': (0xe9fb, False, 'in Tower of Hera'),
'Tower of Hera - Big Chest': (0xe9f8, False, 'in Tower of Hera'),
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'),
'Pyramid': (0x180147, False, 'on the pyramid'),
'Catfish': (0xee185, False, 'with a catfish'),
'Stumpy': (0x330c7, False, 'with tree boy'),
'Digging Game': (0x180148, False, 'underground'),
'Bombos Tablet': (0x180017, False, 'at a monolith'),
'Hype Cave - Top': (0xeb1e, False, 'near a bat-like man'),
'Hype Cave - Middle Right': (0xeb21, False, 'near a bat-like man'),
'Hype Cave - Middle Left': (0xeb24, False, 'near a bat-like man'),
'Hype Cave - Bottom': (0xeb27, False, 'near a bat-like man'),
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'),
'Peg Cave': (0x180006, False, 'alone in a cave'),
'Pyramid Fairy - Left': (0xe980, False, 'near a fairy'),
'Pyramid Fairy - Right': (0xe983, False, 'near a fairy'),
'Brewery': (0xe9ec, False, 'alone in a home'),
'C-Shaped House': (0xe9ef, False, 'alone in a home'),
'Chest Game': (0xeda8, False, 'as a prize'),
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'),
'Mire Shed - Left': (0xea73, False, 'near sparks'),
'Mire Shed - Right': (0xea76, False, 'near sparks'),
'Superbunny Cave - Top': (0xea7c, False, 'in a connection'),
'Superbunny Cave - Bottom': (0xea7f, False, 'in a connection'),
'Spike Cave': (0xea8b, False, 'beyond spikes'),
'Hookshot Cave - Top Right': (0xeb51, False, 'across pits'),
'Hookshot Cave - Top Left': (0xeb54, False, 'across pits'),
'Hookshot Cave - Bottom Right': (0xeb5a, False, 'across pits'),
'Hookshot Cave - Bottom Left': (0xeb57, False, 'across pits'),
'Floating Island': (0x180141, False, 'on an island'),
'Mimic Cave': (0xe9c5, False, 'in a cave of mimicry'),
'Swamp Palace - Entrance': (0xea9d, False, 'in Swamp Palace'),
'Swamp Palace - Map Chest': (0xe986, False, 'in Swamp Palace'),
'Swamp Palace - Big Chest': (0xe989, False, 'in Swamp Palace'),
'Swamp Palace - Compass Chest': (0xeaa0, False, 'in Swamp Palace'),
'Swamp Palace - Big Key Chest': (0xeaa6, False, 'in Swamp Palace'),
'Swamp Palace - West Chest': (0xeaa3, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Left': (0xeaa9, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Right': (0xeaac, False, 'in Swamp Palace'),
'Swamp Palace - Waterfall Room': (0xeaaf, False, 'in Swamp Palace'),
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'),
"Thieves' Town - Big Key Chest": (0xea04, False, "in Thieves' Town"),
"Thieves' Town - Map Chest": (0xea01, False, "in Thieves' Town"),
"Thieves' Town - Compass Chest": (0xea07, False, "in Thieves' Town"),
"Thieves' Town - Ambush Chest": (0xea0a, False, "in Thieves' Town"),
"Thieves' Town - Attic": (0xea0d, False, "in Thieves' Town"),
"Thieves' Town - Big Chest": (0xea10, False, "in Thieves' Town"),
"Thieves' Town - Blind's Cell": (0xea13, False, "in Thieves' Town"),
"Thieves' Town - Boss": (0x180156, False, 'with Blind'),
'Skull Woods - Compass Chest': (0xe992, False, 'in Skull Woods'),
'Skull Woods - Map Chest': (0xe99b, False, 'in Skull Woods'),
'Skull Woods - Big Chest': (0xe998, False, 'in Skull Woods'),
'Skull Woods - Pot Prison': (0xe9a1, False, 'in Skull Woods'),
'Skull Woods - Pinball Room': (0xe9c8, False, 'in Skull Woods'),
'Skull Woods - Big Key Chest': (0xe99e, False, 'in Skull Woods'),
'Skull Woods - Bridge Room': (0xe9fe, False, 'near Mothula'),
'Skull Woods - Boss': (0x180155, False, 'with Mothula'),
'Ice Palace - Compass Chest': (0xe9d4, False, 'in Ice Palace'),
'Ice Palace - Freezor Chest': (0xe995, False, 'in Ice Palace'),
'Ice Palace - Big Chest': (0xe9aa, False, 'in Ice Palace'),
'Ice Palace - Iced T Room': (0xe9e3, False, 'in Ice Palace'),
'Ice Palace - Spike Room': (0xe9e0, False, 'in Ice Palace'),
'Ice Palace - Big Key Chest': (0xe9a4, False, 'in Ice Palace'),
'Ice Palace - Map Chest': (0xe9dd, False, 'in Ice Palace'),
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'),
'Misery Mire - Big Chest': (0xea67, False, 'in Misery Mire'),
'Misery Mire - Map Chest': (0xea6a, False, 'in Misery Mire'),
'Misery Mire - Main Lobby': (0xea5e, False, 'in Misery Mire'),
'Misery Mire - Bridge Chest': (0xea61, False, 'in Misery Mire'),
'Misery Mire - Spike Chest': (0xe9da, False, 'in Misery Mire'),
'Misery Mire - Compass Chest': (0xea64, False, 'in Misery Mire'),
'Misery Mire - Big Key Chest': (0xea6d, False, 'in Misery Mire'),
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'),
'Turtle Rock - Compass Chest': (0xea22, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Left': (0xea1c, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Right': (0xea1f, False, 'in Turtle Rock'),
'Turtle Rock - Chain Chomps': (0xea16, False, 'in Turtle Rock'),
'Turtle Rock - Big Key Chest': (0xea25, False, 'in Turtle Rock'),
'Turtle Rock - Big Chest': (0xea19, False, 'in Turtle Rock'),
'Turtle Rock - Crystaroller Room': (0xea34, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Right': (0xea28, False, 'in Turtle Rock'),
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'),
'Palace of Darkness - Shooter Room': (0xea5b, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Bridge': (0xea3d, False, 'in Palace of Darkness'),
'Palace of Darkness - Stalfos Basement': (0xea49, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Key Chest': (0xea37, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Ledge': (0xea3a, False, 'in Palace of Darkness'),
'Palace of Darkness - Map Chest': (0xea52, False, 'in Palace of Darkness'),
'Palace of Darkness - Compass Chest': (0xea43, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Left': (0xea4c, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Right': (0xea4f, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Top': (0xea55, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Bottom': (0xea58, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Chest': (0xea40, False, 'in Palace of Darkness'),
'Palace of Darkness - Harmless Hellway': (0xea46, False, 'in Palace of Darkness'),
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'),
"Ganons Tower - Bob's Torch": (0x180161, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Left': (0xead9, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Right': (0xeadc, False, "in Ganon's Tower"),
'Ganons Tower - Tile Room': (0xeae2, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Left': (0xeae5, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Right': (0xeae8, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Left': (0xeab8, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Right': (0xeabb, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, False, "in Ganon's Tower"),
'Ganons Tower - Map Chest': (0xead3, False, "in Ganon's Tower"),
'Ganons Tower - Firesnake Room': (0xead0, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, False, "in Ganon's Tower"),
"Ganons Tower - Bob's Chest": (0xeadf, False, "in Ganon's Tower"),
'Ganons Tower - Big Chest': (0xead6, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Left': (0xeaf4, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Right': (0xeaf7, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Chest': (0xeaf1, False, "in Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, False, "atop Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, False, "atop Ganon's Tower"),
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, False, "atop Ganon's Tower"),
'Ganons Tower - Validation Chest': (0xeb06, False, "atop Ganon's Tower"),
'Ganon': (None, False, 'from me'),
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'),
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'),
'Floodgate': (None, False, None),
'Frog': (None, False, None),
'Missing Smith': (None, False, None),
'Dark Blacksmith Ruins': (None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'),
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'),
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'),
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'),
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'),
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'),
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'),
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'),
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')}
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
"Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
"Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
"Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
"Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
"Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
"Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
'King Zora': (0xee1c3, 0x186360, False, 'at a high price'),
"Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'),
'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'),
'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
"King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
"Link's House": (0xe9bc, 0x186190, False, 'in your home'),
'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
"Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
"Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
"Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
"Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
'Library': (0x180012, 0x18635c, False, 'near books'),
'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
'Maze Race': (0x180142, 0x186351, False, 'at the race'),
'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
"Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
'Digging Game': (0x180148, 0x186357, False, 'underground'),
'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
'Floating Island': (0x180141, 0x186350, False, 'on an island'),
'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
"Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
"Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
"Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
"Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
"Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
"Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
"Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
"Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
"Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
"Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
'Ganon': (None, None, False, 'from me'),
'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
'Floodgate': (None, None, False, None),
'Frog': (None, None, False, None),
'Missing Smith': (None, None, False, None),
'Dark Blacksmith Ruins': (None, None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
'Tower of Hera - Prize': (
[0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
'Palace of Darkness - Prize': (
[0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
'Swamp Palace - Prize': (
[0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
'Thieves\' Town - Prize': (
[0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
'Skull Woods - Prize': (
[0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
'Ice Palace - Prize': (
[0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
'Misery Mire - Prize': (
[0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
'Turtle Rock - Prize': (
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name[-1] = "cheat console"

1402
Rom.py

File diff suppressed because it is too large Load Diff

864
Rules.py

File diff suppressed because it is too large Load Diff

View File

@ -107,6 +107,7 @@ Triforce_texts = [
"You get one\nwish. Choose\nwisely, hero!",
"Can you please\nbreak us three\nup? Thanks.",
" Pick us up\n before we\n get dizzy!",
"\n Honk."
]
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!']
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?']
@ -366,7 +367,7 @@ class Credits(object):
SceneLargeCreditLine(23, "Woodsmen's Hut"),
],
'grove': [
SceneSmallCreditLine(19, 'ocarina boy plays again'),
SceneSmallCreditLine(19, 'flute boy plays again'),
SceneLargeCreditLine(23, 'Haunted Grove'),
],
'well': [
@ -427,7 +428,6 @@ class CreditLine(object):
@property
def x(self):
x = 0
if self.align == 'left':
x = 0
elif self.align == 'right':
@ -1311,7 +1311,7 @@ class TextTable(object):
'item_get_bombos',
'item_get_quake',
'item_get_hammer',
'item_get_ocarina',
'item_get_flute',
'item_get_cane_of_somaria',
'item_get_hookshot',
'item_get_bombs',
@ -1552,7 +1552,7 @@ class TextTable(object):
text['item_get_bombos'] = CompressedTextMapper.convert("Let's set everything on fire, and melt things!")
text['item_get_quake'] = CompressedTextMapper.convert("Time to make the earth shake, rattle, and roll!")
text['item_get_hammer'] = CompressedTextMapper.convert("STOP!\n\nHammer Time!") # 66
text['item_get_ocarina'] = CompressedTextMapper.convert("Finally! We can play the Song of Time!")
text['item_get_flute'] = CompressedTextMapper.convert("Finally! We can play the Song of Time!")
text['item_get_cane_of_somaria'] = CompressedTextMapper.convert("Make blocks!\nThrow blocks!\nsplode Blocks!")
text['item_get_hookshot'] = CompressedTextMapper.convert("BOING!!!\nBOING!!!\nSay no more…")
text['item_get_bombs'] = CompressedTextMapper.convert("BOMBS! Use A to pick 'em up, throw 'em, get hurt!")

View File

@ -1,53 +1,84 @@
import os
import subprocess
import sys
import typing
import functools
from yaml import load
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
def int16_as_bytes(value):
value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF]
def int32_as_bytes(value):
value = value & 0xFFFFFFFF
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
def pc_to_snes(value):
return ((value<<1) & 0x7F0000)|(value & 0x7FFF)|0x8000
def snes_to_pc(value):
return ((value & 0x7F0000)>>1)|(value & 0x7FFF)
def parse_player_names(names, players, teams):
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
ret = []
while names or len(ret) < teams:
team = [n[:16] for n in names[:players]]
# where does the 16 character limit come from?
while len(team) != players:
team.append(f"Player {len(team) + 1}")
ret.append(team)
names = names[players:]
return ret
def is_bundled():
return getattr(sys, 'frozen', False)
def local_path(path):
if local_path.cached_path is not None:
if local_path.cached_path:
return os.path.join(local_path.cached_path, path)
if is_bundled():
# we are running in a bundle
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
elif is_bundled():
if hasattr(sys, "_MEIPASS"):
# we are running in a PyInstaller bundle
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
else:
# cx_Freeze
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
# we are running in a normal Python environment
local_path.cached_path = os.path.dirname(os.path.abspath(__file__))
import __main__
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
return os.path.join(local_path.cached_path, path)
local_path.cached_path = None
def output_path(path):
if output_path.cached_path is not None:
if output_path.cached_path:
return os.path.join(output_path.cached_path, path)
if not is_bundled():
if not is_bundled() and not hasattr(sys, "_MEIPASS"):
# this should trigger if it's cx_freeze bundling
output_path.cached_path = '.'
return os.path.join(output_path.cached_path, path)
else:
# has been packaged, so cannot use CWD for output.
# has been PyInstaller packaged, so cannot use CWD for output.
if sys.platform == 'win32':
#windows
# windows
import ctypes.wintypes
CSIDL_PERSONAL = 5 # My Documents
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
CSIDL_PERSONAL = 5 # My Documents
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
@ -96,7 +127,7 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
with open(new_rom, 'rb') as stream:
new_rom_data = bytearray(stream.read())
# extend to 2 mb
old_rom_data.extend(bytearray([0x00] * (2097152 - len(old_rom_data))))
old_rom_data.extend(bytearray([0x00]) * (2097152 - len(old_rom_data)))
out_data = OrderedDict()
for idx, old in enumerate(old_rom_data):
@ -107,8 +138,49 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
if offset - 1 in out_data:
out_data[offset-1].extend(out_data.pop(offset))
with open('data/base2current.json', 'wt') as outfile:
json.dump([{key:value} for key, value in out_data.items()], outfile, separators=(",", ":"))
json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":"))
basemd5 = hashlib.md5()
basemd5.update(new_rom_data)
return "New Rom Hash: " + basemd5.hexdigest()
parse_yaml = functools.partial(load, Loader=Loader)
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
location: int
item: int
found: bool
def get_public_ipv4() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
except Exception as e:
try:
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
except:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
return ip
def get_options() -> dict:
if not hasattr(get_options, "options"):
locations = ("options.yaml", "host.yaml",
local_path("options.yaml"), local_path("host.yaml"))
for location in locations:
if os.path.exists(location):
with open(location) as f:
get_options.options = parse_yaml(f.read())
break
else:
raise FileNotFoundError(f"Could not find {locations[1]} to load options.")
return get_options.options

View File

View File

@ -1,5 +0,0 @@
Mike Lenzen <m.lenzen@gmail.com> https://github.com/mlenzen
Caleb Levy <caleb.levy@berkeley.edu> https://github.com/caleblevy
Marein Könings <mail@marein.org> https://github.com/MareinK
Jad Kik <jadkik94@gmail.com> https://github.com/jadkik
Kuba Marek <blue.cube@seznam.cz> https://github.com/bluecube

View File

@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,55 +0,0 @@
"""collections_extended contains a few extra basic data structures."""
from ._compat import Collection
from .bags import bag, frozenbag
from .setlists import setlist, frozensetlist
from .bijection import bijection
from .range_map import RangeMap, MappedRange
__version__ = '1.0.2'
__all__ = (
'collection',
'setlist',
'frozensetlist',
'bag',
'frozenbag',
'bijection',
'RangeMap',
'MappedRange',
'Collection',
)
def collection(iterable=None, mutable=True, ordered=False, unique=False):
"""Return a Collection with the specified properties.
Args:
iterable (Iterable): collection to instantiate new collection from.
mutable (bool): Whether or not the new collection is mutable.
ordered (bool): Whether or not the new collection is ordered.
unique (bool): Whether or not the new collection contains only unique values.
"""
if iterable is None:
iterable = tuple()
if unique:
if ordered:
if mutable:
return setlist(iterable)
else:
return frozensetlist(iterable)
else:
if mutable:
return set(iterable)
else:
return frozenset(iterable)
else:
if ordered:
if mutable:
return list(iterable)
else:
return tuple(iterable)
else:
if mutable:
return bag(iterable)
else:
return frozenbag(iterable)

View File

@ -1,53 +0,0 @@
"""Python 2/3 compatibility helpers."""
import sys
is_py2 = sys.version_info[0] == 2
if is_py2:
def keys_set(d):
"""Return a set of passed dictionary's keys."""
return set(d.keys())
else:
keys_set = dict.keys
if sys.version_info < (3, 6):
from collections import Sized, Iterable, Container
def _check_methods(C, *methods):
mro = C.__mro__
for method in methods:
for B in mro:
if method in B.__dict__:
if B.__dict__[method] is None:
return NotImplemented
break
else:
return NotImplemented
return True
class Collection(Sized, Iterable, Container):
"""Backport from Python3.6."""
__slots__ = tuple()
@classmethod
def __subclasshook__(cls, C):
if cls is Collection:
return _check_methods(C, "__len__", "__iter__", "__contains__")
return NotImplemented
else:
from collections.abc import Collection
def handle_rich_comp_not_implemented():
"""Correctly handle unimplemented rich comparisons.
In Python 3, return NotImplemented.
In Python 2, raise a TypeError.
"""
if is_py2:
raise TypeError()
else:
return NotImplemented

View File

@ -1,16 +0,0 @@
"""util functions for collections_extended."""
def hash_iterable(it):
"""Perform a O(1) memory hash of an iterable of arbitrary length.
hash(tuple(it)) creates a temporary tuple containing all values from it
which could be a problem if it is large.
See discussion at:
https://groups.google.com/forum/#!msg/python-ideas/XcuC01a8SYs/e-doB9TbDwAJ
"""
hash_value = hash(type(it))
for value in it:
hash_value = hash((hash_value, value))
return hash_value

View File

@ -1,527 +0,0 @@
"""Bag class definitions."""
import heapq
from operator import itemgetter
from collections import Set, MutableSet, Hashable
from . import _compat
class _basebag(Set):
"""Base class for bag classes.
Base class for bag and frozenbag. Is not mutable and not hashable, so there's
no reason to use this instead of either bag or frozenbag.
"""
# Basic object methods
def __init__(self, iterable=None):
"""Create a new basebag.
If iterable isn't given, is None or is empty then the bag starts empty.
Otherwise each element from iterable will be added to the bag
however many times it appears.
This runs in O(len(iterable))
"""
self._dict = dict()
self._size = 0
if iterable:
if isinstance(iterable, _basebag):
for elem, count in iterable._dict.items():
self._dict[elem] = count
self._size += count
else:
for value in iterable:
self._dict[value] = self._dict.get(value, 0) + 1
self._size += 1
def __repr__(self):
if self._size == 0:
return '{0}()'.format(self.__class__.__name__)
else:
repr_format = '{class_name}({values!r})'
return repr_format.format(
class_name=self.__class__.__name__,
values=tuple(self),
)
def __str__(self):
if self._size == 0:
return '{class_name}()'.format(class_name=self.__class__.__name__)
else:
format_single = '{elem!r}'
format_mult = '{elem!r}^{mult}'
strings = []
for elem, mult in self._dict.items():
if mult > 1:
strings.append(format_mult.format(elem=elem, mult=mult))
else:
strings.append(format_single.format(elem=elem))
return '{%s}' % ', '.join(strings)
# New public methods (not overriding/implementing anything)
def num_unique_elements(self):
"""Return the number of unique elements.
This runs in O(1) time
"""
return len(self._dict)
def unique_elements(self):
"""Return a view of unique elements in this bag.
In Python 3:
This runs in O(1) time and returns a view of the unique elements
In Python 2:
This runs in O(n) and returns set of the current elements.
"""
return _compat.keys_set(self._dict)
def count(self, value):
"""Return the number of value present in this bag.
If value is not in the bag no Error is raised, instead 0 is returned.
This runs in O(1) time
Args:
value: The element of self to get the count of
Returns:
int: The count of value in self
"""
return self._dict.get(value, 0)
def nlargest(self, n=None):
"""List the n most common elements and their counts.
List is from the most
common to the least. If n is None, the list all element counts.
Run time should be O(m log m) where m is len(self)
Args:
n (int): The number of elements to return
"""
if n is None:
return sorted(self._dict.items(), key=itemgetter(1), reverse=True)
else:
return heapq.nlargest(n, self._dict.items(), key=itemgetter(1))
@classmethod
def _from_iterable(cls, it):
return cls(it)
@classmethod
def from_mapping(cls, mapping):
"""Create a bag from a dict of elem->count.
Each key in the dict is added if the value is > 0.
"""
out = cls()
for elem, count in mapping.items():
if count > 0:
out._dict[elem] = count
out._size += count
return out
def copy(self):
"""Create a shallow copy of self.
This runs in O(len(self.num_unique_elements()))
"""
return self.from_mapping(self._dict)
# implementing Sized methods
def __len__(self):
"""Return the cardinality of the bag.
This runs in O(1)
"""
return self._size
# implementing Container methods
def __contains__(self, value):
"""Return the multiplicity of the element.
This runs in O(1)
"""
return self._dict.get(value, 0)
# implementing Iterable methods
def __iter__(self):
"""Iterate through all elements.
Multiple copies will be returned if they exist.
"""
for value, count in self._dict.items():
for i in range(count):
yield(value)
# Comparison methods
def _is_subset(self, other):
"""Check that every element in self has a count <= in other.
Args:
other (Set)
"""
if isinstance(other, _basebag):
for elem, count in self._dict.items():
if not count <= other._dict.get(elem, 0):
return False
else:
for elem in self:
if self._dict.get(elem, 0) > 1 or elem not in other:
return False
return True
def _is_superset(self, other):
"""Check that every element in self has a count >= in other.
Args:
other (Set)
"""
if isinstance(other, _basebag):
for elem, count in other._dict.items():
if not self._dict.get(elem, 0) >= count:
return False
else:
for elem in other:
if elem not in self:
return False
return True
def __le__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) <= len(other) and self._is_subset(other)
def __lt__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) < len(other) and self._is_subset(other)
def __gt__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) > len(other) and self._is_superset(other)
def __ge__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) >= len(other) and self._is_superset(other)
def __eq__(self, other):
if not isinstance(other, Set):
return False
if isinstance(other, _basebag):
return self._dict == other._dict
if not len(self) == len(other):
return False
for elem in other:
if self._dict.get(elem, 0) != 1:
return False
return True
def __ne__(self, other):
return not (self == other)
# Operations - &, |, +, -, ^, * and isdisjoint
def __and__(self, other):
"""Intersection is the minimum of corresponding counts.
This runs in O(l + n) where:
n is self.num_unique_elements()
if other is a bag:
l = 1
else:
l = len(other)
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
values = dict()
for elem in self._dict:
values[elem] = min(other._dict.get(elem, 0), self._dict.get(elem, 0))
return self.from_mapping(values)
def isdisjoint(self, other):
"""Return if this bag is disjoint with the passed collection.
This runs in O(len(other))
TODO move isdisjoint somewhere more appropriate
"""
for value in other:
if value in self:
return False
return True
def __or__(self, other):
"""Union is the maximum of all elements.
This runs in O(m + n) where:
n is self.num_unique_elements()
if other is a bag:
m = other.num_unique_elements()
else:
m = len(other)
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
values = dict()
for elem in self.unique_elements() | other.unique_elements():
values[elem] = max(self._dict.get(elem, 0), other._dict.get(elem, 0))
return self.from_mapping(values)
def __add__(self, other):
"""Return a new bag also containing all the elements of other.
self + other = self & other + self | other
This runs in O(m + n) where:
n is self.num_unique_elements()
m is len(other)
Args:
other (Iterable): elements to add to self
"""
out = self.copy()
for value in other:
out._dict[value] = out._dict.get(value, 0) + 1
out._size += 1
return out
def __sub__(self, other):
"""Difference between the sets.
For normal sets this is all x s.t. x in self and x not in other.
For bags this is count(x) = max(0, self.count(x)-other.count(x))
This runs in O(m + n) where:
n is self.num_unique_elements()
m is len(other)
Args:
other (Iterable): elements to remove
"""
out = self.copy()
for value in other:
old_count = out._dict.get(value, 0)
if old_count == 1:
del out._dict[value]
out._size -= 1
elif old_count > 1:
out._dict[value] = old_count - 1
out._size -= 1
return out
def __mul__(self, other):
"""Cartesian product of the two sets.
other can be any iterable.
Both self and other must contain elements that can be added together.
This should run in O(m*n+l) where:
m is the number of unique elements in self
n is the number of unique elements in other
if other is a bag:
l is 0
else:
l is the len(other)
The +l will only really matter when other is an iterable with MANY
repeated elements.
For example: {'a'^2} * 'bbbbbbbbbbbbbbbbbbbbbbbbbb'
The algorithm will be dominated by counting the 'b's
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
values = dict()
for elem, count in self._dict.items():
for other_elem, other_count in other._dict.items():
new_elem = elem + other_elem
new_count = count * other_count
values[new_elem] = new_count
return self.from_mapping(values)
def __xor__(self, other):
"""Symmetric difference between the sets.
other can be any iterable.
This runs in O(m + n) where:
m = len(self)
n = len(other)
"""
return (self - other) | (other - self)
class bag(_basebag, MutableSet):
"""bag is a mutable unhashable bag."""
def pop(self):
"""Remove and return an element of self."""
# TODO can this be done more efficiently (no need to create an iterator)?
it = iter(self)
try:
value = next(it)
except StopIteration:
raise KeyError
self.discard(value)
return value
def add(self, elem):
"""Add elem to self."""
self._dict[elem] = self._dict.get(elem, 0) + 1
self._size += 1
def discard(self, elem):
"""Remove elem from this bag, silent if it isn't present."""
try:
self.remove(elem)
except ValueError:
pass
def remove(self, elem):
"""Remove elem from this bag, raising a ValueError if it isn't present.
Args:
elem: object to remove from self
Raises:
ValueError: if the elem isn't present
"""
old_count = self._dict.get(elem, 0)
if old_count == 0:
raise ValueError
elif old_count == 1:
del self._dict[elem]
else:
self._dict[elem] -= 1
self._size -= 1
def discard_all(self, other):
"""Discard all of the elems from other."""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, other_count in other._dict.items():
old_count = self._dict.get(elem, 0)
new_count = old_count - other_count
if new_count >= 0:
if new_count == 0:
if elem in self:
del self._dict[elem]
else:
self._dict[elem] = new_count
self._size += new_count - old_count
def remove_all(self, other):
"""Remove all of the elems from other.
Raises a ValueError if the multiplicity of any elem in other is greater
than in self.
"""
if not self._is_superset(other):
raise ValueError
self.discard_all(other)
def clear(self):
"""Remove all elements from this bag."""
self._dict = dict()
self._size = 0
# In-place operations
def __ior__(self, other):
"""Set multiplicity of each element to the maximum of the two collections.
if isinstance(other, _basebag):
This runs in O(other.num_unique_elements())
else:
This runs in O(len(other))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, other_count in other._dict.items():
old_count = self._dict.get(elem, 0)
new_count = max(other_count, old_count)
self._dict[elem] = new_count
self._size += new_count - old_count
return self
def __iand__(self, other):
"""Set multiplicity of each element to the minimum of the two collections.
if isinstance(other, _basebag):
This runs in O(other.num_unique_elements())
else:
This runs in O(len(other))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, old_count in set(self._dict.items()):
other_count = other._dict.get(elem, 0)
new_count = min(other_count, old_count)
if new_count == 0:
del self._dict[elem]
else:
self._dict[elem] = new_count
self._size += new_count - old_count
return self
def __ixor__(self, other):
"""Set self to the symmetric difference between the sets.
if isinstance(other, _basebag):
This runs in O(other.num_unique_elements())
else:
This runs in O(len(other))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
other_minus_self = other - self
self -= other
self |= other_minus_self
return self
def __isub__(self, other):
"""Discard the elements of other from self.
if isinstance(it, _basebag):
This runs in O(it.num_unique_elements())
else:
This runs in O(len(it))
"""
self.discard_all(other)
return self
def __iadd__(self, other):
"""Add all of the elements of other to self.
if isinstance(it, _basebag):
This runs in O(it.num_unique_elements())
else:
This runs in O(len(it))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, other_count in other._dict.items():
self._dict[elem] = self._dict.get(elem, 0) + other_count
self._size += other_count
return self
class frozenbag(_basebag, Hashable):
"""frozenbag is an immutable, hashable bab."""
def __hash__(self):
"""Compute the hash value of a frozenbag.
This was copied directly from _collections_abc.Set._hash in Python3 which
is identical to _abcoll.Set._hash
We can't call it directly because Python2 raises a TypeError.
"""
if not hasattr(self, '_hash_value'):
self._hash_value = self._hash()
return self._hash_value

View File

@ -1,94 +0,0 @@
"""Class definition for bijection."""
from collections import MutableMapping, Mapping
class bijection(MutableMapping):
"""A one-to-one onto mapping, a dict with unique values."""
def __init__(self, iterable=None, **kwarg):
"""Create a bijection from an iterable.
Matches dict.__init__.
"""
self._data = {}
self.__inverse = self.__new__(bijection)
self.__inverse._data = {}
self.__inverse.__inverse = self
if iterable is not None:
if isinstance(iterable, Mapping):
for key, value in iterable.items():
self[key] = value
else:
for pair in iterable:
self[pair[0]] = pair[1]
for key, value in kwarg.items():
self[key] = value
def __repr__(self):
if len(self._data) == 0:
return '{0}()'.format(self.__class__.__name__)
else:
repr_format = '{class_name}({values!r})'
return repr_format.format(
class_name=self.__class__.__name__,
values=self._data,
)
@property
def inverse(self):
"""Return the inverse of this bijection."""
return self.__inverse
# Required for MutableMapping
def __len__(self):
return len(self._data)
# Required for MutableMapping
def __getitem__(self, key):
return self._data[key]
# Required for MutableMapping
def __setitem__(self, key, value):
if key in self:
del self.inverse._data[self[key]]
if value in self.inverse:
del self._data[self.inverse[value]]
self._data[key] = value
self.inverse._data[value] = key
# Required for MutableMapping
def __delitem__(self, key):
value = self._data.pop(key)
del self.inverse._data[value]
# Required for MutableMapping
def __iter__(self):
return iter(self._data)
def __contains__(self, key):
return key in self._data
def clear(self):
"""Remove everything from this bijection."""
self._data.clear()
self.inverse._data.clear()
def copy(self):
"""Return a copy of this bijection."""
return bijection(self)
def items(self):
"""See Mapping.items."""
return self._data.items()
def keys(self):
"""See Mapping.keys."""
return self._data.keys()
def values(self):
"""See Mapping.values."""
return self.inverse.keys()
def __eq__(self, other):
return isinstance(other, bijection) and self._data == other._data

View File

@ -1,384 +0,0 @@
"""RangeMap class definition."""
from bisect import bisect_left, bisect_right
from collections import namedtuple, Mapping, MappingView, Set
# Used to mark unmapped ranges
_empty = object()
MappedRange = namedtuple('MappedRange', ('start', 'stop', 'value'))
class KeysView(MappingView, Set):
"""A view of the keys that mark the starts of subranges.
Since iterating over all the keys is impossible, the KeysView only
contains the keys that start each subrange.
"""
__slots__ = ()
@classmethod
def _from_iterable(self, it):
return set(it)
def __contains__(self, key):
loc = self._mapping._bisect_left(key)
return self._mapping._keys[loc] == key and \
self._mapping._values[loc] is not _empty
def __iter__(self):
for item in self._mapping.ranges():
yield item.start
class ItemsView(MappingView, Set):
"""A view of the items that mark the starts of subranges.
Since iterating over all the keys is impossible, the ItemsView only
contains the items that start each subrange.
"""
__slots__ = ()
@classmethod
def _from_iterable(self, it):
return set(it)
def __contains__(self, item):
key, value = item
loc = self._mapping._bisect_left(key)
return self._mapping._keys[loc] == key and \
self._mapping._values[loc] == value
def __iter__(self):
for mapped_range in self._mapping.ranges():
yield (mapped_range.start, mapped_range.value)
class ValuesView(MappingView):
"""A view on the values of a Mapping."""
__slots__ = ()
def __contains__(self, value):
return value in self._mapping._values
def __iter__(self):
for value in self._mapping._values:
if value is not _empty:
yield value
def _check_start_stop(start, stop):
"""Check that start and stop are valid - orderable and in the right order.
Raises:
ValueError: if stop <= start
TypeError: if unorderable
"""
if start is not None and stop is not None and stop <= start:
raise ValueError('stop must be > start')
def _check_key_slice(key):
if not isinstance(key, slice):
raise TypeError('Can only set and delete slices')
if key.step is not None:
raise ValueError('Cannot set or delete slices with steps')
class RangeMap(Mapping):
"""Map ranges of orderable elements to values."""
def __init__(self, iterable=None, **kwargs):
"""Create a RangeMap.
A mapping or other iterable can be passed to initialize the RangeMap.
If mapping is passed, it is interpreted as a mapping from range start
indices to values.
If an iterable is passed, each element will define a range in the
RangeMap and should be formatted (start, stop, value).
default_value is a an optional keyword argument that will initialize the
entire RangeMap to that value. Any missing ranges will be mapped to that
value. However, if ranges are subsequently deleted they will be removed
and *not* mapped to the default_value.
Args:
iterable: A Mapping or an Iterable to initialize from.
default_value: If passed, the return value for all keys less than the
least key in mapping or missing ranges in iterable. If no mapping
or iterable, the return value for all keys.
"""
default_value = kwargs.pop('default_value', _empty)
if kwargs:
raise TypeError('Unknown keyword arguments: %s' % ', '.join(kwargs.keys()))
self._keys = [None]
self._values = [default_value]
if iterable:
if isinstance(iterable, Mapping):
self._init_from_mapping(iterable)
else:
self._init_from_iterable(iterable)
@classmethod
def from_mapping(cls, mapping):
"""Create a RangeMap from a mapping of interval starts to values."""
obj = cls()
obj._init_from_mapping(mapping)
return obj
def _init_from_mapping(self, mapping):
for key, value in sorted(mapping.items()):
self.set(value, key)
@classmethod
def from_iterable(cls, iterable):
"""Create a RangeMap from an iterable of tuples defining each range.
Each element of the iterable is a tuple (start, stop, value).
"""
obj = cls()
obj._init_from_iterable(iterable)
return obj
def _init_from_iterable(self, iterable):
for start, stop, value in iterable:
self.set(value, start=start, stop=stop)
def __str__(self):
range_format = '({range.start}, {range.stop}): {range.value}'
values = ', '.join([range_format.format(range=r) for r in self.ranges()])
return 'RangeMap(%s)' % values
def __repr__(self):
range_format = '({range.start!r}, {range.stop!r}, {range.value!r})'
values = ', '.join([range_format.format(range=r) for r in self.ranges()])
return 'RangeMap([%s])' % values
def _bisect_left(self, key):
"""Return the index of the key or the last key < key."""
if key is None:
return 0
else:
return bisect_left(self._keys, key, lo=1)
def _bisect_right(self, key):
"""Return the index of the first key > key."""
if key is None:
return 1
else:
return bisect_right(self._keys, key, lo=1)
def ranges(self, start=None, stop=None):
"""Generate MappedRanges for all mapped ranges.
Yields:
MappedRange
"""
_check_start_stop(start, stop)
start_loc = self._bisect_right(start)
if stop is None:
stop_loc = len(self._keys)
else:
stop_loc = self._bisect_left(stop)
start_val = self._values[start_loc - 1]
candidate_keys = [start] + self._keys[start_loc:stop_loc] + [stop]
candidate_values = [start_val] + self._values[start_loc:stop_loc]
for i, value in enumerate(candidate_values):
if value is not _empty:
start_key = candidate_keys[i]
stop_key = candidate_keys[i + 1]
yield MappedRange(start_key, stop_key, value)
def __contains__(self, value):
try:
self.__getitem(value) is not _empty
except KeyError:
return False
else:
return True
def __iter__(self):
for key, value in zip(self._keys, self._values):
if value is not _empty:
yield key
def __bool__(self):
if len(self._keys) > 1:
return True
else:
return self._values[0] != _empty
__nonzero__ = __bool__
def __getitem(self, key):
"""Get the value for a key (not a slice)."""
loc = self._bisect_right(key) - 1
value = self._values[loc]
if value is _empty:
raise KeyError(key)
else:
return value
def get(self, key, restval=None):
"""Get the value of the range containing key, otherwise return restval."""
try:
return self.__getitem(key)
except KeyError:
return restval
def get_range(self, start=None, stop=None):
"""Return a RangeMap for the range start to stop.
Returns:
A RangeMap
"""
return self.from_iterable(self.ranges(start, stop))
def set(self, value, start=None, stop=None):
"""Set the range from start to stop to value."""
_check_start_stop(start, stop)
# start_index, stop_index will denote the sections we are replacing
start_index = self._bisect_left(start)
if start is not None: # start_index == 0
prev_value = self._values[start_index - 1]
if prev_value == value:
# We're setting a range where the left range has the same
# value, so create one big range
start_index -= 1
start = self._keys[start_index]
if stop is None:
new_keys = [start]
new_values = [value]
stop_index = len(self._keys)
else:
stop_index = self._bisect_right(stop)
stop_value = self._values[stop_index - 1]
stop_key = self._keys[stop_index - 1]
if stop_key == stop and stop_value == value:
new_keys = [start]
new_values = [value]
else:
new_keys = [start, stop]
new_values = [value, stop_value]
self._keys[start_index:stop_index] = new_keys
self._values[start_index:stop_index] = new_values
def delete(self, start=None, stop=None):
"""Delete the range from start to stop from self.
Raises:
KeyError: If part of the passed range isn't mapped.
"""
_check_start_stop(start, stop)
start_loc = self._bisect_right(start) - 1
if stop is None:
stop_loc = len(self._keys)
else:
stop_loc = self._bisect_left(stop)
for value in self._values[start_loc:stop_loc]:
if value is _empty:
raise KeyError((start, stop))
# this is inefficient, we've already found the sub ranges
self.set(_empty, start=start, stop=stop)
def empty(self, start=None, stop=None):
"""Empty the range from start to stop.
Like delete, but no Error is raised if the entire range isn't mapped.
"""
self.set(_empty, start=start, stop=stop)
def clear(self):
"""Remove all elements."""
self._keys = [None]
self._values = [_empty]
@property
def start(self):
"""Get the start key of the first range.
None if RangeMap is empty or unbounded to the left.
"""
if self._values[0] is _empty:
try:
return self._keys[1]
except IndexError:
# This is empty or everything is mapped to a single value
return None
else:
# This is unbounded to the left
return self._keys[0]
@property
def end(self):
"""Get the stop key of the last range.
None if RangeMap is empty or unbounded to the right.
"""
if self._values[-1] is _empty:
return self._keys[-1]
else:
# This is unbounded to the right
return None
def __eq__(self, other):
if isinstance(other, RangeMap):
return (
self._keys == other._keys and
self._values == other._values
)
else:
return False
def __getitem__(self, key):
try:
_check_key_slice(key)
except TypeError:
return self.__getitem(key)
else:
return self.get_range(key.start, key.stop)
def __setitem__(self, key, value):
_check_key_slice(key)
self.set(value, key.start, key.stop)
def __delitem__(self, key):
_check_key_slice(key)
self.delete(key.start, key.stop)
def __len__(self):
count = 0
for v in self._values:
if v is not _empty:
count += 1
return count
def keys(self):
"""Return a view of the keys."""
return KeysView(self)
def values(self):
"""Return a view of the values."""
return ValuesView(self)
def items(self):
"""Return a view of the item pairs."""
return ItemsView(self)
# Python2 - override slice methods
def __setslice__(self, i, j, value):
"""Implement __setslice__ to override behavior in Python 2.
This is required because empty slices pass integers in python2 as opposed
to None in python 3.
"""
raise SyntaxError('Assigning slices doesn\t work in Python 2, use set')
def __delslice__(self, i, j):
raise SyntaxError('Deleting slices doesn\t work in Python 2, use delete')
def __getslice__(self, i, j):
raise SyntaxError('Getting slices doesn\t work in Python 2, use get_range.')

View File

@ -1,552 +0,0 @@
"""Setlist class definitions."""
import random as random_
from collections import (
Sequence,
Set,
MutableSequence,
MutableSet,
Hashable,
)
from . import _util
class _basesetlist(Sequence, Set):
"""A setlist is an ordered Collection of unique elements.
_basesetlist is the superclass of setlist and frozensetlist. It is immutable
and unhashable.
"""
def __init__(self, iterable=None, raise_on_duplicate=False):
"""Create a setlist.
Args:
iterable (Iterable): Values to initialize the setlist with.
"""
self._list = list()
self._dict = dict()
if iterable:
if raise_on_duplicate:
self._extend(iterable)
else:
self._update(iterable)
def __repr__(self):
if len(self) == 0:
return '{0}()'.format(self.__class__.__name__)
else:
repr_format = '{class_name}({values!r})'
return repr_format.format(
class_name=self.__class__.__name__,
values=tuple(self),
)
# Convenience methods
def _fix_neg_index(self, index):
if index < 0:
index += len(self)
if index < 0:
raise IndexError('index is out of range')
return index
def _fix_end_index(self, index):
if index is None:
return len(self)
else:
return self._fix_neg_index(index)
def _append(self, value):
# Checking value in self will check that value is Hashable
if value in self:
raise ValueError('Value "%s" already present' % str(value))
else:
self._dict[value] = len(self)
self._list.append(value)
def _extend(self, values):
new_values = set()
for value in values:
if value in new_values:
raise ValueError('New values contain duplicates')
elif value in self:
raise ValueError('New values contain elements already present in self')
else:
new_values.add(value)
for value in values:
self._dict[value] = len(self)
self._list.append(value)
def _add(self, item):
if item not in self:
self._dict[item] = len(self)
self._list.append(item)
def _update(self, values):
for value in values:
if value not in self:
self._dict[value] = len(self)
self._list.append(value)
@classmethod
def _from_iterable(cls, it, **kwargs):
return cls(it, **kwargs)
# Implement Container
def __contains__(self, value):
return value in self._dict
# Iterable we get by inheriting from Sequence
# Implement Sized
def __len__(self):
return len(self._list)
# Implement Sequence
def __getitem__(self, index):
if isinstance(index, slice):
return self._from_iterable(self._list[index])
return self._list[index]
def count(self, value):
"""Return the number of occurences of value in self.
This runs in O(1)
Args:
value: The value to count
Returns:
int: 1 if the value is in the setlist, otherwise 0
"""
if value in self:
return 1
else:
return 0
def index(self, value, start=0, end=None):
"""Return the index of value between start and end.
By default, the entire setlist is searched.
This runs in O(1)
Args:
value: The value to find the index of
start (int): The index to start searching at (defaults to 0)
end (int): The index to stop searching at (defaults to the end of the list)
Returns:
int: The index of the value
Raises:
ValueError: If the value is not in the list or outside of start - end
IndexError: If start or end are out of range
"""
try:
index = self._dict[value]
except KeyError:
raise ValueError
else:
start = self._fix_neg_index(start)
end = self._fix_end_index(end)
if start <= index and index < end:
return index
else:
raise ValueError
@classmethod
def _check_type(cls, other, operand_name):
if not isinstance(other, _basesetlist):
message = (
"unsupported operand type(s) for {operand_name}: "
"'{self_type}' and '{other_type}'").format(
operand_name=operand_name,
self_type=cls,
other_type=type(other),
)
raise TypeError(message)
def __add__(self, other):
self._check_type(other, '+')
out = self.copy()
out._extend(other)
return out
# Implement Set
def issubset(self, other):
return self <= other
def issuperset(self, other):
return self >= other
def union(self, other):
out = self.copy()
out.update(other)
return out
def intersection(self, other):
other = set(other)
return self._from_iterable(item for item in self if item in other)
def difference(self, other):
other = set(other)
return self._from_iterable(item for item in self if item not in other)
def symmetric_difference(self, other):
return self.union(other) - self.intersection(other)
def __sub__(self, other):
self._check_type(other, '-')
return self.difference(other)
def __and__(self, other):
self._check_type(other, '&')
return self.intersection(other)
def __or__(self, other):
self._check_type(other, '|')
return self.union(other)
def __xor__(self, other):
self._check_type(other, '^')
return self.symmetric_difference(other)
# Comparison
def __eq__(self, other):
if not isinstance(other, _basesetlist):
return False
if not len(self) == len(other):
return False
for self_elem, other_elem in zip(self, other):
if self_elem != other_elem:
return False
return True
def __ne__(self, other):
return not (self == other)
# New methods
def sub_index(self, sub, start=0, end=None):
"""Return the index of a subsequence.
This runs in O(len(sub))
Args:
sub (Sequence): An Iterable to search for
Returns:
int: The index of the first element of sub
Raises:
ValueError: If sub isn't a subsequence
TypeError: If sub isn't iterable
IndexError: If start or end are out of range
"""
start_index = self.index(sub[0], start, end)
end = self._fix_end_index(end)
if start_index + len(sub) > end:
raise ValueError
for i in range(1, len(sub)):
if sub[i] != self[start_index + i]:
raise ValueError
return start_index
def copy(self):
return self.__class__(self)
class setlist(_basesetlist, MutableSequence, MutableSet):
"""A mutable (unhashable) setlist."""
def __str__(self):
return '{[%s}]' % ', '.join(repr(v) for v in self)
# Helper methods
def _delete_all(self, elems_to_delete, raise_errors):
indices_to_delete = set()
for elem in elems_to_delete:
try:
elem_index = self._dict[elem]
except KeyError:
if raise_errors:
raise ValueError('Passed values contain elements not in self')
else:
if elem_index in indices_to_delete:
if raise_errors:
raise ValueError('Passed vales contain duplicates')
indices_to_delete.add(elem_index)
self._delete_values_by_index(indices_to_delete)
def _delete_values_by_index(self, indices_to_delete):
deleted_count = 0
for i, elem in enumerate(self._list):
if i in indices_to_delete:
deleted_count += 1
del self._dict[elem]
else:
new_index = i - deleted_count
self._list[new_index] = elem
self._dict[elem] = new_index
# Now remove deleted_count items from the end of the list
if deleted_count:
self._list = self._list[:-deleted_count]
# Set/Sequence agnostic
def pop(self, index=-1):
"""Remove and return the item at index."""
value = self._list.pop(index)
del self._dict[value]
return value
def clear(self):
"""Remove all elements from self."""
self._dict = dict()
self._list = list()
# Implement MutableSequence
def __setitem__(self, index, value):
if isinstance(index, slice):
old_values = self[index]
for v in value:
if v in self and v not in old_values:
raise ValueError
self._list[index] = value
self._dict = {}
for i, v in enumerate(self._list):
self._dict[v] = i
else:
index = self._fix_neg_index(index)
old_value = self._list[index]
if value in self:
if value == old_value:
return
else:
raise ValueError
del self._dict[old_value]
self._list[index] = value
self._dict[value] = index
def __delitem__(self, index):
if isinstance(index, slice):
indices_to_delete = set(self.index(e) for e in self._list[index])
self._delete_values_by_index(indices_to_delete)
else:
index = self._fix_neg_index(index)
value = self._list[index]
del self._dict[value]
for elem in self._list[index + 1:]:
self._dict[elem] -= 1
del self._list[index]
def insert(self, index, value):
"""Insert value at index.
Args:
index (int): Index to insert value at
value: Value to insert
Raises:
ValueError: If value already in self
IndexError: If start or end are out of range
"""
if value in self:
raise ValueError
index = self._fix_neg_index(index)
self._dict[value] = index
for elem in self._list[index:]:
self._dict[elem] += 1
self._list.insert(index, value)
def append(self, value):
"""Append value to the end.
Args:
value: Value to append
Raises:
ValueError: If value alread in self
TypeError: If value isn't hashable
"""
self._append(value)
def extend(self, values):
"""Append all values to the end.
If any of the values are present, ValueError will
be raised and none of the values will be appended.
Args:
values (Iterable): Values to append
Raises:
ValueError: If any values are already present or there are duplicates
in the passed values.
TypeError: If any of the values aren't hashable.
"""
self._extend(values)
def __iadd__(self, values):
"""Add all values to the end of self.
Args:
values (Iterable): Values to append
Raises:
ValueError: If any values are already present
"""
self._check_type(values, '+=')
self.extend(values)
return self
def remove(self, value):
"""Remove value from self.
Args:
value: Element to remove from self
Raises:
ValueError: if element is already present
"""
try:
index = self._dict[value]
except KeyError:
raise ValueError('Value "%s" is not present.')
else:
del self[index]
def remove_all(self, elems_to_delete):
"""Remove all elements from elems_to_delete, raises ValueErrors.
See Also:
discard_all
Args:
elems_to_delete (Iterable): Elements to remove.
Raises:
ValueError: If the count of any element is greater in
elems_to_delete than self.
TypeError: If any of the values aren't hashable.
"""
self._delete_all(elems_to_delete, raise_errors=True)
# Implement MutableSet
def add(self, item):
"""Add an item.
Note:
This does not raise a ValueError for an already present value like
append does. This is to match the behavior of set.add
Args:
item: Item to add
Raises:
TypeError: If item isn't hashable.
"""
self._add(item)
def update(self, values):
"""Add all values to the end.
If any of the values are present, silently ignore
them (as opposed to extend which raises an Error).
See also:
extend
Args:
values (Iterable): Values to add
Raises:
TypeError: If any of the values are unhashable.
"""
self._update(values)
def discard_all(self, elems_to_delete):
"""Discard all the elements from elems_to_delete.
This is much faster than removing them one by one.
This runs in O(len(self) + len(elems_to_delete))
Args:
elems_to_delete (Iterable): Elements to discard.
Raises:
TypeError: If any of the values aren't hashable.
"""
self._delete_all(elems_to_delete, raise_errors=False)
def discard(self, value):
"""Discard an item.
Note:
This does not raise a ValueError for a missing value like remove does.
This is to match the behavior of set.discard
"""
try:
self.remove(value)
except ValueError:
pass
def difference_update(self, other):
"""Update self to include only the differene with other."""
other = set(other)
indices_to_delete = set()
for i, elem in enumerate(self):
if elem in other:
indices_to_delete.add(i)
if indices_to_delete:
self._delete_values_by_index(indices_to_delete)
def intersection_update(self, other):
"""Update self to include only the intersection with other."""
other = set(other)
indices_to_delete = set()
for i, elem in enumerate(self):
if elem not in other:
indices_to_delete.add(i)
if indices_to_delete:
self._delete_values_by_index(indices_to_delete)
def symmetric_difference_update(self, other):
"""Update self to include only the symmetric difference with other."""
other = setlist(other)
indices_to_delete = set()
for i, item in enumerate(self):
if item in other:
indices_to_delete.add(i)
for item in other:
self.add(item)
self._delete_values_by_index(indices_to_delete)
def __isub__(self, other):
self._check_type(other, '-=')
self.difference_update(other)
return self
def __iand__(self, other):
self._check_type(other, '&=')
self.intersection_update(other)
return self
def __ior__(self, other):
self._check_type(other, '|=')
self.update(other)
return self
def __ixor__(self, other):
self._check_type(other, '^=')
self.symmetric_difference_update(other)
return self
# New methods
def shuffle(self, random=None):
"""Shuffle all of the elements in self randomly."""
random_.shuffle(self._list, random=random)
for i, elem in enumerate(self._list):
self._dict[elem] = i
def sort(self, *args, **kwargs):
"""Sort this setlist in place."""
self._list.sort(*args, **kwargs)
for index, value in enumerate(self._list):
self._dict[value] = index
class frozensetlist(_basesetlist, Hashable):
"""An immutable (hashable) setlist."""
def __hash__(self):
if not hasattr(self, '_hash_value'):
self._hash_value = _util.hash_iterable(self)
return self._hash_value

View File

@ -2,27 +2,28 @@ version: '{build}'
pull_requests:
do_not_increment_build_number: true
environment:
ProjectVersion: build$(APPVEYOR_BUILD_VERSION)
matrix:
- PYTHON: C:\PYTHON36
- PYTHON: "C:\\Python37-x64\\python.exe"
PYTHON_VERSION: "3.7"
- PYTHON: "C:\\Python38-x64\\python.exe"
PYTHON_VERSION: "3.8"
install:
- ps: 'if(Test-Path env:APPVEYOR_REPO_TAG_NAME) {$env:ProjectVersion=$env:APPVEYOR_REPO_TAG_NAME}'
- '%PYTHON%\python.exe --version'
- '%PYTHON%\Scripts\pip install pyinstaller'
- '%PYTHON%\Scripts\pip install markdown'
- '%PYTHON%\python.exe -m markdown README.md > README.html'
- '%PYTHON%\Scripts\pyinstaller bundle\EntranceRandomizer.spec'
- 'mkdir dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\*.pyd dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\tcl*.dll dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\tk*.dll dist\EntranceRandomizer\ext'
- ps: '$env:ER_Version= &"$env:PYTHON\python.exe" -c "import Main; import re; print(re.match(''[0-9]+\\.[0-9]+\\.[0-9]+'',Main.__version__).group(0))"'
- '"%WIX%\bin\heat.exe" dir "dist\EntranceRandomizer" -sfrag -srd -suid -dr INSTALLDIR -cg ERFiles -ag -template fragment -t bundle\components.xslt -out build\components.wxs'
- '"%WIX%\bin\candle.exe" -out build\ bundle\*.wxs build\*.wxs'
- '"%WIX%\bin\light.exe" -ext WixUIExtension build\*.wixobj -o dist\EntranceRandomizer-Installer-%ProjectVersion%-win32.msi -b dist\EntranceRandomizer'
build: off
- cmd: "%PYTHON% -m pip install --upgrade pip"
- cmd: "%PYTHON% -m pip install -r requirements.txt --upgrade"
- cmd: "%PYTHON% -m pip install --upgrade cx_Freeze"
build_script:
- cmd: "%PYTHON% setup.py build_exe"
artifacts:
- path: dist/EntranceRandomizer*.msi
name: EntranceRandomizer-Installer-$(ProjectVersion)-win32.msi
- path: dist/EntranceRandomizer/
name: EntranceRandomizer-Raw-$(ProjectVersion)-win32.zip
- path: "build\\exe.win-amd64-%PYTHON_VERSION%\\"
name: "Berserker_Multiworld_%APPVEYOR_REPO_BRANCH%_Python%PYTHON_VERSION%-x64"
deploy:
description: 'Appveyor automated build'
provider: GitHub
auth_token:
secure: +cRWefLphFutZuzCcCsNS0tl7nNj/IpnJmfht6hZFh2z9eQdFgcu6zwGS3lWItat
artifact: /.*\.zip/
draft: false
prerelease: false
on:
APPVEYOR_REPO_TAG: true # deploy on tag push only

View File

@ -1,39 +0,0 @@
# -*- mode: python -*-
from PyInstaller.compat import is_win
block_cipher = None
# Todo: the runtime hooks should only be installed on windows
a = Analysis(['../EntranceRandomizer.py'],
pathex=['bundle'],
binaries=[],
datas=[('../data/', 'data/'), ('../README.html', '.')],
hiddenimports=[],
hookspath=[],
runtime_hooks=['bundle/_rt_hook.py'],
excludes=['lzma', 'bz2'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='EntranceRandomizer',
debug=False,
strip=False,
upx=False,
icon='../data/ER.ico',
console=is_win )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
name='EntranceRandomizer')
app = BUNDLE(coll,
name ='EntranceRandomizer.app',
icon = '../data/ER.icns',
bundle_identifier = None)

View File

@ -1,4 +0,0 @@
import sys
import os
sys.path.append(os.path.join(sys._MEIPASS, "ext"))

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi">
<xsl:output method="xml" indent="no"/>
<xsl:strip-space elements="*"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="wix:File[@Source='SourceDir\EntranceRandomizer.exe']">
<xsl:copy-of select="." />
<wix:Shortcut Id="ProgramShortcut"
Name="ALttP Entrance Randomizer"
Advertise="yes"
Description="ALttP Entrance Randomizer"
Directory="ApplicationProgramsFolder" />
</xsl:template>
<xsl:template match="wix:File[@Source='SourceDir\README.hmtl']">
<xsl:copy-of select="." />
<wix:Shortcut Id="ReadmeShortcut"
Name="ALttP Entrance Randomizer README"
Advertise="yes"
Description="ALttP Entrance Randomizer README"
Directory="ApplicationProgramsFolder" />
</xsl:template>
</xsl:stylesheet>

View File

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<!-- Setting the product ID to "*" means all upgrades are treated as major upgrades, and will uninstall the old version
before installing the new one. This is desireable because it means nothing breaks if we fail to follow the component rules precisely. -->
<Product Id="*" Name="ALttP Entrance Randomizer" Language="1033" Version="$(env.ER_Version)" Manufacturer="Randomizer Community" UpgradeCode="0229C621-5F8A-4D59-962A-5826C58B93DD" >
<Package Id="*" InstallerVersion="400" Compressed="yes" InstallScope="perMachine" />
<!-- Allowing downgrades will cause a harmless warning to be emitted. This wearning is not relevant here, allowing downgrades via a new major upgrade is safe for a simple standalone app like this-->
<MajorUpgrade AllowDowngrades="yes"/>
<Media Id="1" Cabinet="contents.cab" EmbedCab="yes" CompressionLevel="high"/>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id='ProgramFilesFolder' Name='PFiles'>
<Directory Id='INSTALLDIR' Name='ALttP Entrance Randomizer'/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="ALttP Entrance Randomizer"/>
</Directory>
</Directory>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="0054698A-5A56-4B36-8176-8FEC1762EF2D">
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\ALttPEntranceRandomizer" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
<Feature Id="Complete"
Title="ALttP Entrance Randomizer"
Description="ALttP Entrance Randomizer"
Level="1">
<ComponentGroupRef Id="ERFiles"/>
<ComponentRef Id="ApplicationShortcut"/>
</Feature>
<Icon Id="ER.ico" SourceFile="Data/ER.ico" />
<Property Id="DISABLEADVTSHORTCUTS" Secure="yes">1</Property>
<Property Id="ARPPRODUCTICON" Value="ER.ico" />
<Property Id="WIXUI_INSTALLDIR">INSTALLDIR</Property>
<UI>
<UIRef Id="WixUI_InstallDir" />
<UIRef Id="WixUI_ErrorProgressText" />
<!-- Skip license page -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
</UI>
</Product>
</Wix>

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

160
easy.yaml Normal file
View File

@ -0,0 +1,160 @@
#More general info here: https://docs.google.com/document/d/1r7qs1-MK7YbFf2d-mEUeTy2wHykIf1ALG9pLtVvUbSw/edit
description: Easy/Open/Normal #please describe your options. Especially useful when you have multiple yamls for different occasions
name: PleaseEnterNameHere #your name ingame, space and "_" gets replaced with a dash "-"
glitches_required: none #there is also no_logic.
item_placement: basic #this is based on Entrance Randomizer, which does not (yet?) support advanced
map_shuffle: #to shuffle dungeon maps into the outside world and other dungeons, as well as other player's worlds in multiworld
on: 0
off: 1
compass_shuffle: #same for compass
on: 0
off: 1
smallkey_shuffle: #same for small keys
on: 0
off: 1
bigkey_shuffle: #same for big keys
on: 0
off: 1
dungeon_items: # alternative to the 4 shuffles above this, does nothing until the respective 4 shuffles are deleted
mc: 0 # shuffle Maps and Compass
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
accessibility:
items: 0 # item accessibility means you can get all inventory items. So a key could lock itself, but you can fill your inventory
locations: 1 # location accessibility means you can access every location in your seed and get all 216 checks
none: 0 # no accessibility means your seed is "beatable only", meaning any items you do not need to beat the game can be unreachable. This can mean you have to defeat ganon with a lamp and master sword.
progressive: #not available in bonta's multiworld at this time. If you want this option, make sure the host uses the correct Multiworld
on: 1 # progressive items, you will always get progressive items like swords in their order: figher sword -> master sword -> tempered sword -> golden sword
off: 0 # turns progressive items off, so you can find, for example, silver arrows before a bow
random: 0 # rolls a 50/50 chance for each potentially progressive item. So, for example, you can have progressive swords but non-progressive mittens
entrance_shuffle:
none: 1 # no entrance shuffle
dungeonssimple: 0 # shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
dungeonsfull: 0 # shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons
simple: 0 #dungeons are shuffled with each other and the other entrances are shuffled with each other
restricted: 0 #dungeons still shuffle along each other but connects other entrances more feely with each other while keeping entrances in one world
full: 0 # mixes caves and dungeons freely, except for confining all entrances to one world
crossed: 0 #introduces cross world connectors
insanity: 0 #any entrance can lead to any other entrance
goals:
ganon: 5 #beat GT and then Ganon
fast_ganon: 4 # Just kill Ganon
dungeons: 1 # All Dungeons, including GT, and Agahnims Tower
pedestal: 0 # Pull the win out of the Pedestal
triforce-hunt: 0 # Collect 20 of 30 Triforce pieces then hand them in in front of Hyrule Castle
tower_open: # Crystals required to open GT
'0': 0
'1': 0
'2': 0
'3': 0
'4': 0
'5': 0
'6': 0
'7': 0
random: 1
ganon_open: # Crystals required to hurt Ganon
'0': 0
'1': 0
'2': 0
'3': 0
'4': 0
'5': 0
'6': 0
'7': 0
random: 1
world_state:
standard: 1 # Do standard escape to bring Zelda to Sanctuary
open: 9 # Start with the ability to skip the standard opening and go where you want
inverted: 0 # You start in the Dark World, the Light World has changes to it's Map and requires a Moon Pearl to not be Bunny
retro: 0 # Keys are universal, you have to buy a quiver, there are take any caves and some other changes. Makes it more like Z1
hints:
'on': 1 # Hint tiles can give useful item location hints on occasion
'off': 0 # You get gameplay hints, but not location/item hints
weapons: # this means swords
randomized: 5 # Your swords can be anywhere
assured: 2 # You start with a sword, the rest are anywhere
vanilla: 3 # Your swords are in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # You don't have a sword. A hammer can be used like a Master Sword in certain situations
item_pool:
normal: 1
hard: 0
expert: 0
crowd_control: 0
item_functionality:
normal: 1
hard: 0
expert: 0
boss_shuffle:
none: 1
simple: 0 # existing bosses gets shuffled around
full: 0 # all bosses exist once, except 3 can appear twice
random: 0 # any boss can appear any number of times
enemy_shuffle:
none: 1
shuffled: 0 # enemies get shuffled around
random: 0 # any enemy can appear any number of times
enemy_damage:
default: 1
shuffled: 0 # damage tables get shuffled, however armor effects are not
random: 0 # all damages are completely shuffled, including armor effects, making it possible red mail is worse than green
enemy_health:
default: 1
easy: 0
hard: 0
expert: 0
pot_shuffle: # Shuffle pots, their contents and whatever is hiding under them. Broken with any door shuffle that is not vanilla, do not combine
on: 0
off: 1
beemizer: # replace items with bees, that will attack you
0: 1
1: 0 # max 15 hearts
2: 0 # max 10 hearts
3: 0
4: 0
timer:
none: 1
timed: 0
timed_ohko: 0
ohko: 0
timed_countdown: 0
display: 0
remote_items: # Warning: currently broken. Stores all your items on the server, effectively sending them to you as if another player picked it up
on: 0 # intended for racing, as the item information is missing from the ROM
off: 1
rom:
sprite:
random: 1
randomonhit: 1
link: 1 # to get other sprite names, open up gui/Creator, select a sprite and write down the sprite name as it is there
disablemusic: off # turn on for V30 MSU packs
extendedmsu: off #turn on to have V31 extended MSU support
quickswap:
on: 1 # press L/R to swap items without opening the menu
off: 0
menuspeed:
normal: 1
instant: 0
double: 0
triple: 0
quadruple: 0
half: 0
heartcolor:
red: 1
blue: 1
green: 1
yellow: 1
random: 0
heartbeep:
double: 0
normal: 1
half: 0
quarter: 0
off: 0
ow_palettes:
default: 1
random: 1 # shuffle the palette of overworld colors
blackout: 0 # makes everything blank, making it almost a blind playthrough
uw_palettes:
default: 1
random: 1 # shuffle the palette of dungeon/cave colors
blackout: 0 # makes everything blank, making it almost a blind playthrough

56
host.yaml Normal file
View File

@ -0,0 +1,56 @@
general_options:
#File name of the v1.0 J rom
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
#options for MultiServer
#null means nothing, for the server this means to default the value
#these overwrite command line arguments!
server_options:
host: null
port: 38281
password: null
multidata: null
savefile: null
disable_save: false
loglevel: "info"
#automatically forward the port that is used, then close that port after 24 hours
port_forward: false
#Disallow !getitem. Old /getitem cannot be blocked this way
disable_item_cheat: false
#Client hint system
#points given to player for each acquired item
location_check_points: 1
#point cost to receive a hint via !hint for players
hint_cost: 1000 #set to 0 if you want free hints
#options for MultiMystery.py
multi_mystery_options:
#teams, however, note that there is currently no way to supply names for teams 2+ through MultiMystery
teams: 1
#Where to place the resulting files
output_path: "MultiMystery"
#location of your Enemizer CLI, available here: https://github.com/Bonta0/Enemizer/releases
enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe"
#folder from which the player yaml files are pulled from
player_files_path: "Players"
#meta file name, within players folder
meta_file_path: "meta.yaml"
#automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
#does nothing if the name is not found
#example: player_name = "Berserker"
player_name: "" # the hosts name
#create a spoiler file
create_spoiler: 1
#Zip the resulting roms
#0 -> Don't
#1 -> Create a zip
#2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly)
zip_roms: 0
# zip diff files, 2-> delete the non-zipped one. Note that diffs are only created if they are placed in a zip.
zip_diffs: 2
#include the spoiler log in the zip, 2 -> delete the non-zipped one
zip_spoiler: 0
#include the multidata file in the zip, 2 -> delete the non-zipped one, which also means the server won't autostart
zip_multidata: 0
#zip algorithm to use
zip_format: 1 # 1 -> zip, 2 -> 7z, 3->bz2
#create roms flagged as race roms
race: 0

47
meta.yaml Normal file
View File

@ -0,0 +1,47 @@
# this file has to be in the Players folder to take effect, keeping the "meta.yaml" name.
# A meta file rolls its own set of options first
# the result will then overwrite each player's option in that particular field
# for example, if a meta.yaml fast_ganon result is rolled, every player will have that fast_ganon goal
# there is the special case of null, which ignores that part of the meta.yaml,
# allowing for a chance for that meta to not take effect
# players can also have a meta_ignore option to ignore specific options
# example of ignore that would be in a player's file:
# meta_ignore:
# world_state:
# inverted
#this means, if world_state is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead.
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
goals:
ganon: 10
fast_ganon: 25
dungeons: 5
pedestal: 10
triforce-hunt: 1
null: 0 # maintain individual goals
world_state:
standard: 10
open: 60
inverted: 10
retro: 10
null: 10 # maintain individual world states
tower_open:
'0': 8
'1': 7
'2': 6
'3': 5
'4': 4
'5': 3
'6': 2
'7': 1
random: 10 # a different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open
ganon_open:
'0': 3
'1': 4
'2': 5
'3': 6
'4': 7
'5': 8
'6': 9
'7': 10
random: 5 # this will mean differing completion times. But leaving it for that surprise effect
#do not use meta rom options at this time.

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
colorama>=0.4.3
websockets>=8.1
PyYAML>=5.3.1
fuzzywuzzy>=0.18.0
bsdiff4>=1.1.9
upnpy>=1.1.5
prompt_toolkit>=3.0.5

105
setup.py Normal file
View File

@ -0,0 +1,105 @@
import os
import shutil
import sys
import sysconfig
from pathlib import Path
import cx_Freeze
is_64bits = sys.maxsize > 2 ** 32
folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(),
version=sysconfig.get_python_version())
buildfolder = Path("build", folder)
sbuildfolder = str(buildfolder)
libfolder = Path(buildfolder, "lib")
library = Path(libfolder, "library.zip")
print("Outputting to: " + str(buildfolder))
build_resources = "exe_resources"
compress = False
holoviews = False
from hashlib import sha3_512
import base64
def _threaded_hash(filepath):
hasher = sha3_512()
hasher.update(open(filepath, "rb").read())
return base64.b85encode(hasher.digest()).decode()
os.makedirs(buildfolder, exist_ok=True)
def manifest_creation():
hashes = {}
manifestpath = os.path.join(buildfolder, "manifest.json")
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor()
for dirpath, dirnames, filenames in os.walk(buildfolder):
for filename in filenames:
path = os.path.join(dirpath, filename)
hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path)
import json
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds")}
manifest["hashes"] = {path: hash.result() for path, hash in hashes.items()}
json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest")
scripts = {"MultiClient.py" : "BerserkerMultiClient",
"MultiMystery.py" : "BerserkerMultiMystery",
"MultiServer.py" : "BerserkerMultiServer",
"gui.py" : "BerserkerMultiCreator",
"Mystery.py" : "BerserkerMystery"}
exes = []
for script, scriptname in scripts.items():
exes.append(cx_Freeze.Executable(
script=script,
targetName=scriptname + ("" if sys.platform == "linux" else ".exe"))
)
import datetime
buildtime = datetime.datetime.now()
cx_Freeze.setup(
name="HonorarPlus",
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
description="HonorarPlus",
executables=exes,
options={
"build_exe": {
"zip_include_packages": ["*"],
"zip_exclude_packages": [],
"include_files": [],
"include_msvcr": True,
"replace_paths": [("*", "")],
"optimize": 2,
"build_exe": buildfolder
},
},
)
def installfile(path):
lbuildfolder = buildfolder
print('copying', path, '->', lbuildfolder)
if path.is_dir():
lbuildfolder /= path.name
if lbuildfolder.is_dir():
shutil.rmtree(lbuildfolder)
shutil.copytree(path, lbuildfolder)
elif path.is_file():
shutil.copy(path, lbuildfolder)
else:
print('Warning,', path, 'not found')
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "QUsb2Snes", "meta.yaml"]
for data in extra_data:
installfile(Path(data))
manifest_creation()