Options: raise Exception if per-game options are in root

Options: implement progression balancing and accessibility on new system
Options: implement the notion of "common" and "per_game_common" options in various systems
Options: centralize item and location name checking
Spoiler: prettier print some lists, sets and dicts
WebHost: add common options into /templates
This commit is contained in:
Fabian Dill 2021-09-17 00:17:54 +02:00
parent abc0220cfa
commit a82bf1bb32
21 changed files with 219 additions and 137 deletions

View File

@ -10,6 +10,7 @@ from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
import secrets import secrets
import random import random
import Options
import Utils import Utils
@ -83,7 +84,6 @@ class MultiWorld():
set_player_attr('item_functionality', 'normal') set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False) set_player_attr('timer', False)
set_player_attr('goal', 'ganon') set_player_attr('goal', 'ganon')
set_player_attr('accessibility', 'items')
set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False) set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False) set_player_attr('powder_patch_required', False)
@ -109,9 +109,6 @@ class MultiWorld():
set_player_attr('blue_clock_time', 2) set_player_attr('blue_clock_time', 2)
set_player_attr('green_clock_time', 4) set_player_attr('green_clock_time', 4)
set_player_attr('can_take_damage', True) set_player_attr('can_take_damage', True)
set_player_attr('progression_balancing', True)
set_player_attr('local_items', set())
set_player_attr('non_local_items', set())
set_player_attr('triforce_pieces_available', 30) set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20) set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off') set_player_attr('shop_shuffle', 'off')
@ -131,10 +128,21 @@ class MultiWorld():
for player in self.player_ids: for player in self.player_ids:
self.custom_data[player] = {} self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option in world_type.options: for option_key in world_type.options:
setattr(self, option, getattr(args, option, {})) setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.common_options:
setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.per_game_common_options:
setattr(self, option_key, getattr(args, option_key, {}))
self.worlds[player] = world_type(self, player) self.worlds[player] = world_type(self, player)
# intended for unittests
def set_default_common_options(self):
for option_key, option in Options.common_options.items():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
for option_key, option in Options.per_game_common_options.items():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
def secure(self): def secure(self):
self.random = secrets.SystemRandom() self.random = secrets.SystemRandom()
self.is_race = True self.is_race = True
@ -384,17 +392,17 @@ class MultiWorld():
"""Check if accessibility rules are fulfilled with current or supplied state.""" """Check if accessibility rules are fulfilled with current or supplied state."""
if not state: if not state:
state = CollectionState(self) state = CollectionState(self)
players = {"none" : set(), players = {"minimal" : set(),
"items": set(), "items": set(),
"locations": set()} "locations": set()}
for player, access in self.accessibility.items(): for player, access in self.accessibility.items():
players[access].add(player) players[access.current_key].add(player)
beatable_fulfilled = False beatable_fulfilled = False
def location_conditition(location : Location): def location_conditition(location : Location):
"""Determine if this location has to be accessible, location is already filtered by location_relevant""" """Determine if this location has to be accessible, location is already filtered by location_relevant"""
if location.player in players["none"]: if location.player in players["minimal"]:
return False return False
return True return True
@ -1003,7 +1011,7 @@ class Spoiler():
self.medallions = {} self.medallions = {}
self.playthrough = {} self.playthrough = {}
self.unreachables = [] self.unreachables = []
self.startinventory = [] self.start_inventory = []
self.locations = {} self.locations = {}
self.paths = {} self.paths = {}
self.shops = [] self.shops = []
@ -1021,7 +1029,7 @@ class Spoiler():
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1] self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
self.startinventory = list(map(str, self.world.precollected_items)) self.start_inventory = list(map(str, self.world.precollected_items))
self.locations = OrderedDict() self.locations = OrderedDict()
listed_locations = set() listed_locations = set()
@ -1103,7 +1111,7 @@ class Spoiler():
out = OrderedDict() out = OrderedDict()
out['Entrances'] = list(self.entrances.values()) out['Entrances'] = list(self.entrances.values())
out.update(self.locations) out.update(self.locations)
out['Starting Inventory'] = self.startinventory out['Starting Inventory'] = self.start_inventory
out['Special'] = self.medallions out['Special'] = self.medallions
if self.hashes: if self.hashes:
out['Hashes'] = self.hashes out['Hashes'] = self.hashes
@ -1123,6 +1131,14 @@ class Spoiler():
return variable return variable
return 'Yes' if variable else 'No' return 'Yes' if variable else 'No'
def write_option(option_key: str, option_obj: type(Options.Option)):
res = getattr(self.world, option_key)[player]
displayname = getattr(option_obj, "displayname", option_key)
try:
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
except:
raise Exception
with open(filename, 'w', encoding="utf-8-sig") as outfile: with open(filename, 'w', encoding="utf-8-sig") as outfile:
outfile.write( outfile.write(
'Archipelago Version %s - Seed: %s\n\n' % ( 'Archipelago Version %s - Seed: %s\n\n' % (
@ -1134,16 +1150,14 @@ class Spoiler():
if self.world.players > 1: if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
outfile.write('Game: %s\n' % self.world.game[player]) outfile.write('Game: %s\n' % self.world.game[player])
if self.world.players > 1: for f_option, option in Options.common_options.items():
outfile.write('Progression Balanced: %s\n' % ( write_option(f_option, option)
'Yes' if self.world.progression_balancing[player] else 'No')) for f_option, option in Options.per_game_common_options.items():
outfile.write('Accessibility: %s\n' % self.world.accessibility[player]) write_option(f_option, option)
options = self.world.worlds[player].options options = self.world.worlds[player].options
if options: if options:
for f_option, option in options.items(): for f_option, option in options.items():
res = getattr(self.world, f_option)[player] write_option(f_option, option)
displayname = getattr(option, "displayname", f_option)
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
if player in self.world.get_game_players("A Link to the Past"): if player in self.world.get_game_players("A Link to the Past"):
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player])) outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
@ -1201,9 +1215,9 @@ class Spoiler():
for recipe in self.world.worlds[player].custom_recipes.values(): for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}") outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
if self.startinventory: if self.start_inventory:
outfile.write('\n\nStarting Inventory:\n\n') outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory)) outfile.write('\n'.join(self.start_inventory))
outfile.write('\n\nLocations:\n\n') outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()])) outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))

View File

@ -36,7 +36,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
has_beaten_game = world.has_beaten_game(maximum_exploration_state) has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place: for item_to_place in items_to_place:
if world.accessibility[item_to_place.player] == 'none': if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) if single_player_placement else not has_beaten_game item_to_place.player) if single_player_placement else not has_beaten_game
else: else:
@ -52,7 +52,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
else: else:
# we filled all reachable spots. Maybe the game can be beaten anyway? # we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.append(item_to_place) unplaced_items.append(item_to_place)
if world.accessibility[item_to_place.player] != 'none' and world.can_beat_game(): if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
logging.warning( logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})') f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
continue continue
@ -87,9 +87,9 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
progitempool.append(item) progitempool.append(item)
elif item.never_exclude: # this only gets nonprogression items which should not appear in excluded locations elif item.never_exclude: # this only gets nonprogression items which should not appear in excluded locations
nonexcludeditempool.append(item) nonexcludeditempool.append(item)
elif item.name in world.local_items[item.player]: elif item.name in world.local_items[item.player].value:
localrestitempool[item.player].append(item) localrestitempool[item.player].append(item)
elif item.name in world.non_local_items[item.player]: elif item.name in world.non_local_items[item.player].value:
nonlocalrestitempool.append(item) nonlocalrestitempool.append(item)
else: else:
restitempool.append(item) restitempool.append(item)

View File

@ -424,6 +424,32 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.") raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
if option_key in game_weights:
try:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
# verify item names existing
if getattr(player_option, "verify_item_name", False):
for item_name in player_option.value:
if item_name not in AutoWorldRegister.world_types[ret.game].item_names:
raise Exception(f"Item {item_name} from option {player_option} "
f"is not a valid item name from {ret.game}")
elif getattr(player_option, "verify_location_name", False):
for location_name in player_option.value:
if location_name not in AutoWorldRegister.world_types[ret.game].location_names:
raise Exception(f"Location {location_name} from option {player_option} "
f"is not a valid location name from {ret.game}")
else:
setattr(ret, option_key, option(option.default))
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))): def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
if "linked_options" in weights: if "linked_options" in weights:
weights = roll_linked_options(weights) weights = roll_linked_options(weights)
@ -450,63 +476,24 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
f"which are not enabled.") f"which are not enabled.")
ret = argparse.Namespace() ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
if option_key in weights:
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.name = get_choice('name', weights) ret.name = get_choice('name', weights)
ret.accessibility = get_choice('accessibility', weights) for option_key, option in Options.common_options.items():
ret.progression_balancing = get_choice('progression_balancing', weights, True) setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
ret.game = get_choice("game", weights) ret.game = get_choice("game", weights)
if ret.game not in weights: if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.") raise Exception(f"No game options for selected game \"{ret.game}\" found.")
world_type = AutoWorldRegister.world_types[ret.game] world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game] game_weights = weights[ret.game]
ret.local_items = set()
for item_name in game_weights.get('local_items', []):
items = world_type.item_name_groups.get(item_name, {item_name})
for item in items:
if item in world_type.item_names:
ret.local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
ret.non_local_items = set()
for item_name in game_weights.get('non_local_items', []):
items = world_type.item_name_groups.get(item_name, {item_name})
for item in items:
if item in world_type.item_names:
ret.non_local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
inventoryweights = game_weights.get('start_inventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice_legacy(item, inventoryweights)
if isinstance(itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
elif itemvalue:
startitems.append(item)
ret.startinventory = startitems
ret.start_hints = set(game_weights.get('start_hints', []))
ret.excluded_locations = set()
for location in game_weights.get('exclude_locations', []):
if location in world_type.location_names:
ret.excluded_locations.add(location)
else:
raise Exception(f"Could not exclude location {location}, as it was not recognized.")
if ret.game in AutoWorldRegister.world_types: if ret.game in AutoWorldRegister.world_types:
for option_name, option in world_type.options.items(): for option_key, option in world_type.options.items():
if option_name in game_weights: handle_option(ret, game_weights, option_key, option)
try: for option_key, option in Options.per_game_common_options.items():
if issubclass(option, Options.OptionDict) or issubclass(option, Options.OptionList): handle_option(ret, game_weights, option_key, option)
setattr(ret, option_name, option.from_any(game_weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
except Exception as e:
raise Exception(f"Error generating option {option_name} in {ret.game}") from e
else:
setattr(ret, option_name, option(option.default))
if "items" in plando_options: if "items" in plando_options:
ret.plando_items = roll_item_plando(world_type, game_weights) ret.plando_items = roll_item_plando(world_type, game_weights)
if ret.game == "Minecraft": if ret.game == "Minecraft":
@ -530,6 +517,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
def roll_item_plando(world_type, weights): def roll_item_plando(world_type, weights):
plando_items = [] plando_items = []
def add_plando_item(item: str, location: str): def add_plando_item(item: str, location: str):
if item not in world_type.item_name_to_id: if item not in world_type.item_name_to_id:
raise Exception(f"Could not plando item {item} as the item was not recognized") raise Exception(f"Could not plando item {item} as the item was not recognized")

22
Main.py
View File

@ -54,14 +54,13 @@ def main(args, seed=None):
world.item_functionality = args.item_functionality.copy() world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy() world.timer = args.timer.copy()
world.goal = args.goal.copy() world.goal = args.goal.copy()
world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options if hasattr(args, "algorithm"): # current GUI options
world.algorithm = args.algorithm world.algorithm = args.algorithm
world.shuffleganon = args.shuffleganon world.shuffleganon = args.shuffleganon
world.custom = args.custom world.custom = args.custom
world.customitemarray = args.customitemarray world.customitemarray = args.customitemarray
world.accessibility = args.accessibility.copy()
world.open_pyramid = args.open_pyramid.copy() world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy() world.boss_shuffle = args.shufflebosses.copy()
world.enemy_health = args.enemy_health.copy() world.enemy_health = args.enemy_health.copy()
@ -76,7 +75,6 @@ def main(args, seed=None):
world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy() world.shop_shuffle = args.shop_shuffle.copy()
world.progression_balancing = args.progression_balancing.copy()
world.shuffle_prizes = args.shuffle_prizes.copy() world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy() world.sprite_pool = args.sprite_pool.copy()
world.dark_room_logic = args.dark_room_logic.copy() world.dark_room_logic = args.dark_room_logic.copy()
@ -115,21 +113,21 @@ def main(args, seed=None):
logger.info('') logger.info('')
for player in world.player_ids: for player in world.player_ids:
for item_name in args.startinventory[player]: for item_name in world.start_inventory[player].value:
world.push_precollected(world.create_item(item_name, player)) world.push_precollected(world.create_item(item_name, player))
for player in world.player_ids: for player in world.player_ids:
if player in world.get_game_players("A Link to the Past"): if player in world.get_game_players("A Link to the Past"):
# enforce pre-defined local items. # enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece') world.local_items[player].value.add('Triforce Piece')
# Not possible to place pendants/crystals out side of boss prizes yet. # Not possible to place pendants/crystals out side of boss prizes yet.
world.non_local_items[player] -= item_name_groups['Pendants'] world.non_local_items[player].value -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals'] world.non_local_items[player].value -= item_name_groups['Crystals']
# items can't be both local and non-local, prefer local # items can't be both local and non-local, prefer local
world.non_local_items[player] -= world.local_items[player] world.non_local_items[player].value -= world.local_items[player].value
logger.info('Creating World.') logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions") AutoWorld.call_all(world, "create_regions")
@ -142,13 +140,13 @@ def main(args, seed=None):
for player in world.player_ids: for player in world.player_ids:
locality_rules(world, player) locality_rules(world, player)
else: else:
world.non_local_items[1] = set() world.non_local_items[1].value = set()
world.local_items[1] = set() world.local_items[1].value = set()
AutoWorld.call_all(world, "set_rules") AutoWorld.call_all(world, "set_rules")
for player in world.player_ids: for player in world.player_ids:
exclusion_rules(world, player, args.excluded_locations[player]) exclusion_rules(world, player, world.exclude_locations[player].value)
AutoWorld.call_all(world, "generate_basic") AutoWorld.call_all(world, "generate_basic")
@ -386,7 +384,7 @@ def create_playthrough(world):
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % ( logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates]) sphere_candidates])
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]): if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). ' raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.') f'Something went terribly wrong here.')
else: else:

View File

@ -43,6 +43,9 @@ class Option(metaclass=AssembleOptions):
# Handled in get_option_name() # Handled in get_option_name()
autodisplayname = False autodisplayname = False
# can be weighted between selections
supports_weighting = True
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.get_current_option_name()})" return f"{self.__class__.__name__}({self.get_current_option_name()})"
@ -81,6 +84,7 @@ class Toggle(Option):
default = 0 default = 0
def __init__(self, value: int): def __init__(self, value: int):
assert value == 0 or value == 1
self.value = value self.value = value
@classmethod @classmethod
@ -119,6 +123,7 @@ class Toggle(Option):
def get_option_name(cls, value): def get_option_name(cls, value):
return ["No", "Yes"][int(value)] return ["No", "Yes"][int(value)]
class DefaultOnToggle(Toggle): class DefaultOnToggle(Toggle):
default = 1 default = 1
@ -158,7 +163,7 @@ class Choice(Option):
elif isinstance(other, int): elif isinstance(other, int):
assert other in self.name_lookup assert other in self.name_lookup
return other == self.value return other == self.value
elif isinstance(other, bool): elif isinstance(other, bool):
return other == bool(self.value) return other == bool(self.value)
else: else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
@ -177,6 +182,7 @@ class Choice(Option):
else: else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class Range(Option, int): class Range(Option, int):
range_start = 0 range_start = 0
range_end = 1 range_end = 1
@ -234,6 +240,7 @@ class OptionNameSet(Option):
class OptionDict(Option): class OptionDict(Option):
default = {} default = {}
supports_weighting = False
def __init__(self, value: typing.Dict[str, typing.Any]): def __init__(self, value: typing.Dict[str, typing.Any]):
self.value: typing.Dict[str, typing.Any] = value self.value: typing.Dict[str, typing.Any] = value
@ -246,14 +253,17 @@ class OptionDict(Option):
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def get_option_name(self, value): def get_option_name(self, value):
return str(value) return ", ".join(f"{key}: {value}" for key, value in self.value.items())
class OptionList(Option): class OptionList(Option, list):
default = [] default = []
supports_weighting = False
value: list
def __init__(self, value: typing.List[str, typing.Any]): def __init__(self, value: typing.List[str, typing.Any]):
self.value = value self.value = value
super(OptionList, self).__init__()
@classmethod @classmethod
def from_text(cls, text: str): def from_text(cls, text: str):
@ -266,23 +276,106 @@ class OptionList(Option):
return cls.from_text(str(data)) return cls.from_text(str(data))
def get_option_name(self, value): def get_option_name(self, value):
return str(value) return ", ".join(self.value)
class OptionSet(Option, set):
default = frozenset()
supports_weighting = False
value: set
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
self.value = set(value)
super(OptionSet, self).__init__()
@classmethod
def from_text(cls, text: str):
return cls([option.strip() for option in text.split(",")])
@classmethod
def from_any(cls, data: typing.Any):
if type(data) == list:
return cls(data)
elif type(data) == set:
return cls(data)
return cls.from_text(str(data))
def get_option_name(self, value):
return ", ".join(self.value)
local_objective = Toggle # local triforce pieces, local dungeon prizes etc. local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
class Accessibility(Choice): class Accessibility(Choice):
"""Set rules for reachability of your items/locations.
Locations: ensure everything can be reached and acquired.
Items: ensure all logically relevant items can be acquired.
Minimal: ensure what is needed to reach your goal can be acquired."""
option_locations = 0 option_locations = 0
option_items = 1 option_items = 1
option_beatable = 2 option_minimal = 2
alias_none = 2
default = 1
class ProgressionBalancing(DefaultOnToggle):
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
common_options = {
"progression_balancing": ProgressionBalancing,
"accessibility": Accessibility
}
class ItemSet(OptionSet):
# implemented by Generate
verify_item_name = True
class LocalItems(ItemSet):
"""Forces these items to be in their native world."""
displayname = "Local Items"
class NonLocalItems(ItemSet):
"""Forces these items to be outside their native world."""
displayname = "Not Local Items"
class StartInventory(OptionDict):
"""Start with these items."""
verify_item_name = True
displayname = "Start Inventory"
class StartHints(ItemSet):
"""Start with these item's locations prefilled into the !hint command."""
displayname = "Start Hints"
class ExcludeLocations(OptionSet):
"""Prevent these locations from having an important item"""
displayname = "Excluded Locations"
verify_location_name = True
per_game_common_options = {
# placeholder until they're actually implemented
"local_items": LocalItems,
"non_local_items": NonLocalItems,
"start_inventory": StartInventory,
"start_hints": StartHints,
"exclude_locations": OptionSet
}
if __name__ == "__main__": if __name__ == "__main__":
from worlds.alttp.Options import Logic from worlds.alttp.Options import Logic
import argparse import argparse
map_shuffle = Toggle map_shuffle = Toggle
compass_shuffle = Toggle compass_shuffle = Toggle
keyshuffle = Toggle keyshuffle = Toggle

View File

@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
build: int build: int
__version__ = "0.1.7" __version__ = "0.1.8"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
import builtins import builtins

View File

@ -5,6 +5,7 @@ import yaml
import json import json
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
import Options
target_folder = os.path.join("WebHostLib", "static", "generated") target_folder = os.path.join("WebHostLib", "static", "generated")
@ -18,10 +19,17 @@ def create():
option.range_end: "maximum value" option.range_end: "maximum value"
} }
return data, notes return data, notes
def default_converter(default_value):
if isinstance(default_value, (set, frozenset)):
return list(default_value)
return default_value
for game_name, world in AutoWorldRegister.world_types.items(): for game_name, world in AutoWorldRegister.world_types.items():
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, options={**world.options, **Options.per_game_common_options},
dictify_range=dictify_range __version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range, default_converter=default_converter,
) )
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f: with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:

View File

@ -36,22 +36,7 @@ accessibility:
progression_balancing: progression_balancing:
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
# The following 4 options can be uncommented and moved into a game's section they should affect
# start_inventory: # Begin the file with the listed items/upgrades
# Please only use items for the correct game, use triggers if need to be have seperated lists.
# Pegasus Boots: on
# Bomb Upgrade (+10): 4
# Arrow Upgrade (+10): 4
# start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client.
# - Moon Pearl
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
# - "Small Keys"
# - "Big Keys"
# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
# - "Progressive Weapons"
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
# - "Master Sword Pedestal"
{%- macro range_option(option) %} {%- macro range_option(option) %}
# you can add additional values between minimum and maximum # you can add additional values between minimum and maximum
{%- set data, notes = dictify_range(option) %} {%- set data, notes = dictify_range(option) %}
@ -69,7 +54,7 @@ progression_balancing:
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} {{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
{%- endfor -%} {%- endfor -%}
{%- else %} {%- else %}
{{ yaml_dump(option.default) | indent(4, first=False) }} {{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }}
{%- endif -%} {%- endif -%}
{%- endfor %} {%- endfor %}
{% if not options %}{}{% endif %} {% if not options %}{}{% endif %}

View File

@ -19,6 +19,7 @@ class TestDungeon(unittest.TestCase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.starting_regions = [] # Where to start exploring self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits self.remove_exits = [] # Block dungeon exits
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']

View File

@ -19,6 +19,7 @@ class TestInverted(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
create_inverted_regions(self.world, 1) create_inverted_regions(self.world, 1)

View File

@ -20,6 +20,7 @@ class TestInvertedBombRules(unittest.TestCase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.world, 1) create_inverted_regions(self.world, 1)
create_dungeons(self.world, 1) create_dungeons(self.world, 1)

View File

@ -20,6 +20,7 @@ class TestInvertedMinor(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
self.world.logic[1] = "minorglitches" self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']

View File

@ -21,6 +21,7 @@ class TestInvertedOWG(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.world.logic[1] = "owglitches" self.world.logic[1] = "owglitches"
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']

View File

@ -20,6 +20,7 @@ class TestMinor(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.world.logic[1] = "minorglitches" self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1) create_regions(self.world, 1)

View File

@ -21,6 +21,7 @@ class TestVanillaOWG(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
self.world.logic[1] = "owglitches" self.world.logic[1] = "owglitches"
create_regions(self.world, 1) create_regions(self.world, 1)

View File

@ -19,6 +19,7 @@ class TestVanilla(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options()
self.world.logic[1] = "noglitches" self.world.logic[1] = "noglitches"
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1) create_regions(self.world, 1)

View File

@ -194,19 +194,8 @@ def parse_arguments(argv, no_defaults=False):
time). time).
''', type=int) ''', type=int)
parser.add_argument('--local_items', default=defval(''),
help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
parser.add_argument('--non_local_items', default=defval(''),
help='Specifies a list of items that will spread across the multiworld (separated by commas)')
parser.add_argument('--custom', default=defval(False), help='Not supported.') parser.add_argument('--custom', default=defval(False), help='Not supported.')
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.') parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about
reaching all locations or all keys.
Locations: You will be able to reach every location in the game.
None: You will be able to reach enough locations to beat the game.
''')
# included for backwards compatibility # included for backwards compatibility
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True)) parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
parser.add_argument('--no-shuffleganon', help='''\ parser.add_argument('--no-shuffleganon', help='''\
@ -222,8 +211,7 @@ def parse_arguments(argv, no_defaults=False):
sprite that will be extracted. sprite that will be extracted.
''') ''')
parser.add_argument('--gui', help='Launch the GUI', action='store_true') parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--progression_balancing', action='store_true', default=defval(False),
help="Enable Multiworld Progression balancing.")
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core')) parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos', parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
"singularity"]) "singularity"])
@ -285,10 +273,10 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'goal', 'difficulty', 'item_functionality', for name in ['logic', 'mode', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'open_pyramid', 'timer', 'shuffle', 'open_pyramid', 'timer',
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time', 'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
'local_items', 'non_local_items', 'accessibility', 'beemizer', 'beemizer',
'shufflebosses', 'enemy_health', 'enemy_damage', 'shufflebosses', 'enemy_health', 'enemy_damage',
'sprite', 'sprite',
"progression_balancing", "triforce_pieces_available", "triforce_pieces_available",
"triforce_pieces_required", "shop_shuffle", "triforce_pieces_required", "shop_shuffle",
"required_medallions", "start_hints", "required_medallions", "start_hints",
"plando_items", "plando_texts", "plando_connections", "er_seeds", "plando_items", "plando_texts", "plando_connections", "er_seeds",

View File

@ -244,7 +244,7 @@ def generate_itempool(world):
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
if world.goal[player] == 'icerodhunt': if world.goal[player] == 'icerodhunt':
world.progression_balancing[player] = False world.progression_balancing[player].value = False
loc = world.get_location('Turtle Rock - Boss', player) loc = world.get_location('Turtle Rock - Boss', player)
world.push_item(loc, ItemFactory('Triforce Piece', player), False) world.push_item(loc, ItemFactory('Triforce Piece', player), False)
world.treasure_hunt_count[player] = 1 world.treasure_hunt_count[player] = 1

View File

@ -27,8 +27,8 @@ def set_rules(world):
else: else:
# Set access rules according to max glitches for multiworld progression. # Set access rules according to max glitches for multiworld progression.
# Set accessibility to none, and shuffle assuming the no logic players can always win # Set accessibility to none, and shuffle assuming the no logic players can always win
world.accessibility[player] = 'none' world.accessibility[player] = world.accessibility[player].from_text("minimal")
world.progression_balancing[player] = False world.progression_balancing[player].value = False
else: else:
world.completion_condition[player] = lambda state: state.has('Triforce', player) world.completion_condition[player] = lambda state: state.has('Triforce', player)

View File

@ -74,9 +74,9 @@ class ALTTPWorld(World):
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]: for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
option = getattr(world, dungeon_item)[player] option = getattr(world, dungeon_item)[player]
if option == "own_world": if option == "own_world":
world.local_items[player] |= self.item_name_groups[option.item_name_group] world.local_items[player].value |= self.item_name_groups[option.item_name_group]
elif option == "different_world": elif option == "different_world":
world.non_local_items[player] |= self.item_name_groups[option.item_name_group] world.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
elif option.in_dungeon: elif option.in_dungeon:
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group] self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon": if option == "original_dungeon":

View File

@ -1,16 +1,16 @@
def locality_rules(world, player): def locality_rules(world, player):
if world.local_items[player]: if world.local_items[player].value:
for location in world.get_locations(): for location in world.get_locations():
if location.player != player: if location.player != player:
forbid_items_for_player(location, world.local_items[player], player) forbid_items_for_player(location, world.local_items[player].value, player)
if world.non_local_items[player]: if world.non_local_items[player].value:
for location in world.get_locations(): for location in world.get_locations():
if location.player == player: if location.player == player:
forbid_items_for_player(location, world.non_local_items[player], player) forbid_items_for_player(location, world.non_local_items[player].value, player)
def exclusion_rules(world, player: int, excluded_locations: set): def exclusion_rules(world, player: int, exclude_locations: set):
for loc_name in excluded_locations: for loc_name in exclude_locations:
location = world.get_location(loc_name, player) location = world.get_location(loc_name, player)
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude)) add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
location.excluded = True location.excluded = True