Merge remote-tracking branch 'origin/OverworldShuffle' into OverworldShuffle

This commit is contained in:
2022-09-18 18:57:25 -07:00
78 changed files with 8114 additions and 2984 deletions

View File

@@ -1,5 +1,5 @@
import random
from Utils import int16_as_bytes
from Utils import int16_as_bytes, snes_to_pc
class SFX(object):
@@ -126,7 +126,7 @@ def shuffle_sfx_data():
random.shuffle(candidates)
# place chained sfx first
random.shuffle(chained_sfx) # todo: sort largest to smallest
random.shuffle(chained_sfx)
chained_sfx = sorted(chained_sfx, key=lambda x: len(x.chain), reverse=True)
for chained in chained_sfx:
chosen_slot = next(x for x in candidates if len(accompaniment_map[x[0]]) - len(chained.chain) >= 0)
@@ -153,16 +153,13 @@ def shuffle_sfx_data():
sfx_table = {
2: 0x1a8c29,
3: 0x1A8D25
2: 0x1A8BD0,
3: 0x1A8CCC
}
# 0x1a8c29
# d8059
sfx_accompaniment_table = {
2: 0x1A8CA7,
3: 0x1A8DA3
2: 0x1A8C4E,
3: 0x1A8D4A
}
@@ -171,9 +168,9 @@ def randomize_sfx(rom):
for shuffled_sfx in sfx_map.values():
for sfx in shuffled_sfx.values():
base_address = sfx_table[sfx.target_set]
base_address = snes_to_pc(sfx_table[sfx.target_set])
rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr))
ac_base = sfx_accompaniment_table[sfx.target_set]
ac_base = snes_to_pc(sfx_accompaniment_table[sfx.target_set])
last = sfx.target_id
if sfx.target_chain:
for chained in sfx.target_chain:

View File

@@ -4,6 +4,7 @@ import json
import os
import random
import shutil
import ssl
from urllib.parse import urlparse
from urllib.request import urlopen
import webbrowser
@@ -149,7 +150,7 @@ class SpriteSelector(object):
try:
task.update_status("Downloading official sprites list")
with urlopen('https://alttpr.com/sprites') as response:
with urlopen('https://alttpr.com/sprites', context=ssl._create_unverified_context()) as response:
sprites_arr = json.loads(response.read().decode("utf-8"))
except Exception as e:
resultmessage = "Error getting list of official sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)

View File

@@ -73,7 +73,8 @@ SETTINGSTOPROCESS = {
"progressives": "progressive",
"accessibility": "accessibility",
"sortingalgo": "algorithm",
"beemizer": "beemizer"
"beemizer": "beemizer",
"restrict_boss_items": "restrict_boss_items"
},
"overworld": {
"overworldshuffle": "ow_shuffle",
@@ -81,13 +82,15 @@ SETTINGSTOPROCESS = {
"keepsimilar": "ow_keepsimilar",
"mixed": "ow_mixed",
"whirlpool": "ow_whirlpool",
"bonk_drops": "bonk_drops",
"overworldflute": "ow_fluteshuffle"
},
"entrance": {
"openpyramid": "openpyramid",
"shuffleganon": "shuffleganon",
"shufflelinks": "shufflelinks",
"entranceshuffle": "shuffle"
"entranceshuffle": "shuffle",
"overworld_map": "overworld_map",
},
"enemizer": {
"enemyshuffle": "shuffleenemies",
@@ -100,9 +103,12 @@ SETTINGSTOPROCESS = {
"compassshuffle": "compassshuffle",
"smallkeyshuffle": "keyshuffle",
"bigkeyshuffle": "bigkeyshuffle",
"keydropshuffle": "keydropshuffle",
"dungeondoorshuffle": "door_shuffle",
"dungeonintensity": "intensity",
"keydropshuffle": "keydropshuffle",
"dropshuffle": "dropshuffle",
"pottery": "pottery",
"colorizepots": "colorizepots",
"potshuffle": "shufflepots",
"experimental": "experimental",
"dungeon_counters": "dungeon_counters",
@@ -118,9 +124,12 @@ SETTINGSTOPROCESS = {
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes",
"reduce_flashing": "reduce_flashing",
"shuffle_sfx": "shuffle_sfx"
"shuffle_sfx": "shuffle_sfx",
'msu_resume': 'msu_resume',
'collection_rate': 'collection_rate',
},
"generation": {
"bps": "bps",
"createspoiler": "create_spoiler",
"createrom": "create_rom",
"calcplaythrough": "calc_playthrough",

View File

@@ -0,0 +1,58 @@
from RoomData import DoorKind, Position
from source.dungeon.RoomObject import RoomObject, DoorObject
class Room:
def __init__(self, layout, layer1, layer2, doors):
self.layout = layout
self.layer1 = layer1
self.layer2 = layer2
self.doors = doors
def write_to_rom(self, address, rom):
rom.write_bytes(address, self.layout)
address += 2
for obj in self.layer1:
rom.write_bytes(address, obj.data)
address += 3
rom.write_bytes(address, [0xFF, 0xFF])
address += 2
for obj in self.layer2:
rom.write_bytes(address, obj.data)
address += 3
rom.write_bytes(address, [0xFF, 0xFF, 0xF0, 0xFF])
address += 4
for door in self.doors:
rom.write_bytes(address, door.get_bytes())
address += 2
rom.write_bytes(address, [0xFF, 0xFF])
return address + 2 # where the data ended
Room0127 = Room([0xE1, 0x00],
[RoomObject(0x0AB600, [0xFE, 0x89, 0x00]),
RoomObject(0x0AB603, [0xA2, 0xA1, 0x61]),
RoomObject(0x0AB606, [0xFE, 0x8E, 0x81]),
RoomObject(0x0AB609, [0xFF, 0x49, 0x02]),
RoomObject(0x0AB60C, [0xD2, 0xA1, 0x62]),
RoomObject(0x0AB60F, [0xFF, 0x4E, 0x83]),
RoomObject(0x0AB612, [0x20, 0xB3, 0xDD]),
RoomObject(0x0AB615, [0x50, 0xB3, 0xDD]),
RoomObject(0x0AB618, [0x33, 0xCB, 0xFA]),
RoomObject(0x0AB61B, [0x3B, 0xCB, 0xFA]),
RoomObject(0x0AB61E, [0x43, 0xCB, 0xFA]),
RoomObject(0x0AB621, [0x4B, 0xCB, 0xFA]),
RoomObject(0x0AB624, [0xBF, 0x94, 0xF9]),
RoomObject(0x0AB627, [0xB3, 0xB3, 0xFA]),
RoomObject(0x0AB62A, [0xCB, 0xB3, 0xFA]),
RoomObject(0x0AB62D, [0xAD, 0xC8, 0xDF]),
RoomObject(0x0AB630, [0xC4, 0xC8, 0xDF]),
RoomObject(0x0AB633, [0xB3, 0xE3, 0xFA]),
RoomObject(0x0AB636, [0xCB, 0xE3, 0xFA]),
RoomObject(0x0AB639, [0x81, 0x93, 0xC0]),
RoomObject(0x0AB63C, [0x81, 0xD2, 0xC0]),
RoomObject(0x0AB63F, [0xE1, 0x93, 0xC0]),
RoomObject(0x0AB642, [0xE1, 0xD2, 0xC0])],
[], [DoorObject(Position.SouthW, DoorKind.CaveEntrance),
DoorObject(Position.SouthE, DoorKind.CaveEntrance)])

View File

@@ -0,0 +1,34 @@
from Utils import snes_to_pc
# Subtype 3 object (0x2xx by jpdasm id - see bank 01)
# B
Normal_Pot = (0xFA, 3, 3)
Shuffled_Pot = (0xFB, 0, 0) # formerly weird pot, or black diagonal thing
class RoomObject:
def __init__(self, address, data):
self.address = address
self.data = data
def change_type(self, new_type):
type_id, datum_a, datum_b = new_type
if 0xF8 <= type_id < 0xFC: # sub type 3
self.data = (self.data[0] & 0xFC) | datum_a, (self.data[1] & 0xFC) | datum_b, type_id
else:
pass # not yet implemented
def write_to_rom(self, rom):
rom.write_bytes(snes_to_pc(self.address), self.data)
class DoorObject:
def __init__(self, pos, kind):
self.pos = pos
self.kind = kind
def get_bytes(self):
return [self.pos.value, self.kind.value]

View File

@@ -1,5 +1,5 @@
from tkinter import ttk, filedialog, messagebox, StringVar, Button, Entry, Frame, Label, E, W, LEFT, RIGHT, X, BOTTOM
from AdjusterMain import adjust
from AdjusterMain import adjust, patch
from argparse import Namespace
from source.classes.SpriteSelector import SpriteSelector
import source.gui.widgets as widgets
@@ -79,7 +79,9 @@ def adjust_page(top, parent, settings):
romEntry2 = Entry(adjustRomFrame, textvariable=self.romVar2)
def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
initdir = os.path.join(os.getcwd(), settings['outputpath']) if 'outputpath' in settings else os.getcwd()
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")],
initialdir=initdir)
if rom:
settings["rom"] = rom
self.romVar2.set(rom)
@@ -103,7 +105,8 @@ def adjust_page(top, parent, settings):
"quickswap": "quickswap",
"nobgm": "disablemusic",
"reduce_flashing": "reduce_flashing",
"shuffle_sfx": "shuffle_sfx"
'msu_resume': 'msu_resume',
"shuffle_sfx": "shuffle_sfx",
}
guiargs = Namespace()
for option in options:
@@ -122,6 +125,58 @@ def adjust_page(top, parent, settings):
messagebox.showinfo(title="Success", message="Rom patched successfully")
adjustButton = Button(self.frames["bottomAdjustFrame"], text='Adjust Rom', command=adjustRom)
adjustButton.pack(side=BOTTOM, padx=(5, 0))
adjustButton.pack(padx=(5, 0))
patchFileFrame = Frame(self.frames["bottomAdjustFrame"])
patchFileLabel = Label(patchFileFrame, text='BPS Patch: ')
self.patchVar = StringVar(value=settings["patch"])
patchEntry = Entry(patchFileFrame, textvariable=self.patchVar)
def PatchSelect():
initdir = os.path.join(os.getcwd(), settings['outputpath']) if 'outputpath' in settings else os.getcwd()
file = filedialog.askopenfilename(filetypes=[("BPS Files", ".bps"), ("All Files", "*")], initialdir=initdir)
if file:
settings["patch"] = file
self.patchVar.set(file)
patchSelectButton = Button(patchFileFrame, text='Select Patch', command=PatchSelect)
patchFileLabel.pack(side=LEFT)
patchEntry.pack(side=LEFT, fill=X, expand=True)
patchSelectButton.pack(side=LEFT)
patchFileFrame.pack(fill=X)
def patchRom():
if output_path.cached_path is None:
output_path.cached_path = top.settings["outputpath"]
options = {
"heartbeep": "heartbeep",
"heartcolor": "heartcolor",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes",
"quickswap": "quickswap",
"nobgm": "disablemusic",
"reduce_flashing": "reduce_flashing",
'msu_resume': 'msu_resume',
"shuffle_sfx": "shuffle_sfx",
}
guiargs = Namespace()
for option in options:
arg = options[option]
setattr(guiargs, arg, self.widgets[option].storageVar.get())
guiargs.patch = self.patchVar.get()
guiargs.baserom = top.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get()
guiargs.sprite = self.sprite
guiargs.outputpath = os.path.dirname(guiargs.patch)
try:
patch(args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
patchButton = Button(self.frames["bottomAdjustFrame"], text='Patch Rom', command=patchRom)
patchButton.pack(side=BOTTOM, padx=(5, 0))
return self,settings

View File

@@ -275,4 +275,9 @@ def create_guiargs(parent):
guiargs = update_deprecated_args(guiargs)
# Key drop shuffle stuff
if guiargs.keydropshuffle:
guiargs.dropshuffle = 1
guiargs.pottery = 'keys' if guiargs.pottery == 'none' else guiargs.pottery
return guiargs

View File

@@ -52,7 +52,7 @@ def loadcliargs(gui, args, settings=None):
gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label)
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg])
# If we're on the Game Options page and it's not about Hints
if subpage == "gameoptions" and not widget == "hints":
if subpage == "gameoptions" and widget not in ["hints", "collection_rate"]:
# Check if we've got settings
# Check if we've got the widget in Adjust settings
hasSettings = settings is not None

View File

@@ -27,7 +27,7 @@ def entrando_page(parent):
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
if self.widgets[key].type == "checkbox" or key == "openpyramid":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)

View File

@@ -15,12 +15,16 @@ def overworld_page(parent):
# Load Overworld Shuffle option widgets as defined by JSON file
# Defns include frame name, widget type, widget options, widget placement attributes
# These get split left & right
self.frames["topOverworldFrame"] = Frame(self)
self.frames["leftOverworldFrame"] = Frame(self)
self.frames["rightOverworldFrame"] = Frame(self)
self.frames["topOverworldFrame"].pack(side=TOP, anchor=NW)
self.frames["leftOverworldFrame"].pack(side=LEFT, anchor=NW, fill=Y)
self.frames["rightOverworldFrame"].pack(anchor=NW, fill=Y)
shuffleLabel = Label(self.frames["topOverworldFrame"], text="Shuffle: ")
shuffleLabel.pack(side=LEFT)
with open(os.path.join("resources","app","gui","randomize","overworld","widgets.json")) as overworldWidgets:
myDict = json.load(overworldWidgets)
@@ -33,7 +37,7 @@ def overworld_page(parent):
packAttrs = {"side":LEFT, "pady":(18,0)}
elif key == "overworldflute":
packAttrs["pady"] = (20,0)
elif key in ["whirlpool", "mixed"]:
elif key in ["mixed", "whirlpool"]:
packAttrs = {"anchor":W, "padx":(79,0)}
self.widgets[key].pack(packAttrs)

159
source/item/District.py Normal file
View File

@@ -0,0 +1,159 @@
from collections import deque
from BaseClasses import CollectionState, RegionType
from Dungeons import dungeon_table
from OWEdges import OWTileRegions
class District(object):
def __init__(self, name, locations, entrances=None, dungeon=None):
self.name = name
self.dungeon = dungeon
self.locations = locations
self.entrances = entrances if entrances else []
self.sphere_one = False
self.dungeons = set()
self.access_points = set()
def create_districts(world):
world.districts = {}
for p in range(1, world.players + 1):
create_district_helper(world, p)
def create_district_helper(world, player):
districts = {}
kak_locations = {'Bottle Merchant', 'Kakariko Tavern', 'Maze Race'}
nw_lw_locations = {'Mushroom', 'Master Sword Pedestal'}
central_lw_locations = {'Sunken Treasure', 'Flute Spot'}
desert_locations = {'Purple Chest', 'Desert Ledge', 'Bombos Tablet'}
lake_locations = {'Hobo', 'Lake Hylia Island'}
east_lw_locations = {"Zora's Ledge", 'King Zora'}
lw_dm_locations = {'Old Man', 'Spectacle Rock', 'Ether Tablet', 'Floating Island'}
east_dw_locations = {'Pyramid', 'Catfish'}
south_dw_locations = {'Stumpy', 'Digging Game'}
voo_north_locations = {'Bumper Cave Ledge'}
ddm_locations = set()
kak_entrances = ['Kakariko Well Cave', 'Bat Cave Cave', 'Elder House (East)', 'Elder House (West)',
'Two Brothers House (East)', 'Two Brothers House (West)', 'Blinds Hideout', 'Chicken House',
'Blacksmiths Hut', 'Sick Kids House', 'Snitch Lady (East)', 'Snitch Lady (West)',
'Bush Covered House', 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Library',
'Kakariko Gamble Game', 'Kakariko Well Drop', 'Bat Cave Drop', 'Tavern North']
nw_lw_entrances = ['North Fairy Cave', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary',
'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Kings Grave', 'Lost Woods Gamble',
'Fortune Teller (Light)', 'Bonk Rock Cave', 'Lumberjack House', 'North Fairy Cave Drop',
'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'Graveyard Cave']
central_lw_entrances = ['Links House', 'Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (West)',
'Hyrule Castle Entrance (East)', 'Agahnims Tower', 'Hyrule Castle Secret Entrance Stairs',
'Dam', 'Bonk Fairy (Light)', 'Light Hype Fairy', 'Hyrule Castle Secret Entrance Drop',
'Cave 45']
desert_entrances = ['Desert Palace Entrance (South)', 'Desert Palace Entrance (West)',
'Desert Palace Entrance (North)', 'Desert Palace Entrance (East)', 'Desert Fairy',
'Aginahs Cave', '50 Rupee Cave', 'Checkerboard Cave']
lake_entrances = ['Capacity Upgrade', 'Mini Moldorm Cave', 'Good Bee Cave', '20 Rupee Cave', 'Ice Rod Cave',
'Cave Shop (Lake Hylia)', 'Lake Hylia Fortune Teller']
east_lw_entrances = ['Eastern Palace', 'Waterfall of Wishing', 'Lake Hylia Fairy', 'Sahasrahlas Hut',
'Long Fairy Cave', 'Potion Shop']
lw_dm_entrances = ['Tower of Hera', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)',
'Spiral Cave', 'Spiral Cave (Bottom)', 'Hookshot Fairy', 'Mimic Cave']
east_dw_entrances = ['Palace of Darkness', 'Pyramid Entrance', 'Pyramid Fairy', 'East Dark World Hint',
'Palace of Darkness Hint', 'Dark Lake Hylia Fairy', 'Dark World Potion Shop', 'Pyramid Hole']
south_dw_entrances = ['Ice Palace', 'Swamp Palace', 'Dark Lake Hylia Ledge Fairy',
'Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Hint', 'Hype Cave',
'Bonk Fairy (Dark)', 'Archery Game', 'Big Bomb Shop', 'Dark Lake Hylia Shop', ]
voo_north_entrances = ['Thieves Town', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)',
'Skull Woods Second Section Door (West)', 'Skull Woods Final Section',
'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Brewery', 'C-Shaped House', 'Chest Game',
'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint',
'Fortune Teller (Dark)', 'Dark World Shop', 'Dark World Lumberjack Shop',
'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)',
'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole']
mire_entrances = ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy']
ddm_entrances = ['Turtle Rock', 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)',
'Turtle Rock Isolated Ledge Entrance', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)',
'Hookshot Cave', 'Hookshot Cave Back Entrance', 'Ganons Tower', 'Spike Cave',
'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy']
if world.is_tile_swapped(0x1b, player):
east_dw_entrances.remove('Pyramid Entrance')
east_dw_entrances.remove('Pyramid Hole')
central_lw_entrances.append('Inverted Pyramid Entrance')
central_lw_entrances.append('Inverted Pyramid Hole')
districts['Kakariko'] = District('Kakariko', kak_locations, entrances=kak_entrances)
districts['Northwest Hyrule'] = District('Northwest Hyrule', nw_lw_locations, entrances=nw_lw_entrances)
districts['Central Hyrule'] = District('Central Hyrule', central_lw_locations, entrances=central_lw_entrances)
districts['The Desert Area'] = District('Desert', desert_locations, entrances=desert_entrances)
districts['Lake Hylia'] = District('Lake Hylia', lake_locations, entrances=lake_entrances)
districts['Eastern Hyrule'] = District('Eastern Hyrule', east_lw_locations, entrances=east_lw_entrances)
districts['Death Mountain'] = District('Death Mountain', lw_dm_locations, entrances=lw_dm_entrances)
districts['East Dark World'] = District('East Dark World', east_dw_locations, entrances=east_dw_entrances)
districts['South Dark World'] = District('South Dark World', south_dw_locations, entrances=south_dw_entrances)
districts['Northwest Dark World'] = District('Northwest Dark World', voo_north_locations,
entrances=voo_north_entrances)
districts['The Mire Area'] = District('The Mire', set(), entrances=mire_entrances)
districts['Dark Death Mountain'] = District('Dark Death Mountain', ddm_locations, entrances=ddm_entrances)
districts.update({x: District(x, set(), dungeon=x) for x in dungeon_table.keys()})
world.districts[player] = districts
def resolve_districts(world):
create_districts(world)
state = CollectionState(world)
state.sweep_for_events()
for player in range(1, world.players + 1):
# these are not static for OWR - but still important
inaccessible = [r for r in inaccessible_regions_std if not world.is_tile_swapped(OWTileRegions[r], player)]
inaccessible = inaccessible + [r for r in inaccessible_regions_inv if world.is_tile_swapped(OWTileRegions[r], player)]
check_set = find_reachable_locations(state, player)
for name, district in world.districts[player].items():
if district.dungeon:
layout = world.dungeon_layouts[player][district.dungeon]
district.locations.update([l.name for r in layout.master_sector.regions
for l in r.locations if not l.item and l.real])
else:
for entrance in district.entrances:
ent = world.get_entrance(entrance, player)
queue = deque([ent.connected_region])
visited = set()
while len(queue) > 0:
region = queue.pop()
visited.add(region)
if region.type == RegionType.Cave:
for location in region.locations:
if not location.item and location.real:
district.locations.add(location.name)
for ext in region.exits:
if ext.connected_region is not None and ext.connected_region not in visited:
queue.appendleft(ext.connected_region)
elif region.type == RegionType.Dungeon and region.dungeon:
district.dungeons.add(region.dungeon.name)
elif region.name in inaccessible:
district.access_points.add(region)
district.sphere_one = len(check_set.intersection(district.locations)) > 0
def find_reachable_locations(state, player):
check_set = set()
for region in state.reachable_regions[player]:
for location in region.locations:
if location.can_reach(state) and not location.forced_item and location.real:
check_set.add(location.name)
return check_set
inaccessible_regions_std = {'Desert Palace Stairs', 'Bumper Cave Ledge', 'Skull Woods Forest (West)',
'Dark Death Mountain Ledge', 'Dark Death Mountain Isolated Ledge',
'Dark Death Mountain Floating Island'}
inaccessible_regions_inv = {'Desert Palace Stairs', 'Maze Race Ledge', 'Desert Ledge',
'Desert Palace Entrance (North) Spot', 'Hyrule Castle Ledge', 'Mountain Entry Ledge'}

813
source/item/FillUtil.py Normal file
View File

@@ -0,0 +1,813 @@
import RaceRandom as random
import logging
from collections import defaultdict
from source.item.District import resolve_districts
from BaseClasses import PotItem, PotFlags
from DoorShuffle import validate_vanilla_reservation
from Dungeons import dungeon_table
from Items import item_table, ItemFactory
from PotShuffle import vanilla_pots
class ItemPoolConfig(object):
def __init__(self):
self.location_groups = None
self.static_placement = None
self.item_pool = None
self.placeholders = None
self.reserved_locations = defaultdict(set)
self.recorded_choices = []
class LocationGroup(object):
def __init__(self, name):
self.name = name
self.locations = []
# flags
self.keyshuffle = False
self.shopsanity = False
self.retro = False
def locs(self, locs):
self.locations = list(locs)
return self
def flags(self, k, s=False, r=False):
self.keyshuffle = k
self.shopsanity = s
self.retro = r
return self
def create_item_pool_config(world):
world.item_pool_config = config = ItemPoolConfig()
player_set = set()
for player in range(1, world.players+1):
if world.restrict_boss_items[player] != 'none':
player_set.add(player)
if world.restrict_boss_items[player] == 'dungeon':
for dungeon, info in dungeon_table.items():
if info.prize:
d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon
config.reserved_locations[player].add(f'{d_name} - Boss')
for dungeon in world.dungeons:
if world.restrict_boss_items[dungeon.player] != 'none':
for item in dungeon.all_items:
if item.map or item.compass:
item.advancement = True
if world.algorithm == 'vanilla_fill':
config.static_placement = {}
config.location_groups = {}
for player in range(1, world.players + 1):
config.static_placement[player] = defaultdict(list)
config.static_placement[player].update(vanilla_mapping)
if world.dropshuffle[player]:
for item, locs in keydrop_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
if world.pottery[player] not in ['none', 'cave']:
for item, locs in potkeys_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
if world.pottery[player] in ['lottery', 'cave', 'dungeon']:
for super_tile, pot_list in vanilla_pots.items():
for pot_index, pot in enumerate(pot_list):
if pot.item not in [PotItem.Key, PotItem.Hole, PotItem.Switch]:
item = pot_items[pot.item]
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
location = f'{pot.room} {descriptor}'
config.static_placement[player][item].append(location)
if world.shopsanity[player]:
for item, locs in shop_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
if world.retro[player]:
for item, locs in retro_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
# universal keys
universal_key_locations = []
for item, locs in vanilla_mapping.items():
if 'Small Key' in item:
universal_key_locations.extend(locs)
if world.dropshuffle[player]:
for item, locs in keydrop_vanilla_mapping.items():
if 'Small Key' in item:
universal_key_locations.extend(locs)
if world.pottery[player] not in ['none', 'cave']:
for item, locs in potkeys_vanilla_mapping.items():
universal_key_locations.extend(locs)
if world.shopsanity[player]:
single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
single_arrow_placement.append('Red Shield Shop - Right')
config.static_placement[player]['Single Arrow'] = single_arrow_placement
universal_key_locations.extend(shop_vanilla_mapping['Small Heart'])
universal_key_locations.extend(shop_vanilla_mapping['Blue Shield'])
config.static_placement[player]['Small Key (Universal)'] = universal_key_locations
config.location_groups[player] = [
LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']),
LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']),
LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']),
LocationGroup('bkgt').locs(mode_grouping['GT Trash'])]
for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']:
config.reserved_locations[player].add(loc_name)
elif world.algorithm == 'major_only':
config.location_groups = [
LocationGroup('MajorItems'),
LocationGroup('Backup')
]
config.item_pool = {}
init_set = mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']
for player in range(1, world.players + 1):
groups = LocationGroup('Major').locs(init_set)
if world.bigkeyshuffle[player]:
groups.locations.extend(mode_grouping['Big Keys'])
if world.dropshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Big Key Drops'])
if world.keyshuffle[player]:
groups.locations.extend(mode_grouping['Small Keys'])
if world.dropshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Key Drops'])
if world.pottery[player] not in ['none', 'cave']:
groups.locations.extend(mode_grouping['Pot Keys'])
if world.compassshuffle[player]:
groups.locations.extend(mode_grouping['Compasses'])
if world.mapshuffle[player]:
groups.locations.extend(mode_grouping['Maps'])
if world.shopsanity[player]:
groups.locations.append('Capacity Upgrade - Left')
groups.locations.append('Capacity Upgrade - Right')
if world.retro[player]:
if world.shopsanity[player]:
groups.locations.extend(retro_vanilla_mapping['Heart Container'])
groups.locations.append('Old Man Sword Cave Item 1')
config.item_pool[player] = determine_major_items(world, player)
config.location_groups[0].locations = set(groups.locations)
config.reserved_locations[player].update(groups.locations)
backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops']
+ mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops'])
config.location_groups[1].locations = set(backup)
elif world.algorithm == 'dungeon_only':
config.location_groups = [
LocationGroup('Dungeons'),
LocationGroup('Backup')
]
config.item_pool = {}
dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] +
mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] +
mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] +
mode_grouping['Big Key Drops'])
for player in range(1, world.players + 1):
config.item_pool[player] = determine_major_items(world, player)
config.location_groups[0].locations = set(dungeon_set)
backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major']
+ mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops'])
config.location_groups[1].locations = set(backup)
def district_item_pool_config(world):
resolve_districts(world)
if world.algorithm == 'district':
config = world.item_pool_config
config.location_groups = [
LocationGroup('Districts'),
]
item_cnt = 0
config.item_pool = {}
for player in range(1, world.players + 1):
config.item_pool[player] = determine_major_items(world, player)
item_cnt += count_major_items(config, world, player)
# set district choices
district_choices = {}
for p in range(1, world.players + 1):
for name, district in world.districts[p].items():
adjustment = 0
if district.dungeon:
adjustment = len([i for i in world.get_dungeon(name, p).all_items
if i.is_inside_dungeon_item(world)])
dist_len = len(district.locations) - adjustment
if name not in district_choices:
district_choices[name] = (district.sphere_one, dist_len)
else:
so, amt = district_choices[name]
district_choices[name] = (so or district.sphere_one, amt + dist_len)
chosen_locations = defaultdict(set)
location_cnt = 0
# choose a sphere one district
sphere_one_choices = [d for d, info in district_choices.items() if info[0]]
sphere_one = random.choice(sphere_one_choices)
so, amt = district_choices[sphere_one]
location_cnt += amt
for player in range(1, world.players + 1):
for location in world.districts[player][sphere_one].locations:
chosen_locations[location].add(player)
del district_choices[sphere_one]
config.recorded_choices.append(sphere_one)
scale_factors = defaultdict(int)
scale_total = 0
for p in range(1, world.players + 1):
ent = 'Agahnims Tower' if world.is_atgt_swapped(player) else 'Ganons Tower'
dungeon = world.get_entrance(ent, p).connected_region.dungeon
if dungeon:
scale = world.crystals_needed_for_gt[p]
scale_total += scale
scale_factors[dungeon.name] += scale
scale_total = max(1, scale_total)
scale_divisors = defaultdict(lambda: 1)
scale_divisors.update(scale_factors)
while location_cnt < item_cnt:
weights = [scale_total / scale_divisors[d] for d in district_choices.keys()]
choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0]
so, amt = district_choices[choice]
location_cnt += amt
for player in range(1, world.players + 1):
for location in world.districts[player][choice].locations:
chosen_locations[location].add(player)
del district_choices[choice]
config.recorded_choices.append(choice)
config.placeholders = location_cnt - item_cnt
config.location_groups[0].locations = chosen_locations
def location_prefilled(location, world, player):
if world.swords[player] == 'vanilla':
return location in vanilla_swords
if world.goal[player] in ['pedestal', 'trinity']:
return location == 'Master Sword Pedestal'
return False
def previously_reserved(location, world, player):
if '- Boss' in location.name:
if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player]
or not world.mapshuffle[player]):
return True
if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player]
or not world.mapshuffle[player]
or not world.bigkeyshuffle[player]
or not (world.keyshuffle[player] or world.retro[player])):
return True
return False
def massage_item_pool(world):
player_pool = defaultdict(list)
for item in world.itempool:
player_pool[item.player].append(item)
for dungeon in world.dungeons:
for item in dungeon.all_items:
if item.is_inside_dungeon_item(world):
player_pool[item.player].append(item)
player_locations = defaultdict(list)
for player in player_pool:
player_locations[player] = [x for x in world.get_unfilled_locations(player) if '- Prize' not in x.name]
discrepancy = len(player_pool[player]) - len(player_locations[player])
if discrepancy:
trash_options = [x for x in player_pool[player] if x.name in trash_items]
random.shuffle(trash_options)
trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True)
while discrepancy > 0 and len(trash_options) > 0:
deleted = trash_options.pop()
world.itempool.remove(deleted)
discrepancy -= 1
if discrepancy > 0:
raise Exception(f'Too many required items in pool, {discrepancy} items cannot be placed')
if world.item_pool_config.placeholders is not None:
removed = 0
single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)']
removed += len(single_rupees)
for x in single_rupees:
world.itempool.remove(x)
if removed < world.item_pool_config.placeholders:
trash_options = [x for x in world.itempool if x.name in trash_items]
random.shuffle(trash_options)
trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True)
while removed < world.item_pool_config.placeholders:
if len(trash_options) == 0:
logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders')
deleted = trash_options.pop()
world.itempool.remove(deleted)
removed += 1
if world.item_pool_config.placeholders > len(single_rupees):
for _ in range(world.item_pool_config.placeholders-len(single_rupees)):
single_rupees.append(ItemFactory('Rupee (1)', random.randint(1, world.players)))
placeholders = random.sample(single_rupees, world.item_pool_config.placeholders)
world.itempool += placeholders
removed -= len(placeholders)
for _ in range(removed):
world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players)))
def replace_trash_item(item_pool, replacement):
trash_options = [x for x in item_pool if x.name in trash_items]
random.shuffle(trash_options)
trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True)
if len(trash_options) == 0:
logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders')
deleted = trash_options.pop()
item_pool.remove(deleted)
replace_item = ItemFactory(replacement, deleted.player)
item_pool.append(replace_item)
return replace_item
def validate_reservation(location, dungeon, world, player):
world.item_pool_config.reserved_locations[player].add(location.name)
if world.doorShuffle[player] != 'vanilla':
return True # we can generate the dungeon somehow most likely
if validate_vanilla_reservation(dungeon, world, player):
return True
world.item_pool_config.reserved_locations[player].remove(location.name)
return False
def count_major_items(config, world, player):
return sum(1 for x in world.itempool if x.name in config.item_pool[player] and x.player == player)
def determine_major_items(world, player):
major_item_set = set(major_items)
if world.progressive == 'off':
pass # now what?
if world.bigkeyshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'})
if world.keyshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'})
if world.compassshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'})
if world.mapshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'Map'})
if world.shopsanity[player]:
major_item_set.add('Bomb Upgrade (+5)')
major_item_set.add('Arrow Upgrade (+5)')
if world.retro[player]:
major_item_set.add('Single Arrow')
major_item_set.add('Small Key (Universal)')
if world.goal in ['triforcehunt', 'trinity']:
major_item_set.add('Triforce Piece')
if world.bombbag[player]:
major_item_set.add('Bomb Upgrade (+10)')
return major_item_set
def classify_major_items(world):
if world.algorithm in ['major_only', 'dungeon_only', 'district']:
config = world.item_pool_config
for item in world.itempool:
if item.name in config.item_pool[item.player]:
if not item.advancement and not item.priority:
if item.smallkey or item.bigkey:
item.advancement = True
else:
item.priority = True
else:
if item.priority:
item.priority = False
def vanilla_fallback(item_to_place, locations, world):
if item_to_place.is_inside_dungeon_item(world):
return [x for x in locations if x.name in vanilla_fallback_dungeon_set
and x.parent_region.dungeon and x.parent_region.dungeon.name == item_to_place.dungeon]
return []
def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion=False):
if world.algorithm == 'vanilla_fill':
config, filtered = world.item_pool_config, []
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
if item_name in config.static_placement[item_to_place.player]:
restricted = config.static_placement[item_to_place.player][item_name]
filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted]
if vanilla_skip and len(filtered) == 0:
return filtered
i = 0
while len(filtered) <= 0:
if i >= len(config.location_groups[item_to_place.player]):
return locations
restricted = config.location_groups[item_to_place.player][i].locations
filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted]
i += 1
return filtered
if world.algorithm in ['major_only', 'dungeon_only']:
config = world.item_pool_config
if item_to_place.name in config.item_pool[item_to_place.player]:
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name in restricted]
if len(filtered) == 0:
restricted = config.location_groups[1].locations
filtered = [l for l in locations if l.name in restricted]
# bias toward certain location in overflow? (thinking about this for major_bias)
return filtered if len(filtered) > 0 else locations
if world.algorithm == 'district':
config = world.item_pool_config
if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]:
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name in restricted and l.player in restricted[l.name]]
return filtered if len(filtered) > 0 else locations
elif potion:
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name not in restricted or l.player not in restricted[l.name]]
if len(filtered) == 0:
raise RuntimeError('Can\'t sell potion of a certain type due to district restriction')
return filtered
return locations
def filter_pot_locations(locations, world):
if world.algorithm == 'district':
config = world.item_pool_config
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name not in restricted or l.player not in restricted[l.name]]
return filtered if len(filtered) > 0 else locations
if world.algorithm == 'vanilla_fill':
filtered = [l for l in locations if l.pot and l.pot.item in [PotItem.Chicken, PotItem.BigMagic]]
return filtered if len(filtered) > 0 else locations
return locations
vanilla_mapping = {
'Green Pendant': ['Eastern Palace - Prize'],
'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],
'Blue Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],
'Crystal 1': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 2': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 3': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 4': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 7': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 5': ['Ice Palace - Prize', 'Misery Mire - Prize'],
'Crystal 6': ['Ice Palace - Prize', 'Misery Mire - Prize'],
'Bow': ['Eastern Palace - Big Chest'],
'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Right'],
'Book of Mudora': ['Library'],
'Hammer': ['Palace of Darkness - Big Chest'],
'Hookshot': ['Swamp Palace - Big Chest'],
'Magic Mirror': ['Old Man'],
'Ocarina': ['Flute Spot'],
'Ocarina (Activated)': ['Flute Spot'],
'Pegasus Boots': ['Sahasrahla'],
'Power Glove': ['Desert Palace - Big Chest'],
'Cape': ["King's Tomb"],
'Mushroom': ['Mushroom'],
'Shovel': ['Stumpy'],
'Lamp': ["Link's House"],
'Magic Powder': ['Potion Shop'],
'Moon Pearl': ['Tower of Hera - Big Chest'],
'Cane of Somaria': ['Misery Mire - Big Chest'],
'Fire Rod': ['Skull Woods - Big Chest'],
'Flippers': ['King Zora'],
'Ice Rod': ['Ice Rod Cave'],
'Titans Mitts': ["Thieves' Town - Big Chest"],
'Bombos': ['Bombos Tablet'],
'Ether': ['Ether Tablet'],
'Quake': ['Catfish'],
'Bottle': ['Bottle Merchant', 'Kakariko Tavern', 'Purple Chest', 'Hobo'],
'Master Sword': ['Master Sword Pedestal'],
'Tempered Sword': ['Blacksmith'],
'Fighter Sword': ["Link's Uncle"],
'Golden Sword': ['Pyramid Fairy - Left'],
'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Left'],
'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"],
'Silver Arrows': ['Pyramid Fairy - Right'],
'Single Arrow': ['Palace of Darkness - Dark Basement - Left'],
'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right',
'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
"Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left'],
'Bombs (3)': ['Floodgate Chest', "Sahasrahla's Hut - Middle", 'Kakariko Well - Bottom', 'Superbunny Cave - Top',
'Mini Moldorm Cave - Far Left', 'Sewers - Secret Room - Left', 'Paradox Cave Upper - Left',
"Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Maze - Top',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - DMs Room - Top Left',
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left',
'Ganons Tower - Mini Helmasaur Room - Right'],
'Blue Mail': ['Ice Palace - Big Chest'],
'Red Mail': ['Ganons Tower - Big Chest'],
'Progressive Armor': ['Ice Palace - Big Chest', 'Ganons Tower - Big Chest'],
'Blue Boomerang': ['Hyrule Castle - Boomerang Chest'],
'Red Boomerang': ['Waterfall Fairy - Left'],
'Blue Shield': ['Secret Passage'],
'Red Shield': ['Waterfall Fairy - Right'],
'Mirror Shield': ['Turtle Rock - Big Chest'],
'Progressive Shield': ['Secret Passage', 'Waterfall Fairy - Right', 'Turtle Rock - Big Chest'],
'Bug Catching Net': ['Sick Kid'],
'Cane of Byrna': ['Spike Cave'],
'Boss Heart Container': ['Desert Palace - Boss', 'Eastern Palace - Boss', 'Tower of Hera - Boss',
'Swamp Palace - Boss', "Thieves' Town - Boss", 'Skull Woods - Boss', 'Ice Palace - Boss',
'Misery Mire - Boss', 'Turtle Rock - Boss', 'Palace of Darkness - Boss'],
'Sanctuary Heart Container': ['Sanctuary'],
'Piece of Heart': ['Sunken Treasure', "Blind's Hideout - Top", "Zora's Ledge", "Aginah's Cave", 'Maze Race',
'Kakariko Well - Top', 'Lost Woods Hideout', 'Lumberjack Tree', 'Cave 45', 'Graveyard Cave',
'Checkerboard Cave', 'Bonk Rock Cave', 'Lake Hylia Island', 'Desert Ledge', 'Spectacle Rock',
'Spectacle Rock Cave', 'Pyramid', 'Digging Game', 'Peg Cave', 'Chest Game', 'Bumper Cave Ledge',
'Mire Shed - Left', 'Floating Island', 'Mimic Cave'],
'Rupee (1)': ['Turtle Rock - Eye Bridge - Top Right', 'Ganons Tower - Compass Room - Top Right'],
'Rupees (5)': ["Hyrule Castle - Zelda's Chest", 'Turtle Rock - Eye Bridge - Top Left',
# 'Palace of Darkness - Harmless Hellway',
'Palace of Darkness - Dark Maze - Bottom',
'Ganons Tower - Validation Chest'],
'Rupees (20)': ["Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left",
"Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle',
'Kakariko Well - Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', 'Hype Cave - Top',
'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom',
'Swamp Palace - West Chest', 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Waterfall Room',
'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest",
'Turtle Rock - Eye Bridge - Bottom Right', 'Ganons Tower - Compass Room - Bottom Left',
'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest",
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
'Rupees (50)': ["Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", 'Spiral Cave', 'Superbunny Cave - Bottom',
'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
'Rupees (100)': ['Eastern Palace - Cannonball Chest'],
'Rupees (300)': ['Mini Moldorm Cave - Generous Guy', 'Sewers - Secret Room - Middle', 'Hype Cave - Generous Guy',
'Brewery', 'C-Shaped House'],
'Magic Upgrade (1/2)': ['Magic Bat'],
'Big Key (Eastern Palace)': ['Eastern Palace - Big Key Chest'],
'Compass (Eastern Palace)': ['Eastern Palace - Compass Chest'],
'Map (Eastern Palace)': ['Eastern Palace - Map Chest'],
'Small Key (Desert Palace)': ['Desert Palace - Torch'],
'Big Key (Desert Palace)': ['Desert Palace - Big Key Chest'],
'Compass (Desert Palace)': ['Desert Palace - Compass Chest'],
'Map (Desert Palace)': ['Desert Palace - Map Chest'],
'Small Key (Tower of Hera)': ['Tower of Hera - Basement Cage'],
'Big Key (Tower of Hera)': ['Tower of Hera - Big Key Chest'],
'Compass (Tower of Hera)': ['Tower of Hera - Compass Chest'],
'Map (Tower of Hera)': ['Tower of Hera - Map Chest'],
'Small Key (Escape)': ['Sewers - Dark Cross'],
'Map (Escape)': ['Hyrule Castle - Map Chest'],
'Small Key (Agahnims Tower)': ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
'Small Key (Palace of Darkness)': ['Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge',
'Palace of Darkness - Stalfos Basement',
'Palace of Darkness - The Arena - Ledge',
'Palace of Darkness - Dark Basement - Right',
'Palace of Darkness - Harmless Hellway'],
# 'Palace of Darkness - Dark Maze - Bottom'],
'Big Key (Palace of Darkness)': ['Palace of Darkness - Big Key Chest'],
'Compass (Palace of Darkness)': ['Palace of Darkness - Compass Chest'],
'Map (Palace of Darkness)': ['Palace of Darkness - Map Chest'],
'Small Key (Thieves Town)': ["Thieves' Town - Blind's Cell"],
'Big Key (Thieves Town)': ["Thieves' Town - Big Key Chest"],
'Compass (Thieves Town)': ["Thieves' Town - Compass Chest"],
'Map (Thieves Town)': ["Thieves' Town - Map Chest"],
'Small Key (Skull Woods)': ['Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Skull Woods - Bridge Room'],
'Big Key (Skull Woods)': ['Skull Woods - Big Key Chest'],
'Compass (Skull Woods)': ['Skull Woods - Compass Chest'],
'Map (Skull Woods)': ['Skull Woods - Map Chest'],
'Small Key (Swamp Palace)': ['Swamp Palace - Entrance'],
'Big Key (Swamp Palace)': ['Swamp Palace - Big Key Chest'],
'Compass (Swamp Palace)': ['Swamp Palace - Compass Chest'],
'Map (Swamp Palace)': ['Swamp Palace - Map Chest'],
'Small Key (Ice Palace)': ['Ice Palace - Iced T Room', 'Ice Palace - Spike Room'],
'Big Key (Ice Palace)': ['Ice Palace - Big Key Chest'],
'Compass (Ice Palace)': ['Ice Palace - Compass Chest'],
'Map (Ice Palace)': ['Ice Palace - Map Chest'],
'Small Key (Misery Mire)': ['Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
'Big Key (Misery Mire)': ['Misery Mire - Big Key Chest'],
'Compass (Misery Mire)': ['Misery Mire - Compass Chest'],
'Map (Misery Mire)': ['Misery Mire - Map Chest'],
'Small Key (Turtle Rock)': ['Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps',
'Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left'],
'Big Key (Turtle Rock)': ['Turtle Rock - Big Key Chest'],
'Compass (Turtle Rock)': ['Turtle Rock - Compass Chest'],
'Map (Turtle Rock)': ['Turtle Rock - Roller Room - Left'],
'Small Key (Ganons Tower)': ["Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room',
'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'],
'Big Key (Ganons Tower)': ['Ganons Tower - Big Key Chest'],
'Compass (Ganons Tower)': ['Ganons Tower - Compass Room - Top Left'],
'Map (Ganons Tower)': ['Ganons Tower - Map Chest']
}
keydrop_vanilla_mapping = {
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Eyegore Key Drop'],
'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop'],
'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'],
'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'],
'Small Key (Skull Woods)': ['Skull Woods - Spike Corner Key Drop'],
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop'],
'Small Key (Misery Mire)': ['Misery Mire - Conveyor Crystal Key Drop'],
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
'Small Key (Ganons Tower)': ['Ganons Tower - Mini Helmasaur Key Drop'],
}
potkeys_vanilla_mapping = {
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key'],
'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"],
'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key'],
'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key',
'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key',
'Swamp Palace - Waterway Pot Key'],
'Small Key (Ice Palace)': ['Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'],
'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key'],
'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key',
'Ganons Tower - Conveyor Star Pits Pot Key'],
}
shop_vanilla_mapping = {
'Red Potion': ['Dark Death Mountain Shop - Left', 'Dark Lake Hylia Shop - Left', 'Dark Lumberjack Shop - Left',
'Village of Outcasts Shop - Left', 'Dark Potion Shop - Left', 'Paradox Shop - Left',
'Kakariko Shop - Left', 'Lake Hylia Shop - Left', 'Potion Shop - Left'],
'Small Heart': ['Dark Death Mountain Shop - Middle', 'Paradox Shop - Middle', 'Kakariko Shop - Middle',
'Lake Hylia Shop - Middle'],
'Bombs (10)': ['Dark Death Mountain Shop - Right', 'Dark Lake Hylia Shop - Right', 'Dark Lumberjack Shop - Right',
'Village of Outcasts Shop - Right', 'Dark Potion Shop - Right', 'Paradox Shop - Right',
'Kakariko Shop - Right', 'Lake Hylia Shop - Right'],
'Blue Shield': ['Dark Lake Hylia Shop - Middle', 'Dark Lumberjack Shop - Middle',
'Village of Outcasts Shop - Middle', 'Dark Potion Shop - Middle'],
'Red Shield': ['Red Shield Shop - Left'],
'Bee': ['Red Shield Shop - Middle'],
'Arrows (10)': ['Red Shield Shop - Right'],
'Bomb Upgrade (+5)': ['Capacity Upgrade - Left'],
'Arrow Upgrade (+5)': ['Capacity Upgrade - Right'],
'Blue Potion': ['Potion Shop - Right'],
'Green Potion': ['Potion Shop - Middle'],
}
retro_vanilla_mapping = {
'Heart Container': ['Take-Any #1 Item 1', 'Take-Any #2 Item 1', 'Take-Any #3 Item 1', 'Take-Any #4 Item 1'],
'Blue Potion': ['Take-Any #1 Item 2', 'Take-Any #2 Item 2', 'Take-Any #3 Item 2', 'Take-Any #4 Item 2'],
'Progressive Sword': ['Old Man Sword Cave Item 1']
}
mode_grouping = {
'Overworld Major': [
"Link's Uncle", 'King Zora', "Link's House", 'Sahasrahla', 'Ice Rod Cave', 'Library',
'Master Sword Pedestal', 'Old Man', 'Ether Tablet', 'Catfish', 'Stumpy', 'Bombos Tablet', 'Mushroom',
'Bottle Merchant', 'Kakariko Tavern', 'Secret Passage', 'Flute Spot', 'Purple Chest',
'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', 'Magic Bat', 'Sick Kid', 'Hobo',
'Potion Shop', 'Spike Cave', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', "King's Tomb",
],
'Big Chests': ['Eastern Palace - Big Chest','Desert Palace - Big Chest', 'Tower of Hera - Big Chest',
'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest',
"Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest',
'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest'],
'Heart Containers': ['Sanctuary', 'Eastern Palace - Boss','Desert Palace - Boss', 'Tower of Hera - Boss',
'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss',
"Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss'],
'Heart Pieces': [
'Bumper Cave Ledge', 'Desert Ledge', 'Lake Hylia Island', 'Floating Island',
'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree',
'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45',
'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave",
'Chest Game', 'Digging Game', 'Mire Shed - Left', 'Mimic Cave'
],
'Big Keys': [
'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest',
'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest',
'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest',
'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest',
],
'Compasses': [
'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest',
'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest',
"Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest',
'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left'
],
'Maps': [
'Hyrule Castle - Map Chest', 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest',
'Tower of Hera - Map Chest', 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest',
'Skull Woods - Map Chest', "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest',
'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest'
],
'Small Keys': [
'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage',
'Castle Tower - Room 03', 'Castle Tower - Dark Maze',
'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right',
'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Shooter Room',
'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - The Arena - Ledge',
"Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room',
'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Misery Mire - Spike Chest',
'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Swamp Palace - Entrance',
'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Roller Room - Right',
'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room',
'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'
],
'Dungeon Trash': [
'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
"Hyrule Castle - Zelda's Chest", 'Eastern Palace - Cannonball Chest', "Thieves' Town - Ambush Chest",
"Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Basement - Left',
'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Dark Maze - Top',
'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', 'Swamp Palace - Waterfall Room',
'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
'Turtle Rock - Eye Bridge - Top Right', 'Swamp Palace - West Chest',
],
'Overworld Trash': [
"Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left",
"Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right',
'Kakariko Well - Bottom', 'Chicken House', 'Floodgate Chest', 'Mini Moldorm Cave - Left',
'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left',
'Mini Moldorm Cave - Far Right', "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right",
"Sahasrahla's Hut - Middle", 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle',
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House',
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom',
'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right',
'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Right'
],
'GT Trash': [
'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Randomizer Room - Top Left',
'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Right',
'Ganons Tower - Randomizer Room - Bottom Left', "Ganons Tower - Bob's Chest",
'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right',
'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Validation Chest',
],
'Key Drops': [
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Eyegore Key Drop',
'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop',
'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop',
'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Mini Helmasuar Key Drop',
],
'Pot Keys': [
'Eastern Palace - Dark Square Pot Key', 'Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key',
"Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key",
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key',
'Misery Mire - Fishbone Pot Key', 'Ganons Tower - Conveyor Cross Pot Key',
'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key',
],
'Big Key Drops': ['Hyrule Castle - Big Key Drop'],
'Shops': [
'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right',
'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Dark Lake Hylia Shop - Left',
'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Dark Lumberjack Shop - Left',
'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', 'Village of Outcasts Shop - Left',
'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left',
'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle',
'Paradox Shop - Right', 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right',
'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Left',
'Capacity Upgrade - Right'
],
'RetroShops': [
'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', 'Take-Any #2 Item 1',
'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2','Take-Any #4 Item 1', 'Take-Any #4 Item 2'
]
}
vanilla_fallback_dungeon_set = set(mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] +
mode_grouping['GT Trash'] + mode_grouping['Small Keys'] +
mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] +
mode_grouping['Big Key Drops'])
major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer',
'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel',
'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove',
'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror',
'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)',
'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Ocarina (Activated)',
'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword',
'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl',
'Progressive Bow', 'Progressive Bow (Alt)'}
vanilla_swords = {"Link's Uncle", 'Master Sword Pedestal', 'Blacksmith', 'Pyramid Fairy - Left'}
trash_items = {
'Nothing': -1,
'Bee Trap': 0,
'Rupee (1)': 1, 'Rupees (5)': 1, 'Small Heart': 1, 'Bee': 1, 'Arrows (5)': 1, 'Chicken': 1, 'Single Bomb': 1,
'Rupees (20)': 2, 'Small Magic': 2,
'Bombs (3)': 3, 'Arrows (10)': 3, 'Bombs (10)': 3, 'Apples': 3,
'Fairy': 4, 'Big Magic': 4, 'Red Potion': 4, 'Blue Shield': 4, 'Rupees (50)': 4, 'Rupees (100)': 4,
'Rupees (300)': 5,
'Piece of Heart': 17
}
pot_items = {
PotItem.Nothing: 'Nothing',
PotItem.Bomb: 'Single Bomb',
PotItem.FiveArrows: 'Arrows (5)', # convert to 10
PotItem.OneRupee: 'Rupee (1)',
PotItem.FiveRupees: 'Rupees (5)',
PotItem.Heart: 'Small Heart',
PotItem.BigMagic: 'Big Magic', # fast fill
PotItem.SmallMagic: 'Small Magic',
PotItem.Chicken: 'Chicken' # fast fill
}
valid_pot_items = {y: x for x, y in pot_items.items()}

View File

@@ -22,7 +22,6 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.
subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
shell=True)

View File

@@ -22,7 +22,6 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.
subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
shell=True)

View File

@@ -0,0 +1,41 @@
import importlib.util
import webbrowser
from tkinter import Tk, Label, Button, Frame
def check_requirements(console=False):
check_packages = {'aenum': 'aenum',
'fast-enum': 'fast_enum',
'python-bps-continued': 'bps',
'colorama': 'colorama',
'aioconsole' : 'aioconsole',
'websockets' : 'websockets',
'pyyaml': 'yaml'}
missing = []
for package, import_name in check_packages.items():
spec = importlib.util.find_spec(import_name)
if spec is None:
missing.append(package)
if len(missing) > 0:
packages = ','.join(missing)
if console:
import logging
logger = logging.getLogger('')
logger.error('You need to install the following python packages:')
logger.error(f'{packages}')
logger.error('See the step about "Installing Platform-specific dependencies":')
logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md')
else:
master = Tk()
master.title('Error')
frame = Frame(master)
frame.pack(expand=True, padx =50, pady=50)
Label(frame, text='You need to install the following python packages:').pack()
Label(frame, text=f'{packages}').pack()
Label(frame, text='See the step about "Installing Platform-specific dependencies":').pack()
url = 'https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md'
link = Label(frame, fg='blue', cursor='hand2', text=url)
link.pack()
link.bind('<Button-1>', lambda e: webbrowser.open_new_tab(url))
Button(master, text='Ok', command=master.destroy).pack()
master.mainloop()

View File

@@ -0,0 +1,124 @@
import subprocess
import sys
import multiprocessing
import concurrent.futures
import argparse
from collections import OrderedDict
cpu_threads = multiprocessing.cpu_count()
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
def main(args=None):
successes = []
errors = []
task_mapping = []
tests = OrderedDict()
successes.append(f"Testing {args.dr} DR with {args.count} Tests" + (f" (intensity={args.tense})" if args.dr in ['basic', 'crossed'] else ""))
print(successes[0])
max_attempts = args.count
pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads)
dead_or_alive = 0
alive = 0
def test(testname: str, command: str):
tests[testname] = [command]
basecommand = f"python3.8 Mystery.py --suppress_rom --suppress_meta"
def gen_seed():
taskcommand = basecommand + " " + command
return subprocess.run(taskcommand, capture_output=True, shell=True, text=True)
for x in range(1, max_attempts + 1):
task = pool.submit(gen_seed)
task.success = False
task.name = testname
task.mode = "Mystery"
task.cmd = basecommand + " " + command
task_mapping.append(task)
for i in range(0, 100):
test("Mystery", "--weights mystery_testsuite.yml")
from tqdm import tqdm
with tqdm(concurrent.futures.as_completed(task_mapping),
total=len(task_mapping), unit="seed(s)",
desc=f"Success rate: 0.00%") as progressbar:
for task in progressbar:
dead_or_alive += 1
try:
result = task.result()
if result.returncode:
errors.append([task.name, task.cmd, result.stderr])
else:
alive += 1
task.success = True
except Exception as e:
raise e
progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}")
def get_results(testname: str):
result = ""
for mode in ['Mystery']:
dead_or_alive = [task.success for task in task_mapping if task.name == testname and task.mode == mode]
alive = [x for x in dead_or_alive if x]
success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%"
successes.append(success)
print(success)
result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t"
return result.strip()
results = []
for t in tests.keys():
results.append(get_results(t))
for result in results:
print(result)
successes.append(result)
return successes, errors
if __name__ == "__main__":
successes = []
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0))
parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1))
parser.add_argument('--help', default=False, action='store_true')
args = parser.parse_args()
if args.help:
parser.print_help()
exit(0)
cpu_threads = args.cpu_threads
for dr in [['mystery', args.count if args.count else 1, 1]]:
for tense in range(1, dr[2] + 1):
args = argparse.Namespace()
args.dr = dr[0]
args.tense = tense
args.count = dr[1]
s, errors = main(args=args)
if successes:
successes += [""] * 2
successes += s
print()
if errors:
with open(f"{dr[0]}{(f'-{tense}' if dr[0] in ['basic', 'crossed'] else '')}-errors.txt", 'w') as stream:
for error in errors:
stream.write(error[0] + "\n")
stream.write(error[1] + "\n")
stream.write(error[2] + "\n\n")
with open("success.txt", "w") as stream:
stream.write(str.join("\n", successes))
input("Press enter to continue")

0
source/test/__init__.py Normal file
View File

324
source/tools/BPS.py Normal file
View File

@@ -0,0 +1,324 @@
# Code derived from https://github.com/marcrobledo/RomPatcher.js (MIT License)
import sys
from time import perf_counter
from collections import defaultdict
from binascii import crc32
try:
from fast_enum import FastEnum
except ImportError:
from enum import IntFlag as FastEnum
def bps_get_vlv_len(data):
length = 0
while True:
x = data & 0x7f
data >>= 7
if data == 0:
length += 1
break
length += 1
data -= 1
return length
def bps_read_vlv(stream):
data, shift = 0, 1
while True:
x = stream.read(1)[0]
data += (x & 0x7f) * shift
if x & 0x80:
return data
shift <<= 7
data += shift
class Bps:
def __init__(self):
self.source_size = 0
self.target_size = 0
self.metadata = ''
self.actions = []
self.source_checksum = 0
self.target_checksum = 0
self.patch_checksum = 0
self.binary_ba = bytearray()
self.offset = 0
def write_to_binary(self):
patch_size = 4
patch_size += bps_get_vlv_len(self.source_size)
patch_size += bps_get_vlv_len(self.target_size)
patch_size += bps_get_vlv_len(len(self.metadata))
patch_size += len(self.metadata)
for action in self.actions:
mode, length, data = action
patch_size += bps_get_vlv_len(((length - 1) << 2) + mode)
if mode == BpsMode.BPS_ACTION_TARGET_READ:
patch_size += length
elif mode == BpsMode.BPS_ACTION_SOURCE_COPY or mode == BpsMode.BPS_ACTION_TARGET_COPY:
patch_size += bps_get_vlv_len((abs(data) << 1) + (1 if data < 0 else 0))
patch_size += 12
self.binary_ba = bytearray(patch_size)
self.write_string('BPS1')
self.bps_write_vlv(self.source_size)
self.bps_write_vlv(self.target_size)
self.bps_write_vlv(len(self.metadata))
self.write_string(self.metadata)
for action in self.actions:
mode, length, data = action
self.bps_write_vlv(((length - 1) << 2) + mode)
if mode == BpsMode.BPS_ACTION_TARGET_READ:
self.write_bytes(data)
elif mode == BpsMode.BPS_ACTION_SOURCE_COPY or mode == BpsMode.BPS_ACTION_TARGET_COPY:
self.bps_write_vlv((abs(data) << 1) + (1 if data < 0 else 0))
self.write_u32(self.source_checksum)
self.write_u32(self.target_checksum)
self.write_u32(self.patch_checksum)
def write_string(self, string):
for ch in string:
self.binary_ba[self.offset] = ord(ch)
self.offset += 1
def write_byte(self, byte):
self.binary_ba[self.offset] = byte
self.offset += 1
def write_bytes(self, m_bytes):
for byte in m_bytes:
self.binary_ba[self.offset] = byte
self.offset += 1
def write_u32(self, data):
self.binary_ba[self.offset] = data & 0x000000ff
self.binary_ba[self.offset+1] = (data & 0x0000ff00) >> 8
self.binary_ba[self.offset+2] = (data & 0x00ff0000) >> 16
self.binary_ba[self.offset+3] = (data & 0xff000000) >> 24
self.offset += 4
def bps_write_vlv(self, data):
while True:
x = data & 0x7f
data >>= 7
if data == 0:
self.write_byte(0x80 | x)
break
self.write_byte(x)
data -= 1
class BpsMode(FastEnum):
BPS_ACTION_SOURCE_READ = 0
BPS_ACTION_TARGET_READ = 1
BPS_ACTION_SOURCE_COPY = 2
BPS_ACTION_TARGET_COPY = 3
def create_bps_from_data(original, modified):
patch = Bps()
patch.source_size = len(original)
patch.target_size = len(modified)
patch.actions = create_bps_linear(original, modified)
patch.source_checksum = crc32(original)
patch.target_checksum = crc32(modified)
patch.write_to_binary()
patch.patch_checksum = crc32(patch.binary_ba[:-4])
patch.offset = len(patch.binary_ba) - 4
patch.write_u32(patch.patch_checksum)
return patch
def create_bps_delta(original, modified):
patch_actions = []
source_data = original
target_data = modified
source_size = len(original)
target_size = len(modified)
source_relative_offset = 0
target_relative_offset = 0
output_offset = 0
source_tree = defaultdict(list)
source_tree_2 = defaultdict(list)
target_tree = defaultdict(list)
t1_start = perf_counter()
for offset in range(0, source_size):
symbol = source_data[offset]
if offset < source_size - 1:
symbol |= source_data[offset + 1] << 8
source_tree[symbol].append(offset)
print(f'Elasped Time 1: {perf_counter()-t1_start}')
source_array = list(source_data)
t2_start = perf_counter()
for offset in range(0, source_size):
symbol = source_array[offset]
if offset < source_size - 1:
symbol |= source_array[offset + 1] << 8
source_tree_2[symbol].append(offset)
print(f'Elasped Time 2: {perf_counter()-t2_start}')
trl = {'target_read_length': 0}
def target_read_flush(buffer):
if buffer['target_read_length']:
action = (BpsMode.BPS_ACTION_TARGET_READ, buffer['target_read_length'], [])
patch_actions.append(action)
offset = output_offset - buffer['target_read_length']
while buffer['target_read_length']:
action[2].append(target_data[offset])
offset += 1
buffer['target_read_length'] -= 1
while output_offset < target_size:
max_length, max_offset, mode = 0, 0, BpsMode.BPS_ACTION_TARGET_READ
symbol = target_data[output_offset]
if output_offset < target_size - 1:
symbol |= target_data[output_offset + 1] << 8
# source read
length, offset = 0, output_offset
while offset < source_size and offset < target_size and source_data[offset] == target_data[offset]:
length += 1
offset += 1
if length > max_length:
max_length, mode = length, BpsMode.BPS_ACTION_SOURCE_READ
# source copy
for node in source_tree[symbol]:
length, x, y = 0, node, output_offset
while x < source_size and y < target_size and source_data[x] == target_data[y]:
length += 1
x += 1
y += 1
if length > max_length:
max_length, max_offset, mode = length, node, BpsMode.BPS_ACTION_SOURCE_COPY
# target copy
for node in target_tree[symbol]:
length, x, y = 0, node, output_offset
while y < target_size and target_data[x] == target_data[y]:
length += 1
x += 1
y += 1
if length > max_length:
max_length, max_offset, mode = length, node, BpsMode.BPS_ACTION_TARGET_COPY
target_tree[symbol].append(output_offset)
# target read
if max_length < 4:
max_length = min(1, target_size - output_offset)
mode = BpsMode.BPS_ACTION_TARGET_READ
if mode != BpsMode.BPS_ACTION_TARGET_READ:
target_read_flush(trl)
if mode == BpsMode.BPS_ACTION_SOURCE_READ:
patch_actions.append((mode, max_length, None))
elif mode == BpsMode.BPS_ACTION_TARGET_READ:
trl['target_read_length'] += max_length
else:
if mode == BpsMode.BPS_ACTION_SOURCE_COPY:
relative_offset = max_offset - source_relative_offset
source_relative_offset = max_offset + max_length
else:
relative_offset = max_offset - target_relative_offset
target_relative_offset = max_offset + max_length
patch_actions.append((mode, max_length, relative_offset))
output_offset += max_length
target_read_flush(trl)
return patch_actions
def create_bps_linear(original, modified):
patch_actions = []
source_data = original
target_data = modified
source_size = len(original)
target_size = len(modified)
target_relative_offset = 0
output_offset = 0
trl = {'target_read_length': 0}
def target_read_flush(buffer):
if buffer['target_read_length']:
action = (BpsMode.BPS_ACTION_TARGET_READ, buffer['target_read_length'], [])
patch_actions.append(action)
offset = output_offset - buffer['target_read_length']
while buffer['target_read_length']:
action[2].append(target_data[offset])
offset += 1
buffer['target_read_length'] -= 1
eof = min(source_size, target_size)
while output_offset < target_size:
src_length, n = 0, 0
while output_offset + n < eof:
if source_data[output_offset + n] != target_data[output_offset + n]:
break
src_length += 1
n += 1
rle_length, n = 0, 1
while output_offset + n < target_size:
if target_data[output_offset] != target_data[output_offset + n]:
break
rle_length += 1
n += 1
if rle_length >= 4:
trl['target_read_length'] += 1
output_offset += 1
target_read_flush(trl)
relative_offset = (output_offset - 1) - target_relative_offset
patch_actions.append((BpsMode.BPS_ACTION_TARGET_COPY, rle_length, relative_offset))
output_offset += rle_length
target_relative_offset = output_offset - 1
elif src_length >= 4:
target_read_flush(trl)
patch_actions.append((BpsMode.BPS_ACTION_SOURCE_READ, src_length, None))
output_offset += src_length
else:
trl['target_read_length'] += 1
output_offset += 1
target_read_flush(trl)
return patch_actions
if __name__ == '__main__':
with open(sys.argv[1], 'rb') as source:
sourcedata = source.read()
with open(sys.argv[2], 'rb') as target:
targetdata = target.read()
patch = create_bps_from_data(sourcedata, targetdata)
with open(sys.argv[3], 'wb') as patchfile:
patchfile.write(patch.binary_ba)

284
source/tools/Bias.py Normal file
View File

@@ -0,0 +1,284 @@
# Demo script showing bias in VT and ER boss shuffle algorithms, with proposed fixes.
import random
boss_location_list = [
'GT_top',
'GT_mid',
'TH',
'SW',
'EP',
'DP',
'PD',
'SP',
'TT',
'IP',
'MM',
'TR',
'GT_bot'
]
entrance_rando_boss_location_list = [
'GT_top',
'TH',
'SW',
'GT_mid',
'EP',
'DP',
'PD',
'SP',
'TT',
'IP',
'MM',
'TR',
'GT_bot'
]
boss_list = [
'Armos',
'Lanmo',
'Moldorm',
'Helma',
'Arrghus',
'Moth',
'Blind',
'Khold',
'Vitty',
'Trinexx'
]
results_dict = {
'GT_top': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'GT_mid': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'TH': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'SW': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'EP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'DP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'PD': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'SP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'TT': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'IP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'MM': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'TR': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'GT_bot': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0}
}
def can_place_boss(boss:str, dungeon_name: str) -> bool:
if (dungeon_name == 'GT_top'):
if boss in {'Armos', 'Arrghus', 'Blind', 'Trinexx', 'Lanmo'}:
return False
elif (dungeon_name == 'GT_mid'):
if boss == 'Blind':
return False
elif (dungeon_name == 'TH'):
if boss in {'Armos', 'Arrghus', 'Blind', 'Trinexx', 'Lanmo'}:
return False
elif (dungeon_name == 'SW'):
if boss == 'Trinexx':
return False
return True
def resetresults():
for boss_location in boss_location_list:
for boss in boss_list:
results_dict[boss_location][boss] = 0
def printresults(iterations):
for boss_location in boss_location_list:
print("Location: " + boss_location.ljust(6) + ' ', end='')
for boss in boss_list:
percentagestr = "{0:.2%}".format(results_dict[boss_location][boss] / iterations)
print(boss + ": " + percentagestr.rjust(6) + ' ', end='')
restrictive_count = results_dict[boss_location]['Armos'] + results_dict[boss_location]['Arrghus'] + results_dict[boss_location]['Blind'] + results_dict[boss_location]['Trinexx'] + results_dict[boss_location]['Lanmo']
restrictive_pct = "{0:.2%}".format(restrictive_count / iterations)
nonrestrictive_count = results_dict[boss_location]['Moldorm'] + results_dict[boss_location]['Helma'] + results_dict[boss_location]['Moth'] + results_dict[boss_location]['Khold'] + results_dict[boss_location]['Vitty']
nonrestrictive_pct = "{0:.2%}".format(nonrestrictive_count / iterations)
print("Restrictive: " + restrictive_pct.rjust(7) + " Non-restrictive: " + nonrestrictive_pct.rjust(7))
# Demonstration of reshuffle algorithm at:
# https://github.com/sporchia/alttp_vt_randomizer/blob/master/app/Randomizer.php#L399
def vt(reshuffle):
for i in range(iterations):
dupes = random.sample(boss_list, 3)
temp_full_boss_list = boss_list + dupes
shuffled_boss_list = temp_full_boss_list.copy()
random.shuffle(shuffled_boss_list)
for boss_location in boss_location_list:
boss = shuffled_boss_list.pop(0)
while (not can_place_boss(boss, boss_location)):
shuffled_boss_list.append(boss)
# Proposed fix
if reshuffle:
random.shuffle(shuffled_boss_list)
boss = shuffled_boss_list.pop(0)
results_dict[boss_location][boss] = results_dict[boss_location][boss] + 1
# Demonstration of reshuffle algorithm at:
# https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/Dev/Bosses.py#L203
def er(reshuffle):
for i in range(iterations):
dupes = random.sample(boss_list, 3)
temp_full_boss_list = boss_list + dupes
shuffled_boss_list = temp_full_boss_list.copy()
random.shuffle(shuffled_boss_list)
for boss_location in entrance_rando_boss_location_list:
boss = next((b for b in shuffled_boss_list if can_place_boss(b, boss_location)), None)
shuffled_boss_list.remove(boss)
results_dict[boss_location][boss] = results_dict[boss_location][boss] + 1
# Proposed fix
if reshuffle:
random.shuffle(shuffled_boss_list)
def er_unique():
for i in range(iterations):
temp_full_boss_list = boss_list
moldorm2 = random.choice([b for b in boss_list
if b not in ["Armos", "Arrghus", "Blind", "Trinexx", "Lanmo"]])
lanmo2 = random.choice([b for b in boss_list
if b not in [moldorm2, "Blind"]])
armos2 = random.choice([b for b in boss_list if b not in [moldorm2, lanmo2]])
gt_bosses = [moldorm2, lanmo2, armos2]
shuffled_boss_list = temp_full_boss_list.copy()
for boss_location in entrance_rando_boss_location_list:
if '_' in boss_location:
boss = random.choice([b for b in gt_bosses if can_place_boss(b, boss_location)])
gt_bosses.remove(boss)
else:
boss = random.choice([b for b in shuffled_boss_list if can_place_boss(b, boss_location)])
shuffled_boss_list.remove(boss)
results_dict[boss_location][boss] += 1
def er_unique_chaos():
for i in range(iterations):
temp_full_boss_list = boss_list
shuffled_boss_list = temp_full_boss_list.copy()
for boss_location in entrance_rando_boss_location_list:
if '_' in boss_location:
boss = random.choice(b for b in boss_list if can_place_boss(b, boss_location))
else:
boss = random.choice(b for b in shuffled_boss_list if can_place_boss(b, boss_location))
shuffled_boss_list.remove(boss)
results_dict[boss_location][boss] += 1
if __name__ == '__main__':
iterations = 100000
# reshuffle = False
#
# vt(reshuffle)
#
# print("Original results:")
#
# printresults(iterations)
# resetresults()
#
# reshuffle = True
#
# vt(reshuffle)
#
# print("")
# print("Reshuffled results:")
#
# printresults(iterations)
# resetresults()
#
# #ER
# reshuffle = False
# er(reshuffle)
#
# print("")
# print("ER Original results:")
#
# printresults(iterations)
# resetresults()
#
# reshuffle = True
# er(reshuffle)
#
# print("")
# print("ER Reshuffled results:")
#
# printresults(iterations)
# resetresults()
er_unique()
print("")
print("ER Unique results:")
printresults(iterations)
resetresults()
er_unique_chaos()
print("")
print("ER Chaos results:")
printresults(iterations)
# Results:
#Original results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.00% Helma: 19.89% Arrghus: 0.00% Moth: 20.01% Blind: 0.00% Khold: 20.14% Vitty: 19.96% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 11.05% Lanmo: 11.16% Moldorm: 11.00% Helma: 11.35% Arrghus: 11.07% Moth: 11.00% Blind: 0.00% Khold: 11.01% Vitty: 11.20% Trinexx: 11.15% Restrictive: 44.43% Non-restrictive: 55.57%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 20.06% Helma: 19.95% Arrghus: 0.00% Moth: 20.07% Blind: 0.00% Khold: 19.88% Vitty: 20.05% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 11.30% Lanmo: 11.04% Moldorm: 11.30% Helma: 10.95% Arrghus: 11.09% Moth: 11.04% Blind: 11.15% Khold: 11.15% Vitty: 10.98% Trinexx: 0.00% Restrictive: 44.59% Non-restrictive: 55.42%
#Location: EP Armos: 9.89% Lanmo: 9.90% Moldorm: 10.02% Helma: 9.97% Arrghus: 10.17% Moth: 9.98% Blind: 9.95% Khold: 10.16% Vitty: 9.90% Trinexx: 10.07% Restrictive: 49.97% Non-restrictive: 50.03%
#Location: DP Armos: 9.98% Lanmo: 10.09% Moldorm: 9.97% Helma: 9.95% Arrghus: 10.17% Moth: 10.01% Blind: 9.87% Khold: 10.02% Vitty: 9.88% Trinexx: 10.04% Restrictive: 50.16% Non-restrictive: 49.84%
#Location: PD Armos: 10.12% Lanmo: 9.96% Moldorm: 9.85% Helma: 9.90% Arrghus: 10.31% Moth: 9.91% Blind: 9.96% Khold: 9.87% Vitty: 9.94% Trinexx: 10.17% Restrictive: 50.52% Non-restrictive: 49.48%
#Location: SP Armos: 10.41% Lanmo: 10.35% Moldorm: 9.61% Helma: 9.66% Arrghus: 10.29% Moth: 9.60% Blind: 10.47% Khold: 9.67% Vitty: 9.53% Trinexx: 10.42% Restrictive: 51.93% Non-restrictive: 48.07%
#Location: TT Armos: 11.04% Lanmo: 10.98% Moldorm: 8.86% Helma: 8.95% Arrghus: 10.98% Moth: 9.12% Blind: 11.18% Khold: 8.98% Vitty: 9.03% Trinexx: 10.89% Restrictive: 55.06% Non-restrictive: 44.94%
#Location: IP Armos: 12.10% Lanmo: 11.89% Moldorm: 7.81% Helma: 7.83% Arrghus: 11.94% Moth: 7.89% Blind: 12.84% Khold: 7.87% Vitty: 7.99% Trinexx: 11.84% Restrictive: 60.62% Non-restrictive: 39.38%
#Location: MM Armos: 13.67% Lanmo: 13.66% Moldorm: 6.00% Helma: 6.17% Arrghus: 13.48% Moth: 6.18% Blind: 15.06% Khold: 6.25% Vitty: 6.14% Trinexx: 13.41% Restrictive: 69.28% Non-restrictive: 30.72%
#Location: TR Armos: 15.35% Lanmo: 15.49% Moldorm: 3.87% Helma: 3.90% Arrghus: 15.31% Moth: 3.78% Blind: 19.06% Khold: 3.89% Vitty: 3.82% Trinexx: 15.53% Restrictive: 80.74% Non-restrictive: 19.26%
#Location: GT_bot Armos: 15.24% Lanmo: 15.24% Moldorm: 1.54% Helma: 1.51% Arrghus: 15.22% Moth: 1.52% Blind: 20.44% Khold: 1.51% Vitty: 1.53% Trinexx: 26.25% Restrictive: 92.38% Non-restrictive: 7.62%
#
#Reshuffled results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.02% Helma: 19.97% Arrghus: 0.00% Moth: 20.02% Blind: 0.00% Khold: 20.07% Vitty: 19.92% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 12.23% Lanmo: 12.05% Moldorm: 10.23% Helma: 10.24% Arrghus: 12.11% Moth: 10.31% Blind: 0.00% Khold: 10.36% Vitty: 10.33% Trinexx: 12.13% Restrictive: 48.52% Non-restrictive: 51.48%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 19.86% Helma: 20.09% Arrghus: 0.00% Moth: 20.23% Blind: 0.00% Khold: 19.83% Vitty: 19.99% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 13.23% Lanmo: 13.34% Moldorm: 9.05% Helma: 9.02% Arrghus: 13.37% Moth: 9.10% Blind: 14.97% Khold: 8.98% Vitty: 8.93% Trinexx: 0.00% Restrictive: 54.92% Non-restrictive: 45.08%
#Location: EP Armos: 11.54% Lanmo: 11.50% Moldorm: 7.96% Helma: 7.82% Arrghus: 11.76% Moth: 7.81% Blind: 12.84% Khold: 7.79% Vitty: 8.03% Trinexx: 12.95% Restrictive: 60.59% Non-restrictive: 39.41%
#Location: DP Armos: 11.55% Lanmo: 11.49% Moldorm: 7.93% Helma: 7.85% Arrghus: 11.72% Moth: 7.69% Blind: 12.80% Khold: 7.99% Vitty: 7.74% Trinexx: 13.25% Restrictive: 60.80% Non-restrictive: 39.20%
#Location: PD Armos: 11.79% Lanmo: 11.47% Moldorm: 7.81% Helma: 7.78% Arrghus: 11.61% Moth: 7.91% Blind: 12.81% Khold: 7.78% Vitty: 8.00% Trinexx: 13.02% Restrictive: 60.71% Non-restrictive: 39.29%
#Location: SP Armos: 11.59% Lanmo: 11.56% Moldorm: 7.90% Helma: 7.75% Arrghus: 11.52% Moth: 8.02% Blind: 12.67% Khold: 7.92% Vitty: 7.85% Trinexx: 13.23% Restrictive: 60.57% Non-restrictive: 39.43%
#Location: TT Armos: 11.64% Lanmo: 11.64% Moldorm: 7.77% Helma: 7.73% Arrghus: 11.70% Moth: 7.67% Blind: 12.89% Khold: 8.05% Vitty: 7.85% Trinexx: 13.07% Restrictive: 60.93% Non-restrictive: 39.07%
#Location: IP Armos: 11.54% Lanmo: 11.77% Moldorm: 7.95% Helma: 7.97% Arrghus: 11.49% Moth: 7.80% Blind: 12.64% Khold: 7.87% Vitty: 7.89% Trinexx: 13.10% Restrictive: 60.54% Non-restrictive: 39.47%
#Location: MM Armos: 11.69% Lanmo: 11.65% Moldorm: 7.91% Helma: 7.88% Arrghus: 11.59% Moth: 7.78% Blind: 12.80% Khold: 7.73% Vitty: 7.92% Trinexx: 13.04% Restrictive: 60.77% Non-restrictive: 39.23%
#Location: TR Armos: 11.58% Lanmo: 11.82% Moldorm: 7.75% Helma: 7.83% Arrghus: 11.62% Moth: 7.79% Blind: 12.76% Khold: 7.90% Vitty: 7.76% Trinexx: 13.19% Restrictive: 60.97% Non-restrictive: 39.03%
#Location: GT_bot Armos: 11.54% Lanmo: 11.60% Moldorm: 7.91% Helma: 7.87% Arrghus: 11.62% Moth: 7.90% Blind: 12.78% Khold: 7.82% Vitty: 7.86% Trinexx: 13.10% Restrictive: 60.65% Non-restrictive: 39.35%
#
#ER Original results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 19.94% Helma: 19.87% Arrghus: 0.00% Moth: 19.98% Blind: 0.00% Khold: 20.31% Vitty: 19.90% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 14.15% Lanmo: 14.33% Moldorm: 4.51% Helma: 4.53% Arrghus: 14.19% Moth: 4.56% Blind: 0.00% Khold: 4.61% Vitty: 4.49% Trinexx: 34.62% Restrictive: 77.30% Non-restrictive: 22.70%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 19.93% Helma: 19.98% Arrghus: 0.00% Moth: 20.10% Blind: 0.00% Khold: 19.75% Vitty: 20.24% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 21.52% Lanmo: 21.56% Moldorm: 2.76% Helma: 2.80% Arrghus: 21.50% Moth: 2.82% Blind: 21.48% Khold: 2.81% Vitty: 2.75% Trinexx: 0.00% Restrictive: 86.05% Non-restrictive: 13.95%
#Location: EP Armos: 11.39% Lanmo: 11.33% Moldorm: 5.89% Helma: 5.69% Arrghus: 11.32% Moth: 5.83% Blind: 24.71% Khold: 5.82% Vitty: 5.76% Trinexx: 12.27% Restrictive: 71.01% Non-restrictive: 28.99%
#Location: DP Armos: 11.62% Lanmo: 11.66% Moldorm: 8.05% Helma: 8.32% Arrghus: 11.75% Moth: 8.13% Blind: 12.46% Khold: 8.10% Vitty: 8.18% Trinexx: 11.73% Restrictive: 59.22% Non-restrictive: 40.78%
#Location: PD Armos: 11.01% Lanmo: 10.77% Moldorm: 9.18% Helma: 9.20% Arrghus: 10.89% Moth: 9.03% Blind: 10.83% Khold: 9.13% Vitty: 9.17% Trinexx: 10.79% Restrictive: 54.29% Non-restrictive: 45.71%
#Location: SP Armos: 10.26% Lanmo: 10.17% Moldorm: 9.73% Helma: 9.61% Arrghus: 10.33% Moth: 9.69% Blind: 10.31% Khold: 9.84% Vitty: 9.61% Trinexx: 10.45% Restrictive: 51.52% Non-restrictive: 48.48%
#Location: TT Armos: 10.12% Lanmo: 10.01% Moldorm: 10.07% Helma: 9.97% Arrghus: 10.16% Moth: 9.92% Blind: 9.95% Khold: 9.73% Vitty: 9.96% Trinexx: 10.11% Restrictive: 50.35% Non-restrictive: 49.65%
#Location: IP Armos: 10.03% Lanmo: 10.01% Moldorm: 10.00% Helma: 10.10% Arrghus: 9.89% Moth: 9.93% Blind: 10.13% Khold: 9.99% Vitty: 9.80% Trinexx: 10.13% Restrictive: 50.18% Non-restrictive: 49.82%
#Location: MM Armos: 9.89% Lanmo: 9.99% Moldorm: 10.09% Helma: 10.10% Arrghus: 10.02% Moth: 9.93% Blind: 10.03% Khold: 10.00% Vitty: 10.06% Trinexx: 9.89% Restrictive: 49.81% Non-restrictive: 50.19%
#Location: TR Armos: 10.03% Lanmo: 10.10% Moldorm: 10.01% Helma: 9.92% Arrghus: 9.94% Moth: 10.01% Blind: 10.04% Khold: 9.95% Vitty: 9.92% Trinexx: 10.08% Restrictive: 50.19% Non-restrictive: 49.81%
#Location: GT_bot Armos: 9.90% Lanmo: 10.04% Moldorm: 9.88% Helma: 9.85% Arrghus: 10.00% Moth: 10.04% Blind: 10.09% Khold: 10.09% Vitty: 10.11% Trinexx: 10.01% Restrictive: 50.03% Non-restrictive: 49.97%
#
#ER Reshuffled results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.11% Helma: 19.81% Arrghus: 0.00% Moth: 20.22% Blind: 0.00% Khold: 20.08% Vitty: 19.78% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 13.17% Lanmo: 13.37% Moldorm: 9.05% Helma: 9.14% Arrghus: 13.23% Moth: 9.29% Blind: 0.00% Khold: 8.96% Vitty: 9.11% Trinexx: 14.69% Restrictive: 54.45% Non-restrictive: 45.55%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 20.03% Helma: 19.72% Arrghus: 0.00% Moth: 20.17% Blind: 0.00% Khold: 19.86% Vitty: 20.23% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 13.38% Lanmo: 13.41% Moldorm: 9.27% Helma: 9.33% Arrghus: 13.41% Moth: 9.27% Blind: 13.48% Khold: 9.28% Vitty: 9.17% Trinexx: 0.00% Restrictive: 53.69% Non-restrictive: 46.31%
#Location: EP Armos: 11.50% Lanmo: 11.56% Moldorm: 7.90% Helma: 8.06% Arrghus: 11.58% Moth: 8.11% Blind: 12.72% Khold: 7.93% Vitty: 7.93% Trinexx: 12.70% Restrictive: 60.06% Non-restrictive: 39.94%
#Location: DP Armos: 11.66% Lanmo: 11.51% Moldorm: 8.05% Helma: 7.96% Arrghus: 11.43% Moth: 7.83% Blind: 12.94% Khold: 7.89% Vitty: 7.88% Trinexx: 12.87% Restrictive: 60.40% Non-restrictive: 39.60%
#Location: PD Armos: 11.36% Lanmo: 11.53% Moldorm: 7.89% Helma: 7.96% Arrghus: 11.46% Moth: 7.72% Blind: 13.12% Khold: 8.07% Vitty: 8.02% Trinexx: 12.87% Restrictive: 60.34% Non-restrictive: 39.66%
#Location: SP Armos: 11.27% Lanmo: 11.38% Moldorm: 7.89% Helma: 7.98% Arrghus: 11.56% Moth: 7.89% Blind: 13.10% Khold: 8.17% Vitty: 8.08% Trinexx: 12.68% Restrictive: 59.99% Non-restrictive: 40.01%
#Location: TT Armos: 11.32% Lanmo: 11.49% Moldorm: 7.91% Helma: 8.16% Arrghus: 11.44% Moth: 7.97% Blind: 12.93% Khold: 8.05% Vitty: 7.97% Trinexx: 12.77% Restrictive: 59.94% Non-restrictive: 40.06%
#Location: IP Armos: 11.69% Lanmo: 11.74% Moldorm: 7.91% Helma: 8.00% Arrghus: 11.34% Moth: 7.87% Blind: 12.80% Khold: 7.82% Vitty: 7.95% Trinexx: 12.88% Restrictive: 60.44% Non-restrictive: 39.56%
#Location: MM Armos: 11.57% Lanmo: 11.31% Moldorm: 8.13% Helma: 7.90% Arrghus: 11.32% Moth: 7.91% Blind: 13.15% Khold: 7.97% Vitty: 7.82% Trinexx: 12.91% Restrictive: 60.26% Non-restrictive: 39.74%
#Location: TR Armos: 11.44% Lanmo: 11.30% Moldorm: 8.04% Helma: 8.13% Arrghus: 11.47% Moth: 7.90% Blind: 12.82% Khold: 8.09% Vitty: 7.96% Trinexx: 12.84% Restrictive: 59.87% Non-restrictive: 40.13%
#Location: GT_bot Armos: 11.36% Lanmo: 11.53% Moldorm: 7.87% Helma: 7.99% Arrghus: 11.49% Moth: 7.96% Blind: 13.01% Khold: 7.98% Vitty: 8.00% Trinexx: 12.80% Restrictive: 60.19% Non-restrictive: 39.81

0
source/tools/__init__.py Normal file
View File