Compare commits

20 Commits

Author SHA1 Message Date
b1a71ef9b1 Merge in OW Shuffle 0.7.0.2 (#3)
Reviewed-on: #3
2026-02-03 23:47:48 +00:00
699d395b3d Update texts 2026-02-03 17:46:18 -06:00
6859f5cd24 Update prize positions on world map 2026-01-31 13:02:07 -06:00
8a7cc02282 Merge remote-tracking branch 'codemann/OverworldShuffle' into codemann_OverworldShuffle 2026-01-31 08:57:53 -06:00
codemann8
ac17d0ead8 Merge branch 'OverworldShuffleDev' into OverworldShuffle 2026-01-30 12:41:38 -06:00
codemann8
ad8dff3b2d Version bump 0.7.0.2 2026-01-30 12:41:26 -06:00
codemann8
f1aef298ef Minor efficiency 2026-01-30 12:35:42 -06:00
950df6b1d0 Remove empty bottle from fairy refill pool 2026-01-29 22:36:25 -06:00
codemann8
2a24048e87 Fix buffered sword issue when transitioning to water with sword out 2026-01-29 21:26:32 -06:00
codemann8
afe888a87a Fix Link sprite using vanilla coords on map check in Special OW 2026-01-29 20:53:24 -06:00
dab247807f Update baserom 2026-01-29 15:53:14 -06:00
codemann8
09254850d3 Fixed error with bombbag + non-shopsanity 2026-01-28 20:03:56 -06:00
codemann8
31f634396a Fixed issue with map checks showing opposite world icons in Inverted 2026-01-28 19:49:20 -06:00
codemann8
daa54e8aad Fixed palette issue with map checks when Link stands in grass/water 2026-01-28 18:40:51 -06:00
codemann8
b16777ede7 Added new PlasmaKappa tribute TF text 2026-01-28 18:13:58 -06:00
fb96d04e56 Update baserom 2026-01-27 22:12:20 -06:00
54981b5c51 Update baserom to fix OW Map palette issue 2026-01-27 00:26:09 -06:00
f43fb18bad Remove some more dead files 2026-01-25 22:29:38 -06:00
ff4f546ec9 Add collections-extended as a proper dependency 2026-01-25 22:27:22 -06:00
codemann8
83254f4c78 Moved Flute Shuffle 2026-01-25 00:06:19 -06:00
28 changed files with 326 additions and 2199 deletions

View File

@@ -1 +0,0 @@
<randall.rupper@gmail.com> <randall.rupper@sirsidynix.com>

View File

@@ -1,5 +1,13 @@
# Changelog
# 0.7.0.2
- Fixed money error for bombbag/take-anys
- Fixed palette issue for map check/flute menu
- Fixed issue with dungeon icons showing in opposite world in Inverted
- Fixed Links position in map checks when in Special OW areas
- Fixed buffered sword issue in OW Shuffle water transitions
- Added PlasmaKappa tribute TF room text
# 0.7.0.1
- Fixed buggy sprites in post-Aga Zora's Domain
- Fixed L/R map switch when in special OW screens

View File

@@ -1112,7 +1112,7 @@ def balance_money_progression(world):
slot = shop_to_location_table[location.parent_region.name].index(location.name)
shop = location.parent_region.shop
shop_item = shop.inventory[slot]
if location.item and interesting_item(location, location.item, world, location.item.player):
if shop_item and location.item and interesting_item(location, location.item, world, location.item.player):
if location.item.name.startswith('Rupee') and loc_player == location.item.player:
if shop_item['price'] < rupee_chart[location.item.name]:
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block

View File

@@ -43,8 +43,8 @@ alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod
progressivegloves = ['Progressive Glove'] * 2
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)']
normalbottles = ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)']
hardbottles = ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Bee)', 'Bottle (Good Bee)']
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)
@@ -364,9 +364,10 @@ def generate_itempool(world, player):
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
loc = world.get_location(location, player)
world.push_item(loc, ItemFactory(item, player), False)
loc.event = True
loc.locked = True
if world.shopsanity[player] and not skip_pool_adjustments:
for shop in world.shops[player]:
@@ -463,11 +464,11 @@ def generate_itempool(world, player):
# shuffle bottle refills
if world.difficulty[player] in ['hard', 'expert']:
waterfall_bottle = hardbottles[random.randint(0, 5)]
pyramid_bottle = hardbottles[random.randint(0, 5)]
waterfall_bottle = hardbottles[random.randint(0, 4)]
pyramid_bottle = hardbottles[random.randint(0, 4)]
else:
waterfall_bottle = normalbottles[random.randint(0, 6)]
pyramid_bottle = normalbottles[random.randint(0, 6)]
waterfall_bottle = normalbottles[random.randint(0, 5)]
pyramid_bottle = normalbottles[random.randint(0, 5)]
world.bottle_refills[player] = (waterfall_bottle, pyramid_bottle)
set_up_shops(world, player)

View File

@@ -50,7 +50,6 @@ from Items import ItemFactory
from KeyDoorShuffle import validate_key_placement
from OverworldGlitchRules import create_owg_connections
from OverworldShuffle import (
create_dynamic_flute_exits,
create_dynamic_mirror_exits,
link_overworld,
update_world_regions,
@@ -86,6 +85,7 @@ from source.item.FillUtil import (
verify_item_pool_config,
)
from source.overworld.EntranceShuffle2 import link_entrances_new
from source.overworld.FluteShuffle import create_dynamic_flute_exits
from source.rom.DataTables import init_data_tables
from source.tools.BPS import create_bps_from_data
from UnderworldGlitchRules import (

View File

@@ -25,6 +25,11 @@ from OWEdges import (
)
from Regions import mark_light_dark_world_regions
from source.overworld.EntranceShuffle2 import connect_simple
from source.overworld.FluteShuffle import (
default_flute_connections,
flute_data,
shuffle_flute_spots,
)
from Utils import bidict
parallel_links_new = None # needs to be globally available, reset every new generation/player
@@ -349,6 +354,7 @@ def link_overworld(world, player):
world.owcrossededges[player].extend(edge_set)
assert len(world.owcrossededges[player]) == len(set(world.owcrossededges[player])), "Same edge candidate added to crossed edges"
# TODO: Someday don't force parallels also? Keeping for now to ensure full reachability
for edge in copy.deepcopy(world.owcrossededges[player]):
if edge in parallel_links_new:
if parallel_links_new[edge] not in world.owcrossededges[player]:
@@ -587,169 +593,8 @@ def link_overworld(world, player):
# flute shuffle
logging.getLogger('').debug('Shuffling flute spots')
def connect_flutes(flute_destinations):
for o in range(0, len(flute_destinations)):
owid = flute_destinations[o]
regions = flute_data[owid][0]
if not world.is_tile_swapped(owid, player):
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[0], player)
else:
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player)
if world.owFluteShuffle[player] == 'vanilla':
flute_spots = default_flute_connections.copy()
sort_flute_spots(world, player, flute_spots)
world.owflutespots[player] = flute_spots
connect_flutes(flute_spots)
else:
flute_spots = 8
flute_pool = list(flute_data.keys())
new_spots = list()
ignored_regions = set()
used_flute_regions = []
forbidden_spots = []
forbidden_regions = []
shuffle_flute_spots(world, player)
def addSpot(owid, ignore_proximity, forced):
if world.owFluteShuffle[player] == 'balanced':
def getIgnored(regionname, base_owid, owid):
region = world.get_region(regionname, player)
for exit in region.exits:
if exit.connected_region is not None and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and exit.connected_region.name not in new_ignored:
if exit.connected_region.name in OWTileRegions and (OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid):
new_ignored.add(exit.connected_region.name)
getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name])
if regionname in one_way_ledges:
for ledge_region in one_way_ledges[regionname]:
if ledge_region not in new_ignored:
new_ignored.add(ledge_region)
getIgnored(ledge_region, base_owid, OWTileRegions[ledge_region])
if not world.is_tile_swapped(owid, player):
new_region = flute_data[owid][0][0]
else:
new_region = flute_data[owid][0][1]
if new_region in ignored_regions and not forced:
return False
new_ignored = {new_region}
getIgnored(new_region, OWTileRegions[new_region], OWTileRegions[new_region])
if not ignore_proximity and not forced and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions):
return False
ignored_regions.update(new_ignored)
if owid in flute_pool:
flute_pool.remove(owid)
if ignore_proximity and not forced:
logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}')
logging.getLogger('').debug(f'Placing flute at: {hex(owid)}')
new_spots.append(owid)
else:
# TODO: Inspect later, seems to happen only with 'random' flute shuffle
logging.getLogger('').warning(f'Warning: Attempted to place flute spot not in pool: {hex(owid)}')
return True
if world.customizer:
custom_spots = world.customizer.get_owflutespots()
if custom_spots and player in custom_spots:
if 'force' in custom_spots[player]:
for id in custom_spots[player]['force']:
owid = id & 0xBF
addSpot(owid, True, True)
flute_spots -= 1
if not world.is_tile_swapped(owid, player):
used_flute_regions.append(flute_data[owid][0][0])
else:
used_flute_regions.append(flute_data[owid][0][1])
if 'forbid' in custom_spots[player]:
for id in custom_spots[player]['forbid']:
owid = id & 0xBF
if owid not in new_spots:
forbidden_spots.append(owid)
if not world.is_tile_swapped(owid, player):
forbidden_regions.append(flute_data[owid][0][0])
else:
forbidden_regions.append(flute_data[owid][0][1])
# determine sectors (isolated groups of regions) to place flute spots
flute_regions = {(f[0][0] if (o not in world.owswaps[player][0]) != (world.mode[player] == 'inverted') else f[0][1]) : o for o, f in flute_data.items() if o not in new_spots and o not in forbidden_spots}
flute_sectors = [(len([r for l in s for r in l]), [r for l in s for r in l if r in flute_regions]) for s in world.owsectors[player]]
flute_sectors = [s for s in flute_sectors if len(s[1]) > 0]
region_total = sum([c for c,_ in flute_sectors])
sector_total = len(flute_sectors)
empty_sector_total = 0
sector_has_spot = []
# determine which sectors still need a flute spot
for sector in flute_sectors:
already_has_spot = any(region in sector for region in used_flute_regions)
sector_has_spot.append(already_has_spot)
if not already_has_spot:
empty_sector_total += 1
if flute_spots < empty_sector_total:
logging.getLogger('').warning(f'Warning: Not every sector can have a flute spot, generation might fail')
# pretend like some of the empty sectors already have a flute spot, don't know if they will be reachable
for i in range(len(flute_sectors)):
if not sector_has_spot[i]:
sector_has_spot[i] = True
empty_sector_total -= 1
if flute_spots == empty_sector_total:
break
# distribute flute spots for each sector
for i in range(len(flute_sectors)):
sector = flute_sectors[i]
sector_total -= 1
if not sector_has_spot[i]:
empty_sector_total -= 1
spots_to_place = min(flute_spots - empty_sector_total, max(0 if sector_has_spot[i] else 1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5)))
target_spots = len(new_spots) + spots_to_place
logging.getLogger('').debug(f'Sector of {sector[0]} regions gets {spots_to_place} spot(s)')
if 0x30 in flute_pool and 0x30 not in forbidden_spots and len(new_spots) < target_spots and ('Desert Teleporter Ledge' in sector[1] or 'Mire Teleporter Ledge' in sector[1]):
addSpot(0x30, True, True) # guarantee desert/mire access
random.shuffle(sector[1])
f = 0
t = 0
while len(new_spots) < target_spots:
if f >= len(sector[1]):
f = 0
t += 1
if t > 5:
raise GenerationException('Infinite loop detected in flute shuffle')
owid = flute_regions[sector[1][f]]
if owid not in new_spots and owid not in forbidden_spots:
addSpot(owid, t > 0, False)
f += 1
region_total -= sector[0]
flute_spots -= spots_to_place
# connect new flute spots
sort_flute_spots(world, player, new_spots)
world.owflutespots[player] = new_spots
connect_flutes(new_spots)
# update spoiler
s = list(map(lambda x: ' ' if x not in new_spots else 'F', [i for i in range(0x40)]))
text_output = flute_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07],
s[0x00], s[0x03], s[0x05],
s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f],
s[0x0a], s[0x0f],
s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17],
s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e],
s[0x22], s[0x25], s[0x1a], s[0x1d],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e],
s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25],
s[0x3a],s[0x3b],s[0x3c], s[0x3f],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f],
s[0x32],s[0x33],s[0x34], s[0x37],
s[0x30], s[0x35],
s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('flute', text_output, new_spots, player)
create_dynamic_flute_exits(world, player)
def connect_custom(world, connected_edges, groups, forced, player):
forced_crossed, forced_noncrossed = forced
@@ -1325,28 +1170,6 @@ def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player):
groups[(mode, wrld, dir, terrain, parallel, count, group_name)][i].extend(matches)
return groups
def sort_flute_spots(world, player, flute_spots):
if world.owLayout[player] != 'grid':
flute_spots.sort(key=lambda id: flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04)
else:
world_layout = world.owgrid[player][0] if world.mode[player] != 'inverted' else world.owgrid[player][1]
layout_list = sum(world_layout, [])
layout_map = {id & 0xBF: i for i, id in enumerate(layout_list)}
flute_spots.sort(key=lambda id: layout_map[flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04])
def create_dynamic_flute_exits(world, player):
flute_in_pool = True if player not in world.customitemarray else any(i for i, n in world.customitemarray[player].items() if i == 'flute' and n > 0)
if not flute_in_pool:
return
for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']):
if region.type == (RegionType.LightWorld if world.mode[player] != 'inverted' else RegionType.DarkWorld):
exitname = 'Flute From ' + region.name
exit = Entrance(region.player, exitname, region)
exit.spot_type = 'Flute'
exit.connect(world.get_region('Flute Sky', player))
region.exits.append(exit)
world.initialize_regions()
def get_mirror_exit_name(from_region, to_region):
if from_region in mirror_connections and to_region in mirror_connections[from_region]:
if len(mirror_connections[from_region]) == 1:
@@ -1918,10 +1741,6 @@ default_whirlpool_connections = [
((0x35, 'Lake Hylia Whirlpool', 'Lake Hylia Water'), (0x0f, 'Zora Whirlpool', 'Zora Waterfall Water')),
((0x12, 'Kakariko Pond Whirlpool', 'Kakariko Pond Area'), (0x3f, 'Octoballoon Whirlpool', 'Octoballoon Water')),
((0x55, 'Qirn Jump Whirlpool', 'Qirn Jump Water'), (0x7f, 'Bomber Corner Whirlpool', 'Bomber Corner Water'))
]
default_flute_connections = [
0x03, 0x16, 0x18, 0x2c, 0x2f, 0x30, 0x3b, 0x3f
]
ow_connections = {
@@ -2589,51 +2408,6 @@ isolated_regions = [
'Pyramid Water'
]
flute_data = {
#OWID LW Region DW Region Slot VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX AltVRAM AltBGY AltBGX AltCamY AltCamX AltUnk1 AltUnk2 AltIconY AltIconX
0x00: (['Lost Woods East Area', 'Skull Woods Forest'], 0x09, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290),
0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x01d8, 0x0518),
0x03: (['West Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'], 0x0b, 0x1600, 0x02ca, 0x060e, 0x0328, 0x0678, 0x0337, 0x0683, 0xfff6, 0xfff2, 0x03bb, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850),
0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x03c8, 0x0d98),
0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0150, 0x0ea0),
0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0390, 0x04d8),
0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x0360, 0x0f58),
0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x04f8, 0x0088),
0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x05f8, 0x0318),
0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x05b8, 0x04f8),
0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x05b8, 0x0738),
0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x05f0, 0x0918, 0x0580, 0x0948),
0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x0548, 0x0b78),
0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x05e8, 0x0c9f),
0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x0580, 0x0f48),
0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x0830, 0x0240, 0x07c8, 0x01f8),
0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0518),
0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07f8, 0x07f8),
0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0750, 0x0a70),
0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09a0, 0x0cb0),
0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0978, 0x04e8),
0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x0918, 0x0b18),
0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0ba8, 0x00b8),
0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0b30, 0x02e0),
0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b60, 0x04f8),
0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b80, 0x0760),
0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0bb0, 0x08a8),
0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bf0, 0x0ab8),
0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0ac8, 0x0c70),
0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0bc0, 0x0f00),
0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0ff0, 0x0070),
0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0d00, 0x0528),
0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0ce0, 0x0688),
0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0dd0, 0x08e8),
#0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0dc8, 0x0a90),
0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0fd8, 0x0da8),
0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d98, 0x0ed0),
0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0ee0, 0x0570),
0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0fd0, 0x0770),
0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0fa8, 0x0898),
0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0fd0, 0x0ef0)
}
ow_loc_prize_table = {
'Master Sword Pedestal': (0x06d, 0x070),
'Hobo': (0xb80, 0xb90),
@@ -2714,23 +2488,3 @@ H(38)| sss s| +-+-+-+-+-+-+-+-+
+-+ | s +-+-+-+ s +-+
Zora: |s| H(38)| |s|s|s| |s|
+-+ +---+-+-+-+---+-+"""
flute_spoiler_table = \
""" 0 1 2 3 4 5 6 7
+---+-+---+---+-+
01234567 A(00)| |s| | |s|
+--------+ | s +-+ s | s +-+
A(00)|s ss s s| B(08)| |s| | |s|
B(08)| s s| +-+-+-+-+-+-+-+-+
C(10)|ssssssss| C(10)|s|s|s|s|s|s|s|s|
D(18)|s ss ss | +-+-+-+-+-+-+-+-+
E(20)| s s | D(18)| |s| |s| |
F(28)|ssssssss| | s +-+ s +-+ s |
G(30)|s ssss s| E(20)| |s| |s| |
H(38)| sss s| +-+-+-+-+-+-+-+-+
+--------+ F(28)|s|s|s|s|s|s|s|s|
+-+-+-+-+-+-+-+-+
G(30)| |s|s|s| |s|
| s +-+-+-+ s +-+
H(38)| |s|s|s| |s|
+---+-+-+-+---+-+"""

12
Rom.py
View File

@@ -47,6 +47,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings
from source.item.FillUtil import valid_pot_items
from source.overworld.EntranceData import door_addresses, ow_prize_table
from source.overworld.EntranceShuffle2 import exit_ids
from source.overworld.FluteShuffle import default_flute_connections, flute_data
from Text import (
Blacksmiths_texts,
Blind_texts,
@@ -84,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc
from Versions import DRVersion, GKVersion, ORVersion
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '33cd5e308266cf2273c80de1b1df3dac'
RANDOMIZERBASEHASH = 'a746c6916c3ca8e89df7d7ac79d354dd'
class JsonRom(object):
@@ -536,7 +537,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
else:
flute_spots = world.owflutespots[player]
owFlags |= 0x0100
write_int16(rom, snes_to_pc(0x0AB7F7), 0xEAEA)
if world.owFluteShuffle[player] != 'vanilla':
write_int16(rom, snes_to_pc(0x0AB80B), 0xEAEA)
flute_writes = [(f, flute_data[f][1]) for f in flute_spots]
for o in range(0, len(flute_writes)):
@@ -1573,14 +1575,14 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
idx = int((map_index-2)/2)
elif isinstance(ent, int):
# vanilla icon positions
x_map_position = [0x0F30, 0x0170, 0xFF00, 0x0790, 0x0F30, 0x0160, 0x00F0, 0x0CB0, 0x0900, 0x0240, 0x0F30]
y_map_position = [0x06E0, 0x0E50, 0xFF00, 0x0FD0, 0x06E0, 0x0D80, 0x0160, 0x0E80, 0x0130, 0x0840, 0x01B0]
x_map_position = [0x0F40, 0x0140, 0xFF00, 0x0778, 0x0F40, 0x0148, 0x00B0, 0x0CA0, 0x0900, 0x01F8, 0x0F20]
y_map_position = [0x0660, 0x0D00, 0xFF00, 0x0F50, 0x0660, 0x0D00, 0x00C0, 0x0E00, 0x0100, 0x0800, 0x0100]
idx = ent
owid = owid_map[idx]
map_x = x_map_position[idx]
map_y = y_map_position[idx]
if owid != 0xFF:
if (owid < 0x40) == (world.is_tile_swapped(owid, player)):
if (owid < 0x40) == (owid in world.owswaps[player][0]):
coord_flags |= 0x8000 # world indicator flag
if coord_flags & 0x4000 == 0:
map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0)

View File

@@ -135,6 +135,7 @@ Triforce_texts = [
" I promise the\n next seed will\n be better.",
"\n Honk.",
" Breakfast\n is served!",
"\n send help",
]
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!']
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?']
@@ -388,7 +389,6 @@ DeathMountain_texts = [
]
LostWoods_texts = [
'thieves\' stump',
'He\'s got wood',
] * 2 + [
"the forest thief",
"dancing pickles",

View File

@@ -1,3 +1,3 @@
GKVersion = '1.0.0'
ORVersion = '0.7.0.1'
ORVersion = '0.7.0.2'
DRVersion = '1.5.2-u'

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
version: '{build}'
pull_requests:
do_not_increment_build_number: true
environment:
ProjectVersion: build$(APPVEYOR_BUILD_VERSION)
matrix:
- PYTHON: C:\PYTHON36
install:
- ps: 'if(Test-Path env:APPVEYOR_REPO_TAG_NAME) {$env:ProjectVersion=$env:APPVEYOR_REPO_TAG_NAME}'
- '%PYTHON%\python.exe --version'
- '%PYTHON%\Scripts\pip install pyinstaller'
- '%PYTHON%\Scripts\pip install markdown'
- '%PYTHON%\python.exe -m markdown README.md > README.html'
- '%PYTHON%\Scripts\pyinstaller bundle\EntranceRandomizer.spec'
- 'mkdir dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\*.pyd dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\tcl*.dll dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\tk*.dll dist\EntranceRandomizer\ext'
- ps: '$env:ER_Version= &"$env:PYTHON\python.exe" -c "import Main; import re; print(re.match(''[0-9]+\\.[0-9]+\\.[0-9]+'',Main.__version__).group(0))"'
- '"%WIX%\bin\heat.exe" dir "dist\EntranceRandomizer" -sfrag -srd -suid -dr INSTALLDIR -cg ERFiles -ag -template fragment -t bundle\components.xslt -out build\components.wxs'
- '"%WIX%\bin\candle.exe" -out build\ bundle\*.wxs build\*.wxs'
- '"%WIX%\bin\light.exe" -ext WixUIExtension build\*.wixobj -o dist\EntranceRandomizer-Installer-%ProjectVersion%-win32.msi -b dist\EntranceRandomizer'
build: off
artifacts:
- path: dist/EntranceRandomizer*.msi
name: EntranceRandomizer-Installer-$(ProjectVersion)-win32.msi
- path: dist/EntranceRandomizer/
name: EntranceRandomizer-Raw-$(ProjectVersion)-win32.zip

Binary file not shown.

View File

@@ -1,21 +0,0 @@
Notes:
Ice Spike Stairs seems to be off and maybe Lonely Freezor
Pod bk ledge stairs
AgaTower usually doesn't have enough Key Door candidates
(Testing) Camera spots in Aga Tower: Push Statue, Lone Statue maybe?
(Testing Waterfall Now) Spiral stairs without keys look funny - change to Incognito instead of Normal
(Can't Fix yet) Key door on Layer 1 not working (Aga Tower)
(Can't Fix yet) Key door in HC Lobby E door not passable - look into it?
Pairing Notes
30-34 4f-37 #$18,-24
30-34 37-1f #$18,-24
30-34 1f-1f???
30-35 1f-1f
30-34 36-1e #$18,-24
30-34 1e-1f
inconsistencies are caused by $0492 being set wrong

View File

@@ -7,6 +7,7 @@ requires-python = ">=3.7"
dependencies = [
"aenum>=3.1.16",
"aioconsole>=0.6.2",
"collections-extended>=2.0.2",
"colorama>=0.4.6",
"distro>=1.9.0",
"fast-enum>=1.3.0",

View File

@@ -11,6 +11,7 @@ from yaml.representer import Representer
import RaceRandom as random
from BaseClasses import DoorType, LocationType
from OverworldShuffle import default_flute_connections, flute_data
from source.overworld.FluteShuffle import default_flute_connections, flute_data
from source.tools.MysteryUtils import get_weights, roll_settings
from Utils import HexInt, hex_representer

View File

@@ -0,0 +1,270 @@
import copy
import logging
import RaceRandom as random
from BaseClasses import Entrance, RegionType, Terrain
from DungeonGenerator import GenerationException
from OWEdges import OWTileRegions
from source.overworld.EntranceShuffle2 import connect_simple
def shuffle_flute_spots(world, player):
def connect_flutes(flute_destinations):
for o in range(0, len(flute_destinations)):
owid = flute_destinations[o]
regions = flute_data[owid][0]
if not world.is_tile_swapped(owid, player):
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[0], player)
else:
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player)
if world.owFluteShuffle[player] == 'vanilla':
flute_spots = default_flute_connections.copy()
sort_flute_spots(world, player, flute_spots)
world.owflutespots[player] = flute_spots
connect_flutes(flute_spots)
else:
from OverworldShuffle import one_way_ledges
flute_spots = 8
flute_pool = list(flute_data.keys())
new_spots = list()
ignored_regions = set()
used_flute_regions = []
forbidden_spots = []
forbidden_regions = []
def addSpot(owid, ignore_proximity, forced):
if world.owFluteShuffle[player] == 'balanced':
def getIgnored(regionname, base_owid, owid):
region = world.get_region(regionname, player)
for exit in region.exits:
if exit.connected_region is not None and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and exit.connected_region.name not in new_ignored:
if exit.connected_region.name in OWTileRegions and (OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid):
new_ignored.add(exit.connected_region.name)
getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name])
if regionname in one_way_ledges:
for ledge_region in one_way_ledges[regionname]:
if ledge_region not in new_ignored:
new_ignored.add(ledge_region)
getIgnored(ledge_region, base_owid, OWTileRegions[ledge_region])
if not world.is_tile_swapped(owid, player):
new_region = flute_data[owid][0][0]
else:
new_region = flute_data[owid][0][1]
if new_region in ignored_regions and not forced:
return False
new_ignored = {new_region}
getIgnored(new_region, OWTileRegions[new_region], OWTileRegions[new_region])
if not ignore_proximity and not forced and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions):
return False
ignored_regions.update(new_ignored)
if owid in flute_pool:
flute_pool.remove(owid)
if ignore_proximity and not forced:
logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}')
logging.getLogger('').debug(f'Placing flute at: {hex(owid)}')
new_spots.append(owid)
else:
# TODO: Inspect later, seems to happen only with 'random' flute shuffle
logging.getLogger('').warning(f'Warning: Attempted to place flute spot not in pool: {hex(owid)}')
return True
if world.customizer:
custom_spots = world.customizer.get_owflutespots()
if custom_spots and player in custom_spots:
if 'force' in custom_spots[player]:
for id in custom_spots[player]['force']:
owid = id & 0xBF
addSpot(owid, True, True)
flute_spots -= 1
if not world.is_tile_swapped(owid, player):
used_flute_regions.append(flute_data[owid][0][0])
else:
used_flute_regions.append(flute_data[owid][0][1])
if 'forbid' in custom_spots[player]:
for id in custom_spots[player]['forbid']:
owid = id & 0xBF
if owid not in new_spots:
forbidden_spots.append(owid)
if not world.is_tile_swapped(owid, player):
forbidden_regions.append(flute_data[owid][0][0])
else:
forbidden_regions.append(flute_data[owid][0][1])
# determine sectors (isolated groups of regions) to place flute spots
flute_regions = {(f[0][0] if (o not in world.owswaps[player][0]) != (world.mode[player] == 'inverted') else f[0][1]) : o for o, f in flute_data.items() if o not in new_spots and o not in forbidden_spots}
flute_sectors = [(len([r for l in s for r in l]), [r for l in s for r in l if r in flute_regions]) for s in world.owsectors[player]]
flute_sectors = [s for s in flute_sectors if len(s[1]) > 0]
region_total = sum([c for c,_ in flute_sectors])
sector_total = len(flute_sectors)
empty_sector_total = 0
sector_has_spot = []
# determine which sectors still need a flute spot
for sector in flute_sectors:
already_has_spot = any(region in sector for region in used_flute_regions)
sector_has_spot.append(already_has_spot)
if not already_has_spot:
empty_sector_total += 1
if flute_spots < empty_sector_total:
logging.getLogger('').warning(f'Warning: Not every sector can have a flute spot, generation might fail')
# pretend like some of the empty sectors already have a flute spot, don't know if they will be reachable
for i in range(len(flute_sectors)):
if not sector_has_spot[i]:
sector_has_spot[i] = True
empty_sector_total -= 1
if flute_spots == empty_sector_total:
break
# distribute flute spots for each sector
for i in range(len(flute_sectors)):
sector = flute_sectors[i]
sector_total -= 1
if not sector_has_spot[i]:
empty_sector_total -= 1
spots_to_place = min(flute_spots - empty_sector_total, max(0 if sector_has_spot[i] else 1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5)))
target_spots = len(new_spots) + spots_to_place
logging.getLogger('').debug(f'Sector of {sector[0]} regions gets {spots_to_place} spot(s)')
if 0x30 in flute_pool and 0x30 not in forbidden_spots and len(new_spots) < target_spots and ('Desert Teleporter Ledge' in sector[1] or 'Mire Teleporter Ledge' in sector[1]):
addSpot(0x30, True, True) # guarantee desert/mire access
random.shuffle(sector[1])
f = 0
t = 0
while len(new_spots) < target_spots:
if f >= len(sector[1]):
f = 0
t += 1
if t > 5:
raise GenerationException('Infinite loop detected in flute shuffle')
owid = flute_regions[sector[1][f]]
if owid not in new_spots and owid not in forbidden_spots:
addSpot(owid, t > 0, False)
f += 1
region_total -= sector[0]
flute_spots -= spots_to_place
# connect new flute spots
sort_flute_spots(world, player, new_spots)
world.owflutespots[player] = new_spots
connect_flutes(new_spots)
# update spoiler
#new_spots = list(map(lambda o: flute_data[o][1], new_spots))
s = list(map(lambda x: ' ' if x not in new_spots else 'F', [i for i in range(0x40)]))
text_output = flute_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07],
s[0x00], s[0x03], s[0x05],
s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f],
s[0x0a], s[0x0f],
s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17],
s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e],
s[0x22], s[0x25], s[0x1a], s[0x1d],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e],
s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25],
s[0x3a],s[0x3b],s[0x3c], s[0x3f],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f],
s[0x32],s[0x33],s[0x34], s[0x37],
s[0x30], s[0x35],
s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('flute', text_output, new_spots, player)
create_dynamic_flute_exits(world, player)
def sort_flute_spots(world, player, flute_spots):
if world.owLayout[player] != 'grid':
flute_spots.sort(key=lambda id: flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04)
else:
world_layout = world.owgrid[player][0] if world.mode[player] != 'inverted' else world.owgrid[player][1]
layout_list = sum(world_layout, [])
layout_map = {id & 0xBF: i for i, id in enumerate(layout_list)}
flute_spots.sort(key=lambda id: layout_map[flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04])
def create_dynamic_flute_exits(world, player):
flute_in_pool = True if player not in world.customitemarray else any(i for i, n in world.customitemarray[player].items() if i == 'flute' and n > 0)
if not flute_in_pool:
return
for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']):
if region.type == (RegionType.LightWorld if world.mode[player] != 'inverted' else RegionType.DarkWorld):
exitname = 'Flute From ' + region.name
exit = Entrance(region.player, exitname, region)
exit.spot_type = 'Flute'
exit.connect(world.get_region('Flute Sky', player))
region.exits.append(exit)
world.initialize_regions()
default_flute_connections = [
0x03, 0x16, 0x18, 0x2c, 0x2f, 0x30, 0x3b, 0x3f
]
flute_data = {
#OWID LW Region DW Region Slot VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX AltVRAM AltBGY AltBGX AltCamY AltCamX AltUnk1 AltUnk2 AltIconY AltIconX
0x00: (['Lost Woods East Area', 'Skull Woods Forest'], 0x09, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290),
0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x01d8, 0x0518),
0x03: (['West Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'], 0x0b, 0x1600, 0x02ca, 0x060e, 0x0328, 0x0678, 0x0337, 0x0683, 0xfff6, 0xfff2, 0x03bb, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850),
0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x03c8, 0x0d98),
0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0150, 0x0ea0),
0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0390, 0x04d8),
0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x0360, 0x0f58),
0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x04f8, 0x0088),
0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x05f8, 0x0318),
0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x05b8, 0x04f8),
0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x05b8, 0x0738),
0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x05f0, 0x0918, 0x0580, 0x0948),
0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x0548, 0x0b78),
0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x05e8, 0x0c9f),
0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x0580, 0x0f48),
0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x0830, 0x0240, 0x07c8, 0x01f8),
0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0518),
0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07f8, 0x07f8),
0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0750, 0x0a70),
0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09a0, 0x0cb0),
0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0978, 0x04e8),
0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x0918, 0x0b18),
0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0ba8, 0x00b8),
0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0b30, 0x02e0),
0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b60, 0x04f8),
0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b80, 0x0760),
0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0bb0, 0x08a8),
0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bf0, 0x0ab8),
0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0ac8, 0x0c70),
0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0bc0, 0x0f00),
0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0ff0, 0x0070),
0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0d00, 0x0528),
0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0ce0, 0x0688),
0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0dd0, 0x08e8),
#0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0dc8, 0x0a90),
0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0fd8, 0x0da8),
0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d98, 0x0ed0),
0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0ee0, 0x0570),
0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0fd0, 0x0770),
0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0fa8, 0x0898),
0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0fd0, 0x0ef0)
}
flute_spoiler_table = \
""" 0 1 2 3 4 5 6 7
+---+-+---+---+-+
01234567 A(00)| |s| | |s|
+--------+ | s +-+ s | s +-+
A(00)|s ss s s| B(08)| |s| | |s|
B(08)| s s| +-+-+-+-+-+-+-+-+
C(10)|ssssssss| C(10)|s|s|s|s|s|s|s|s|
D(18)|s ss ss | +-+-+-+-+-+-+-+-+
E(20)| s s | D(18)| |s| |s| |
F(28)|ssssssss| | s +-+ s +-+ s |
G(30)|s ssss s| E(20)| |s| |s| |
H(38)| sss s| +-+-+-+-+-+-+-+-+
+--------+ F(28)|s|s|s|s|s|s|s|s|
+-+-+-+-+-+-+-+-+
G(30)| |s|s|s| |s|
| s +-+-+-+ s +-+
H(38)| |s|s|s| |s|
+---+-+-+-+---+-+"""

View File

@@ -1,9 +1,10 @@
import copy
import logging
import RaceRandom as random
import random as _random
from typing import List, Dict, Optional, Set, Tuple
from BaseClasses import OWEdge, World, Direction, Terrain
from typing import Dict, List, Optional, Set, Tuple
import RaceRandom as random
from BaseClasses import Direction, OWEdge, Terrain, World
from OverworldShuffle import connect_two_way, validate_layout
ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False

View File

@@ -2,10 +2,13 @@ import logging
import os
from datetime import datetime
from typing import Dict, List
from PIL import Image, ImageDraw
from BaseClasses import Direction, OWEdge
from source.overworld.LayoutGenerator import Screen
def get_edge_lists(grid: List[List[List[int]]],
overworld_screens: Dict[int, Screen],
large_screen_quadrant_info: Dict[int, Dict]) -> Dict:

11
uv.lock generated
View File

@@ -49,6 +49,7 @@ dependencies = [
{ name = "aenum" },
{ name = "aioconsole", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" },
{ name = "aioconsole", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" },
{ name = "collections-extended" },
{ name = "colorama" },
{ name = "distro" },
{ name = "fast-enum" },
@@ -64,6 +65,7 @@ dependencies = [
requires-dist = [
{ name = "aenum", specifier = ">=3.1.16" },
{ name = "aioconsole", specifier = ">=0.6.2" },
{ name = "collections-extended", specifier = ">=2.0.2" },
{ name = "colorama", specifier = ">=0.4.6" },
{ name = "distro", specifier = ">=1.9.0" },
{ name = "fast-enum", specifier = ">=1.3.0" },
@@ -72,6 +74,15 @@ requires-dist = [
{ name = "websockets", specifier = ">=11.0.3" },
]
[[package]]
name = "collections-extended"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/62/40/e92bd575a99b7fb1fe9f3198ff4addc7d9f66e3d7e976b4d1b5bca6e049d/collections-extended-2.0.2.tar.gz", hash = "sha256:d68ebd1e2208444e392aa0016e5c085bd897f89296f5c1860787de5935994114", size = 22753, upload-time = "2022-01-23T18:58:38.99Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/65/8f409bbb1faf21714ebebfa553a34023a6657bbb2f85f43141be4e4ffe5e/collections_extended-2.0.2-py3-none-any.whl", hash = "sha256:bc096dda430b9455f51287943f7f63fbe53a4de78c444cc38c44e996af306ac5", size = 24330, upload-time = "2022-01-23T18:58:40.662Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"