Item Plando Support
This commit is contained in:
parent
3d53adf45c
commit
f3b6be2b20
|
@ -5,7 +5,7 @@ from enum import Enum, unique
|
|||
import logging
|
||||
import json
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from typing import Union, Optional, List, Set, Dict
|
||||
from typing import Union, Optional, List, Set, Dict, NamedTuple
|
||||
import secrets
|
||||
import random
|
||||
|
||||
|
@ -16,7 +16,7 @@ from Items import item_name_groups
|
|||
|
||||
class World(object):
|
||||
debug_types = False
|
||||
player_names: list
|
||||
player_names: Dict[int, List[str]]
|
||||
_region_cache: dict
|
||||
difficulty_requirements: dict
|
||||
required_medallions: dict
|
||||
|
@ -135,6 +135,7 @@ class World(object):
|
|||
set_player_attr('sprite_pool', [])
|
||||
set_player_attr('dark_room_logic', "lamp")
|
||||
set_player_attr('restrict_dungeon_item_on_boss', False)
|
||||
set_player_attr('plando_items', [])
|
||||
|
||||
def secure(self):
|
||||
self.random = secrets.SystemRandom()
|
||||
|
@ -1037,6 +1038,9 @@ class Item(object):
|
|||
self.world = None
|
||||
self.player = player
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.player == other.player
|
||||
|
||||
@property
|
||||
def crystal(self) -> bool:
|
||||
return self.type == 'Crystal'
|
||||
|
@ -1452,3 +1456,10 @@ class Spoiler(object):
|
|||
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
||||
|
||||
outfile.write('\n'.join(path_listings))
|
||||
|
||||
|
||||
class PlandoItem(NamedTuple):
|
||||
item: str
|
||||
location: str
|
||||
world: Union[bool, str] = False # False -> own world, True -> not own world
|
||||
from_pool: bool = True # if item should be removed from item pool
|
||||
|
|
|
@ -355,6 +355,10 @@ def parse_arguments(argv, no_defaults=False):
|
|||
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
|
||||
|
||||
ret = parser.parse_args(argv)
|
||||
|
||||
# cannot be set through CLI currently
|
||||
ret.plando_items = {}
|
||||
|
||||
ret.glitch_boots = not ret.disable_glitch_boots
|
||||
if ret.timer == "none":
|
||||
ret.timer = False
|
||||
|
@ -382,9 +386,10 @@ def parse_arguments(argv, no_defaults=False):
|
|||
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||
'heartbeep', "skip_progression_balancing", "triforce_pieces_available",
|
||||
"triforce_pieces_required", "shop_shuffle",
|
||||
"triforce_pieces_required", "shop_shuffle", "plando_items",
|
||||
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
|
||||
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'restrict_dungeon_item_on_boss',
|
||||
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'restrict_dungeon_item_on_boss',
|
||||
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
|
|
10
Fill.py
10
Fill.py
|
@ -54,7 +54,8 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
|
|||
for location in region.locations:
|
||||
if location.item and not location.event:
|
||||
placements.append(location)
|
||||
|
||||
# fill in name of world for item
|
||||
item_to_place.world = world
|
||||
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
|
@ -128,9 +129,12 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
|||
world.random.shuffle(fill_locations)
|
||||
|
||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||
standard_keyshuffle_players = {player for player, mode in world.mode.items() if mode == 'standard' and
|
||||
world.keyshuffle[player] is True}
|
||||
if standard_keyshuffle_players:
|
||||
progitempool.sort(
|
||||
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and world.mode[item.player] == 'standard' and
|
||||
world.keyshuffle[item.player] else 0)
|
||||
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and
|
||||
item.player in standard_keyshuffle_players else 0)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
|
|
48
Main.py
48
Main.py
|
@ -9,7 +9,7 @@ import time
|
|||
import zlib
|
||||
import concurrent.futures
|
||||
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, PlandoItem
|
||||
from Items import ItemFactory, item_table, item_name_groups
|
||||
from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance
|
||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
|
@ -87,6 +87,7 @@ def main(args, seed=None):
|
|||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
world.sprite_pool = args.sprite_pool.copy()
|
||||
world.dark_room_logic = args.dark_room_logic.copy()
|
||||
world.plando_items = args.plando_items.copy()
|
||||
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
|
||||
|
||||
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
|
||||
|
@ -172,10 +173,49 @@ def main(args, seed=None):
|
|||
|
||||
fill_prizes(world)
|
||||
|
||||
logger.info("Running Item Plando")
|
||||
|
||||
world_name_lookup = {world.player_names[player_id][0]: player_id for player_id in world.player_ids}
|
||||
|
||||
for player in world.player_ids:
|
||||
placement: PlandoItem
|
||||
for placement in world.plando_items[player]:
|
||||
target_world: int = placement.world
|
||||
if target_world is False or world.players == 1:
|
||||
target_world = player # in own world
|
||||
elif target_world is True: # in any other world
|
||||
target_world = player
|
||||
while target_world == player:
|
||||
target_world = world.random.randint(1, world.players + 1)
|
||||
elif target_world is None: # any random world
|
||||
target_world = world.random.randint(1, world.players + 1)
|
||||
elif type(target_world) == int: # target world by player id
|
||||
pass
|
||||
else: # find world by name
|
||||
target_world = world_name_lookup[target_world]
|
||||
|
||||
location = world.get_location(placement.location, target_world)
|
||||
if location.item:
|
||||
raise Exception(f"Cannot place item into already filled location {location}.")
|
||||
item = ItemFactory(placement.item, player)
|
||||
if placement.from_pool:
|
||||
try:
|
||||
world.itempool.remove(item)
|
||||
except ValueError:
|
||||
logger.warning(f"Could not remove {item} from pool as it's already missing from it.")
|
||||
|
||||
if location.can_fill(world.state, item, False):
|
||||
world.push_item(location, item, collect=False)
|
||||
location.event = True # flag location to be checked during fill
|
||||
location.locked = True
|
||||
logger.debug(f"Plando placed {item} at {location}")
|
||||
else:
|
||||
raise Exception(f"Can't place {item} at {location} due to fill condition not met.")
|
||||
|
||||
logger.info('Placing Dungeon Items.')
|
||||
|
||||
shuffled_locations = None
|
||||
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
|
||||
if args.algorithm in ['balanced', 'vt26'] or any(
|
||||
list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
|
||||
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
|
||||
fill_dungeons_restrictive(world)
|
||||
else:
|
||||
|
@ -188,7 +228,7 @@ def main(args, seed=None):
|
|||
elif args.algorithm == 'vt25':
|
||||
distribute_items_restrictive(world, False)
|
||||
elif args.algorithm == 'vt26':
|
||||
distribute_items_restrictive(world, True, shuffled_locations)
|
||||
distribute_items_restrictive(world, True)
|
||||
elif args.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world, True)
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ if __name__ == "__main__":
|
|||
player_files_path = multi_mystery_options["player_files_path"]
|
||||
target_player_count = multi_mystery_options["players"]
|
||||
race = multi_mystery_options["race"]
|
||||
plando_options = multi_mystery_options["plando_options"]
|
||||
create_spoiler = multi_mystery_options["create_spoiler"]
|
||||
zip_roms = multi_mystery_options["zip_roms"]
|
||||
zip_diffs = multi_mystery_options["zip_diffs"]
|
||||
|
@ -104,7 +105,7 @@ if __name__ == "__main__":
|
|||
|
||||
command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
|
||||
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
|
||||
f"--outputpath \"{output_path}\" --teams {teams}"
|
||||
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\""
|
||||
|
||||
if create_spoiler:
|
||||
command += " --create_spoiler"
|
||||
|
|
35
Mystery.py
35
Mystery.py
|
@ -7,6 +7,7 @@ import typing
|
|||
import os
|
||||
|
||||
import ModuleUpdate
|
||||
from BaseClasses import PlandoItem
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
|
@ -44,10 +45,13 @@ def mystery_argparse():
|
|||
parser.add_argument('--create_diff', action="store_true")
|
||||
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255),
|
||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||
parser.add_argument('--plando', default="bosses",
|
||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
|
||||
return args
|
||||
|
||||
|
||||
|
@ -144,7 +148,8 @@ def main(args=None, callback=ERmain):
|
|||
if args.enemizercli:
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
|
||||
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
|
||||
for k, v in weights_cache.items()}
|
||||
player_path_cache = {}
|
||||
for player in range(1, args.multi + 1):
|
||||
player_path_cache[player] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
|
||||
|
@ -167,7 +172,8 @@ def main(args=None, callback=ERmain):
|
|||
path = player_path_cache[player]
|
||||
if path:
|
||||
try:
|
||||
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
|
||||
settings = settings_cache[path] if settings_cache[path] else \
|
||||
roll_settings(weights_cache[path], args.plando)
|
||||
if settings.sprite and not os.path.isfile(settings.sprite) and not Sprite.get_sprite_from_name(
|
||||
settings.sprite):
|
||||
logging.warning(
|
||||
|
@ -275,7 +281,13 @@ boss_shuffle_options = {None: 'none',
|
|||
}
|
||||
|
||||
|
||||
def roll_settings(weights):
|
||||
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
return random.random() < (float(percentage) / 100)
|
||||
|
||||
|
||||
def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"))):
|
||||
ret = argparse.Namespace()
|
||||
if "linked_options" in weights:
|
||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||
|
@ -283,7 +295,7 @@ def roll_settings(weights):
|
|||
if "name" not in option_set:
|
||||
raise ValueError("One of your linked options does not have a name.")
|
||||
try:
|
||||
if random.random() < (float(option_set["percentage"]) / 100):
|
||||
if roll_percentage(option_set["percentage"]):
|
||||
logging.debug(f"Linked option {option_set['name']} triggered.")
|
||||
logging.debug(f'Applying {option_set["options"]}')
|
||||
new_options = set(option_set["options"]) - set(weights)
|
||||
|
@ -415,7 +427,7 @@ def roll_settings(weights):
|
|||
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
ret.shufflebosses = boss_shuffle_options[boss_shuffle]
|
||||
else:
|
||||
elif "bosses" in plando_options:
|
||||
options = boss_shuffle.lower().split(";")
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
|
@ -427,6 +439,8 @@ def roll_settings(weights):
|
|||
else:
|
||||
bosses.append(boss)
|
||||
ret.shufflebosses = ";".join(bosses + [remainder_shuffle])
|
||||
else:
|
||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||
|
||||
ret.enemy_shuffle = {'none': False,
|
||||
'shuffled': 'shuffled',
|
||||
|
@ -534,6 +548,17 @@ def roll_settings(weights):
|
|||
|
||||
ret.non_local_items = ",".join(ret.non_local_items)
|
||||
|
||||
ret.plando_items = []
|
||||
if "items" in plando_options:
|
||||
options = weights.get("plando_items", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
item = get_choice("item", placement)
|
||||
location = get_choice("location", placement)
|
||||
from_pool = get_choice("from_pool", placement, True)
|
||||
location_world = get_choice("world", placement, False)
|
||||
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool))
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
|
||||
|
|
106
Utils.py
106
Utils.py
|
@ -1,10 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
def tuplize_version(version: str) -> typing.Tuple[int, ...]:
|
||||
return Version(*(int(piece, 10) for piece in version.split(".")))
|
||||
|
||||
|
||||
class Version(typing.NamedTuple):
|
||||
major: int
|
||||
minor: int
|
||||
|
@ -87,6 +89,7 @@ def local_path(*path):
|
|||
|
||||
return os.path.join(local_path.cached_path, *path)
|
||||
|
||||
|
||||
local_path.cached_path = None
|
||||
|
||||
|
||||
|
@ -98,8 +101,10 @@ def output_path(*path):
|
|||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
output_path.cached_path = None
|
||||
|
||||
|
||||
def open_file(filename):
|
||||
if sys.platform == 'win32':
|
||||
os.startfile(filename)
|
||||
|
@ -107,6 +112,7 @@ def open_file(filename):
|
|||
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
|
||||
subprocess.call([open_command, filename])
|
||||
|
||||
|
||||
def close_console():
|
||||
if sys.platform == 'win32':
|
||||
# windows
|
||||
|
@ -143,6 +149,7 @@ class Hint(typing.NamedTuple):
|
|||
def __hash__(self):
|
||||
return hash((self.receiving_player, self.finding_player, self.location, self.item, self.entrance))
|
||||
|
||||
|
||||
def get_public_ipv4() -> str:
|
||||
import socket
|
||||
import urllib.request
|
||||
|
@ -158,6 +165,7 @@ def get_public_ipv4() -> str:
|
|||
pass # we could be offline, in a local game, so no point in erroring out
|
||||
return ip
|
||||
|
||||
|
||||
def get_public_ipv6() -> str:
|
||||
import socket
|
||||
import urllib.request
|
||||
|
@ -173,56 +181,56 @@ def get_public_ipv6() -> str:
|
|||
|
||||
def get_default_options() -> dict:
|
||||
if not hasattr(get_default_options, "options"):
|
||||
options = dict()
|
||||
|
||||
# Refer to host.yaml for comments as to what all these options mean.
|
||||
generaloptions = dict()
|
||||
generaloptions["rom_file"] = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
generaloptions["qusb2snes"] = "QUsb2Snes\\QUsb2Snes.exe"
|
||||
generaloptions["rom_start"] = True
|
||||
generaloptions["output_path"] = "output"
|
||||
options["general_options"] = generaloptions
|
||||
options = {
|
||||
"general_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
"qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
|
||||
"rom_start": True,
|
||||
"output_path": "output",
|
||||
},
|
||||
"server_options": {
|
||||
"host": None,
|
||||
"port": 38281,
|
||||
"password": None,
|
||||
"multidata": None,
|
||||
"savefile": None,
|
||||
"disable_save": False,
|
||||
"loglevel": "info",
|
||||
"server_password": None,
|
||||
"disable_item_cheat": False,
|
||||
"location_check_points": 1,
|
||||
"hint_cost": 1000,
|
||||
"forfeit_mode": "goal",
|
||||
"remaining_mode": "goal",
|
||||
"auto_shutdown": 0,
|
||||
"compatibility": 2,
|
||||
},
|
||||
"multi_mystery_options": {
|
||||
"teams": 1,
|
||||
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
|
||||
"player_files_path": "Players",
|
||||
"players": 0,
|
||||
"weights_file_path": "weights.yaml",
|
||||
"meta_file_path": "meta.yaml",
|
||||
"player_name": "",
|
||||
"create_spoiler": 1,
|
||||
"zip_roms": 0,
|
||||
"zip_diffs": 2,
|
||||
"zip_spoiler": 0,
|
||||
"zip_multidata": 1,
|
||||
"zip_format": 1,
|
||||
"race": 0,
|
||||
"cpu_threads": 0,
|
||||
"max_attempts": 0,
|
||||
"take_first_working": False,
|
||||
"keep_all_seeds": False,
|
||||
"log_output_path": "Output Logs",
|
||||
"log_level": None,
|
||||
"plando_options": "bosses",
|
||||
}
|
||||
}
|
||||
|
||||
serveroptions = dict()
|
||||
serveroptions["host"] = None
|
||||
serveroptions["port"] = 38281
|
||||
serveroptions["password"] = None
|
||||
serveroptions["multidata"] = None
|
||||
serveroptions["savefile"] = None
|
||||
serveroptions["disable_save"] = False
|
||||
serveroptions["loglevel"] = "info"
|
||||
serveroptions["server_password"] = None
|
||||
serveroptions["disable_item_cheat"] = False
|
||||
serveroptions["location_check_points"] = 1
|
||||
serveroptions["hint_cost"] = 1000
|
||||
serveroptions["forfeit_mode"] = "goal"
|
||||
serveroptions["remaining_mode"] = "goal"
|
||||
serveroptions["auto_shutdown"] = 0
|
||||
serveroptions["compatibility"] = 2
|
||||
options["server_options"] = serveroptions
|
||||
|
||||
multimysteryoptions = dict()
|
||||
multimysteryoptions["teams"] = 1
|
||||
multimysteryoptions["enemizer_path"] = "EnemizerCLI/EnemizerCLI.Core.exe"
|
||||
multimysteryoptions["player_files_path"] = "Players"
|
||||
multimysteryoptions["players"] = 0
|
||||
multimysteryoptions["weights_file_path"] = "weights.yaml"
|
||||
multimysteryoptions["meta_file_path"] = "meta.yaml"
|
||||
multimysteryoptions["player_name"] = ""
|
||||
multimysteryoptions["create_spoiler"] = 1
|
||||
multimysteryoptions["zip_roms"] = 0
|
||||
multimysteryoptions["zip_diffs"] = 2
|
||||
multimysteryoptions["zip_spoiler"] = 0
|
||||
multimysteryoptions["zip_multidata"] = 1
|
||||
multimysteryoptions["zip_format"] = 1
|
||||
multimysteryoptions["race"] = 0
|
||||
multimysteryoptions["cpu_threads"] = 0
|
||||
multimysteryoptions["max_attempts"] = 0
|
||||
multimysteryoptions["take_first_working"] = False
|
||||
multimysteryoptions["keep_all_seeds"] = False
|
||||
multimysteryoptions["log_output_path"] = "Output Logs"
|
||||
multimysteryoptions["log_level"] = None
|
||||
options["multi_mystery_options"] = multimysteryoptions
|
||||
get_default_options.options = options
|
||||
return get_default_options.options
|
||||
|
||||
|
@ -254,6 +262,7 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
|||
dest[key] = update_options(value, dest[key], filename, new_keys)
|
||||
return dest
|
||||
|
||||
|
||||
def get_options() -> dict:
|
||||
if not hasattr(get_options, "options"):
|
||||
locations = ("options.yaml", "host.yaml",
|
||||
|
@ -350,7 +359,6 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
|||
return romfile, False
|
||||
|
||||
|
||||
|
||||
class ReceivedItem(typing.NamedTuple):
|
||||
item: int
|
||||
location: int
|
||||
|
|
|
@ -73,7 +73,7 @@ def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[
|
|||
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
||||
else:
|
||||
try:
|
||||
rolled_results[filename] = roll_settings(yaml_data)
|
||||
rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
||||
else:
|
||||
|
|
Loading…
Reference in New Issue