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:
parent
abc0220cfa
commit
a82bf1bb32
|
@ -10,6 +10,7 @@ from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
|
|||
import secrets
|
||||
import random
|
||||
|
||||
import Options
|
||||
import Utils
|
||||
|
||||
|
||||
|
@ -83,7 +84,6 @@ class MultiWorld():
|
|||
set_player_attr('item_functionality', 'normal')
|
||||
set_player_attr('timer', False)
|
||||
set_player_attr('goal', 'ganon')
|
||||
set_player_attr('accessibility', 'items')
|
||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||
set_player_attr('swamp_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('green_clock_time', 4)
|
||||
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_required', 20)
|
||||
set_player_attr('shop_shuffle', 'off')
|
||||
|
@ -131,10 +128,21 @@ class MultiWorld():
|
|||
for player in self.player_ids:
|
||||
self.custom_data[player] = {}
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
for option in world_type.options:
|
||||
setattr(self, option, getattr(args, option, {}))
|
||||
for option_key in world_type.options:
|
||||
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)
|
||||
|
||||
# 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):
|
||||
self.random = secrets.SystemRandom()
|
||||
self.is_race = True
|
||||
|
@ -384,17 +392,17 @@ class MultiWorld():
|
|||
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
||||
if not state:
|
||||
state = CollectionState(self)
|
||||
players = {"none" : set(),
|
||||
players = {"minimal" : set(),
|
||||
"items": set(),
|
||||
"locations": set()}
|
||||
for player, access in self.accessibility.items():
|
||||
players[access].add(player)
|
||||
players[access.current_key].add(player)
|
||||
|
||||
beatable_fulfilled = False
|
||||
|
||||
def location_conditition(location : Location):
|
||||
"""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 True
|
||||
|
||||
|
@ -1003,7 +1011,7 @@ class Spoiler():
|
|||
self.medallions = {}
|
||||
self.playthrough = {}
|
||||
self.unreachables = []
|
||||
self.startinventory = []
|
||||
self.start_inventory = []
|
||||
self.locations = {}
|
||||
self.paths = {}
|
||||
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'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()
|
||||
listed_locations = set()
|
||||
|
@ -1103,7 +1111,7 @@ class Spoiler():
|
|||
out = OrderedDict()
|
||||
out['Entrances'] = list(self.entrances.values())
|
||||
out.update(self.locations)
|
||||
out['Starting Inventory'] = self.startinventory
|
||||
out['Starting Inventory'] = self.start_inventory
|
||||
out['Special'] = self.medallions
|
||||
if self.hashes:
|
||||
out['Hashes'] = self.hashes
|
||||
|
@ -1123,6 +1131,14 @@ class Spoiler():
|
|||
return variable
|
||||
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:
|
||||
outfile.write(
|
||||
'Archipelago Version %s - Seed: %s\n\n' % (
|
||||
|
@ -1134,16 +1150,14 @@ class Spoiler():
|
|||
if self.world.players > 1:
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
|
||||
outfile.write('Game: %s\n' % self.world.game[player])
|
||||
if self.world.players > 1:
|
||||
outfile.write('Progression Balanced: %s\n' % (
|
||||
'Yes' if self.world.progression_balancing[player] else 'No'))
|
||||
outfile.write('Accessibility: %s\n' % self.world.accessibility[player])
|
||||
for f_option, option in Options.common_options.items():
|
||||
write_option(f_option, option)
|
||||
for f_option, option in Options.per_game_common_options.items():
|
||||
write_option(f_option, option)
|
||||
options = self.world.worlds[player].options
|
||||
if options:
|
||||
for f_option, option in options.items():
|
||||
res = getattr(self.world, f_option)[player]
|
||||
displayname = getattr(option, "displayname", f_option)
|
||||
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
|
||||
write_option(f_option, option)
|
||||
|
||||
if player in self.world.get_game_players("A Link to the Past"):
|
||||
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():
|
||||
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'.join(self.startinventory))
|
||||
outfile.write('\n'.join(self.start_inventory))
|
||||
|
||||
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()]))
|
||||
|
|
8
Fill.py
8
Fill.py
|
@ -36,7 +36,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
|||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
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,
|
||||
item_to_place.player) if single_player_placement else not has_beaten_game
|
||||
else:
|
||||
|
@ -52,7 +52,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
|||
else:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
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(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
|
||||
continue
|
||||
|
@ -87,9 +87,9 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
|||
progitempool.append(item)
|
||||
elif item.never_exclude: # this only gets nonprogression items which should not appear in excluded locations
|
||||
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)
|
||||
elif item.name in world.non_local_items[item.player]:
|
||||
elif item.name in world.non_local_items[item.player].value:
|
||||
nonlocalrestitempool.append(item)
|
||||
else:
|
||||
restitempool.append(item)
|
||||
|
|
86
Generate.py
86
Generate.py
|
@ -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.")
|
||||
|
||||
|
||||
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",))):
|
||||
if "linked_options" in 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.")
|
||||
|
||||
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.accessibility = get_choice('accessibility', weights)
|
||||
ret.progression_balancing = get_choice('progression_balancing', weights, True)
|
||||
for option_key, option in Options.common_options.items():
|
||||
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
||||
ret.game = get_choice("game", weights)
|
||||
if ret.game not in weights:
|
||||
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
|
||||
world_type = AutoWorldRegister.world_types[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:
|
||||
for option_name, option in world_type.options.items():
|
||||
if option_name in game_weights:
|
||||
try:
|
||||
if issubclass(option, Options.OptionDict) or issubclass(option, Options.OptionList):
|
||||
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))
|
||||
for option_key, option in world_type.options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
if "items" in plando_options:
|
||||
ret.plando_items = roll_item_plando(world_type, game_weights)
|
||||
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):
|
||||
plando_items = []
|
||||
|
||||
def add_plando_item(item: str, location: str):
|
||||
if item not in world_type.item_name_to_id:
|
||||
raise Exception(f"Could not plando item {item} as the item was not recognized")
|
||||
|
|
22
Main.py
22
Main.py
|
@ -54,14 +54,13 @@ def main(args, seed=None):
|
|||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.goal = args.goal.copy()
|
||||
world.local_items = args.local_items.copy()
|
||||
|
||||
if hasattr(args, "algorithm"): # current GUI options
|
||||
world.algorithm = args.algorithm
|
||||
world.shuffleganon = args.shuffleganon
|
||||
world.custom = args.custom
|
||||
world.customitemarray = args.customitemarray
|
||||
|
||||
world.accessibility = args.accessibility.copy()
|
||||
world.open_pyramid = args.open_pyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.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_required = args.triforce_pieces_required.copy()
|
||||
world.shop_shuffle = args.shop_shuffle.copy()
|
||||
world.progression_balancing = args.progression_balancing.copy()
|
||||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
world.sprite_pool = args.sprite_pool.copy()
|
||||
world.dark_room_logic = args.dark_room_logic.copy()
|
||||
|
@ -115,21 +113,21 @@ def main(args, seed=None):
|
|||
logger.info('')
|
||||
|
||||
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))
|
||||
|
||||
for player in world.player_ids:
|
||||
if player in world.get_game_players("A Link to the Past"):
|
||||
# enforce pre-defined local items.
|
||||
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.
|
||||
world.non_local_items[player] -= item_name_groups['Pendants']
|
||||
world.non_local_items[player] -= item_name_groups['Crystals']
|
||||
world.non_local_items[player].value -= item_name_groups['Pendants']
|
||||
world.non_local_items[player].value -= item_name_groups['Crystals']
|
||||
|
||||
# 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.')
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
|
@ -142,13 +140,13 @@ def main(args, seed=None):
|
|||
for player in world.player_ids:
|
||||
locality_rules(world, player)
|
||||
else:
|
||||
world.non_local_items[1] = set()
|
||||
world.local_items[1] = set()
|
||||
world.non_local_items[1].value = set()
|
||||
world.local_items[1].value = set()
|
||||
|
||||
AutoWorld.call_all(world, "set_rules")
|
||||
|
||||
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")
|
||||
|
||||
|
@ -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)' % (
|
||||
location.item.name, location.item.player, location.name, location.player) for location in
|
||||
sphere_candidates])
|
||||
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
|
||||
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
||||
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
||||
f'Something went terribly wrong here.')
|
||||
else:
|
||||
|
|
103
Options.py
103
Options.py
|
@ -43,6 +43,9 @@ class Option(metaclass=AssembleOptions):
|
|||
# Handled in get_option_name()
|
||||
autodisplayname = False
|
||||
|
||||
# can be weighted between selections
|
||||
supports_weighting = True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.get_current_option_name()})"
|
||||
|
||||
|
@ -81,6 +84,7 @@ class Toggle(Option):
|
|||
default = 0
|
||||
|
||||
def __init__(self, value: int):
|
||||
assert value == 0 or value == 1
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
|
@ -119,6 +123,7 @@ class Toggle(Option):
|
|||
def get_option_name(cls, value):
|
||||
return ["No", "Yes"][int(value)]
|
||||
|
||||
|
||||
class DefaultOnToggle(Toggle):
|
||||
default = 1
|
||||
|
||||
|
@ -158,7 +163,7 @@ class Choice(Option):
|
|||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup
|
||||
return other == self.value
|
||||
elif isinstance(other, bool):
|
||||
elif isinstance(other, bool):
|
||||
return other == bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
@ -177,6 +182,7 @@ class Choice(Option):
|
|||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
|
||||
class Range(Option, int):
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
|
@ -234,6 +240,7 @@ class OptionNameSet(Option):
|
|||
|
||||
class OptionDict(Option):
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
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)}")
|
||||
|
||||
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 = []
|
||||
supports_weighting = False
|
||||
value: list
|
||||
|
||||
def __init__(self, value: typing.List[str, typing.Any]):
|
||||
self.value = value
|
||||
super(OptionList, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
|
@ -266,23 +276,106 @@ class OptionList(Option):
|
|||
return cls.from_text(str(data))
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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_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__":
|
||||
|
||||
from worlds.alttp.Options import Logic
|
||||
import argparse
|
||||
|
||||
map_shuffle = Toggle
|
||||
compass_shuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
|
|
2
Utils.py
2
Utils.py
|
@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
|||
build: int
|
||||
|
||||
|
||||
__version__ = "0.1.7"
|
||||
__version__ = "0.1.8"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
|
|
|
@ -5,6 +5,7 @@ import yaml
|
|||
import json
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import Options
|
||||
|
||||
target_folder = os.path.join("WebHostLib", "static", "generated")
|
||||
|
||||
|
@ -18,10 +19,17 @@ def create():
|
|||
option.range_end: "maximum value"
|
||||
}
|
||||
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():
|
||||
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
|
||||
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range
|
||||
options={**world.options, **Options.per_game_common_options},
|
||||
__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:
|
||||
|
|
|
@ -36,22 +36,7 @@ accessibility:
|
|||
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
|
||||
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) %}
|
||||
# you can add additional values between minimum and maximum
|
||||
{%- 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 %}
|
||||
{%- endfor -%}
|
||||
{%- else %}
|
||||
{{ yaml_dump(option.default) | indent(4, first=False) }}
|
||||
{{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }}
|
||||
{%- endif -%}
|
||||
{%- endfor %}
|
||||
{% if not options %}{}{% endif %}
|
||||
|
|
|
@ -19,6 +19,7 @@ class TestDungeon(unittest.TestCase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.starting_regions = [] # Where to start exploring
|
||||
self.remove_exits = [] # Block dungeon exits
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
|
|
@ -19,6 +19,7 @@ class TestInverted(TestBase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
self.world.mode[1] = "inverted"
|
||||
create_inverted_regions(self.world, 1)
|
||||
|
|
|
@ -20,6 +20,7 @@ class TestInvertedBombRules(unittest.TestCase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_inverted_regions(self.world, 1)
|
||||
create_dungeons(self.world, 1)
|
||||
|
|
|
@ -20,6 +20,7 @@ class TestInvertedMinor(TestBase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.world.mode[1] = "inverted"
|
||||
self.world.logic[1] = "minorglitches"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
|
|
@ -21,6 +21,7 @@ class TestInvertedOWG(TestBase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.world.logic[1] = "owglitches"
|
||||
self.world.mode[1] = "inverted"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
|
|
|
@ -20,6 +20,7 @@ class TestMinor(TestBase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.world.logic[1] = "minorglitches"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_regions(self.world, 1)
|
||||
|
|
|
@ -21,6 +21,7 @@ class TestVanillaOWG(TestBase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
self.world.logic[1] = "owglitches"
|
||||
create_regions(self.world, 1)
|
||||
|
|
|
@ -19,6 +19,7 @@ class TestVanilla(TestBase):
|
|||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
self.world.logic[1] = "noglitches"
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_regions(self.world, 1)
|
||||
|
|
|
@ -194,19 +194,8 @@ def parse_arguments(argv, no_defaults=False):
|
|||
time).
|
||||
''', 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('--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
|
||||
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
|
||||
parser.add_argument('--no-shuffleganon', help='''\
|
||||
|
@ -222,8 +211,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
sprite that will be extracted.
|
||||
''')
|
||||
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('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos',
|
||||
"singularity"])
|
||||
|
@ -285,10 +273,10 @@ def parse_arguments(argv, no_defaults=False):
|
|||
for name in ['logic', 'mode', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'open_pyramid', 'timer',
|
||||
'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',
|
||||
'sprite',
|
||||
"progression_balancing", "triforce_pieces_available",
|
||||
"triforce_pieces_available",
|
||||
"triforce_pieces_required", "shop_shuffle",
|
||||
"required_medallions", "start_hints",
|
||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
||||
|
|
|
@ -244,7 +244,7 @@ def generate_itempool(world):
|
|||
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
|
||||
|
||||
if world.goal[player] == 'icerodhunt':
|
||||
world.progression_balancing[player] = False
|
||||
world.progression_balancing[player].value = False
|
||||
loc = world.get_location('Turtle Rock - Boss', player)
|
||||
world.push_item(loc, ItemFactory('Triforce Piece', player), False)
|
||||
world.treasure_hunt_count[player] = 1
|
||||
|
|
|
@ -27,8 +27,8 @@ def set_rules(world):
|
|||
else:
|
||||
# Set access rules according to max glitches for multiworld progression.
|
||||
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
||||
world.accessibility[player] = 'none'
|
||||
world.progression_balancing[player] = False
|
||||
world.accessibility[player] = world.accessibility[player].from_text("minimal")
|
||||
world.progression_balancing[player].value = False
|
||||
|
||||
else:
|
||||
world.completion_condition[player] = lambda state: state.has('Triforce', player)
|
||||
|
|
|
@ -74,9 +74,9 @@ class ALTTPWorld(World):
|
|||
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
|
||||
option = getattr(world, dungeon_item)[player]
|
||||
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":
|
||||
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:
|
||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||
if option == "original_dungeon":
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
def locality_rules(world, player):
|
||||
if world.local_items[player]:
|
||||
if world.local_items[player].value:
|
||||
for location in world.get_locations():
|
||||
if location.player != player:
|
||||
forbid_items_for_player(location, world.local_items[player], player)
|
||||
if world.non_local_items[player]:
|
||||
forbid_items_for_player(location, world.local_items[player].value, player)
|
||||
if world.non_local_items[player].value:
|
||||
for location in world.get_locations():
|
||||
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):
|
||||
for loc_name in excluded_locations:
|
||||
def exclusion_rules(world, player: int, exclude_locations: set):
|
||||
for loc_name in exclude_locations:
|
||||
location = world.get_location(loc_name, player)
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
||||
location.excluded = True
|
||||
|
|
Loading…
Reference in New Issue