Merge remote-tracking branch 'origin/master'
# Conflicts: # MultiServer.py
This commit is contained in:
commit
a88d5a6e1c
|
@ -8,3 +8,10 @@ build
|
|||
bundle/components.wxs
|
||||
dist
|
||||
README.html
|
||||
.vs/
|
||||
*multidata
|
||||
*multisave
|
||||
EnemizerCLI/
|
||||
.mypy_cache/
|
||||
RaceRom.py
|
||||
weights/
|
||||
|
|
13
Adjuster.py
13
Adjuster.py
|
@ -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
|
||||
|
|
|
@ -3,34 +3,29 @@ 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.clock()
|
||||
start = time.process_time()
|
||||
logger = logging.getLogger('')
|
||||
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 == 2097152 and os.path.splitext(args.rom)[-1].lower() == '.sfc':
|
||||
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
|
||||
rom = LocalRom(args.rom, False)
|
||||
if os.path.isfile(args.baserom):
|
||||
baserom = LocalRom(args.baserom, 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))
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
logger.debug('Total Time: %s', time.clock() - start)
|
||||
logger.debug('Total Time: %s', time.process_time() - start)
|
||||
|
||||
return args
|
||||
|
|
903
BaseClasses.py
903
BaseClasses.py
File diff suppressed because it is too large
Load Diff
180
Bosses.py
180
Bosses.py
|
@ -4,101 +4,101 @@ import random
|
|||
from BaseClasses import Boss
|
||||
from Fill import FillError
|
||||
|
||||
def BossFactory(boss):
|
||||
def BossFactory(boss, player):
|
||||
if boss is None:
|
||||
return None
|
||||
if boss in boss_table:
|
||||
enemizer_name, defeat_rule = boss_table[boss]
|
||||
return Boss(boss, enemizer_name, defeat_rule)
|
||||
return Boss(boss, enemizer_name, defeat_rule, player)
|
||||
|
||||
logging.getLogger('').error('Unknown Boss: %s', boss)
|
||||
return None
|
||||
|
||||
def ArmosKnightsDefeatRule(state):
|
||||
def ArmosKnightsDefeatRule(state, player):
|
||||
# Magic amounts are probably a bit overkill
|
||||
return (
|
||||
state.has_blunt_weapon() or
|
||||
(state.has('Cane of Somaria') and state.can_extend_magic(10)) or
|
||||
(state.has('Cane of Byrna') and state.can_extend_magic(16)) or
|
||||
(state.has('Ice Rod') and state.can_extend_magic(32)) or
|
||||
(state.has('Fire Rod') and state.can_extend_magic(32)) or
|
||||
state.has('Blue Boomerang') or
|
||||
state.has('Red Boomerang'))
|
||||
state.has_blunt_weapon(player) or
|
||||
(state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or
|
||||
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or
|
||||
(state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 32)) or
|
||||
state.has('Blue Boomerang', player) or
|
||||
state.has('Red Boomerang', player))
|
||||
|
||||
def LanmolasDefeatRule(state):
|
||||
def LanmolasDefeatRule(state, player):
|
||||
# TODO: Allow the canes here?
|
||||
return (
|
||||
state.has_blunt_weapon() or
|
||||
state.has('Fire Rod') or
|
||||
state.has('Ice Rod') or
|
||||
state.can_shoot_arrows())
|
||||
state.has_blunt_weapon(player) or
|
||||
state.has('Fire Rod', player) or
|
||||
state.has('Ice Rod', player) or
|
||||
state.can_shoot_arrows(player))
|
||||
|
||||
def MoldormDefeatRule(state):
|
||||
return state.has_blunt_weapon()
|
||||
def MoldormDefeatRule(state, player):
|
||||
return state.has_blunt_weapon(player)
|
||||
|
||||
def HelmasaurKingDefeatRule(state):
|
||||
return state.has_blunt_weapon() or state.can_shoot_arrows()
|
||||
def HelmasaurKingDefeatRule(state, player):
|
||||
return state.has_blunt_weapon(player) or state.can_shoot_arrows(player)
|
||||
|
||||
def ArrghusDefeatRule(state):
|
||||
if not state.has('Hookshot'):
|
||||
def ArrghusDefeatRule(state, player):
|
||||
if not state.has('Hookshot', player):
|
||||
return False
|
||||
# TODO: ideally we would have a check for bow and silvers, which combined with the
|
||||
# hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature
|
||||
# makes this complicated
|
||||
if state.has_blunt_weapon():
|
||||
if state.has_blunt_weapon(player):
|
||||
return True
|
||||
|
||||
return ((state.has('Fire Rod') and (state.can_shoot_arrows() or state.can_extend_magic(12))) or #assuming mostly gitting two puff with one shot
|
||||
(state.has('Ice Rod') and (state.can_shoot_arrows() or state.can_extend_magic(16))))
|
||||
return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 12))) or #assuming mostly gitting two puff with one shot
|
||||
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
|
||||
|
||||
|
||||
def MothulaDefeatRule(state):
|
||||
def MothulaDefeatRule(state, player):
|
||||
return (
|
||||
state.has_blunt_weapon() or
|
||||
(state.has('Fire Rod') and state.can_extend_magic(10)) or
|
||||
state.has_blunt_weapon(player) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or
|
||||
# TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply
|
||||
# to non-vanilla locations, so are harder to test, so sticking with what VT has for now:
|
||||
(state.has('Cane of Somaria') and state.can_extend_magic(16)) or
|
||||
(state.has('Cane of Byrna') and state.can_extend_magic(16)) or
|
||||
state.can_get_good_bee()
|
||||
(state.has('Cane of Somaria', player) and state.can_extend_magic(player, 16)) or
|
||||
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or
|
||||
state.can_get_good_bee(player)
|
||||
)
|
||||
|
||||
def BlindDefeatRule(state):
|
||||
return state.has_blunt_weapon() or state.has('Cane of Somaria') or state.has('Cane of Byrna')
|
||||
def BlindDefeatRule(state, player):
|
||||
return state.has_blunt_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
|
||||
|
||||
def KholdstareDefeatRule(state):
|
||||
def KholdstareDefeatRule(state, player):
|
||||
return (
|
||||
(
|
||||
state.has('Fire Rod') or
|
||||
state.has('Fire Rod', player) or
|
||||
(
|
||||
state.has('Bombos') and
|
||||
# FIXME: the following only actually works for the vanilla location for swordless mode
|
||||
(state.has_sword() or state.world.mode == 'swordless')
|
||||
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[player] == 'swordless')
|
||||
)
|
||||
) and
|
||||
(
|
||||
state.has_blunt_weapon() or
|
||||
(state.has('Fire Rod') and state.can_extend_magic(20)) or
|
||||
# FIXME: this actually only works for the vanilla location for swordless mode
|
||||
state.has_blunt_weapon(player) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or
|
||||
# FIXME: this actually only works for the vanilla location for swordless
|
||||
(
|
||||
state.has('Fire Rod') and
|
||||
state.has('Bombos') and
|
||||
state.world.mode == 'swordless' and
|
||||
state.can_extend_magic(16)
|
||||
state.has('Fire Rod', player) and
|
||||
state.has('Bombos', player) and
|
||||
state.world.swords[player] == 'swordless' and
|
||||
state.can_extend_magic(player, 16)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def VitreousDefeatRule(state):
|
||||
return state.can_shoot_arrows() or state.has_blunt_weapon()
|
||||
def VitreousDefeatRule(state, player):
|
||||
return state.can_shoot_arrows(player) or state.has_blunt_weapon(player)
|
||||
|
||||
def TrinexxDefeatRule(state):
|
||||
if not (state.has('Fire Rod') and state.has('Ice Rod')):
|
||||
def TrinexxDefeatRule(state, player):
|
||||
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
|
||||
return False
|
||||
return state.has('Hammer') or state.has_beam_sword() or (state.has_sword() and state.can_extend_magic(32))
|
||||
return state.has('Hammer', player) or state.has_beam_sword(player) or (state.has_sword(player) and state.can_extend_magic(player, 32))
|
||||
|
||||
def AgahnimDefeatRule(state):
|
||||
return state.has_sword() or state.has('Hammer') or state.has('Bug Catching Net')
|
||||
def AgahnimDefeatRule(state, player):
|
||||
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
|
||||
|
||||
boss_table = {
|
||||
'Armos Knights': ('Armos', ArmosKnightsDefeatRule),
|
||||
|
@ -115,15 +115,15 @@ boss_table = {
|
|||
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
|
||||
}
|
||||
|
||||
def can_place_boss(world, boss, dungeon_name, level=None):
|
||||
if world.mode 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 == 'Ganons Tower' and level == 'top':
|
||||
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'top':
|
||||
if boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]:
|
||||
return False
|
||||
|
||||
if dungeon_name == 'Ganons Tower' and level == 'middle':
|
||||
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'middle':
|
||||
if boss in ["Blind"]:
|
||||
return False
|
||||
|
||||
|
@ -137,37 +137,55 @@ def can_place_boss(world, boss, dungeon_name, level=None):
|
|||
return False
|
||||
return True
|
||||
|
||||
def place_bosses(world):
|
||||
if world.boss_shuffle == 'none':
|
||||
def place_bosses(world, player):
|
||||
if world.boss_shuffle[player] == 'none':
|
||||
return
|
||||
# Most to least restrictive order
|
||||
boss_locations = [
|
||||
['Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
['Skull Woods', None],
|
||||
['Ganons Tower', 'middle'],
|
||||
['Eastern Palace', None],
|
||||
['Desert Palace', None],
|
||||
['Palace of Darkness', None],
|
||||
['Swamp Palace', None],
|
||||
['Thieves Town', None],
|
||||
['Ice Palace', None],
|
||||
['Misery Mire', None],
|
||||
['Turtle Rock', None],
|
||||
['Ganons Tower', 'bottom'],
|
||||
]
|
||||
if world.mode[player] != 'inverted':
|
||||
boss_locations = [
|
||||
['Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
['Skull Woods', None],
|
||||
['Ganons Tower', 'middle'],
|
||||
['Eastern Palace', None],
|
||||
['Desert Palace', None],
|
||||
['Palace of Darkness', None],
|
||||
['Swamp Palace', None],
|
||||
['Thieves Town', None],
|
||||
['Ice Palace', None],
|
||||
['Misery Mire', None],
|
||||
['Turtle Rock', None],
|
||||
['Ganons Tower', 'bottom'],
|
||||
]
|
||||
else:
|
||||
boss_locations = [
|
||||
['Inverted Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
['Skull Woods', None],
|
||||
['Inverted Ganons Tower', 'middle'],
|
||||
['Eastern Palace', None],
|
||||
['Desert Palace', None],
|
||||
['Palace of Darkness', None],
|
||||
['Swamp Palace', None],
|
||||
['Thieves Town', None],
|
||||
['Ice Palace', None],
|
||||
['Misery Mire', None],
|
||||
['Turtle Rock', None],
|
||||
['Inverted Ganons Tower', 'bottom'],
|
||||
]
|
||||
|
||||
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.mode == 'swordless':
|
||||
world.get_dungeon('Ice Palace').boss = BossFactory('Kholdstare')
|
||||
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)]
|
||||
|
@ -177,20 +195,20 @@ def place_bosses(world):
|
|||
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).bosses[level] = BossFactory(boss)
|
||||
elif world.boss_shuffle == "chaos": #all bosses chosen at random
|
||||
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
|
||||
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)
|
||||
|
||||
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
|
||||
world.get_dungeon(loc).bosses[level] = BossFactory(boss)
|
||||
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
|
||||
|
|
116
Dungeons.py
116
Dungeons.py
|
@ -6,44 +6,53 @@ from Fill import fill_restrictive
|
|||
from Items import ItemFactory
|
||||
|
||||
|
||||
def create_dungeons(world):
|
||||
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)
|
||||
dungeon.boss = BossFactory(default_boss)
|
||||
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).dungeon = dungeon
|
||||
world.get_region(region, player).dungeon = dungeon
|
||||
dungeon.world = world
|
||||
return dungeon
|
||||
|
||||
ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'], None, [ItemFactory('Small Key (Escape)')], [ItemFactory('Map (Escape)')])
|
||||
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'], ItemFactory('Big Key (Eastern Palace)'), [], ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)']))
|
||||
DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)'), [ItemFactory('Small Key (Desert Palace)')], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)']))
|
||||
ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], ItemFactory('Big Key (Tower of Hera)'), [ItemFactory('Small Key (Tower of Hera)')], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)']))
|
||||
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2), [])
|
||||
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)'), ItemFactory(['Small Key (Palace of Darkness)'] * 6), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)']))
|
||||
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)'), [ItemFactory('Small Key (Thieves Town)')], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)']))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)'), ItemFactory(['Small Key (Skull Woods)'] * 2), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)']))
|
||||
SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)'), [ItemFactory('Small Key (Swamp Palace)')], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)']))
|
||||
IP = make_dungeon('Ice Palace', 'Kholdstare', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)'), ItemFactory(['Small Key (Ice Palace)'] * 2), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)']))
|
||||
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)'), ItemFactory(['Small Key (Misery Mire)'] * 3), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)']))
|
||||
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)'), ItemFactory(['Small Key (Turtle Rock)'] * 4), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)']))
|
||||
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)'), ItemFactory(['Small Key (Ganons Tower)'] * 4), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)']))
|
||||
ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'], None, [ItemFactory('Small Key (Escape)', player)], [ItemFactory('Map (Escape)', player)])
|
||||
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'], ItemFactory('Big Key (Eastern Palace)', player), [], ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
|
||||
DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
|
||||
ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
|
||||
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)', player), ItemFactory(['Small Key (Palace of Darkness)'] * 6, player), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
|
||||
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), ItemFactory(['Small Key (Skull Woods)'] * 2, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
|
||||
SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player), [ItemFactory('Small Key (Swamp Palace)', player)], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
|
||||
IP = make_dungeon('Ice Palace', 'Kholdstare', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], 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))
|
||||
|
||||
GT.bosses['bottom'] = BossFactory('Armos Knights')
|
||||
GT.bosses['middle'] = BossFactory('Lanmolas')
|
||||
GT.bosses['top'] = BossFactory('Moldorm')
|
||||
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:
|
||||
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2', ['Inverted 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))
|
||||
|
||||
world.dungeons = [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
|
||||
GT.bosses['bottom'] = BossFactory('Armos Knights', player)
|
||||
GT.bosses['middle'] = BossFactory('Lanmolas', player)
|
||||
GT.bosses['top'] = BossFactory('Moldorm', player)
|
||||
|
||||
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
|
||||
|
||||
def fill_dungeons(world):
|
||||
freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest']
|
||||
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
if world.retro:
|
||||
world.push_item(world.get_location('Skull Woods - Pinball Room'), ItemFactory('Small Key (Universal)'), False)
|
||||
else:
|
||||
world.push_item(world.get_location('Skull Woods - Pinball Room'), ItemFactory('Small Key (Skull Woods)'), False)
|
||||
world.get_location('Skull Woods - Pinball Room').event = True
|
||||
for player in range(1, world.players + 1):
|
||||
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
|
||||
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)
|
||||
pinball_room.event = True
|
||||
pinball_room.locked = True
|
||||
|
||||
dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons]
|
||||
|
||||
|
@ -70,8 +79,8 @@ def fill_dungeons(world):
|
|||
|
||||
world.push_item(bk_location, big_key, False)
|
||||
bk_location.event = True
|
||||
bk_location.locked = True
|
||||
dungeon_locations.remove(bk_location)
|
||||
all_state.clear_cached_unreachable()
|
||||
big_key = None
|
||||
|
||||
# next place small keys
|
||||
|
@ -89,60 +98,61 @@ def fill_dungeons(world):
|
|||
small_keys.append(small_key)
|
||||
dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items))
|
||||
# infinite regression protection
|
||||
if loopcnt < 30:
|
||||
if loopcnt < (30 * world.players):
|
||||
break
|
||||
else:
|
||||
raise RuntimeError('No suitable location for %s' % small_key)
|
||||
|
||||
world.push_item(sk_location, small_key, False)
|
||||
sk_location.event = True
|
||||
sk_location.locked = True
|
||||
dungeon_locations.remove(sk_location)
|
||||
all_state.clear_cached_unreachable()
|
||||
|
||||
if small_keys:
|
||||
# key placement not finished, loop again
|
||||
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)
|
||||
|
||||
world.state.clear_cached_unreachable()
|
||||
|
||||
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()
|
||||
|
||||
skull_woods_big_chest = world.get_location('Skull Woods - Pinball Room')
|
||||
if world.retro:
|
||||
world.push_item(skull_woods_big_chest, ItemFactory('Small Key (Universal)'), False)
|
||||
else:
|
||||
world.push_item(skull_woods_big_chest, ItemFactory('Small Key (Skull Woods)'), False)
|
||||
skull_woods_big_chest.event = True
|
||||
shuffled_locations.remove(skull_woods_big_chest)
|
||||
for player in range(1, world.players + 1):
|
||||
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
|
||||
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)
|
||||
pinball_room.event = True
|
||||
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)
|
||||
|
||||
world.state.clear_cached_unreachable()
|
||||
|
||||
|
||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import logging
|
||||
import random
|
||||
import textwrap
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
from Gui import guiMain
|
||||
from Main import main
|
||||
from Rom import get_sprite_from_name
|
||||
from Utils import is_bundled, close_console
|
||||
|
||||
|
||||
|
@ -16,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:
|
||||
|
@ -29,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', 'swordless'],
|
||||
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.
|
||||
|
@ -37,14 +46,26 @@ def start():
|
|||
but may lead to weird rain state issues if you exit
|
||||
through the Hyrule Castle side exits before rescuing
|
||||
Zelda in a full shuffle.
|
||||
Swordless: Like Open, but with no swords. Curtains in
|
||||
Skull Woods and Agahnims Tower are removed,
|
||||
Agahnim\'s Tower barrier can be destroyed with
|
||||
hammer. Misery Mire and Turtle Rock can be opened
|
||||
without a sword. Hammer damages Ganon. Ether and
|
||||
Bombos Tablet can be activated with Hammer (and Book).
|
||||
Inverted: Starting locations are Dark Sanctuary in West Dark
|
||||
World or at Link's House, which is shuffled freely.
|
||||
Requires the moon pearl to be Link in the Light World
|
||||
instead of a bunny.
|
||||
''')
|
||||
parser.add_argument('--goal', default='ganon', const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
|
||||
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.
|
||||
Assured: Start game with a sword already.
|
||||
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
|
||||
Tower are removed, Agahnim\'s Tower barrier can be
|
||||
destroyed with hammer. Misery Mire and Turtle Rock
|
||||
can be opened without a sword. Hammer damages Ganon.
|
||||
Ether and Bombos Tablet can be activated with Hammer
|
||||
(and Book). Bombos pads have been added in Ice
|
||||
Palace, to allow for an alternative to firerod.
|
||||
Vanilla: Swords are in vanilla locations.
|
||||
''')
|
||||
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
|
||||
|
@ -56,16 +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=['easy', 'normal', 'hard', 'expert', 'insane'],
|
||||
parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
|
||||
help='''\
|
||||
Select game difficulty. Affects available itempool. (default: %(default)s)
|
||||
Easy: An easy setting with extra equipment.
|
||||
Normal: Normal difficulty.
|
||||
Hard: A harder setting with less equipment and reduced health.
|
||||
Expert: A harder yet setting with minimum equipment and health.
|
||||
Insane: A setting with the absolute minimum in equipment and no extra health.
|
||||
''')
|
||||
parser.add_argument('--timer', default='none', const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
|
||||
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=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.
|
||||
|
@ -85,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
|
||||
|
@ -99,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
|
||||
|
@ -122,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
|
||||
|
@ -146,8 +172,27 @@ def start():
|
|||
The dungeon variants only mix up dungeons and keep the rest of
|
||||
the overworld vanilla.
|
||||
''')
|
||||
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('--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.
|
||||
This setting does not apply when the all dungeons goal is
|
||||
selected. (default: %(default)s)
|
||||
Random: Picks a random value between 0 and 7 (inclusive).
|
||||
0-7: Number of crystals needed
|
||||
''')
|
||||
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('--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.
|
||||
|
@ -156,48 +201,50 @@ 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('--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('--beatableonly', help='''\
|
||||
Only check if the game is beatable with placement. Do not
|
||||
ensure all locations are reachable. This only has an effect
|
||||
on the restrictive algorithm currently.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--hints', 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', 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,
|
||||
|
@ -207,19 +254,61 @@ def start():
|
|||
''')
|
||||
parser.add_argument('--suppress_rom', help='Do not create an output rom file.', action='store_true')
|
||||
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
|
||||
# Deliberately not documented, only useful for vt site integration right now:
|
||||
parser.add_argument('--shufflebosses', help=argparse.SUPPRESS, default='none', const='none', nargs='?', choices=['none', 'basic', 'normal', 'chaos'])
|
||||
parser.add_argument('--jsonout', action='store_true', help='''\
|
||||
Output .json patch to stdout instead of a patched rom. Used
|
||||
for VT site integration, do not use otherwise.
|
||||
''')
|
||||
args = parser.parse_args()
|
||||
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('--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')
|
||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||
parser.add_argument('--outputname')
|
||||
|
||||
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.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',
|
||||
'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']:
|
||||
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
|
||||
# probably wants the gui. Users of the bundled build who want the command line
|
||||
# interface shouuld specify at least one option, possibly setting a value to a
|
||||
# default if they like all the defaults
|
||||
from Gui import guiMain
|
||||
close_console()
|
||||
guiMain()
|
||||
sys.exit(0)
|
||||
|
@ -228,9 +317,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)
|
||||
|
@ -240,6 +329,7 @@ def start():
|
|||
logging.basicConfig(format='%(message)s', level=loglevel)
|
||||
|
||||
if args.gui:
|
||||
from Gui import guiMain
|
||||
guiMain(args)
|
||||
elif args.count is not None:
|
||||
seed = args.seed
|
||||
|
|
2119
EntranceShuffle.py
2119
EntranceShuffle.py
File diff suppressed because it is too large
Load Diff
176
Fill.py
176
Fill.py
|
@ -1,6 +1,9 @@
|
|||
import random
|
||||
import logging
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
|
||||
class FillError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
@ -158,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:
|
||||
|
@ -166,35 +169,51 @@ def fill_restrictive(world, base_state, locations, itempool):
|
|||
new_state.sweep_for_events()
|
||||
return new_state
|
||||
|
||||
while itempool and locations:
|
||||
item_to_place = itempool.pop()
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
unplaced_items = []
|
||||
|
||||
perform_access_check = True
|
||||
if world.check_beatable_only:
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state)
|
||||
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)
|
||||
|
||||
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]
|
||||
|
||||
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 not world.check_beatable_only:
|
||||
logging.getLogger('').warning('Not all items placed. Game beatable anyway.')
|
||||
break
|
||||
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()
|
||||
|
@ -207,8 +226,13 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
|
|||
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
|
||||
|
||||
# fill in gtower locations with trash first
|
||||
if world.ganonstower_vanilla:
|
||||
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name]
|
||||
for player in range(1, world.players + 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:
|
||||
|
@ -221,6 +245,9 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
|
|||
random.shuffle(fill_locations)
|
||||
fill_locations.reverse()
|
||||
|
||||
# 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)
|
||||
|
||||
random.shuffle(fill_locations)
|
||||
|
@ -289,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
|
||||
|
@ -297,3 +324,100 @@ def flood_items(world):
|
|||
world.push_item(location, item_to_place, True)
|
||||
itempool.remove(item_to_place)
|
||||
break
|
||||
|
||||
def balance_multiworld_progression(world):
|
||||
state = CollectionState(world)
|
||||
checked_locations = []
|
||||
unchecked_locations = world.get_locations().copy()
|
||||
random.shuffle(unchecked_locations)
|
||||
|
||||
reachable_locations_count = {}
|
||||
for player in range(1, world.players + 1):
|
||||
reachable_locations_count[player] = 0
|
||||
|
||||
def get_sphere_locations(sphere_state, 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:
|
||||
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
||||
for location in sphere_locations:
|
||||
unchecked_locations.remove(location)
|
||||
reachable_locations_count[location.player] += 1
|
||||
|
||||
if checked_locations:
|
||||
threshold = max(reachable_locations_count.values()) - 20
|
||||
|
||||
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
|
||||
if balancing_players:
|
||||
balancing_state = state.copy()
|
||||
balancing_unchecked_locations = unchecked_locations.copy()
|
||||
balancing_reachables = reachable_locations_count.copy()
|
||||
balancing_sphere = sphere_locations.copy()
|
||||
candidate_items = []
|
||||
while True:
|
||||
for location in balancing_sphere:
|
||||
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 and not location.locked:
|
||||
candidate_items.append(location)
|
||||
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
|
||||
for location in balancing_sphere:
|
||||
balancing_unchecked_locations.remove(location)
|
||||
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()
|
||||
reducing_state = state.copy()
|
||||
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]:
|
||||
reducing_state.collect(location.item, True, location)
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
|
||||
if world.has_beaten_game(balancing_state):
|
||||
if not world.has_beaten_game(reducing_state):
|
||||
items_to_replace.append(testing)
|
||||
else:
|
||||
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
|
||||
if reachable_locations_count[player] + len(reduced_sphere) < threshold:
|
||||
items_to_replace.append(testing)
|
||||
|
||||
replaced_items = False
|
||||
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, old_location.event = True, False
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
if replaced_items:
|
||||
for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]):
|
||||
unchecked_locations.remove(location)
|
||||
reachable_locations_count[location.player] += 1
|
||||
sphere_locations.append(location)
|
||||
|
||||
for location in sphere_locations:
|
||||
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.')
|
||||
|
|
430
Gui.py
430
Gui.py
|
@ -2,14 +2,16 @@
|
|||
from argparse import Namespace
|
||||
from glob import glob
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import os
|
||||
import shutil
|
||||
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, Entry, Spinbox, Button, filedialog, messagebox, ttk
|
||||
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
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,18 +60,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)
|
||||
beatableOnlyVar = IntVar()
|
||||
beatableOnlyCheckbutton = Checkbutton(checkBoxFrame, text="Only ensure seed is beatable, not all items must be reachable", variable=beatableOnlyVar)
|
||||
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)
|
||||
|
@ -81,34 +85,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)
|
||||
beatableOnlyCheckbutton.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)
|
||||
|
||||
romDialogFrame = Frame(fileDialogFrame)
|
||||
baseRomLabel = Label(romDialogFrame, text='Base Rom')
|
||||
romVar = StringVar()
|
||||
romEntry = Entry(romDialogFrame, textvariable=romVar)
|
||||
disableMusicVar = IntVar()
|
||||
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar)
|
||||
disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
|
||||
|
||||
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
|
||||
|
@ -128,26 +129,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", "*")])
|
||||
romVar.set(rom)
|
||||
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)
|
||||
|
||||
modeFrame = Frame(drowDownFrame)
|
||||
modeVar = StringVar()
|
||||
modeVar.set('open')
|
||||
modeOptionMenu = OptionMenu(modeFrame, modeVar, 'standard', 'open', 'swordless')
|
||||
modeOptionMenu = OptionMenu(modeFrame, modeVar, 'standard', 'open', 'inverted')
|
||||
modeOptionMenu.pack(side=RIGHT)
|
||||
modeLabel = Label(modeFrame, text='Game Mode')
|
||||
modeLabel = Label(modeFrame, text='Game mode')
|
||||
modeLabel.pack(side=LEFT)
|
||||
|
||||
logicFrame = Frame(drowDownFrame)
|
||||
|
@ -166,14 +229,46 @@ def guiMain(args=None):
|
|||
goalLabel = Label(goalFrame, text='Game goal')
|
||||
goalLabel.pack(side=LEFT)
|
||||
|
||||
crystalsGTFrame = Frame(drowDownFrame)
|
||||
crystalsGTVar = StringVar()
|
||||
crystalsGTVar.set('7')
|
||||
crystalsGTOptionMenu = OptionMenu(crystalsGTFrame, crystalsGTVar, '0', '1', '2', '3', '4', '5', '6', '7', 'random')
|
||||
crystalsGTOptionMenu.pack(side=RIGHT)
|
||||
crystalsGTLabel = Label(crystalsGTFrame, text='Crystals to open Ganon\'s Tower')
|
||||
crystalsGTLabel.pack(side=LEFT)
|
||||
|
||||
crystalsGanonFrame = Frame(drowDownFrame)
|
||||
crystalsGanonVar = StringVar()
|
||||
crystalsGanonVar.set('7')
|
||||
crystalsGanonOptionMenu = OptionMenu(crystalsGanonFrame, crystalsGanonVar, '0', '1', '2', '3', '4', '5', '6', '7', 'random')
|
||||
crystalsGanonOptionMenu.pack(side=RIGHT)
|
||||
crystalsGanonLabel = Label(crystalsGanonFrame, text='Crystals to fight Ganon')
|
||||
crystalsGanonLabel.pack(side=LEFT)
|
||||
|
||||
swordFrame = Frame(drowDownFrame)
|
||||
swordVar = StringVar()
|
||||
swordVar.set('random')
|
||||
swordOptionMenu = OptionMenu(swordFrame, swordVar, 'random', 'assured', 'swordless', 'vanilla')
|
||||
swordOptionMenu.pack(side=RIGHT)
|
||||
swordLabel = Label(swordFrame, text='Sword availability')
|
||||
swordLabel.pack(side=LEFT)
|
||||
|
||||
difficultyFrame = Frame(drowDownFrame)
|
||||
difficultyVar = StringVar()
|
||||
difficultyVar.set('normal')
|
||||
difficultyOptionMenu = OptionMenu(difficultyFrame, difficultyVar, 'easy', 'normal', 'hard', 'expert', 'insane')
|
||||
difficultyOptionMenu = OptionMenu(difficultyFrame, difficultyVar, 'normal', 'hard', 'expert')
|
||||
difficultyOptionMenu.pack(side=RIGHT)
|
||||
difficultyLabel = Label(difficultyFrame, text='Game difficulty')
|
||||
difficultyLabel = Label(difficultyFrame, text='Difficulty: item pool')
|
||||
difficultyLabel.pack(side=LEFT)
|
||||
|
||||
itemfunctionFrame = Frame(drowDownFrame)
|
||||
itemfunctionVar = StringVar()
|
||||
itemfunctionVar.set('normal')
|
||||
itemfunctionOptionMenu = OptionMenu(itemfunctionFrame, itemfunctionVar, 'normal', 'hard', 'expert')
|
||||
itemfunctionOptionMenu.pack(side=RIGHT)
|
||||
itemfunctionLabel = Label(itemfunctionFrame, text='Difficulty: item functionality')
|
||||
itemfunctionLabel.pack(side=LEFT)
|
||||
|
||||
timerFrame = Frame(drowDownFrame)
|
||||
timerVar = StringVar()
|
||||
timerVar.set('none')
|
||||
|
@ -190,6 +285,14 @@ def guiMain(args=None):
|
|||
progressiveLabel = Label(progressiveFrame, text='Progressive equipment')
|
||||
progressiveLabel.pack(side=LEFT)
|
||||
|
||||
accessibilityFrame = Frame(drowDownFrame)
|
||||
accessibilityVar = StringVar()
|
||||
accessibilityVar.set('items')
|
||||
accessibilityOptionMenu = OptionMenu(accessibilityFrame, accessibilityVar, 'items', 'locations', 'none')
|
||||
accessibilityOptionMenu.pack(side=RIGHT)
|
||||
accessibilityLabel = Label(accessibilityFrame, text='Item accessibility')
|
||||
accessibilityLabel.pack(side=LEFT)
|
||||
|
||||
algorithmFrame = Frame(drowDownFrame)
|
||||
algorithmVar = StringVar()
|
||||
algorithmVar.set('balanced')
|
||||
|
@ -206,61 +309,112 @@ def guiMain(args=None):
|
|||
shuffleLabel = Label(shuffleFrame, text='Entrance shuffle algorithm')
|
||||
shuffleLabel.pack(side=LEFT)
|
||||
|
||||
heartbeepFrame = Frame(drowDownFrame)
|
||||
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)
|
||||
|
||||
heartcolorFrame = Frame(drowDownFrame)
|
||||
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)
|
||||
|
||||
fastMenuFrame = Frame(drowDownFrame)
|
||||
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)
|
||||
|
||||
modeFrame.pack(expand=True, anchor=E)
|
||||
logicFrame.pack(expand=True, anchor=E)
|
||||
goalFrame.pack(expand=True, anchor=E)
|
||||
crystalsGTFrame.pack(expand=True, anchor=E)
|
||||
crystalsGanonFrame.pack(expand=True, anchor=E)
|
||||
swordFrame.pack(expand=True, anchor=E)
|
||||
difficultyFrame.pack(expand=True, anchor=E)
|
||||
itemfunctionFrame.pack(expand=True, anchor=E)
|
||||
timerFrame.pack(expand=True, anchor=E)
|
||||
progressiveFrame.pack(expand=True, anchor=E)
|
||||
accessibilityFrame.pack(expand=True, anchor=E)
|
||||
algorithmFrame.pack(expand=True, anchor=E)
|
||||
shuffleFrame.pack(expand=True, anchor=E)
|
||||
heartbeepFrame.pack(expand=True, anchor=E)
|
||||
heartcolorFrame.pack(expand=True, anchor=E)
|
||||
fastMenuFrame.pack(expand=True, anchor=E)
|
||||
|
||||
bottomFrame = Frame(randomizerWindow)
|
||||
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+E, padx=3)
|
||||
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
|
||||
enemizerCLIlabel.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:
|
||||
enemizerCLIpathVar.set(path)
|
||||
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
|
||||
enemizerCLIbrowseButton.pack(side=LEFT)
|
||||
|
||||
potShuffleVar = IntVar()
|
||||
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
|
||||
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=1, column=1)
|
||||
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
|
||||
enemizerBossLabel.pack(side=LEFT)
|
||||
enemizerBossVar = StringVar()
|
||||
enemizerBossVar.set('none')
|
||||
enemizerBossOption = OptionMenu(enemizerBossFrame, enemizerBossVar, 'none', 'basic', 'normal', 'chaos')
|
||||
enemizerBossOption.pack(side=LEFT)
|
||||
|
||||
enemizerDamageFrame = Frame(enemizerFrame)
|
||||
enemizerDamageFrame.grid(row=1, column=2)
|
||||
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
|
||||
enemizerDamageLabel.pack(side=LEFT)
|
||||
enemizerDamageVar = StringVar()
|
||||
enemizerDamageVar.set('default')
|
||||
enemizerDamageOption = OptionMenu(enemizerDamageFrame, enemizerDamageVar, 'default', 'shuffled', 'chaos')
|
||||
enemizerDamageOption.pack(side=LEFT)
|
||||
|
||||
enemizerHealthFrame = Frame(enemizerFrame)
|
||||
enemizerHealthFrame.grid(row=1, column=3)
|
||||
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
|
||||
enemizerHealthLabel.pack(side=LEFT)
|
||||
enemizerHealthVar = StringVar()
|
||||
enemizerHealthVar.set('default')
|
||||
enemizerHealthOption = OptionMenu(enemizerHealthFrame, enemizerHealthVar, 'default', 'easy', 'normal', 'hard', 'expert')
|
||||
enemizerHealthOption.pack(side=LEFT)
|
||||
|
||||
bottomFrame = Frame(randomizerWindow, pady=5)
|
||||
|
||||
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, textvariable=seedVar)
|
||||
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
|
||||
countLabel = Label(bottomFrame, text='Count')
|
||||
countVar = StringVar()
|
||||
countSpinbox = Spinbox(bottomFrame, from_=1, to=100, textvariable=countVar)
|
||||
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()
|
||||
guiargs.logic = logicVar.get()
|
||||
guiargs.goal = goalVar.get()
|
||||
guiargs.crystals_gt = crystalsGTVar.get()
|
||||
guiargs.crystals_ganon = crystalsGanonVar.get()
|
||||
guiargs.swords = swordVar.get()
|
||||
guiargs.difficulty = difficultyVar.get()
|
||||
guiargs.item_functionality = itemfunctionVar.get()
|
||||
guiargs.timer = timerVar.get()
|
||||
guiargs.progressive = progressiveVar.get()
|
||||
guiargs.accessibility = accessibilityVar.get()
|
||||
guiargs.algorithm = algorithmVar.get()
|
||||
guiargs.shuffle = shuffleVar.get()
|
||||
guiargs.heartbeep = heartbeepVar.get()
|
||||
|
@ -268,14 +422,24 @@ 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.beatableonly = bool(beatableOnlyVar.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 = enemyShuffleVar.get()
|
||||
guiargs.enemy_health = enemizerHealthVar.get()
|
||||
guiargs.enemy_damage = enemizerDamageVar.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()),
|
||||
int(icerodVar.get()), int(bombosVar.get()), int(etherVar.get()), int(quakeVar.get()), int(lampVar.get()), int(hammerVar.get()), int(shovelVar.get()), int(fluteVar.get()), int(bugnetVar.get()),
|
||||
|
@ -284,12 +448,16 @@ 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())]
|
||||
guiargs.shufflebosses = None
|
||||
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
|
||||
# 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 guiargs.count is not None:
|
||||
seed = guiargs.seed
|
||||
|
@ -299,13 +467,18 @@ 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")
|
||||
|
||||
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
|
||||
|
||||
seedLabel.pack(side=LEFT)
|
||||
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))
|
||||
countSpinbox.pack(side=LEFT)
|
||||
|
@ -317,6 +490,7 @@ def guiMain(args=None):
|
|||
rightHalfFrame.pack(side=RIGHT)
|
||||
topFrame.pack(side=TOP)
|
||||
bottomFrame.pack(side=BOTTOM)
|
||||
enemizerFrame.pack(side=BOTTOM, fill=BOTH)
|
||||
|
||||
# Adjuster Controls
|
||||
|
||||
|
@ -384,24 +558,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")
|
||||
|
@ -434,19 +626,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')
|
||||
|
@ -802,7 +994,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))
|
||||
|
@ -810,7 +1002,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))
|
||||
|
@ -818,7 +1010,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))
|
||||
|
@ -826,7 +1018,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))
|
||||
|
@ -842,7 +1034,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))
|
||||
|
@ -858,12 +1050,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')
|
||||
|
@ -912,14 +1112,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')
|
||||
|
@ -944,6 +1136,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')
|
||||
|
@ -976,6 +1176,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')
|
||||
|
@ -992,14 +1200,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))
|
||||
beatableOnlyVar.set(int(args.beatableonly))
|
||||
quickSwapVar.set(int(args.quickswap))
|
||||
disableMusicVar.set(int(args.disablemusic))
|
||||
if args.count:
|
||||
|
@ -1007,10 +1218,15 @@ def guiMain(args=None):
|
|||
if args.seed:
|
||||
seedVar.set(str(args.seed))
|
||||
modeVar.set(args.mode)
|
||||
swordVar.set(args.swords)
|
||||
difficultyVar.set(args.difficulty)
|
||||
itemfunctionVar.set(args.item_functionality)
|
||||
timerVar.set(args.timer)
|
||||
progressiveVar.set(args.progressive)
|
||||
accessibilityVar.set(args.accessibility)
|
||||
goalVar.set(args.goal)
|
||||
crystalsGTVar.set(args.crystals_gt)
|
||||
crystalsGanonVar.set(args.crystals_ganon)
|
||||
algorithmVar.set(args.algorithm)
|
||||
shuffleVar.set(args.shuffle)
|
||||
heartbeepVar.set(args.heartbeep)
|
||||
|
|
|
@ -0,0 +1,336 @@
|
|||
import collections
|
||||
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):
|
||||
|
||||
world.regions += [
|
||||
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'],
|
||||
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
|
||||
'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
|
||||
'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
|
||||
'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier', 'Lake Hylia Island',
|
||||
'Bonk Rock Cave', 'Library', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
|
||||
'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Light World River Drop',
|
||||
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
|
||||
'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)',
|
||||
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game',
|
||||
'East Dark World Mirror Spot', 'West Dark World Mirror Spot', 'South Dark World Mirror Spot', 'Cave 45', 'Checkerboard Cave', 'Mire Mirror Spot', 'Hammer Peg Area Mirror Spot',
|
||||
'Shopping Mall Mirror Spot', 'Skull Woods Mirror Spot', 'Inverted Pyramid Entrance','Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes', 'Bush Covered Lawn Outer Bushes',
|
||||
'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']),
|
||||
create_lw_region(player, 'Bush Covered Lawn', None, ['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']),
|
||||
create_lw_region(player, 'Bomb Hut Area', None, ['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']),
|
||||
create_lw_region(player, 'Hyrule Castle Secret Entrance Area', None, ['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']),
|
||||
create_lw_region(player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']),
|
||||
create_lw_region(player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']),
|
||||
create_cave_region(player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
|
||||
"Blind\'s Hideout - Left",
|
||||
"Blind\'s Hideout - Right",
|
||||
"Blind\'s Hideout - Far Left",
|
||||
"Blind\'s Hideout - Far Right"]),
|
||||
create_lw_region(player, 'Northeast Light World', None, ['Zoras River', 'Waterfall of Wishing', 'Potion Shop Outer Rock', 'Northeast Dark World Mirror Spot']),
|
||||
create_lw_region(player, 'Potion Shop Area', None, ['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock', 'Potion Shop Mirror Spot', 'Potion Shop River Drop']),
|
||||
create_lw_region(player, 'Graveyard Cave Area', None, ['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']),
|
||||
create_lw_region(player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
|
||||
create_cave_region(player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
|
||||
create_lw_region(player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
|
||||
create_cave_region(player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
|
||||
create_lw_region(player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
|
||||
create_cave_region(player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
|
||||
create_cave_region(player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
|
||||
create_cave_region(player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
|
||||
create_cave_region(player, 'Inverted Links House', 'your house', ['Link\'s House'], ['Inverted Links House Exit']),
|
||||
create_cave_region(player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
|
||||
create_cave_region(player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
|
||||
create_cave_region(player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
|
||||
create_cave_region(player, 'Snitch Lady (East)', 'a boring house'),
|
||||
create_cave_region(player, 'Snitch Lady (West)', 'a boring house'),
|
||||
create_cave_region(player, 'Bush Covered House', 'the grass man'),
|
||||
create_cave_region(player, 'Tavern (Front)', 'the tavern'),
|
||||
create_cave_region(player, 'Light World Bomb Hut', 'a restock room'),
|
||||
create_cave_region(player, 'Kakariko Shop', 'a common shop'),
|
||||
create_cave_region(player, 'Fortune Teller (Light)', 'a fortune teller'),
|
||||
create_cave_region(player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
|
||||
create_cave_region(player, 'Lumberjack House', 'a boring house'),
|
||||
create_cave_region(player, 'Bonk Fairy (Light)', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Swamp Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Chicken House', 'a house with a chest', ['Chicken House']),
|
||||
create_cave_region(player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
|
||||
create_cave_region(player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
|
||||
create_cave_region(player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
|
||||
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
|
||||
create_cave_region(player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
|
||||
create_cave_region(player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
|
||||
create_lw_region(player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
|
||||
create_cave_region(player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
|
||||
create_cave_region(player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
|
||||
create_cave_region(player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
|
||||
create_lw_region(player, 'Hobo Bridge', ['Hobo']),
|
||||
create_cave_region(player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
|
||||
create_cave_region(player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
|
||||
create_cave_region(player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
|
||||
create_cave_region(player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
|
||||
create_cave_region(player, 'Cave 45', 'a cave with an item', ['Cave 45']),
|
||||
create_cave_region(player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
|
||||
create_cave_region(player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
|
||||
create_cave_region(player, 'Long Fairy Cave', 'a fairy fountain'),
|
||||
create_cave_region(player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
|
||||
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
|
||||
create_cave_region(player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
|
||||
create_cave_region(player, 'Good Bee Cave', 'a cold bee'),
|
||||
create_cave_region(player, '20 Rupee Cave', 'a cave with some cash'),
|
||||
create_cave_region(player, 'Cave Shop (Lake Hylia)', 'a common shop'),
|
||||
create_cave_region(player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
|
||||
create_cave_region(player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
|
||||
create_cave_region(player, 'Library', 'the library', ['Library']),
|
||||
create_cave_region(player, 'Kakariko Gamble Game', 'a game of chance'),
|
||||
create_cave_region(player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
|
||||
create_lw_region(player, 'Lake Hylia Island', ['Lake Hylia Island']),
|
||||
create_cave_region(player, 'Capacity Upgrade', 'the queen of fairies'),
|
||||
create_cave_region(player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
|
||||
create_lw_region(player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)', 'Maze Race Mirror Spot']),
|
||||
create_cave_region(player, '50 Rupee Cave', 'a cave with some cash'),
|
||||
create_lw_region(player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)', 'Desert Ledge Drop']),
|
||||
create_lw_region(player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']),
|
||||
create_lw_region(player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
|
||||
create_lw_region(player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks', 'Desert Palace North Mirror Spot']),
|
||||
create_dungeon_region(player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
|
||||
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
|
||||
create_dungeon_region(player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
|
||||
create_dungeon_region(player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
|
||||
create_dungeon_region(player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
|
||||
create_dungeon_region(player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
|
||||
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
|
||||
create_lw_region(player, 'Master Sword Meadow', ['Master Sword Pedestal']),
|
||||
create_cave_region(player, 'Lost Woods Gamble', 'a game of chance'),
|
||||
create_lw_region(player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
|
||||
create_dungeon_region(player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest'],
|
||||
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']),
|
||||
create_dungeon_region(player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
|
||||
create_dungeon_region(player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'], ['Sewers Door']),
|
||||
create_dungeon_region(player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||
'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']),
|
||||
create_dungeon_region(player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
|
||||
create_dungeon_region(player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
|
||||
create_dungeon_region(player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
|
||||
create_cave_region(player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
|
||||
create_cave_region(player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
|
||||
create_cave_region(player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
|
||||
create_lw_region(player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave',
|
||||
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Mirror Spot']),
|
||||
create_cave_region(player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
|
||||
create_lw_region(player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)', 'Bumper Cave Ledge Mirror Spot']),
|
||||
create_cave_region(player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
|
||||
create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
|
||||
create_cave_region(player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
|
||||
create_lw_region(player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy',
|
||||
'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
|
||||
create_cave_region(player, 'Hookshot Fairy', 'fairies deep in a cave'),
|
||||
create_cave_region(player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
|
||||
create_cave_region(player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
|
||||
'Paradox Cave Lower - Left',
|
||||
'Paradox Cave Lower - Right',
|
||||
'Paradox Cave Lower - Far Right',
|
||||
'Paradox Cave Lower - Middle',
|
||||
'Paradox Cave Upper - Left',
|
||||
'Paradox Cave Upper - Right'],
|
||||
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
|
||||
create_cave_region(player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
|
||||
create_cave_region(player, 'Light World Death Mountain Shop', 'a common shop'),
|
||||
create_lw_region(player, 'East Death Mountain (Top)', ['Floating Island'], ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)', 'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access',
|
||||
'Floating Island Mirror Spot']),
|
||||
create_lw_region(player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']),
|
||||
create_lw_region(player, 'Mimic Cave Ledge', None, ['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']),
|
||||
create_cave_region(player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
|
||||
create_cave_region(player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
|
||||
create_lw_region(player, 'Fairy Ascension Plateau', None, ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
|
||||
create_cave_region(player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
|
||||
create_cave_region(player, 'Fairy Ascension Cave (Drop)', 'a connector', None, ['Fairy Ascension Cave Pots']),
|
||||
create_cave_region(player, 'Fairy Ascension Cave (Top)', 'a connector', None, ['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
|
||||
create_lw_region(player, 'Fairy Ascension Ledge', None, ['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']),
|
||||
create_lw_region(player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'], ['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop', 'Death Mountain (Top) Mirror Spot']),
|
||||
create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
|
||||
create_dungeon_region(player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
|
||||
create_dungeon_region(player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
|
||||
create_dungeon_region(player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
|
||||
|
||||
create_dw_region(player, 'East Dark World', ['Pyramid'], ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness', 'Dark Lake Hylia Drop (East)',
|
||||
'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']),
|
||||
create_dw_region(player, 'Northeast Dark World', ['Catfish'], ['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass', 'NEDW Flute', 'Dark Lake Hylia Teleporter']),
|
||||
create_cave_region(player, 'Palace of Darkness Hint', 'a storyteller'),
|
||||
create_cave_region(player, 'East Dark World Hint', 'a storyteller'),
|
||||
create_dw_region(player, 'South Dark World', ['Stumpy', 'Digging Game'], ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock', 'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)',
|
||||
'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']),
|
||||
create_cave_region(player, 'Inverted Big Bomb Shop', 'the bomb shop'),
|
||||
create_cave_region(player, 'Archery Game', 'a game of skill'),
|
||||
create_dw_region(player, 'Dark Lake Hylia', None, ['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']),
|
||||
create_dw_region(player, 'Dark Lake Hylia Ledge', None, ['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']),
|
||||
create_cave_region(player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
|
||||
create_cave_region(player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
|
||||
create_cave_region(player, 'Hype Cave', 'a bounty of five items', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
|
||||
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
|
||||
create_dw_region(player, 'West Dark World', ['Frog'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock',
|
||||
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop',
|
||||
'West Dark World Teleporter', 'WDW Flute']),
|
||||
create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop', 'Dark Grassy Lawn Flute']),
|
||||
create_dw_region(player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']),
|
||||
create_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
|
||||
create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
create_cave_region(player, 'Village of Outcasts Shop', 'a common shop'),
|
||||
create_cave_region(player, 'Dark Lake Hylia Shop', 'a common shop'),
|
||||
create_cave_region(player, 'Dark World Lumberjack Shop', 'a common shop'),
|
||||
create_cave_region(player, 'Dark World Potion Shop', 'a common shop'),
|
||||
create_cave_region(player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
|
||||
create_cave_region(player, 'Pyramid Fairy', 'a cave with two chests', ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
|
||||
create_cave_region(player, 'Brewery', 'a house with a chest', ['Brewery']),
|
||||
create_cave_region(player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
|
||||
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(player, 'Red Shield Shop', 'the rare shop'),
|
||||
create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller'),
|
||||
create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
|
||||
create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
|
||||
create_dw_region(player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']),
|
||||
create_dw_region(player, 'Dark Desert', None, ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']),
|
||||
create_dw_region(player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
|
||||
create_cave_region(player, 'Mire Shed', 'a cave with two chests', ['Mire Shed - Left', 'Mire Shed - Right']),
|
||||
create_cave_region(player, 'Dark Desert Hint', 'a storyteller'),
|
||||
create_dw_region(player, 'Dark Death Mountain', None, ['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)', 'Hookshot Cave', 'Turtle Rock',
|
||||
'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)', 'Turtle Rock Tail Drop', 'DDM Flute']),
|
||||
create_dw_region(player, 'Dark Death Mountain Ledge', None, ['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']),
|
||||
create_dw_region(player, 'Turtle Rock (Top)', None, ['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']),
|
||||
create_dw_region(player, 'Dark Death Mountain Isolated Ledge', None, ['Turtle Rock Isolated Ledge Entrance']),
|
||||
create_dw_region(player, 'Dark Death Mountain (East Bottom)', None, ['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']),
|
||||
create_cave_region(player, 'Superbunny Cave', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'],
|
||||
['Superbunny Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)']),
|
||||
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||
create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
|
||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
|
||||
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']),
|
||||
create_cave_region(player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||
|
||||
create_dungeon_region(player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
|
||||
create_dungeon_region(player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
|
||||
create_dungeon_region(player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']),
|
||||
create_dungeon_region(player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
|
||||
'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']),
|
||||
create_dungeon_region(player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']),
|
||||
create_dungeon_region(player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
|
||||
'Thieves\' Town - Map Chest',
|
||||
'Thieves\' Town - Compass Chest',
|
||||
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
|
||||
create_dungeon_region(player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
|
||||
'Thieves\' Town - Big Chest',
|
||||
'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']),
|
||||
create_dungeon_region(player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
|
||||
create_dungeon_region(player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
|
||||
create_dungeon_region(player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
|
||||
create_dungeon_region(player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
|
||||
create_dungeon_region(player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(player, 'Ice Palace (Entrance)', 'Ice Palace', None, ['Ice Palace Entrance Room', 'Ice Palace Exit']),
|
||||
create_dungeon_region(player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest',
|
||||
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
|
||||
create_dungeon_region(player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
|
||||
create_dungeon_region(player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']),
|
||||
create_dungeon_region(player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
|
||||
create_dungeon_region(player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
|
||||
create_dungeon_region(player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
|
||||
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
|
||||
create_dungeon_region(player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
|
||||
create_dungeon_region(player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
|
||||
create_dungeon_region(player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
|
||||
create_dungeon_region(player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
|
||||
'Turtle Rock - Roller Room - Right'], ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
|
||||
create_dungeon_region(player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
|
||||
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
|
||||
create_dungeon_region(player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
|
||||
create_dungeon_region(player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right'],
|
||||
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Inverted Ganons Tower Exit']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
|
||||
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'],
|
||||
['Ganons Tower (Bottom) (East)']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
|
||||
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
|
||||
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
|
||||
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'],
|
||||
['Ganons Tower (Bottom) (West)']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
|
||||
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
|
||||
'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']),
|
||||
create_dungeon_region(player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
|
||||
create_dungeon_region(player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
|
||||
create_cave_region(player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
|
||||
create_cave_region(player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
|
||||
create_dw_region(player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
|
||||
|
||||
# to simplify flute connections
|
||||
create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing'])
|
||||
]
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
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.get_regions(player) if region.type == RegionType.DarkWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
current.is_dark_world = True
|
||||
for exit in current.exits:
|
||||
if exit.connected_region.type == RegionType.LightWorld:
|
||||
# Don't venture into the dark world
|
||||
continue
|
||||
if exit.connected_region not in seen:
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
current.is_light_world = True
|
||||
for exit in current.exits:
|
||||
if exit.connected_region.type == RegionType.DarkWorld:
|
||||
# Don't venture into the light world
|
||||
continue
|
||||
if exit.connected_region not in seen:
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
594
ItemList.py
594
ItemList.py
|
@ -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', 'Bow', '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', 'Ocarina', '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']
|
||||
|
@ -21,69 +21,25 @@ basicgloves = ['Power Glove', 'Titans Mitts']
|
|||
normalbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)']
|
||||
hardbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Bee)', 'Bottle (Good Bee)']
|
||||
|
||||
normalbaseitems = (['Silver Arrows', 'Magic Upgrade (1/2)', 'Single Arrow', 'Sanctuary Heart Container', 'Arrows (10)', 'Bombs (3)'] +
|
||||
normalbaseitems = (['Magic Upgrade (1/2)', 'Single Arrow', 'Sanctuary Heart Container', 'Arrows (10)', 'Bombs (10)'] +
|
||||
['Rupees (300)'] * 4 + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 24)
|
||||
normalfirst15extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Arrows (10)'] * 6 + ['Bombs (3)'] * 6
|
||||
normalsecond15extra = ['Bombs (3)'] * 9 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)'] + ['Bombs (10)']
|
||||
normalsecond15extra = ['Bombs (3)'] * 10 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)']
|
||||
normalthird10extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 3 + ['Arrows (10)', 'Rupee (1)', 'Rupees (5)']
|
||||
normalfourth5extra = ['Arrows (10)'] * 2 + ['Rupees (20)'] * 2 + ['Rupees (5)']
|
||||
normalfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2
|
||||
|
||||
|
||||
easybaseitems = (['Sanctuary Heart Container'] + ['Rupees (300)'] * 4 + ['Magic Upgrade (1/2)'] * 2 + ['Lamp'] * 2 + ['Silver Arrows'] * 2 +
|
||||
['Boss Heart Container'] * 10 + ['Piece of Heart'] * 12)
|
||||
easyextra = ['Piece of Heart'] * 12 + ['Rupees (300)']
|
||||
easylimitedextra = ['Boss Heart Container'] * 3 # collapsing down the 12 pieces of heart
|
||||
easyfirst15extra = ['Rupees (100)'] + ['Arrows (10)'] * 7 + ['Bombs (3)'] * 7
|
||||
easysecond10extra = ['Bombs (3)'] * 7 + ['Rupee (1)', 'Rupees (50)', 'Bombs (10)']
|
||||
easythird5extra = ['Rupees (50)'] * 2 + ['Bombs (3)'] * 2 + ['Arrows (10)']
|
||||
easyfinal25extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 14 + ['Rupee (1)'] + ['Arrows (10)'] * 4 + ['Rupees (5)'] * 2
|
||||
easytimedotherextra = ['Red Clock'] * 5
|
||||
|
||||
hardbaseitems = ['Silver Arrows', 'Single Arrow', 'Bombs (10)'] + ['Rupees (300)'] * 4 + ['Boss Heart Container'] * 6 + ['Piece of Heart'] * 20 + ['Rupees (5)'] * 7 + ['Bombs (3)'] * 4
|
||||
hardfirst20extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Bombs (3)'] * 5 + ['Rupees (5)'] * 10 + ['Arrows (10)', 'Rupee (1)']
|
||||
hardsecond10extra = ['Rupees (5)'] * 5 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)']
|
||||
hardthird10extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 3 + ['Rupees (5)'] * 3
|
||||
hardfourth10extra = ['Arrows (10)'] * 2 + ['Rupees (20)'] * 7 + ['Rupees (5)']
|
||||
hardfinal20extra = ['Rupees (20)'] * 18 + ['Rupees (5)'] * 2
|
||||
|
||||
expertbaseitems = (['Rupees (300)'] * 4 + ['Single Arrow', 'Silver Arrows', 'Boss Heart Container', 'Rupee (1)', 'Bombs (10)'] + ['Piece of Heart'] * 20 + ['Rupees (5)'] * 2 +
|
||||
['Bombs (3)'] * 9 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupees (20)'] * 2)
|
||||
expertfirst15extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Rupees (5)'] * 12
|
||||
expertsecond15extra = ['Rupees (5)'] * 10 + ['Rupees (20)'] * 5
|
||||
expertthird10extra = ['Rupees (50)'] * 4 + ['Rupees (5)'] * 2 + ['Arrows (10)'] * 3 + ['Rupee (1)']
|
||||
expertfourth5extra = ['Rupees (5)'] * 5
|
||||
expertfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2
|
||||
|
||||
insanebaseitems = ['Rupees (300)'] * 4 + ['Single Arrow', 'Bombs (10)', 'Rupee (1)'] + ['Rupees (5)'] * 24 + ['Bombs (3)'] * 9 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupees (20)'] * 5
|
||||
insanefirst15extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Rupees (5)'] * 12
|
||||
insanesecond15extra = ['Rupees (5)'] * 10 + ['Rupees (20)'] * 5
|
||||
insanethird10extra = ['Rupees (50)'] * 4 + ['Rupees (5)'] * 2 + ['Arrows (10)'] * 3 + ['Rupee (1)']
|
||||
insanefourth5extra = ['Rupees (5)'] * 5
|
||||
insanefinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2
|
||||
|
||||
Difficulty = namedtuple('Difficulty',
|
||||
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
|
||||
'basicshield', 'progressivearmor', 'basicarmor', 'swordless',
|
||||
'progressivesword', 'basicsword', 'timedohko', 'timedother',
|
||||
'triforcehunt', 'triforce_pieces_required', 'retro', 'conditional_extras',
|
||||
'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
|
||||
|
||||
def easy_conditional_extras(timer, _goal, _mode, pool, placed_items):
|
||||
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
||||
if extraitems < len(easyextra):
|
||||
return easylimitedextra
|
||||
if timer in ['timed', 'timed-countdown']:
|
||||
return easytimedotherextra
|
||||
return []
|
||||
|
||||
def no_conditional_extras(*_args):
|
||||
return []
|
||||
|
||||
|
||||
difficulties = {
|
||||
'normal': Difficulty(
|
||||
baseitems = normalbaseitems,
|
||||
|
@ -95,184 +51,217 @@ 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,
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
triforce_pieces_required = 20,
|
||||
retro = ['Small Key (Universal)'] * 17 + ['Rupees (20)'] * 10,
|
||||
conditional_extras = no_conditional_extras,
|
||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit = 4,
|
||||
progressive_shield_limit = 3,
|
||||
progressive_armor_limit = 2,
|
||||
progressive_bow_limit = 2,
|
||||
progressive_bottle_limit = 4,
|
||||
),
|
||||
'easy': Difficulty(
|
||||
baseitems = easybaseitems,
|
||||
bottles = normalbottles,
|
||||
bottle_count = 8,
|
||||
same_bottle = False,
|
||||
progressiveshield = ['Progressive Shield'] * 6,
|
||||
basicshield = ['Blue Shield', 'Red Shield', 'Mirror Shield'] * 2,
|
||||
progressivearmor = ['Progressive Armor'] * 4,
|
||||
basicarmor = ['Blue Mail', 'Red Mail'] * 2,
|
||||
swordless = ['Rupees (20)'] * 8,
|
||||
progressivesword = ['Progressive Sword'] * 7,
|
||||
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'] *2 + ['Fighter Sword'],
|
||||
timedohko = ['Green Clock'] * 25,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 5, # +5 more Red Clocks if there is room
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
triforce_pieces_required = 20,
|
||||
retro = ['Small Key (Universal)'] * 27,
|
||||
conditional_extras = easy_conditional_extras,
|
||||
extras = [easyextra, easyfirst15extra, easysecond10extra, easythird5extra, easyfinal25extra],
|
||||
progressive_sword_limit = 4,
|
||||
progressive_shield_limit = 3,
|
||||
progressive_armor_limit = 2,
|
||||
progressive_bottle_limit = 4,
|
||||
boss_heart_container_limit = 255,
|
||||
heart_piece_limit = 255,
|
||||
),
|
||||
'hard': Difficulty(
|
||||
baseitems = hardbaseitems,
|
||||
baseitems = normalbaseitems,
|
||||
bottles = hardbottles,
|
||||
bottle_count = 4,
|
||||
same_bottle = False,
|
||||
progressiveshield = ['Progressive Shield'] * 3,
|
||||
basicshield = ['Blue Shield', 'Red Shield', 'Red Shield'],
|
||||
progressivearmor = ['Progressive Armor'] * 2,
|
||||
basicarmor = ['Progressive Armor'] * 2, #only the first one will upgrade, making this equivalent to two blue mail
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'],
|
||||
timedohko = ['Green Clock'] * 20,
|
||||
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,
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
triforce_pieces_required = 20,
|
||||
retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15,
|
||||
conditional_extras = no_conditional_extras,
|
||||
extras = [hardfirst20extra, hardsecond10extra, hardthird10extra, hardfourth10extra, hardfinal20extra],
|
||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit = 3,
|
||||
progressive_shield_limit = 2,
|
||||
progressive_armor_limit = 1,
|
||||
progressive_armor_limit = 0,
|
||||
progressive_bow_limit = 1,
|
||||
progressive_bottle_limit = 4,
|
||||
boss_heart_container_limit = 6,
|
||||
heart_piece_limit = 16,
|
||||
),
|
||||
'expert': Difficulty(
|
||||
baseitems = expertbaseitems,
|
||||
baseitems = normalbaseitems,
|
||||
bottles = hardbottles,
|
||||
bottle_count = 4,
|
||||
same_bottle = False,
|
||||
progressiveshield = ['Progressive Shield'] * 3,
|
||||
basicshield = ['Progressive Shield'] * 3, #only the first one will upgrade, making this equivalent to two blue shields
|
||||
progressivearmor = [],
|
||||
basicarmor = [],
|
||||
progressivearmor = ['Progressive Armor'] * 2, # neither will count
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
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,
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
triforce_pieces_required = 20,
|
||||
retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15,
|
||||
conditional_extras = no_conditional_extras,
|
||||
extras = [expertfirst15extra, expertsecond15extra, expertthird10extra, expertfourth5extra, expertfinal25extra],
|
||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit = 2,
|
||||
progressive_shield_limit = 1,
|
||||
progressive_armor_limit = 0,
|
||||
progressive_bow_limit = 1,
|
||||
progressive_bottle_limit = 4,
|
||||
),
|
||||
'insane': Difficulty(
|
||||
baseitems = insanebaseitems,
|
||||
bottles = hardbottles,
|
||||
bottle_count = 4,
|
||||
same_bottle = False,
|
||||
progressiveshield = [],
|
||||
basicshield = [],
|
||||
progressivearmor = [],
|
||||
basicarmor = [],
|
||||
swordless = ['Rupees (20)'] * 3 + ['Silver Arrows'],
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
triforce_pieces_required = 20,
|
||||
retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15,
|
||||
conditional_extras = no_conditional_extras,
|
||||
extras = [insanefirst15extra, insanesecond15extra, insanethird10extra, insanefourth5extra, insanefinal25extra],
|
||||
progressive_sword_limit = 2,
|
||||
progressive_shield_limit = 0,
|
||||
progressive_armor_limit = 0,
|
||||
progressive_bottle_limit = 4,
|
||||
boss_heart_container_limit = 2,
|
||||
heart_piece_limit = 8,
|
||||
),
|
||||
}
|
||||
|
||||
def generate_itempool(world):
|
||||
if (world.difficulty not in ['easy', 'normal', 'hard', 'expert', 'insane'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals']
|
||||
or world.mode not in ['open', 'standard', 'swordless'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
|
||||
def generate_itempool(world, player):
|
||||
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 not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
|
||||
raise NotImplementedError('Not supported yet')
|
||||
|
||||
if world.timer in ['ohko', 'timed-ohko']:
|
||||
world.can_take_damage = False
|
||||
|
||||
world.push_item('Ganon', ItemFactory('Triforce'), False)
|
||||
world.get_location('Ganon').event = True
|
||||
world.push_item('Agahnim 1', ItemFactory('Beat Agahnim 1'), False)
|
||||
world.get_location('Agahnim 1').event = True
|
||||
world.push_item('Agahnim 2', ItemFactory('Beat Agahnim 2'), False)
|
||||
world.get_location('Agahnim 2').event = True
|
||||
world.push_item('Dark Blacksmith Ruins', ItemFactory('Pick Up Purple Chest'), False)
|
||||
world.get_location('Dark Blacksmith Ruins').event = True
|
||||
world.push_item('Frog', ItemFactory('Get Frog'), False)
|
||||
world.get_location('Frog').event = True
|
||||
world.push_item('Missing Smith', ItemFactory('Return Smith'), False)
|
||||
world.get_location('Missing Smith').event = True
|
||||
world.push_item('Floodgate', ItemFactory('Open Floodgate'), False)
|
||||
world.get_location('Floodgate').event = True
|
||||
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[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[player]
|
||||
region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
|
||||
world.clear_location_cache()
|
||||
|
||||
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)
|
||||
world.get_location('Agahnim 1', player).event = True
|
||||
world.get_location('Agahnim 1', player).locked = True
|
||||
world.push_item(world.get_location('Agahnim 2', player), ItemFactory('Beat Agahnim 2', player), False)
|
||||
world.get_location('Agahnim 2', player).event = True
|
||||
world.get_location('Agahnim 2', player).locked = True
|
||||
world.push_item(world.get_location('Dark Blacksmith Ruins', player), ItemFactory('Pick Up Purple Chest', player), False)
|
||||
world.get_location('Dark Blacksmith Ruins', player).event = True
|
||||
world.get_location('Dark Blacksmith Ruins', player).locked = True
|
||||
world.push_item(world.get_location('Frog', player), ItemFactory('Get Frog', player), False)
|
||||
world.get_location('Frog', player).event = True
|
||||
world.get_location('Frog', player).locked = True
|
||||
world.push_item(world.get_location('Missing Smith', player), ItemFactory('Return Smith', player), False)
|
||||
world.get_location('Missing Smith', player).event = True
|
||||
world.get_location('Missing Smith', player).locked = True
|
||||
world.push_item(world.get_location('Floodgate', player), ItemFactory('Open Floodgate', player), False)
|
||||
world.get_location('Floodgate', player).event = True
|
||||
world.get_location('Floodgate', player).locked = True
|
||||
|
||||
# set up item pool
|
||||
if world.custom:
|
||||
(pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.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, world.shuffle[player], world.difficulty[player], world.timer, 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, 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.retro)
|
||||
world.itempool = ItemFactory(pool)
|
||||
for (location, item) in placed_items:
|
||||
world.push_item(location, ItemFactory(item), False)
|
||||
world.get_location(location).event = True
|
||||
(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[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player])
|
||||
|
||||
for item in precollected_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
|
||||
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
|
||||
|
||||
items = ItemFactory(pool, player)
|
||||
|
||||
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(get_dungeon_item_pool(world))
|
||||
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 ['easy', 'normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
|
||||
[item for item in world.itempool if item.name == 'Boss Heart Container'][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'][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)]
|
||||
world.required_medallions = (mm_medallion, tr_medallion)
|
||||
world.required_medallions[player] = (mm_medallion, tr_medallion)
|
||||
|
||||
place_bosses(world)
|
||||
set_up_shops(world)
|
||||
place_bosses(world, player)
|
||||
set_up_shops(world, player)
|
||||
|
||||
if world.retro:
|
||||
set_up_take_anys(world)
|
||||
if world.retro[player]:
|
||||
set_up_take_anys(world, player)
|
||||
|
||||
create_dynamic_shop_locations(world)
|
||||
create_dynamic_shop_locations(world, player)
|
||||
|
||||
take_any_locations = [
|
||||
'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut',
|
||||
|
@ -280,126 +269,128 @@ take_any_locations = [
|
|||
'Bonk Fairy (Dark)', 'Lake Hylia Healer Fairy', 'Swamp Healer Fairy', 'Desert Healer Fairy',
|
||||
'Dark Lake Hylia Healer Fairy', 'Dark Lake Hylia Ledge Healer Fairy', 'Dark Desert Healer Fairy',
|
||||
'Dark Death Mountain Healer Fairy', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave',
|
||||
'Kakariko Gamble Game', 'Capacity Upgrade', '50 Rupee Cave', 'Lost Woods Gamble', 'Hookshot Fairy',
|
||||
'Kakariko Gamble Game', '50 Rupee Cave', 'Lost Woods Gamble', 'Hookshot Fairy',
|
||||
'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint',
|
||||
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
|
||||
|
||||
def set_up_take_anys(world):
|
||||
def set_up_take_anys(world, player):
|
||||
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')
|
||||
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player)
|
||||
world.regions.append(old_man_take_any)
|
||||
world.dynamic_regions.append(old_man_take_any)
|
||||
|
||||
reg = regions.pop()
|
||||
entrance = world.get_region(reg).entrances[0]
|
||||
connect_entrance(world, entrance, old_man_take_any)
|
||||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance, old_man_take_any, 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']
|
||||
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
|
||||
if swords:
|
||||
sword = random.choice(swords)
|
||||
world.itempool.remove(sword)
|
||||
world.itempool.append(ItemFactory('Rupees (20)'))
|
||||
world.itempool.append(ItemFactory('Rupees (20)', player))
|
||||
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True)
|
||||
else:
|
||||
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0)
|
||||
|
||||
for num in range(4):
|
||||
take_any = Region("Take-Any #{}".format(num+1), RegionType.Cave, 'a cave of choice')
|
||||
take_any = Region("Take-Any #{}".format(num+1), RegionType.Cave, 'a cave of choice', player)
|
||||
world.regions.append(take_any)
|
||||
world.dynamic_regions.append(take_any)
|
||||
|
||||
target, room_id = random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
|
||||
reg = regions.pop()
|
||||
entrance = world.get_region(reg).entrances[0]
|
||||
connect_entrance(world, entrance, take_any)
|
||||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance, take_any, 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):
|
||||
def create_dynamic_shop_locations(world, player):
|
||||
for shop in world.shops:
|
||||
for i, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
if item['create_location']:
|
||||
loc = Location("{} Item {}".format(shop.region.name, i+1), parent=shop.region)
|
||||
shop.region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
if shop.region.player == player:
|
||||
for i, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
if item['create_location']:
|
||||
loc = Location(player, "{} Item {}".format(shop.region.name, i+1), parent=shop.region)
|
||||
shop.region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
|
||||
world.clear_location_cache()
|
||||
world.clear_location_cache()
|
||||
|
||||
world.push_item(loc, ItemFactory(item['item']), False)
|
||||
loc.event = True
|
||||
world.push_item(loc, ItemFactory(item['item'], player), False)
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
|
||||
def fill_prizes(world, attempts=15):
|
||||
crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'])
|
||||
crystal_locations = [world.get_location('Turtle Rock - Prize'), world.get_location('Eastern Palace - Prize'), world.get_location('Desert Palace - Prize'), world.get_location('Tower of Hera - Prize'), world.get_location('Palace of Darkness - Prize'),
|
||||
world.get_location('Thieves\' Town - Prize'), world.get_location('Skull Woods - Prize'), world.get_location('Swamp Palace - Prize'), world.get_location('Ice Palace - Prize'),
|
||||
world.get_location('Misery Mire - Prize')]
|
||||
placed_prizes = [loc.item.name for loc in crystal_locations if loc.item is not None]
|
||||
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
|
||||
empty_crystal_locations = [loc for loc in crystal_locations if loc.item is None]
|
||||
all_state = world.get_all_state(keys=True)
|
||||
for player in range(1, world.players + 1):
|
||||
crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player)
|
||||
crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player),
|
||||
world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player),
|
||||
world.get_location('Misery Mire - Prize', player)]
|
||||
placed_prizes = [loc.item.name for loc in crystal_locations if loc.item is not None]
|
||||
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
|
||||
empty_crystal_locations = [loc for loc in crystal_locations if loc.item is None]
|
||||
|
||||
while attempts:
|
||||
attempts -= 1
|
||||
try:
|
||||
prizepool = list(unplaced_prizes)
|
||||
prize_locs = list(empty_crystal_locations)
|
||||
random.shuffle(prizepool)
|
||||
random.shuffle(prize_locs)
|
||||
fill_restrictive(world, world.get_all_state(keys=True), prize_locs, prizepool)
|
||||
except FillError:
|
||||
logging.getLogger('').info("Failed to place dungeon prizes. Will retry %s more times", attempts)
|
||||
for location in empty_crystal_locations:
|
||||
location.item = None
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise FillError('Unable to place dungeon prizes')
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
prizepool = list(unplaced_prizes)
|
||||
prize_locs = list(empty_crystal_locations)
|
||||
random.shuffle(prizepool)
|
||||
random.shuffle(prize_locs)
|
||||
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 - attempt - 1)
|
||||
for location in empty_crystal_locations:
|
||||
location.item = None
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise FillError('Unable to place dungeon prizes')
|
||||
|
||||
|
||||
def set_up_shops(world):
|
||||
# Changes to basic Shops
|
||||
def set_up_shops(world, player):
|
||||
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops
|
||||
|
||||
for shop in world.shops:
|
||||
shop.active = True
|
||||
|
||||
if world.retro:
|
||||
rss = world.get_region('Red Shield Shop').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], 5):
|
||||
shop.active = True
|
||||
if world.retro[player]:
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
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.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, retro):
|
||||
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
|
||||
treasure_hunt_icon = None
|
||||
|
||||
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'
|
||||
|
||||
|
@ -409,13 +400,11 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro):
|
|||
pool.extend(basicgloves)
|
||||
|
||||
lamps_needed_for_dark_rooms = 1
|
||||
if difficulty == 'easy':
|
||||
lamps_needed_for_dark_rooms = 3
|
||||
|
||||
# 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'))
|
||||
place_item('Link\'s House', 'Magic Mirror')
|
||||
place_item('Sanctuary', 'Moon Pearl')
|
||||
else:
|
||||
pool.extend(['Magic Mirror', 'Moon Pearl'])
|
||||
|
||||
|
@ -446,22 +435,36 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro):
|
|||
else:
|
||||
pool.extend(diff.basicarmor)
|
||||
|
||||
if mode == 'swordless':
|
||||
pool.extend(diff.swordless)
|
||||
elif mode == 'standard':
|
||||
if want_progressives():
|
||||
placed_items.append(('Link\'s Uncle', 'Progressive Sword'))
|
||||
pool.extend(diff.progressivesword)
|
||||
else:
|
||||
placed_items.append(('Link\'s Uncle', 'Fighter Sword'))
|
||||
pool.extend(diff.basicsword)
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
elif swords != 'swordless':
|
||||
pool.extend(diff.basicbow)
|
||||
else:
|
||||
if want_progressives():
|
||||
pool.extend(diff.progressivesword)
|
||||
pool.extend(['Progressive Sword'])
|
||||
pool.extend(['Bow', 'Silver Arrows'])
|
||||
|
||||
if swords == 'swordless':
|
||||
pool.extend(diff.swordless)
|
||||
elif swords == 'vanilla':
|
||||
swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
|
||||
random.shuffle(swords_to_use)
|
||||
|
||||
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':
|
||||
place_item('Master Sword Pedestal', swords_to_use.pop())
|
||||
else:
|
||||
pool.extend(diff.basicsword)
|
||||
pool.extend(['Fighter Sword'])
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
else:
|
||||
pool.extend(diff.progressivesword if want_progressives() else diff.basicsword)
|
||||
if swords == 'assured':
|
||||
if want_progressives():
|
||||
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)
|
||||
|
||||
|
@ -479,17 +482,13 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro):
|
|||
treasure_hunt_count = diff.triforce_pieces_required
|
||||
treasure_hunt_icon = 'Triforce Piece'
|
||||
|
||||
cond_extras = diff.conditional_extras(timer, goal, mode, pool, placed_items)
|
||||
pool.extend(cond_extras)
|
||||
extraitems -= len(cond_extras)
|
||||
|
||||
for extra in diff.extras:
|
||||
if extraitems > 0:
|
||||
pool.extend(extra)
|
||||
extraitems -= len(extra)
|
||||
|
||||
if goal == 'pedestal':
|
||||
placed_items.append(('Master Sword Pedestal', 'Triforce'))
|
||||
if goal == 'pedestal' and swords != 'vanilla':
|
||||
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]
|
||||
|
@ -498,29 +497,34 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro):
|
|||
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, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
|
||||
|
||||
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, retro, customitemarray):
|
||||
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])
|
||||
|
@ -581,14 +585,14 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, r
|
|||
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]
|
||||
|
||||
lamps_needed_for_dark_rooms = 1
|
||||
if difficulty == 'easy':
|
||||
lamps_needed_for_dark_rooms = customitemarray[12]
|
||||
|
||||
# expert+ difficulties produce the same contents for
|
||||
# all bottles, since only one bottle is available
|
||||
|
@ -599,12 +603,12 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, r
|
|||
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
|
||||
|
||||
|
@ -616,32 +620,25 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, r
|
|||
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 progressive == 'off':
|
||||
placed_items.append(('Link\'s Uncle', 'Fighter Sword'))
|
||||
pool.extend(['Fighter Sword'] * max((customitemarray[32] - 1), 0))
|
||||
pool.extend(['Progressive Sword'] * customitemarray[36])
|
||||
else:
|
||||
placed_items.append(('Link\'s Uncle', 'Progressive Sword'))
|
||||
pool.extend(['Fighter Sword'] * customitemarray[32])
|
||||
pool.extend(['Progressive Sword'] * max((customitemarray[36] - 1), 0))
|
||||
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(['Fighter Sword'] * customitemarray[32])
|
||||
pool.extend(['Progressive Sword'] * customitemarray[36])
|
||||
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:
|
||||
|
@ -653,28 +650,31 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, r
|
|||
if itemtotal < total_items_to_place:
|
||||
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
|
||||
|
||||
return (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
|
||||
|
||||
# A quick test to ensure all combinations generate the correct amount of items.
|
||||
def test():
|
||||
for difficulty in ['easy', 'normal', 'hard', 'expert', 'insane']:
|
||||
for difficulty in ['normal', 'hard', 'expert']:
|
||||
for goal in ['ganon', 'triforcehunt', 'pedestal']:
|
||||
for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
|
||||
for mode in ['open', 'standard', 'swordless']:
|
||||
for progressive in ['on', 'off']:
|
||||
for shuffle in ['full', 'insane']:
|
||||
for retro in [True, False]:
|
||||
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, retro)
|
||||
count = len(out[0]) + len(out[1])
|
||||
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)
|
||||
count = len(out[0]) + len(out[1])
|
||||
|
||||
correct_count = total_items_to_place
|
||||
if goal in ['pedestal']:
|
||||
# pedestal goals generate one extra item
|
||||
correct_count += 1
|
||||
if retro:
|
||||
correct_count += 28
|
||||
|
||||
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, retro))
|
||||
correct_count = total_items_to_place
|
||||
if goal == 'pedestal' and swords != 'vanilla':
|
||||
# pedestal goals generate one extra item
|
||||
correct_count += 1
|
||||
if retro:
|
||||
correct_count += 28
|
||||
try:
|
||||
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro))
|
||||
except AssertionError as e:
|
||||
print(e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
|
19
Items.py
19
Items.py
|
@ -3,7 +3,7 @@ import logging
|
|||
from BaseClasses import Item
|
||||
|
||||
|
||||
def ItemFactory(items):
|
||||
def ItemFactory(items, player):
|
||||
ret = []
|
||||
singleton = False
|
||||
if isinstance(items, str):
|
||||
|
@ -12,7 +12,7 @@ def ItemFactory(items):
|
|||
for item in items:
|
||||
if item in item_table:
|
||||
advancement, priority, type, code, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text = item_table[item]
|
||||
ret.append(Item(item, advancement, priority, type, code, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text))
|
||||
ret.append(Item(item, advancement, priority, type, code, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text, player))
|
||||
else:
|
||||
logging.getLogger('').warning('Unknown Item: %s', item)
|
||||
return None
|
||||
|
@ -24,6 +24,8 @@ def ItemFactory(items):
|
|||
|
||||
# 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'),
|
||||
|
@ -42,8 +44,8 @@ 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'),
|
||||
|
@ -160,11 +162,12 @@ 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),
|
||||
|
|
355
Main.py
355
Main.py
|
@ -3,44 +3,34 @@ import copy
|
|||
from itertools import zip_longest
|
||||
import json
|
||||
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 EntranceShuffle import link_entrances
|
||||
from Rom import patch_rom, Sprite, LocalRom, JsonRom
|
||||
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, 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
|
||||
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
|
||||
|
||||
__version__ = '0.6.2'
|
||||
|
||||
logic_hash = [134, 166, 181, 191, 228, 89, 188, 200, 5, 157, 217, 139, 180, 198, 106, 104,
|
||||
88, 223, 138, 28, 54, 18, 216, 129, 248, 19, 109, 220, 159, 75, 238, 57,
|
||||
231, 183, 143, 167, 114, 176, 82, 169, 179, 94, 115, 193, 252, 222, 52, 245,
|
||||
33, 208, 39, 122, 177, 136, 29, 161, 210, 165, 6, 125, 146, 212, 101, 185,
|
||||
65, 247, 253, 85, 171, 147, 71, 148, 203, 202, 230, 1, 13, 64, 254, 141,
|
||||
32, 93, 152, 4, 92, 16, 195, 204, 246, 201, 11, 7, 189, 97, 9, 91,
|
||||
237, 215, 163, 131, 142, 34, 111, 196, 120, 127, 168, 211, 227, 61, 187, 110,
|
||||
190, 162, 59, 80, 225, 186, 37, 154, 76, 72, 27, 17, 79, 206, 207, 243,
|
||||
184, 197, 153, 48, 119, 99, 2, 151, 51, 67, 121, 175, 38, 224, 87, 242,
|
||||
45, 22, 155, 244, 209, 117, 214, 213, 194, 126, 236, 73, 133, 70, 49, 140,
|
||||
229, 108, 156, 124, 105, 226, 44, 23, 112, 102, 173, 219, 14, 116, 58, 103,
|
||||
55, 10, 95, 251, 84, 118, 160, 78, 63, 250, 31, 41, 35, 255, 170, 25,
|
||||
66, 172, 98, 249, 68, 8, 113, 21, 46, 24, 137, 149, 81, 130, 42, 164,
|
||||
50, 12, 158, 15, 47, 182, 30, 40, 36, 83, 77, 205, 20, 241, 3, 132,
|
||||
0, 60, 96, 62, 74, 178, 53, 56, 135, 174, 145, 86, 107, 233, 218, 221,
|
||||
43, 150, 100, 69, 235, 26, 234, 192, 199, 144, 232, 128, 239, 123, 240, 90]
|
||||
from Utils import output_path, parse_player_names
|
||||
|
||||
__version__ = '0.6.3-pre'
|
||||
|
||||
def main(args, seed=None):
|
||||
start = time.clock()
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.process_time()
|
||||
|
||||
# initialize the world
|
||||
world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, 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, 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)
|
||||
|
@ -49,26 +39,69 @@ def main(args, seed=None):
|
|||
world.seed = int(seed)
|
||||
random.seed(world.seed)
|
||||
|
||||
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
|
||||
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.difficulty_requirements = difficulties[world.difficulty]
|
||||
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
||||
|
||||
create_regions(world)
|
||||
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
create_dungeons(world)
|
||||
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)
|
||||
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)
|
||||
else:
|
||||
create_inverted_regions(world, player)
|
||||
create_shops(world, player)
|
||||
create_dungeons(world, player)
|
||||
|
||||
logger.info('Shuffling the World about.')
|
||||
|
||||
link_entrances(world)
|
||||
mark_light_world_regions(world)
|
||||
for player in range(1, world.players + 1):
|
||||
if world.mode[player] != 'inverted':
|
||||
link_entrances(world, player)
|
||||
mark_light_world_regions(world, player)
|
||||
else:
|
||||
link_inverted_entrances(world, player)
|
||||
mark_dark_world_regions(world, player)
|
||||
|
||||
logger.info('Generating Item Pool.')
|
||||
|
||||
generate_itempool(world)
|
||||
for player in range(1, world.players + 1):
|
||||
generate_itempool(world, player)
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
set_rules(world)
|
||||
for player in range(1, world.players + 1):
|
||||
set_rules(world, player)
|
||||
|
||||
logger.info('Placing Dungeon Prizes.')
|
||||
|
||||
|
@ -77,7 +110,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)
|
||||
|
@ -95,132 +129,216 @@ 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)
|
||||
|
||||
logger.info('Calculating playthrough.')
|
||||
|
||||
create_playthrough(world)
|
||||
if world.players > 1:
|
||||
logger.info('Balancing multiworld progression.')
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
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' % (world.logic, world.difficulty, 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)
|
||||
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
|
||||
|
||||
rom_names = []
|
||||
jsonout = {}
|
||||
if not args.suppress_rom:
|
||||
for team in range(world.teams):
|
||||
for player in range(1, world.players + 1):
|
||||
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)
|
||||
|
||||
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)
|
||||
if not args.jsonout:
|
||||
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
|
||||
|
||||
if args.race:
|
||||
patch_race_rom(rom)
|
||||
|
||||
rom_names.append((player, team, list(rom.name)))
|
||||
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:
|
||||
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 '')
|
||||
|
||||
outfilepname = f'_T{team+1}' if world.teams > 1 else ''
|
||||
if world.players > 1:
|
||||
outfilepname += f'_P{player}'
|
||||
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 in ['none', 'display'] else "-" + world.timer,
|
||||
world.shuffle[player], world.algorithm, mcsb_name,
|
||||
"-retro" if world.retro[player] else "",
|
||||
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "",
|
||||
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
|
||||
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
|
||||
|
||||
multidata = zlib.compress(json.dumps((parsed_names, rom_names,
|
||||
[((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:
|
||||
rom = JsonRom()
|
||||
jsonout["multidata"] = list(multidata)
|
||||
else:
|
||||
rom = LocalRom(args.rom)
|
||||
patch_rom(world, rom, bytearray(logic_hash), args.heartbeep, args.heartcolor, sprite)
|
||||
if args.jsonout:
|
||||
print(json.dumps({'patch': rom.patches, 'spoiler': world.spoiler.to_json()}))
|
||||
else:
|
||||
rom.write_to_file(args.jsonout or output_path('%s.sfc' % outfilebase))
|
||||
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
|
||||
f.write(multidata)
|
||||
|
||||
if args.create_spoiler and not args.jsonout:
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
|
||||
if args.jsonout:
|
||||
print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()}))
|
||||
elif args.create_spoiler and not args.skip_playthrough:
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
logger.debug('Total Time: %s', time.clock() - start)
|
||||
logger.debug('Total Time: %s', time.process_time() - start)
|
||||
|
||||
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.shuffle, world.logic, world.mode, world.difficulty, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints)
|
||||
ret.required_medallions = list(world.required_medallions)
|
||||
ret.swamp_patch_required = world.swamp_patch_required
|
||||
ret.ganon_at_pyramid = world.ganon_at_pyramid
|
||||
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 = 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.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.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
|
||||
create_regions(ret)
|
||||
create_dungeons(ret)
|
||||
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()
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(ret, player)
|
||||
else:
|
||||
create_inverted_regions(ret, player)
|
||||
create_shops(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
|
||||
copy_dynamic_regions_and_locations(world, ret)
|
||||
|
||||
# copy bosses
|
||||
for dungeon in world.dungeons:
|
||||
for level, boss in dungeon.bosses.items():
|
||||
ret.get_dungeon(dungeon.name).bosses[level] = boss
|
||||
ret.get_dungeon(dungeon.name, dungeon.player).bosses[level] = boss
|
||||
|
||||
for shop in world.shops:
|
||||
copied_shop = ret.get_region(shop.region.name).shop
|
||||
copied_shop.active = shop.active
|
||||
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
|
||||
copied_shop.inventory = copy.copy(shop.inventory)
|
||||
|
||||
# connect copied world
|
||||
for region in world.regions:
|
||||
copied_region = ret.get_region(region.name)
|
||||
copied_region = ret.get_region(region.name, region.player)
|
||||
copied_region.is_light_world = region.is_light_world
|
||||
copied_region.is_dark_world = region.is_dark_world
|
||||
for entrance in region.entrances:
|
||||
ret.get_entrance(entrance.name).connect(copied_region)
|
||||
ret.get_entrance(entrance.name, entrance.player).connect(copied_region)
|
||||
|
||||
# fill locations
|
||||
for location in world.get_locations():
|
||||
if location.item is not None:
|
||||
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type)
|
||||
ret.get_location(location.name).item = item
|
||||
item.location = ret.get_location(location.name)
|
||||
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).event = True
|
||||
ret.get_location(location.name, location.player).event = True
|
||||
if location.locked:
|
||||
ret.get_location(location.name, location.player).locked = True
|
||||
|
||||
# copy remaining itempool. No item in itempool should have an assigned location
|
||||
for item in world.itempool:
|
||||
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type))
|
||||
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 = list(world.state.prog_items)
|
||||
ret.state.prog_items = world.state.prog_items.copy()
|
||||
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
||||
|
||||
set_rules(ret)
|
||||
for player in range(1, world.players + 1):
|
||||
set_rules(ret, player)
|
||||
|
||||
return ret
|
||||
|
||||
def copy_dynamic_regions_and_locations(world, ret):
|
||||
for region in world.dynamic_regions:
|
||||
new_reg = Region(region.name, region.type, region.hint_text)
|
||||
new_reg = Region(region.name, region.type, region.hint_text, region.player)
|
||||
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:
|
||||
new_loc = Location(location.name, location.address, location.crystal, location.hint_text, location.parent_region)
|
||||
new_reg = ret.get_region(location.parent_region.name)
|
||||
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player)
|
||||
new_loc = Location(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg)
|
||||
# todo: this is potentially dangerous. later refactor so we
|
||||
# can apply dynamic region rules on top of copied world like other rules
|
||||
new_loc.access_rule = location.access_rule
|
||||
new_loc.always_allow = location.always_allow
|
||||
new_loc.item_rule = location.item_rule
|
||||
new_reg.locations.append(new_loc)
|
||||
|
||||
ret.clear_location_cache()
|
||||
|
||||
|
||||
def create_playthrough(world):
|
||||
|
@ -228,12 +346,8 @@ def create_playthrough(world):
|
|||
old_world = world
|
||||
world = copy_world(world)
|
||||
|
||||
# in treasure hunt and pedestal goals, ganon is invincible
|
||||
if world.goal in ['pedestal', 'triforcehunt']:
|
||||
world.get_location('Ganon').item = None
|
||||
|
||||
# if we only check for beatable, we can do this sanity check first before writing down spheres
|
||||
if world.check_beatable_only 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
|
||||
|
@ -244,8 +358,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
|
||||
|
@ -263,10 +376,11 @@ 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 at %s' % (location.item.name, location.name) for location in sphere_candidates])
|
||||
if not world.check_beatable_only:
|
||||
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 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
|
||||
|
@ -274,12 +388,10 @@ def create_playthrough(world):
|
|||
to_delete = []
|
||||
for location in sphere:
|
||||
# we remove the item at location and check if game is still beatable
|
||||
logging.getLogger('').debug('Checking if %s is required to beat the game.', location.item.name)
|
||||
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]):
|
||||
if world.can_beat_game():
|
||||
if world.can_beat_game(state_cache[num]):
|
||||
to_delete.append(location)
|
||||
else:
|
||||
# still required, got to keep it around
|
||||
|
@ -289,6 +401,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
|
||||
|
@ -299,8 +419,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))
|
||||
|
||||
|
@ -315,7 +434,7 @@ def create_playthrough(world):
|
|||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
|
||||
# store the required locations for statistical analysis
|
||||
old_world.required_locations = [location.name for sphere in collection_spheres for location in sphere]
|
||||
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
|
||||
|
||||
def flist_to_iter(node):
|
||||
while node:
|
||||
|
@ -330,9 +449,17 @@ def create_playthrough(world):
|
|||
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||
return list(pathpairs)
|
||||
|
||||
old_world.spoiler.paths = {location.name : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere}
|
||||
if any(exit == 'Pyramid Fairy' for path in old_world.spoiler.paths.values() for (_, exit) in path):
|
||||
old_world.spoiler.paths['Big Bomb Shop'] = get_path(state, world.get_region('Big Bomb Shop'))
|
||||
old_world.spoiler.paths = dict()
|
||||
for player in range(1, world.players + 1):
|
||||
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[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}
|
||||
|
|
|
@ -0,0 +1,929 @@
|
|||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
import Items
|
||||
import Regions
|
||||
|
||||
while True:
|
||||
try:
|
||||
import aioconsole
|
||||
break
|
||||
except ImportError:
|
||||
aioconsole = None
|
||||
print('Required python module "aioconsole" not found, press enter to install it')
|
||||
input()
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'aioconsole'])
|
||||
|
||||
while True:
|
||||
try:
|
||||
import websockets
|
||||
break
|
||||
except ImportError:
|
||||
websockets = None
|
||||
print('Required python module "websockets" not found, press enter to install it')
|
||||
input()
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
|
||||
|
||||
try:
|
||||
import colorama
|
||||
except ImportError:
|
||||
colorama = None
|
||||
|
||||
class ReceivedItem:
|
||||
def __init__(self, item, location, player):
|
||||
self.item = item
|
||||
self.location = location
|
||||
self.player = player
|
||||
|
||||
class Context:
|
||||
def __init__(self, snes_address, server_address, password):
|
||||
self.snes_address = snes_address
|
||||
self.server_address = server_address
|
||||
|
||||
self.exit_event = asyncio.Event()
|
||||
|
||||
self.input_queue = asyncio.Queue()
|
||||
self.input_requests = 0
|
||||
|
||||
self.snes_socket = None
|
||||
self.snes_state = SNES_DISCONNECTED
|
||||
self.snes_attached_device = None
|
||||
self.snes_reconnect_address = None
|
||||
self.snes_recv_queue = asyncio.Queue()
|
||||
self.snes_request_lock = asyncio.Lock()
|
||||
self.is_sd2snes = False
|
||||
self.snes_write_buffer = []
|
||||
|
||||
self.server_task = None
|
||||
self.socket = None
|
||||
self.password = password
|
||||
|
||||
self.team = None
|
||||
self.slot = None
|
||||
self.player_names = {}
|
||||
self.locations_checked = set()
|
||||
self.items_received = []
|
||||
self.awaiting_rom = False
|
||||
self.rom = None
|
||||
self.auth = None
|
||||
|
||||
def color_code(*args):
|
||||
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||
'magenta': 35, 'cyan': 36, 'white': 37 , 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
||||
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
||||
return '\033[' + ';'.join([str(codes[arg]) for arg in args]) + 'm'
|
||||
|
||||
def color(text, *args):
|
||||
return color_code(*args) + text + color_code('reset')
|
||||
|
||||
RECONNECT_DELAY = 30
|
||||
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
ROMNAME_START = SRAM_START + 0x2000
|
||||
ROMNAME_SIZE = 0x15
|
||||
|
||||
INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
|
||||
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
|
||||
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
||||
|
||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||
"Blind's Hideout - Left": (0x11d, 0x20),
|
||||
"Blind's Hideout - Right": (0x11d, 0x40),
|
||||
"Blind's Hideout - Far Left": (0x11d, 0x80),
|
||||
"Blind's Hideout - Far Right": (0x11d, 0x100),
|
||||
'Secret Passage': (0x55, 0x10),
|
||||
'Waterfall Fairy - Left': (0x114, 0x10),
|
||||
'Waterfall Fairy - Right': (0x114, 0x20),
|
||||
"King's Tomb": (0x113, 0x10),
|
||||
'Floodgate Chest': (0x10b, 0x10),
|
||||
"Link's House": (0x104, 0x10),
|
||||
'Kakariko Tavern': (0x103, 0x10),
|
||||
'Chicken House': (0x108, 0x10),
|
||||
"Aginah's Cave": (0x10a, 0x10),
|
||||
"Sahasrahla's Hut - Left": (0x105, 0x10),
|
||||
"Sahasrahla's Hut - Middle": (0x105, 0x20),
|
||||
"Sahasrahla's Hut - Right": (0x105, 0x40),
|
||||
'Kakariko Well - Top': (0x2f, 0x10),
|
||||
'Kakariko Well - Left': (0x2f, 0x20),
|
||||
'Kakariko Well - Middle': (0x2f, 0x40),
|
||||
'Kakariko Well - Right': (0x2f, 0x80),
|
||||
'Kakariko Well - Bottom': (0x2f, 0x100),
|
||||
'Lost Woods Hideout': (0xe1, 0x200),
|
||||
'Lumberjack Tree': (0xe2, 0x200),
|
||||
'Cave 45': (0x11b, 0x400),
|
||||
'Graveyard Cave': (0x11b, 0x200),
|
||||
'Checkerboard Cave': (0x126, 0x200),
|
||||
'Mini Moldorm Cave - Far Left': (0x123, 0x10),
|
||||
'Mini Moldorm Cave - Left': (0x123, 0x20),
|
||||
'Mini Moldorm Cave - Right': (0x123, 0x40),
|
||||
'Mini Moldorm Cave - Far Right': (0x123, 0x80),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x123, 0x400),
|
||||
'Ice Rod Cave': (0x120, 0x10),
|
||||
'Bonk Rock Cave': (0x124, 0x10),
|
||||
'Desert Palace - Big Chest': (0x73, 0x10),
|
||||
'Desert Palace - Torch': (0x73, 0x400),
|
||||
'Desert Palace - Map Chest': (0x74, 0x10),
|
||||
'Desert Palace - Compass Chest': (0x85, 0x10),
|
||||
'Desert Palace - Big Key Chest': (0x75, 0x10),
|
||||
'Desert Palace - Boss': (0x33, 0x800),
|
||||
'Eastern Palace - Compass Chest': (0xa8, 0x10),
|
||||
'Eastern Palace - Big Chest': (0xa9, 0x10),
|
||||
'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
|
||||
'Eastern Palace - Big Key Chest': (0xb8, 0x10),
|
||||
'Eastern Palace - Map Chest': (0xaa, 0x10),
|
||||
'Eastern Palace - Boss': (0xc8, 0x800),
|
||||
'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
|
||||
'Hyrule Castle - Map Chest': (0x72, 0x10),
|
||||
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
|
||||
'Sewers - Dark Cross': (0x32, 0x10),
|
||||
'Sewers - Secret Room - Left': (0x11, 0x10),
|
||||
'Sewers - Secret Room - Middle': (0x11, 0x20),
|
||||
'Sewers - Secret Room - Right': (0x11, 0x40),
|
||||
'Sanctuary': (0x12, 0x10),
|
||||
'Castle Tower - Room 03': (0xe0, 0x10),
|
||||
'Castle Tower - Dark Maze': (0xd0, 0x10),
|
||||
'Spectacle Rock Cave': (0xea, 0x400),
|
||||
'Paradox Cave Lower - Far Left': (0xef, 0x10),
|
||||
'Paradox Cave Lower - Left': (0xef, 0x20),
|
||||
'Paradox Cave Lower - Right': (0xef, 0x40),
|
||||
'Paradox Cave Lower - Far Right': (0xef, 0x80),
|
||||
'Paradox Cave Lower - Middle': (0xef, 0x100),
|
||||
'Paradox Cave Upper - Left': (0xff, 0x10),
|
||||
'Paradox Cave Upper - Right': (0xff, 0x20),
|
||||
'Spiral Cave': (0xfe, 0x10),
|
||||
'Tower of Hera - Basement Cage': (0x87, 0x400),
|
||||
'Tower of Hera - Map Chest': (0x77, 0x10),
|
||||
'Tower of Hera - Big Key Chest': (0x87, 0x10),
|
||||
'Tower of Hera - Compass Chest': (0x27, 0x20),
|
||||
'Tower of Hera - Big Chest': (0x27, 0x10),
|
||||
'Tower of Hera - Boss': (0x7, 0x800),
|
||||
'Hype Cave - Top': (0x11e, 0x10),
|
||||
'Hype Cave - Middle Right': (0x11e, 0x20),
|
||||
'Hype Cave - Middle Left': (0x11e, 0x40),
|
||||
'Hype Cave - Bottom': (0x11e, 0x80),
|
||||
'Hype Cave - Generous Guy': (0x11e, 0x400),
|
||||
'Peg Cave': (0x127, 0x400),
|
||||
'Pyramid Fairy - Left': (0x116, 0x10),
|
||||
'Pyramid Fairy - Right': (0x116, 0x20),
|
||||
'Brewery': (0x106, 0x10),
|
||||
'C-Shaped House': (0x11c, 0x10),
|
||||
'Chest Game': (0x106, 0x400),
|
||||
'Mire Shed - Left': (0x10d, 0x10),
|
||||
'Mire Shed - Right': (0x10d, 0x20),
|
||||
'Superbunny Cave - Top': (0xf8, 0x10),
|
||||
'Superbunny Cave - Bottom': (0xf8, 0x20),
|
||||
'Spike Cave': (0x117, 0x10),
|
||||
'Hookshot Cave - Top Right': (0x3c, 0x10),
|
||||
'Hookshot Cave - Top Left': (0x3c, 0x20),
|
||||
'Hookshot Cave - Bottom Right': (0x3c, 0x80),
|
||||
'Hookshot Cave - Bottom Left': (0x3c, 0x40),
|
||||
'Mimic Cave': (0x10c, 0x10),
|
||||
'Swamp Palace - Entrance': (0x28, 0x10),
|
||||
'Swamp Palace - Map Chest': (0x37, 0x10),
|
||||
'Swamp Palace - Big Chest': (0x36, 0x10),
|
||||
'Swamp Palace - Compass Chest': (0x46, 0x10),
|
||||
'Swamp Palace - Big Key Chest': (0x35, 0x10),
|
||||
'Swamp Palace - West Chest': (0x34, 0x10),
|
||||
'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
|
||||
'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
|
||||
'Swamp Palace - Waterfall Room': (0x66, 0x10),
|
||||
'Swamp Palace - Boss': (0x6, 0x800),
|
||||
"Thieves' Town - Big Key Chest": (0xdb, 0x20),
|
||||
"Thieves' Town - Map Chest": (0xdb, 0x10),
|
||||
"Thieves' Town - Compass Chest": (0xdc, 0x10),
|
||||
"Thieves' Town - Ambush Chest": (0xcb, 0x10),
|
||||
"Thieves' Town - Attic": (0x65, 0x10),
|
||||
"Thieves' Town - Big Chest": (0x44, 0x10),
|
||||
"Thieves' Town - Blind's Cell": (0x45, 0x10),
|
||||
"Thieves' Town - Boss": (0xac, 0x800),
|
||||
'Skull Woods - Compass Chest': (0x67, 0x10),
|
||||
'Skull Woods - Map Chest': (0x58, 0x20),
|
||||
'Skull Woods - Big Chest': (0x58, 0x10),
|
||||
'Skull Woods - Pot Prison': (0x57, 0x20),
|
||||
'Skull Woods - Pinball Room': (0x68, 0x10),
|
||||
'Skull Woods - Big Key Chest': (0x57, 0x10),
|
||||
'Skull Woods - Bridge Room': (0x59, 0x10),
|
||||
'Skull Woods - Boss': (0x29, 0x800),
|
||||
'Ice Palace - Compass Chest': (0x2e, 0x10),
|
||||
'Ice Palace - Freezor Chest': (0x7e, 0x10),
|
||||
'Ice Palace - Big Chest': (0x9e, 0x10),
|
||||
'Ice Palace - Iced T Room': (0xae, 0x10),
|
||||
'Ice Palace - Spike Room': (0x5f, 0x10),
|
||||
'Ice Palace - Big Key Chest': (0x1f, 0x10),
|
||||
'Ice Palace - Map Chest': (0x3f, 0x10),
|
||||
'Ice Palace - Boss': (0xde, 0x800),
|
||||
'Misery Mire - Big Chest': (0xc3, 0x10),
|
||||
'Misery Mire - Map Chest': (0xc3, 0x20),
|
||||
'Misery Mire - Main Lobby': (0xc2, 0x10),
|
||||
'Misery Mire - Bridge Chest': (0xa2, 0x10),
|
||||
'Misery Mire - Spike Chest': (0xb3, 0x10),
|
||||
'Misery Mire - Compass Chest': (0xc1, 0x10),
|
||||
'Misery Mire - Big Key Chest': (0xd1, 0x10),
|
||||
'Misery Mire - Boss': (0x90, 0x800),
|
||||
'Turtle Rock - Compass Chest': (0xd6, 0x10),
|
||||
'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
|
||||
'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
|
||||
'Turtle Rock - Chain Chomps': (0xb6, 0x10),
|
||||
'Turtle Rock - Big Key Chest': (0x14, 0x10),
|
||||
'Turtle Rock - Big Chest': (0x24, 0x10),
|
||||
'Turtle Rock - Crystaroller Room': (0x4, 0x10),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10),
|
||||
'Turtle Rock - Boss': (0xa4, 0x800),
|
||||
'Palace of Darkness - Shooter Room': (0x9, 0x10),
|
||||
'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20),
|
||||
'Palace of Darkness - Stalfos Basement': (0xa, 0x10),
|
||||
'Palace of Darkness - Big Key Chest': (0x3a, 0x10),
|
||||
'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10),
|
||||
'Palace of Darkness - Map Chest': (0x2b, 0x10),
|
||||
'Palace of Darkness - Compass Chest': (0x1a, 0x20),
|
||||
'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10),
|
||||
'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20),
|
||||
'Palace of Darkness - Dark Maze - Top': (0x19, 0x10),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20),
|
||||
'Palace of Darkness - Big Chest': (0x1a, 0x10),
|
||||
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
|
||||
'Palace of Darkness - Boss': (0x5a, 0x800),
|
||||
"Ganons Tower - Bob's Torch": (0x8c, 0x400),
|
||||
'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
|
||||
'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
|
||||
'Ganons Tower - Tile Room': (0x8d, 0x10),
|
||||
'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10),
|
||||
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
|
||||
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
|
||||
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
|
||||
'Ganons Tower - Map Chest': (0x8b, 0x10),
|
||||
'Ganons Tower - Firesnake Room': (0x7d, 0x10),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80),
|
||||
"Ganons Tower - Bob's Chest": (0x8c, 0x80),
|
||||
'Ganons Tower - Big Chest': (0x8c, 0x10),
|
||||
'Ganons Tower - Big Key Room - Left': (0x1c, 0x20),
|
||||
'Ganons Tower - Big Key Room - Right': (0x1c, 0x40),
|
||||
'Ganons Tower - Big Key Chest': (0x1c, 0x10),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
||||
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
||||
location_table_npc = {'Mushroom': 0x1000,
|
||||
'King Zora': 0x2,
|
||||
'Sahasrahla': 0x10,
|
||||
'Blacksmith': 0x400,
|
||||
'Magic Bat': 0x8000,
|
||||
'Sick Kid': 0x4,
|
||||
'Library': 0x80,
|
||||
'Potion Shop': 0x2000,
|
||||
'Old Man': 0x1,
|
||||
'Ether Tablet': 0x100,
|
||||
'Catfish': 0x20,
|
||||
'Stumpy': 0x8,
|
||||
'Bombos Tablet': 0x200}
|
||||
location_table_ow = {'Flute Spot': 0x2a,
|
||||
'Sunken Treasure': 0x3b,
|
||||
"Zora's Ledge": 0x81,
|
||||
'Lake Hylia Island': 0x35,
|
||||
'Maze Race': 0x28,
|
||||
'Desert Ledge': 0x30,
|
||||
'Master Sword Pedestal': 0x80,
|
||||
'Spectacle Rock': 0x3,
|
||||
'Pyramid': 0x5b,
|
||||
'Digging Game': 0x68,
|
||||
'Bumper Cave Ledge': 0x4a,
|
||||
'Floating Island': 0x5}
|
||||
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
|
||||
'Purple Chest': (0x3c9, 0x10),
|
||||
"Link's Uncle": (0x3c6, 0x1),
|
||||
'Hobo': (0x3c9, 0x1)}
|
||||
|
||||
SNES_DISCONNECTED = 0
|
||||
SNES_CONNECTING = 1
|
||||
SNES_CONNECTED = 2
|
||||
SNES_ATTACHED = 3
|
||||
|
||||
async def snes_connect(ctx : Context, address):
|
||||
if ctx.snes_socket is not None:
|
||||
print('Already connected to snes')
|
||||
return
|
||||
|
||||
ctx.snes_state = SNES_CONNECTING
|
||||
recv_task = None
|
||||
|
||||
address = f"ws://{address}" if "://" not in address else address
|
||||
|
||||
print("Connecting to QUsb2snes at %s ..." % address)
|
||||
|
||||
try:
|
||||
ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
|
||||
ctx.snes_state = SNES_CONNECTED
|
||||
|
||||
DeviceList_Request = {
|
||||
"Opcode" : "DeviceList",
|
||||
"Space" : "SNES"
|
||||
}
|
||||
await ctx.snes_socket.send(json.dumps(DeviceList_Request))
|
||||
|
||||
reply = json.loads(await ctx.snes_socket.recv())
|
||||
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
|
||||
|
||||
if not devices:
|
||||
raise Exception('No device found')
|
||||
|
||||
print("Available devices:")
|
||||
for id, device in enumerate(devices):
|
||||
print("[%d] %s" % (id + 1, device))
|
||||
|
||||
device = None
|
||||
if len(devices) == 1:
|
||||
device = devices[0]
|
||||
elif ctx.snes_reconnect_address:
|
||||
if ctx.snes_attached_device[1] in devices:
|
||||
device = ctx.snes_attached_device[1]
|
||||
else:
|
||||
device = devices[ctx.snes_attached_device[0]]
|
||||
else:
|
||||
while True:
|
||||
print("Select a device:")
|
||||
choice = await console_input(ctx)
|
||||
if choice is None:
|
||||
raise Exception('Abort input')
|
||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
|
||||
print("Invalid choice (%s)" % choice)
|
||||
continue
|
||||
|
||||
device = devices[int(choice) - 1]
|
||||
break
|
||||
|
||||
print("Attaching to " + device)
|
||||
|
||||
Attach_Request = {
|
||||
"Opcode" : "Attach",
|
||||
"Space" : "SNES",
|
||||
"Operands" : [device]
|
||||
}
|
||||
await ctx.snes_socket.send(json.dumps(Attach_Request))
|
||||
ctx.snes_state = SNES_ATTACHED
|
||||
ctx.snes_attached_device = (devices.index(device), device)
|
||||
|
||||
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
||||
print("SD2SNES Detected")
|
||||
ctx.is_sd2snes = True
|
||||
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
||||
reply = json.loads(await ctx.snes_socket.recv())
|
||||
if reply and 'Results' in reply:
|
||||
print(reply['Results'])
|
||||
else:
|
||||
ctx.is_sd2snes = False
|
||||
|
||||
ctx.snes_reconnect_address = address
|
||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||
|
||||
except Exception as e:
|
||||
if recv_task is not None:
|
||||
if not ctx.snes_socket.closed:
|
||||
await ctx.snes_socket.close()
|
||||
else:
|
||||
if ctx.snes_socket is not None:
|
||||
if not ctx.snes_socket.closed:
|
||||
await ctx.snes_socket.close()
|
||||
ctx.snes_socket = None
|
||||
ctx.snes_state = SNES_DISCONNECTED
|
||||
if not ctx.snes_reconnect_address:
|
||||
print("Error connecting to snes (%s)" % e)
|
||||
else:
|
||||
print(f"Error connecting to snes, attempt again in {RECONNECT_DELAY}s")
|
||||
asyncio.create_task(snes_autoreconnect(ctx))
|
||||
|
||||
async def snes_autoreconnect(ctx: Context):
|
||||
await asyncio.sleep(RECONNECT_DELAY)
|
||||
if ctx.snes_reconnect_address and ctx.snes_socket is None:
|
||||
await snes_connect(ctx, ctx.snes_reconnect_address)
|
||||
|
||||
async def snes_recv_loop(ctx : Context):
|
||||
try:
|
||||
async for msg in ctx.snes_socket:
|
||||
ctx.snes_recv_queue.put_nowait(msg)
|
||||
print("Snes disconnected")
|
||||
except Exception as e:
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logging.exception(e)
|
||||
print("Lost connection to the snes, type /snes to reconnect")
|
||||
finally:
|
||||
socket, ctx.snes_socket = ctx.snes_socket, None
|
||||
if socket is not None and not socket.closed:
|
||||
await socket.close()
|
||||
|
||||
ctx.snes_state = SNES_DISCONNECTED
|
||||
ctx.snes_recv_queue = asyncio.Queue()
|
||||
ctx.hud_message_queue = []
|
||||
|
||||
ctx.rom = None
|
||||
|
||||
if ctx.snes_reconnect_address:
|
||||
print(f"...reconnecting in {RECONNECT_DELAY}s")
|
||||
asyncio.create_task(snes_autoreconnect(ctx))
|
||||
|
||||
async def snes_read(ctx : Context, address, size):
|
||||
try:
|
||||
await ctx.snes_request_lock.acquire()
|
||||
|
||||
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
|
||||
return None
|
||||
|
||||
GetAddress_Request = {
|
||||
"Opcode" : "GetAddress",
|
||||
"Space" : "SNES",
|
||||
"Operands" : [hex(address)[2:], hex(size)[2:]]
|
||||
}
|
||||
try:
|
||||
await ctx.snes_socket.send(json.dumps(GetAddress_Request))
|
||||
except websockets.ConnectionClosed:
|
||||
return None
|
||||
|
||||
data = bytes()
|
||||
while len(data) < size:
|
||||
try:
|
||||
data += await asyncio.wait_for(ctx.snes_recv_queue.get(), 5)
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
|
||||
if len(data) != size:
|
||||
print('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
||||
if len(data):
|
||||
print(str(data))
|
||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||
await ctx.snes_socket.close()
|
||||
return None
|
||||
|
||||
return data
|
||||
finally:
|
||||
ctx.snes_request_lock.release()
|
||||
|
||||
async def snes_write(ctx : Context, write_list):
|
||||
try:
|
||||
await ctx.snes_request_lock.acquire()
|
||||
|
||||
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
|
||||
return False
|
||||
|
||||
PutAddress_Request = {
|
||||
"Opcode" : "PutAddress",
|
||||
"Operands" : []
|
||||
}
|
||||
|
||||
if ctx.is_sd2snes:
|
||||
cmd = b'\x00\xE2\x20\x48\xEB\x48'
|
||||
|
||||
for address, data in write_list:
|
||||
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
||||
print("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
|
||||
return False
|
||||
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
|
||||
cmd += b'\xA9' # LDA
|
||||
cmd += bytes([byte])
|
||||
cmd += b'\x8F' # STA.l
|
||||
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
|
||||
|
||||
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
|
||||
|
||||
PutAddress_Request['Space'] = 'CMD'
|
||||
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd)-1)[2:], "2C00", "1"]
|
||||
try:
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(cmd)
|
||||
except websockets.ConnectionClosed:
|
||||
return False
|
||||
else:
|
||||
PutAddress_Request['Space'] = 'SNES'
|
||||
try:
|
||||
#will pack those requests as soon as qusb2snes actually supports that for real
|
||||
for address, data in write_list:
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(data)
|
||||
except websockets.ConnectionClosed:
|
||||
return False
|
||||
|
||||
return True
|
||||
finally:
|
||||
ctx.snes_request_lock.release()
|
||||
|
||||
def snes_buffered_write(ctx : Context, address, data):
|
||||
if len(ctx.snes_write_buffer) > 0 and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address:
|
||||
ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data)
|
||||
else:
|
||||
ctx.snes_write_buffer.append((address, data))
|
||||
|
||||
async def snes_flush_writes(ctx : Context):
|
||||
if not ctx.snes_write_buffer:
|
||||
return
|
||||
|
||||
await snes_write(ctx, ctx.snes_write_buffer)
|
||||
ctx.snes_write_buffer = []
|
||||
|
||||
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
|
||||
|
||||
async def server_loop(ctx : Context, address = None):
|
||||
if ctx.socket is not None:
|
||||
print('Already connected')
|
||||
return
|
||||
|
||||
if address is None:
|
||||
address = ctx.server_address
|
||||
|
||||
while not address:
|
||||
print('Enter multiworld server address')
|
||||
address = await console_input(ctx)
|
||||
|
||||
address = f"ws://{address}" if "://" not in address else address
|
||||
port = urllib.parse.urlparse(address).port or 38281
|
||||
|
||||
print('Connecting to multiworld server at %s' % address)
|
||||
try:
|
||||
ctx.socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
||||
print('Connected')
|
||||
ctx.server_address = address
|
||||
|
||||
async for data in ctx.socket:
|
||||
for msg in json.loads(data):
|
||||
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
|
||||
await process_server_cmd(ctx, cmd, args)
|
||||
print('Disconnected from multiworld server, type /connect to reconnect')
|
||||
except ConnectionRefusedError:
|
||||
print('Connection refused by the multiworld server')
|
||||
except (OSError, websockets.InvalidURI):
|
||||
print('Failed to connect to the multiworld server')
|
||||
except Exception as e:
|
||||
print('Lost connection to the multiworld server, type /connect to reconnect')
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logging.exception(e)
|
||||
finally:
|
||||
ctx.awaiting_rom = False
|
||||
ctx.auth = None
|
||||
ctx.items_received = []
|
||||
socket, ctx.socket = ctx.socket, None
|
||||
if socket is not None and not socket.closed:
|
||||
await socket.close()
|
||||
ctx.server_task = None
|
||||
if ctx.server_address:
|
||||
print(f"... reconnecting in {RECONNECT_DELAY}s")
|
||||
asyncio.create_task(server_autoreconnect(ctx))
|
||||
|
||||
async def server_autoreconnect(ctx: Context):
|
||||
await asyncio.sleep(RECONNECT_DELAY)
|
||||
if ctx.server_address and ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
||||
|
||||
async def process_server_cmd(ctx : Context, cmd, args):
|
||||
if cmd == 'RoomInfo':
|
||||
print('--------------------------------')
|
||||
print('Room Information:')
|
||||
print('--------------------------------')
|
||||
if args['password']:
|
||||
print('Password required')
|
||||
if len(args['players']) < 1:
|
||||
print('No player connected')
|
||||
else:
|
||||
args['players'].sort()
|
||||
current_team = 0
|
||||
print('Connected players:')
|
||||
print(' Team #1')
|
||||
for team, slot, name in args['players']:
|
||||
if team != current_team:
|
||||
print(f' Team #{team + 1}')
|
||||
current_team = team
|
||||
print(' %s (Player %d)' % (name, slot))
|
||||
await server_auth(ctx, args['password'])
|
||||
|
||||
if cmd == 'ConnectionRefused':
|
||||
if 'InvalidPassword' in args:
|
||||
print('Invalid password')
|
||||
ctx.password = None
|
||||
await server_auth(ctx, True)
|
||||
if 'InvalidRom' in args:
|
||||
raise Exception('Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes')
|
||||
if 'SlotAlreadyTaken' in args:
|
||||
raise Exception('Player slot already in use for that team')
|
||||
raise Exception('Connection refused by the multiworld host')
|
||||
|
||||
if cmd == 'Connected':
|
||||
ctx.team, ctx.slot = args[0]
|
||||
ctx.player_names = {p: n for p, n in args[1]}
|
||||
if ctx.locations_checked:
|
||||
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
|
||||
|
||||
if cmd == 'ReceivedItems':
|
||||
start_index, items = args
|
||||
if start_index == 0:
|
||||
ctx.items_received = []
|
||||
elif start_index != len(ctx.items_received):
|
||||
sync_msg = [['Sync']]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
|
||||
await send_msgs(ctx.socket, sync_msg)
|
||||
if start_index == len(ctx.items_received):
|
||||
for item in items:
|
||||
ctx.items_received.append(ReceivedItem(*item))
|
||||
|
||||
if cmd == 'ItemSent':
|
||||
player_sent, location, player_recvd, item = args
|
||||
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
|
||||
player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta')
|
||||
player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta')
|
||||
print('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location)))
|
||||
|
||||
if cmd == 'Print':
|
||||
print(args)
|
||||
|
||||
async def server_auth(ctx : Context, password_requested):
|
||||
if password_requested and not ctx.password:
|
||||
print('Enter the password required to join this game:')
|
||||
ctx.password = await console_input(ctx)
|
||||
if ctx.rom is None:
|
||||
ctx.awaiting_rom = True
|
||||
print('No ROM detected, awaiting snes connection to authenticate to the multiworld server')
|
||||
return
|
||||
ctx.awaiting_rom = False
|
||||
ctx.auth = ctx.rom.copy()
|
||||
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'rom': ctx.auth}]])
|
||||
|
||||
async def console_input(ctx : Context):
|
||||
ctx.input_requests += 1
|
||||
return await ctx.input_queue.get()
|
||||
|
||||
async def disconnect(ctx: Context):
|
||||
if ctx.socket is not None and not ctx.socket.closed:
|
||||
await ctx.socket.close()
|
||||
if ctx.server_task is not None:
|
||||
await ctx.server_task
|
||||
|
||||
async def connect(ctx: Context, address=None):
|
||||
await disconnect(ctx)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx, address))
|
||||
|
||||
async def console_loop(ctx : Context):
|
||||
while not ctx.exit_event.is_set():
|
||||
input = await aioconsole.ainput()
|
||||
|
||||
if ctx.input_requests > 0:
|
||||
ctx.input_requests -= 1
|
||||
ctx.input_queue.put_nowait(input)
|
||||
continue
|
||||
|
||||
command = shlex.split(input)
|
||||
if not command:
|
||||
continue
|
||||
|
||||
if command[0] == '/exit':
|
||||
ctx.exit_event.set()
|
||||
|
||||
if command[0] == '/installcolors' and 'colorama' not in sys.modules:
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'colorama'])
|
||||
global colorama
|
||||
import colorama
|
||||
colorama.init()
|
||||
|
||||
if command[0] == '/snes':
|
||||
ctx.snes_reconnect_address = None
|
||||
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else ctx.snes_address))
|
||||
if command[0] in ['/snes_close', '/snes_quit']:
|
||||
ctx.snes_reconnect_address = None
|
||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||
await ctx.snes_socket.close()
|
||||
|
||||
if command[0] in ['/connect', '/reconnect']:
|
||||
ctx.server_address = None
|
||||
asyncio.create_task(connect(ctx, command[1] if len(command) > 1 else None))
|
||||
if command[0] == '/disconnect':
|
||||
ctx.server_address = None
|
||||
asyncio.create_task(disconnect(ctx))
|
||||
if command[0][:1] != '/':
|
||||
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
|
||||
|
||||
if command[0] == '/received':
|
||||
print('Received items:')
|
||||
for index, item in enumerate(ctx.items_received, 1):
|
||||
print('%s from %s (%s) (%d/%d in list)' % (
|
||||
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||
get_location_name_from_address(item.location), index, len(ctx.items_received)))
|
||||
|
||||
if command[0] == '/missing':
|
||||
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
|
||||
if location not in ctx.locations_checked:
|
||||
print('Missing: ' + location)
|
||||
if command[0] == '/getitem' and len(command) > 1:
|
||||
item = input[9:]
|
||||
item_id = Items.item_table[item][3] if item in Items.item_table else None
|
||||
if type(item_id) is int and item_id in range(0x100):
|
||||
print('Sending item: ' + item)
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
|
||||
else:
|
||||
print('Invalid item: ' + item)
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
def get_item_name_from_id(code):
|
||||
items = [k for k, i in Items.item_table.items() if type(i[3]) is int and i[3] == code]
|
||||
return items[0] if items else 'Unknown item'
|
||||
|
||||
def get_location_name_from_address(address):
|
||||
if type(address) is str:
|
||||
return address
|
||||
|
||||
locs = [k for k, l in Regions.location_table.items() if type(l[0]) is int and l[0] == address]
|
||||
return locs[0] if locs else 'Unknown location'
|
||||
|
||||
async def track_locations(ctx : Context, roomid, roomdata):
|
||||
new_locations = []
|
||||
def new_check(location):
|
||||
ctx.locations_checked.add(location)
|
||||
print("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
|
||||
new_locations.append(Regions.location_table[location][0])
|
||||
|
||||
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
||||
if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
|
||||
new_check(location)
|
||||
|
||||
uw_begin = 0x129
|
||||
uw_end = 0
|
||||
uw_unchecked = {}
|
||||
for location, (roomid, mask) in location_table_uw.items():
|
||||
if location not in ctx.locations_checked:
|
||||
uw_unchecked[location] = (roomid, mask)
|
||||
uw_begin = min(uw_begin, roomid)
|
||||
uw_end = max(uw_end, roomid + 1)
|
||||
if uw_begin < uw_end:
|
||||
uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2)
|
||||
if uw_data is not None:
|
||||
for location, (roomid, mask) in uw_unchecked.items():
|
||||
offset = (roomid - uw_begin) * 2
|
||||
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
|
||||
if roomdata & mask != 0:
|
||||
new_check(location)
|
||||
|
||||
ow_begin = 0x82
|
||||
ow_end = 0
|
||||
ow_unchecked = {}
|
||||
for location, screenid in location_table_ow.items():
|
||||
if location not in ctx.locations_checked:
|
||||
ow_unchecked[location] = screenid
|
||||
ow_begin = min(ow_begin, screenid)
|
||||
ow_end = max(ow_end, screenid + 1)
|
||||
if ow_begin < ow_end:
|
||||
ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin)
|
||||
if ow_data is not None:
|
||||
for location, screenid in ow_unchecked.items():
|
||||
if ow_data[screenid - ow_begin] & 0x40 != 0:
|
||||
new_check(location)
|
||||
|
||||
if not all([location in ctx.locations_checked for location in location_table_npc.keys()]):
|
||||
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
|
||||
if npc_data is not None:
|
||||
npc_value = npc_data[0] | (npc_data[1] << 8)
|
||||
for location, mask in location_table_npc.items():
|
||||
if npc_value & mask != 0 and location not in ctx.locations_checked:
|
||||
new_check(location)
|
||||
|
||||
if not all([location in ctx.locations_checked for location in location_table_misc.keys()]):
|
||||
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
|
||||
if misc_data is not None:
|
||||
for location, (offset, mask) in location_table_misc.items():
|
||||
assert(0x3c6 <= offset <= 0x3c9)
|
||||
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked:
|
||||
new_check(location)
|
||||
|
||||
await send_msgs(ctx.socket, [['LocationChecks', new_locations]])
|
||||
|
||||
async def game_watcher(ctx : Context):
|
||||
while not ctx.exit_event.is_set():
|
||||
await asyncio.sleep(2)
|
||||
|
||||
if not ctx.rom:
|
||||
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
|
||||
continue
|
||||
|
||||
ctx.rom = list(rom)
|
||||
ctx.locations_checked = set()
|
||||
if ctx.awaiting_rom:
|
||||
await server_auth(ctx, False)
|
||||
|
||||
if ctx.auth and ctx.auth != ctx.rom:
|
||||
print("ROM change detected, please reconnect to the multiworld server")
|
||||
await disconnect(ctx)
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if gamemode is None or gamemode[0] not in INGAME_MODES:
|
||||
continue
|
||||
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
assert(RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2)
|
||||
recv_item = data[2]
|
||||
assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4)
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6)
|
||||
roomdata = data[6]
|
||||
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
print('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
|
||||
recv_index += 1
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
|
||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = Context(args.snes, args.connect, args.password)
|
||||
|
||||
input_task = asyncio.create_task(console_loop(ctx))
|
||||
|
||||
await snes_connect(ctx, ctx.snes_address)
|
||||
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
||||
|
||||
watcher_task = asyncio.create_task(game_watcher(ctx))
|
||||
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
ctx.snes_reconnect_address = None
|
||||
|
||||
await watcher_task
|
||||
|
||||
if ctx.socket is not None and not ctx.socket.closed:
|
||||
await ctx.socket.close()
|
||||
if ctx.server_task is not None:
|
||||
await ctx.server_task
|
||||
|
||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||
await ctx.snes_socket.close()
|
||||
|
||||
while ctx.input_requests > 0:
|
||||
ctx.input_queue.put_nowait(None)
|
||||
ctx.input_requests -= 1
|
||||
|
||||
await input_task
|
||||
|
||||
if __name__ == '__main__':
|
||||
if 'colorama' in sys.modules:
|
||||
colorama.init()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
|
||||
loop.close()
|
||||
|
||||
if 'colorama' in sys.modules:
|
||||
colorama.deinit()
|
|
@ -0,0 +1,124 @@
|
|||
__author__ = "Berserker55" # you can find me on the ALTTP Randomizer Discord
|
||||
__version__ = 1.5
|
||||
|
||||
"""
|
||||
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
|
||||
This script itself should be placed within the Bonta Multiworld folder, that you download in step 1
|
||||
"""
|
||||
|
||||
####config####
|
||||
#location of your Enemizer CLI, available here: https://github.com/Bonta0/Enemizer/releases
|
||||
enemizer_location:str = "EnemizerCLI/EnemizerCLI.Core.exe"
|
||||
|
||||
#Where to place the resulting files
|
||||
outputpath:str = "MultiMystery"
|
||||
|
||||
#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:str = ""
|
||||
|
||||
#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:int = 1
|
||||
|
||||
#create a spoiler file
|
||||
create_spoiler:bool = True
|
||||
|
||||
#create roms as race coms
|
||||
race:bool= False
|
||||
|
||||
#folder from which the player yaml files are pulled from
|
||||
player_files_folder:str = "Players"
|
||||
|
||||
#Version of python to use for Bonta Multiworld. Probably leave this as is, if you don't know what this does.
|
||||
#can be tagged for bitness, for example "3.8-32" would be latest installed 3.8 on 32 bits
|
||||
py_version:str = "3.7"
|
||||
####end of config####
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def feedback(text:str):
|
||||
print(text)
|
||||
input("Press Enter to ignore and probably crash.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"{__author__}'s MultiMystery Launcher V{__version__}")
|
||||
if not os.path.exists(enemizer_location):
|
||||
feedback(f"Enemizer not found at {enemizer_location}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
|
||||
if not os.path.exists("Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"):
|
||||
feedback("Base rom is expected as Zelda no Densetsu - Kamigami no Triforce (Japan).sfc in the Multiworld root folder please place/rename it there.")
|
||||
player_files = []
|
||||
os.makedirs(player_files_folder, exist_ok=True)
|
||||
for file in os.listdir(player_files_folder):
|
||||
if file.lower().endswith(".yaml"):
|
||||
player_files.append(file)
|
||||
print(f"Player {file[:-5]} found.")
|
||||
player_count = len(player_files)
|
||||
if player_count == 0:
|
||||
feedback(f"No player files found. Please put them in a {player_files_folder} folder.")
|
||||
else:
|
||||
print(player_count, "Players found.")
|
||||
|
||||
player_string = ""
|
||||
for i,file in enumerate(player_files):
|
||||
player_string += f"--p{i+1} {os.path.join(player_files_folder, file)} "
|
||||
|
||||
player_names = list(file[:-5] for file in player_files)
|
||||
|
||||
command = f"py -{py_version} Mystery.py --multi {len(player_files)} {player_string} " \
|
||||
f"--names {','.join(player_names)} --enemizercli {enemizer_location} " \
|
||||
f"--outputpath {outputpath}" + " --create_spoiler" if create_spoiler else "" + " --race" if race else ""
|
||||
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 seed.")
|
||||
seedname = ""
|
||||
|
||||
for segment in text.split():
|
||||
if segment.startswith("M"):
|
||||
seedname = segment
|
||||
break
|
||||
|
||||
multidataname = f"ER_{seedname}_multidata"
|
||||
|
||||
romfilename = ""
|
||||
if player_name:
|
||||
try:
|
||||
index = player_names.index(player_name)
|
||||
except IndexError:
|
||||
print(f"Could not find Player {player_name}")
|
||||
else:
|
||||
romfilename = os.path.join(outputpath, f"ER_{seedname}_P{index+1}_{player_name}.sfc")
|
||||
import webbrowser
|
||||
if os.path.exists(romfilename):
|
||||
print(f"Launching ROM file {romfilename}")
|
||||
webbrowser.open(romfilename)
|
||||
|
||||
if zip_roms:
|
||||
zipname = os.path.join(outputpath, f"ER_{seedname}.zip")
|
||||
print(f"Creating zipfile {zipname}")
|
||||
import zipfile
|
||||
with zipfile.ZipFile(zipname, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for file in os.listdir(outputpath):
|
||||
if file.endswith(".sfc") and seedname in file:
|
||||
zf.write(os.path.join(outputpath, file), file)
|
||||
print(f"Packed {file} into zipfile {zipname}")
|
||||
if zip_roms == 2 and player_name.lower() not in file.lower():
|
||||
os.remove(file)
|
||||
print(f"Removed file {file} that is now present in the zipfile")
|
||||
|
||||
subprocess.call(f"py -{py_version} MultiServer.py --multidata {os.path.join(outputpath, multidataname)}")
|
|
@ -314,7 +314,7 @@ async def console(ctx : Context):
|
|||
if command[0] == '/hint':
|
||||
for (team,slot), name in ctx.player_names.items():
|
||||
if len(command) == 1:
|
||||
print("Use /hint {Playername} {Itemname}\nFor example /hint Berserker Lamp")
|
||||
print("Use /hint {Playername} {itemname}\nFor example /hint Berserker Lamp")
|
||||
elif name.lower() == command[1].lower():
|
||||
item = " ".join(command[2:])
|
||||
if item in Items.item_table:
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
import argparse
|
||||
import logging
|
||||
import random
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
|
||||
def parse_yaml(txt):
|
||||
def strip(s):
|
||||
s = s.strip()
|
||||
return '' if not s else s.strip('"') if s[0] == '"' else s.strip("'") if s[0] == "'" else s
|
||||
ret = {}
|
||||
indents = {len(txt) - len(txt.lstrip(' ')): ret}
|
||||
for line in txt.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
name, val = line.split(':', 1)
|
||||
val = strip(val)
|
||||
spaces = len(name) - len(name.lstrip(' '))
|
||||
name = strip(name)
|
||||
if val:
|
||||
indents[spaces][name] = val
|
||||
else:
|
||||
newdict = {}
|
||||
indents[spaces][name] = newdict
|
||||
indents[spaces+2] = newdict
|
||||
return ret
|
||||
|
||||
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('--names', default='')
|
||||
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')
|
||||
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 = f'M{random.randint(0, 999999999)}'
|
||||
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
|
||||
|
||||
weights_cache = {}
|
||||
if args.weights:
|
||||
weights_cache[args.weights] = get_weights(args.weights)
|
||||
print(f"Weights: {args.weights} >> {weights_cache[args.weights]['description']}")
|
||||
for player in range(1, args.multi + 1):
|
||||
path = getattr(args, f'p{player}')
|
||||
if path:
|
||||
if path not in weights_cache:
|
||||
try:
|
||||
weights_cache[path] = get_weights(path)
|
||||
except:
|
||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.")
|
||||
print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}")
|
||||
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.names = args.names
|
||||
erargs.create_spoiler = args.create_spoiler
|
||||
erargs.race = args.race
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = args.outputpath
|
||||
|
||||
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()}
|
||||
|
||||
for player in range(1, args.multi + 1):
|
||||
path = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
|
||||
if path:
|
||||
try:
|
||||
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
|
||||
for k, v in vars(settings).items():
|
||||
if v is not None:
|
||||
getattr(erargs, k)[player] = v
|
||||
except:
|
||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.")
|
||||
else:
|
||||
raise RuntimeError(f'No weights specified for player {player}')
|
||||
|
||||
# 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)
|
||||
|
||||
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 roll_settings(weights):
|
||||
def get_choice(option, root=weights):
|
||||
if option not in root:
|
||||
return None
|
||||
if type(root[option]) is not dict:
|
||||
return root[option]
|
||||
if not root[option]:
|
||||
return None
|
||||
return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0]
|
||||
|
||||
ret = argparse.Namespace()
|
||||
|
||||
glitches_required = get_choice('glitches_required')
|
||||
if glitches_required not in ['none', 'no_logic']:
|
||||
print("Only NMG and No Logic supported")
|
||||
glitches_required = 'none'
|
||||
ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
|
||||
|
||||
item_placement = get_choice('item_placement')
|
||||
# not supported in ER
|
||||
|
||||
dungeon_items = get_choice('dungeon_items')
|
||||
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full']
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
|
||||
|
||||
ret.accessibility = get_choice('accessibility')
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle')
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
ret.goal = {'ganon': 'ganon',
|
||||
'fast_ganon': 'crystals',
|
||||
'dungeons': 'dungeons',
|
||||
'pedestal': 'pedestal',
|
||||
'triforce-hunt': 'triforcehunt'
|
||||
}[get_choice('goals')]
|
||||
ret.openpyramid = ret.goal == 'fast_ganon'
|
||||
|
||||
ret.crystals_gt = get_choice('tower_open')
|
||||
|
||||
ret.crystals_ganon = get_choice('ganon_open')
|
||||
|
||||
ret.mode = get_choice('world_state')
|
||||
if ret.mode == 'retro':
|
||||
ret.mode = 'open'
|
||||
ret.retro = True
|
||||
|
||||
ret.hints = get_choice('hints') == 'on'
|
||||
|
||||
ret.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[get_choice('weapons')]
|
||||
|
||||
ret.difficulty = get_choice('item_pool')
|
||||
|
||||
ret.item_functionality = get_choice('item_functionality')
|
||||
|
||||
ret.shufflebosses = {'none': 'none',
|
||||
'simple': 'basic',
|
||||
'full': 'normal',
|
||||
'random': 'chaos'
|
||||
}[get_choice('boss_shuffle')]
|
||||
|
||||
ret.shuffleenemies = {'none': 'none',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[get_choice('enemy_shuffle')]
|
||||
|
||||
ret.enemy_damage = {'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[get_choice('enemy_damage')]
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health')
|
||||
|
||||
ret.shufflepots = get_choice('pot_shuffle') == 'on'
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights else 0
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
if get_choice(item, inventoryweights) == 'on':
|
||||
startitems.append(item)
|
||||
ret.startinventory = ','.join(startitems)
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
ret.sprite = get_choice('sprite', romweights)
|
||||
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
|
||||
ret.quickswap = get_choice('quickswap', romweights) == 'on'
|
||||
ret.fastmenu = get_choice('menuspeed', romweights)
|
||||
ret.heartcolor = get_choice('heartcolor', romweights)
|
||||
ret.heartbeep = 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()
|
86
Plando.py
86
Plando.py
|
@ -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
|
||||
|
@ -19,21 +19,12 @@ from Main import create_playthrough
|
|||
|
||||
__version__ = '0.2-dev'
|
||||
|
||||
logic_hash = [182, 244, 144, 92, 149, 200, 93, 183, 124, 169, 226, 46, 111, 163, 5, 193, 13, 112, 125, 101, 128, 84, 31, 67, 107, 94, 184, 100, 189, 18, 8, 171,
|
||||
142, 57, 173, 38, 37, 211, 253, 131, 98, 239, 167, 116, 32, 186, 70, 148, 66, 151, 143, 86, 59, 83, 16, 51, 240, 152, 60, 242, 190, 117, 76, 122,
|
||||
15, 221, 62, 39, 174, 177, 223, 34, 150, 50, 178, 238, 95, 219, 10, 162, 222, 0, 165, 202, 74, 36, 206, 209, 251, 105, 175, 135, 121, 88, 214, 247,
|
||||
154, 161, 71, 19, 85, 157, 40, 96, 225, 27, 230, 49, 231, 207, 64, 35, 249, 134, 132, 108, 63, 24, 4, 127, 255, 14, 145, 23, 81, 216, 113, 90, 194,
|
||||
110, 65, 229, 43, 1, 11, 168, 147, 103, 156, 77, 80, 220, 28, 227, 213, 198, 172, 79, 75, 140, 44, 146, 188, 17, 6, 102, 56, 235, 166, 89, 218, 246,
|
||||
99, 78, 187, 126, 119, 196, 69, 137, 181, 55, 20, 215, 199, 130, 9, 45, 58, 185, 91, 33, 197, 72, 115, 195, 114, 29, 30, 233, 141, 129, 155, 159, 47,
|
||||
224, 236, 21, 234, 191, 136, 104, 87, 106, 26, 73, 250, 248, 228, 48, 53, 243, 237, 241, 61, 180, 12, 208, 245, 232, 192, 2, 7, 170, 123, 176, 160, 201,
|
||||
153, 217, 252, 158, 25, 205, 22, 133, 254, 138, 203, 118, 210, 204, 82, 97, 52, 164, 68, 139, 120, 109, 54, 3, 41, 179, 212, 42]
|
||||
|
||||
|
||||
def main(args):
|
||||
start_time = time.clock()
|
||||
start_time = time.process_time()
|
||||
|
||||
# initialize the world
|
||||
world = World('vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None)
|
||||
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()
|
||||
|
@ -46,16 +37,16 @@ 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)
|
||||
create_dungeons(world)
|
||||
create_regions(world, 1)
|
||||
create_dungeons(world, 1)
|
||||
|
||||
link_entrances(world)
|
||||
link_entrances(world, 1)
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
set_rules(world)
|
||||
set_rules(world, 1)
|
||||
|
||||
logger.info('Fill the world.')
|
||||
|
||||
|
@ -63,8 +54,8 @@ def main(args):
|
|||
|
||||
fill_world(world, args.plando, text_patches)
|
||||
|
||||
if world.get_entrance('Dam').connected_region.name != 'Dam' or world.get_entrance('Swamp Palace').connected_region.name != 'Swamp Palace (Entrance)':
|
||||
world.swamp_patch_required = True
|
||||
if world.get_entrance('Dam', 1).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', 1).connected_region.name != 'Swamp Palace (Entrance)':
|
||||
world.swamp_patch_required[1] = True
|
||||
|
||||
logger.info('Calculating playthrough.')
|
||||
|
||||
|
@ -78,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, rom, logic_hash, 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':
|
||||
|
@ -99,7 +87,7 @@ def main(args):
|
|||
world.spoiler.to_file('%s_Spoiler.txt' % outfilebase)
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
logger.debug('Total Time: %s', time.clock() - start_time)
|
||||
logger.debug('Total Time: %s', time.process_time() - start_time)
|
||||
|
||||
return world
|
||||
|
||||
|
@ -124,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'
|
||||
|
@ -142,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'
|
||||
|
@ -174,33 +162,33 @@ def fill_world(world, plando, text_patches):
|
|||
continue
|
||||
|
||||
locationstr, itemstr = line.split(':', 1)
|
||||
location = world.get_location(locationstr.strip())
|
||||
location = world.get_location(locationstr.strip(), 1)
|
||||
if location is None:
|
||||
logger.warning('Unknown location: %s', locationstr)
|
||||
continue
|
||||
else:
|
||||
item = ItemFactory(itemstr.strip())
|
||||
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)
|
||||
connect_two_way(world, entrance.strip(), exit.strip())
|
||||
connect_two_way(world, entrance.strip(), exit.strip(), 1)
|
||||
elif '=>' in line:
|
||||
entrance, exit = line.split('=>', 1)
|
||||
connect_entrance(world, entrance.strip(), exit.strip())
|
||||
connect_entrance(world, entrance.strip(), exit.strip(), 1)
|
||||
elif '<=' in line:
|
||||
entrance, exit = line.split('<=', 1)
|
||||
connect_exit(world, exit.strip(), entrance.strip())
|
||||
connect_exit(world, exit.strip(), entrance.strip(), 1)
|
||||
|
||||
world.required_medallions = (mm_medallion, tr_medallion)
|
||||
world.required_medallions[1] = (mm_medallion, tr_medallion)
|
||||
|
||||
# set up Agahnim Events
|
||||
world.get_location('Agahnim 1').event = True
|
||||
world.get_location('Agahnim 1').item = ItemFactory('Beat Agahnim 1')
|
||||
world.get_location('Agahnim 2').event = True
|
||||
world.get_location('Agahnim 2').item = ItemFactory('Beat Agahnim 2')
|
||||
world.get_location('Agahnim 1', 1).event = True
|
||||
world.get_location('Agahnim 1', 1).item = ItemFactory('Beat Agahnim 1', 1)
|
||||
world.get_location('Agahnim 2', 1).event = True
|
||||
world.get_location('Agahnim 2', 1).item = ItemFactory('Beat Agahnim 2', 1)
|
||||
|
||||
|
||||
def start():
|
||||
|
@ -221,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()
|
||||
|
@ -232,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
|
||||
|
|
62
README.md
62
README.md
|
@ -43,6 +43,16 @@ Special notes:
|
|||
- 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.
|
||||
|
||||
|
@ -85,12 +95,6 @@ This is only noticeably different if the the Ganon shuffle option is enabled.
|
|||
|
||||
## Game Difficulty
|
||||
|
||||
### Easy
|
||||
|
||||
This setting doubles the number of swords, shields, armors, bottles, and silver arrows in the item pool.
|
||||
This setting will also triple the number of Lamps available, and all will be obtainable before dark rooms.
|
||||
Within dungeons, the number of items found will be displayed on screen if there is no timer.
|
||||
|
||||
### Normal
|
||||
|
||||
This is the default setting that has an item pool most similar to the original
|
||||
|
@ -108,11 +112,6 @@ the player from having fairies in bottles.
|
|||
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.
|
||||
|
||||
### Insane
|
||||
|
||||
This setting is a modest step up from Expert. The main difference is that the player will never find any
|
||||
additional health.
|
||||
|
||||
## Timer Setting
|
||||
|
||||
### None
|
||||
|
@ -122,7 +121,7 @@ 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.
|
||||
This will prevent the dungeon item count feature in Easy and Compass shuffle from working.
|
||||
|
||||
### Timed
|
||||
|
||||
|
@ -265,12 +264,12 @@ generate spoilers for statistical analysis.
|
|||
|
||||
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
|
||||
## Map/Compass/Small Key/Big Key shuffle (aka 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
|
||||
These settings allow dungeon specific items 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
|
||||
|
@ -288,10 +287,6 @@ In further concert with the Bow changes, all arrows under pots, in chests, and e
|
|||
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.
|
||||
|
||||
## Only Ensure Seed Beatable
|
||||
|
||||
If set, will only ensure the goal can be achieved, but not necessarily that all locations are reachable. Currently only affects VT25, VT26 and balanced algorithms.
|
||||
|
||||
## 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.
|
||||
|
@ -336,7 +331,7 @@ Output a Spoiler File (default: False)
|
|||
Select the game logic (default: noglitches)
|
||||
|
||||
```
|
||||
--mode [{standard,open,swordless}]
|
||||
--mode [{standard,open,swordless,inverted}]
|
||||
```
|
||||
|
||||
Select the game mode. (default: open)
|
||||
|
@ -348,11 +343,17 @@ Select the game mode. (default: open)
|
|||
Select the game completion goal. (default: ganon)
|
||||
|
||||
```
|
||||
--difficulty [{easy,normal,hard,expert,insane}]
|
||||
--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}]
|
||||
```
|
||||
|
@ -421,10 +422,10 @@ Alters the rate at which the menu opens and closes. (default: normal)
|
|||
Disables game music, resulting in the game sound being just the SFX. (default: False)
|
||||
|
||||
```
|
||||
--keysanity
|
||||
--mapshuffle --compassshuffle --keyshuffle --bigkeyshuffle
|
||||
```
|
||||
|
||||
Enable Keysanity (default: False)
|
||||
Respectively enable Map/Compass/SmallKey/BigKey shuffle (default: False)
|
||||
|
||||
```
|
||||
--retro
|
||||
|
@ -432,13 +433,6 @@ Enable Keysanity (default: False)
|
|||
|
||||
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}]
|
||||
```
|
||||
|
@ -458,10 +452,10 @@ Select the color of Link\'s heart meter. (default: red)
|
|||
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)
|
||||
|
||||
```
|
||||
--beatableonly
|
||||
--accessibility [{items,locations,none}]
|
||||
```
|
||||
|
||||
Enables the "Only Ensure Seed Beatable" option (default: False)
|
||||
Sets the item/location accessibility rules. (default: items)
|
||||
|
||||
```
|
||||
--hints
|
||||
|
|
1023
Regions.py
1023
Regions.py
File diff suppressed because it is too large
Load Diff
21
Text.py
21
Text.py
|
@ -25,7 +25,8 @@ Uncle_texts = [
|
|||
'Forward this message to 10 other people or this seed will be awful.',
|
||||
'I hope you like your seeds bootless and fluteless.',
|
||||
'10\n9\n8\n7\n6\n5\n4\n3\n2\n1\nGo!',
|
||||
'I\'m off to visit cousin Fritzl.'
|
||||
'I\'m off to visit cousin Fritzl.',
|
||||
'Don\'t forget to check Antlion Cave.'
|
||||
] * 2 + [
|
||||
"We're out of\nWeetabix. To\nthe store!",
|
||||
"This seed is\nbootless\nuntil boots.",
|
||||
|
@ -534,7 +535,7 @@ class MultiByteCoreTextMapper(object):
|
|||
"{INTRO}": [0x6E, 0x00, 0x77, 0x07, 0x7A, 0x03, 0x6B, 0x02, 0x67],
|
||||
"{NOTEXT}": [0x6E, 0x00, 0x6B, 0x04],
|
||||
"{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03],
|
||||
"{C:GREEN}": [0x77, 0x07],
|
||||
"{C:GREEN}": [0x77, 0x07],
|
||||
"{C:YELLOW}": [0x77, 0x02],
|
||||
}
|
||||
|
||||
|
@ -551,10 +552,10 @@ class MultiByteCoreTextMapper(object):
|
|||
linespace = wrap
|
||||
line = lines.pop(0)
|
||||
if line.startswith('{'):
|
||||
if line == '{PAGEBREAK}':
|
||||
if lineindex % 3 != 0:
|
||||
# insert a wait for keypress, unless we just did so
|
||||
outbuf.append(0x7E)
|
||||
if line == '{PAGEBREAK}':
|
||||
if lineindex % 3 != 0:
|
||||
# insert a wait for keypress, unless we just did so
|
||||
outbuf.append(0x7E)
|
||||
lineindex = 0
|
||||
outbuf.extend(cls.special_commands[line])
|
||||
continue
|
||||
|
@ -1431,6 +1432,7 @@ class TextTable(object):
|
|||
'desert_thief_question_yes',
|
||||
'desert_thief_after_item_get',
|
||||
'desert_thief_reassure',
|
||||
'pond_item_bottle_filled'
|
||||
]
|
||||
|
||||
for msg in messages_to_zero:
|
||||
|
@ -1882,5 +1884,12 @@ class TextTable(object):
|
|||
# 190
|
||||
text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("How did you get up here?")
|
||||
text['fish_money'] = CompressedTextMapper.convert("It's a secret to everyone.")
|
||||
text['sign_ganons_tower'] = CompressedTextMapper.convert("You need all 7 crystals to enter.")
|
||||
text['sign_ganon'] = CompressedTextMapper.convert("You need all 7 crystals to beat Ganon.")
|
||||
text['ganon_phase_3_no_bow'] = CompressedTextMapper.convert("You have no bow. Dingus!")
|
||||
text['ganon_phase_3_no_silvers_alt'] = CompressedTextMapper.convert("You can't best me without silver arrows!")
|
||||
text['ganon_phase_3_no_silvers'] = CompressedTextMapper.convert("You can't best me without silver arrows!")
|
||||
text['ganon_phase_3_silvers'] = CompressedTextMapper.convert("Oh no! Silver! My one true weakness!")
|
||||
text['murahdahla'] = CompressedTextMapper.convert("Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n{PAUSE3}\n… … …\nWait! you can see me? I knew I should have\nhidden in a hollow tree.")
|
||||
text['end_pad_data'] = bytearray([0xfb])
|
||||
text['terminator'] = bytearray([0xFF, 0xFF])
|
||||
|
|
21
Utils.py
21
Utils.py
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
@ -16,6 +17,18 @@ def pc_to_snes(value):
|
|||
def snes_to_pc(value):
|
||||
return ((value & 0x7F0000)>>1)|(value & 0x7FFF)
|
||||
|
||||
def parse_player_names(names, players, teams):
|
||||
names = [n for n in re.split(r'[, ]', names) if n]
|
||||
ret = []
|
||||
while names or len(ret) < teams:
|
||||
team = [n[:16] for n in names[:players]]
|
||||
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)
|
||||
|
||||
|
@ -87,14 +100,6 @@ def close_console():
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
def new_logic_array():
|
||||
import random
|
||||
l = list(range(256))
|
||||
random.SystemRandom().shuffle(l)
|
||||
chunks = [l[i:i + 16] for i in range(0, len(l), 16)]
|
||||
lines = [", ".join([str(j) for j in i]) for i in chunks]
|
||||
print("logic_hash = ["+",\n ".join(lines)+"]")
|
||||
|
||||
def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', new_rom='working.sfc'):
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
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
|
|
@ -0,0 +1,191 @@
|
|||
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.
|
|
@ -0,0 +1,55 @@
|
|||
"""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)
|
|
@ -0,0 +1,53 @@
|
|||
"""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
|
|
@ -0,0 +1,16 @@
|
|||
"""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
|
|
@ -0,0 +1,527 @@
|
|||
"""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
|
|
@ -0,0 +1,94 @@
|
|||
"""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
|
|
@ -0,0 +1,384 @@
|
|||
"""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.')
|
|
@ -0,0 +1,552 @@
|
|||
"""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
|
|
@ -24,7 +24,7 @@ exe = EXE(pyz,
|
|||
debug=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
icon='data/ER.ico',
|
||||
icon='../data/ER.ico',
|
||||
console=is_win )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
|
@ -35,5 +35,5 @@ coll = COLLECT(exe,
|
|||
name='EntranceRandomizer')
|
||||
app = BUNDLE(coll,
|
||||
name ='EntranceRandomizer.app',
|
||||
icon = 'data/ER.icns',
|
||||
icon = '../data/ER.icns',
|
||||
bundle_identifier = None)
|
||||
|
|
File diff suppressed because one or more lines are too long
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.
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.
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.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue