2020-03-02 23:12:14 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
import copy
|
2018-01-19 02:51:43 +00:00
|
|
|
from enum import Enum, unique
|
2017-05-16 19:23:47 +00:00
|
|
|
import logging
|
2017-07-18 10:44:13 +00:00
|
|
|
import json
|
2021-02-20 01:30:55 +00:00
|
|
|
import functools
|
2020-05-10 09:27:13 +00:00
|
|
|
from collections import OrderedDict, Counter, deque
|
2022-02-05 14:49:19 +00:00
|
|
|
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, TYPE_CHECKING
|
2020-07-14 05:01:51 +00:00
|
|
|
import secrets
|
|
|
|
import random
|
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
import Options
|
2021-09-13 00:01:15 +00:00
|
|
|
import Utils
|
2022-01-30 12:57:12 +00:00
|
|
|
import NetUtils
|
2021-09-13 00:01:15 +00:00
|
|
|
|
2022-02-05 14:49:19 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from worlds import AutoWorld
|
|
|
|
auto_world = AutoWorld.World
|
|
|
|
else:
|
|
|
|
auto_world = object
|
|
|
|
|
|
|
|
|
|
|
|
class Group(TypedDict):
|
|
|
|
name: str
|
|
|
|
game: str
|
|
|
|
world: auto_world
|
|
|
|
players: Set[int]
|
|
|
|
|
2020-07-14 05:01:51 +00:00
|
|
|
|
2020-10-24 03:38:56 +00:00
|
|
|
class MultiWorld():
|
2020-04-10 18:54:18 +00:00
|
|
|
debug_types = False
|
2021-08-09 07:15:41 +00:00
|
|
|
player_name: Dict[int, str]
|
2021-07-21 16:08:15 +00:00
|
|
|
_region_cache: Dict[int, Dict[str, Region]]
|
2020-03-02 23:12:14 +00:00
|
|
|
difficulty_requirements: dict
|
|
|
|
required_medallions: dict
|
2020-10-07 17:51:46 +00:00
|
|
|
dark_room_logic: Dict[int, str]
|
|
|
|
restrict_dungeon_item_on_boss: Dict[int, bool]
|
2021-01-02 21:41:03 +00:00
|
|
|
plando_texts: List[Dict[str, str]]
|
2021-07-21 16:08:15 +00:00
|
|
|
plando_items: List
|
|
|
|
plando_connections: List
|
2021-07-21 20:55:44 +00:00
|
|
|
worlds: Dict[int, Any]
|
2022-02-05 14:49:19 +00:00
|
|
|
groups: Dict[int, Group]
|
2021-07-20 19:19:53 +00:00
|
|
|
is_race: bool = False
|
2021-10-10 14:50:01 +00:00
|
|
|
precollected_items: Dict[int, List[Item]]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-04-10 04:36:06 +00:00
|
|
|
class AttributeProxy():
|
|
|
|
def __init__(self, rule):
|
|
|
|
self.rule = rule
|
2021-04-10 16:45:11 +00:00
|
|
|
|
2021-04-10 04:36:06 +00:00
|
|
|
def __getitem__(self, player) -> bool:
|
|
|
|
return self.rule(player)
|
|
|
|
|
2021-03-14 07:38:02 +00:00
|
|
|
def __init__(self, players: int):
|
2021-03-14 21:59:41 +00:00
|
|
|
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
|
2019-04-18 09:23:24 +00:00
|
|
|
self.players = players
|
2022-01-30 12:57:12 +00:00
|
|
|
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
|
2021-03-22 20:14:19 +00:00
|
|
|
self.glitch_triforce = False
|
2021-03-14 07:38:02 +00:00
|
|
|
self.algorithm = 'balanced'
|
2021-08-29 14:02:28 +00:00
|
|
|
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
|
2022-02-05 14:49:19 +00:00
|
|
|
self.groups = {}
|
2017-05-15 18:28:04 +00:00
|
|
|
self.regions = []
|
2018-02-17 23:38:54 +00:00
|
|
|
self.shops = []
|
2017-05-15 18:28:04 +00:00
|
|
|
self.itempool = []
|
2017-05-20 12:03:15 +00:00
|
|
|
self.seed = None
|
2021-05-15 22:21:00 +00:00
|
|
|
self.seed_name: str = "Unavailable"
|
2021-10-10 14:50:01 +00:00
|
|
|
self.precollected_items = {player: [] for player in self.player_ids}
|
2017-05-15 18:28:04 +00:00
|
|
|
self.state = CollectionState(self)
|
2019-01-20 07:01:02 +00:00
|
|
|
self._cached_entrances = None
|
2017-05-15 18:28:04 +00:00
|
|
|
self._cached_locations = None
|
|
|
|
self._entrance_cache = {}
|
|
|
|
self._location_cache = {}
|
2017-06-04 14:15:59 +00:00
|
|
|
self.required_locations = []
|
2017-06-03 19:27:34 +00:00
|
|
|
self.light_world_light_cone = False
|
2017-06-03 13:46:05 +00:00
|
|
|
self.dark_world_light_cone = False
|
2018-01-22 02:43:44 +00:00
|
|
|
self.rupoor_cost = 10
|
2017-08-01 16:58:42 +00:00
|
|
|
self.aga_randomness = True
|
2017-06-04 11:10:22 +00:00
|
|
|
self.lock_aga_door_in_escape = False
|
2018-09-23 02:51:54 +00:00
|
|
|
self.save_and_quit_from_boss = True
|
2021-03-14 07:38:02 +00:00
|
|
|
self.custom = False
|
|
|
|
self.customitemarray = []
|
|
|
|
self.shuffle_ganon = True
|
2017-07-18 10:44:13 +00:00
|
|
|
self.spoiler = Spoiler(self)
|
2021-10-06 09:32:49 +00:00
|
|
|
self.fix_trock_doors = self.AttributeProxy(
|
|
|
|
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
|
|
|
self.fix_skullwoods_exit = self.AttributeProxy(
|
|
|
|
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
|
|
self.fix_palaceofdarkness_exit = self.AttributeProxy(
|
|
|
|
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
|
|
self.fix_trock_exit = self.AttributeProxy(
|
|
|
|
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
2021-04-10 04:36:06 +00:00
|
|
|
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2019-12-17 20:09:33 +00:00
|
|
|
for player in range(1, players + 1):
|
|
|
|
def set_player_attr(attr, val):
|
|
|
|
self.__dict__.setdefault(attr, {})[player] = val
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2021-04-10 01:03:46 +00:00
|
|
|
set_player_attr('tech_tree_layout_prerequisites', {})
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('_region_cache', {})
|
2021-03-14 07:38:02 +00:00
|
|
|
set_player_attr('shuffle', "vanilla")
|
|
|
|
set_player_attr('logic', "noglitches")
|
|
|
|
set_player_attr('mode', 'open')
|
|
|
|
set_player_attr('difficulty', 'normal')
|
|
|
|
set_player_attr('item_functionality', 'normal')
|
|
|
|
set_player_attr('timer', False)
|
|
|
|
set_player_attr('goal', 'ganon')
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
|
|
|
set_player_attr('swamp_patch_required', False)
|
|
|
|
set_player_attr('powder_patch_required', False)
|
|
|
|
set_player_attr('ganon_at_pyramid', True)
|
|
|
|
set_player_attr('ganonstower_vanilla', True)
|
2019-12-18 19:45:51 +00:00
|
|
|
set_player_attr('can_access_trock_eyebridge', None)
|
|
|
|
set_player_attr('can_access_trock_front', None)
|
|
|
|
set_player_attr('can_access_trock_big_chest', None)
|
|
|
|
set_player_attr('can_access_trock_middle', None)
|
|
|
|
set_player_attr('fix_fake_world', True)
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('difficulty_requirements', None)
|
|
|
|
set_player_attr('boss_shuffle', 'none')
|
|
|
|
set_player_attr('enemy_health', 'default')
|
|
|
|
set_player_attr('enemy_damage', 'default')
|
2021-11-03 05:34:11 +00:00
|
|
|
set_player_attr('beemizer_total_chance', 0)
|
|
|
|
set_player_attr('beemizer_trap_chance', 0)
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('escape_assist', [])
|
|
|
|
set_player_attr('open_pyramid', False)
|
2019-12-21 09:42:59 +00:00
|
|
|
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
|
|
|
set_player_attr('treasure_hunt_count', 0)
|
2020-03-04 12:55:03 +00:00
|
|
|
set_player_attr('clock_mode', False)
|
2020-10-28 23:20:59 +00:00
|
|
|
set_player_attr('countdown_start_time', 10)
|
|
|
|
set_player_attr('red_clock_time', -2)
|
|
|
|
set_player_attr('blue_clock_time', 2)
|
|
|
|
set_player_attr('green_clock_time', 4)
|
2020-02-03 02:52:57 +00:00
|
|
|
set_player_attr('can_take_damage', True)
|
2020-06-17 08:02:54 +00:00
|
|
|
set_player_attr('triforce_pieces_available', 30)
|
2020-06-07 13:22:24 +00:00
|
|
|
set_player_attr('triforce_pieces_required', 20)
|
2020-08-23 13:03:06 +00:00
|
|
|
set_player_attr('shop_shuffle', 'off')
|
2020-09-20 02:35:45 +00:00
|
|
|
set_player_attr('shuffle_prizes', "g")
|
2020-10-06 20:22:03 +00:00
|
|
|
set_player_attr('sprite_pool', [])
|
2020-10-07 17:51:46 +00:00
|
|
|
set_player_attr('dark_room_logic', "lamp")
|
2021-01-02 11:49:43 +00:00
|
|
|
set_player_attr('plando_items', [])
|
2021-01-02 15:44:58 +00:00
|
|
|
set_player_attr('plando_texts', {})
|
2021-01-02 21:41:03 +00:00
|
|
|
set_player_attr('plando_connections', [])
|
2021-02-21 19:17:24 +00:00
|
|
|
set_player_attr('game', "A Link to the Past")
|
2021-02-22 10:18:53 +00:00
|
|
|
set_player_attr('completion_condition', lambda state: True)
|
2021-04-23 23:16:49 +00:00
|
|
|
self.custom_data = {}
|
2021-06-11 12:22:44 +00:00
|
|
|
self.worlds = {}
|
2021-10-06 09:32:49 +00:00
|
|
|
self.slot_seeds = {}
|
|
|
|
|
2022-02-05 14:49:19 +00:00
|
|
|
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
|
|
|
"""Create a group with name and return the assigned player ID and group.
|
|
|
|
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
|
|
|
for group_id, group in self.groups.items():
|
|
|
|
if group["name"] == name:
|
|
|
|
group["players"] |= players
|
|
|
|
return group_id, group
|
|
|
|
new_id: int = self.players + len(self.groups) + 1
|
|
|
|
from worlds import AutoWorld
|
|
|
|
self.game[new_id] = game
|
|
|
|
self.custom_data[new_id] = {}
|
|
|
|
self.player_types[new_id] = NetUtils.SlotType.group
|
|
|
|
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
|
|
|
for option_key, option in world_type.options.items():
|
|
|
|
getattr(self, option_key)[new_id] = option(option.default)
|
|
|
|
for option_key, option in Options.common_options.items():
|
|
|
|
getattr(self, option_key)[new_id] = option(option.default)
|
|
|
|
for option_key, option in Options.per_game_common_options.items():
|
|
|
|
getattr(self, option_key)[new_id] = option(option.default)
|
|
|
|
|
|
|
|
self.worlds[new_id] = world_type(self, new_id)
|
|
|
|
|
|
|
|
self.player_name[new_id] = name
|
|
|
|
# TODO: remove when LttP are transitioned over
|
|
|
|
self.difficulty_requirements[new_id] = self.difficulty_requirements[next(iter(players))]
|
|
|
|
|
|
|
|
new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
|
|
|
|
world=self.worlds[new_id])
|
|
|
|
|
|
|
|
# instead of collect/remove overwrites, should encode sending as Events so they show up in spoiler log
|
|
|
|
def group_collect(state, item) -> bool:
|
|
|
|
changed = False
|
|
|
|
for player in new_group["players"]:
|
|
|
|
max(self.worlds[player].collect(state, item), changed)
|
|
|
|
return changed
|
|
|
|
|
|
|
|
def group_remove(state, item) -> bool:
|
|
|
|
changed = False
|
|
|
|
for player in new_group["players"]:
|
|
|
|
max(self.worlds[player].remove(state, item), changed)
|
|
|
|
return changed
|
|
|
|
|
|
|
|
new_world = new_group["world"]
|
|
|
|
new_world.collect = group_collect
|
|
|
|
new_world.remove = group_remove
|
|
|
|
|
|
|
|
self.worlds[new_id] = new_world
|
|
|
|
|
|
|
|
return new_id, new_group
|
|
|
|
|
2022-02-05 15:55:11 +00:00
|
|
|
def get_player_groups(self, player) -> Set[int]:
|
2022-02-05 14:49:19 +00:00
|
|
|
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
|
|
|
self.seed = get_seed(seed)
|
|
|
|
if secure:
|
|
|
|
self.secure()
|
|
|
|
else:
|
|
|
|
self.random.seed(self.seed)
|
|
|
|
self.seed_name = name if name else str(self.seed)
|
|
|
|
self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in
|
|
|
|
range(1, self.players + 1)}
|
2021-06-11 16:02:48 +00:00
|
|
|
|
|
|
|
def set_options(self, args):
|
|
|
|
from worlds import AutoWorld
|
|
|
|
for player in self.player_ids:
|
2021-04-23 23:16:49 +00:00
|
|
|
self.custom_data[player] = {}
|
2021-07-04 14:18:21 +00:00
|
|
|
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
2021-09-16 22:17:54 +00:00
|
|
|
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, {}))
|
2021-07-04 14:18:21 +00:00
|
|
|
self.worlds[player] = world_type(self, player)
|
2021-03-20 23:47:17 +00:00
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
# 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})
|
|
|
|
|
2020-07-14 05:01:51 +00:00
|
|
|
def secure(self):
|
|
|
|
self.random = secrets.SystemRandom()
|
2021-07-20 18:21:27 +00:00
|
|
|
self.is_race = True
|
2020-07-14 05:01:51 +00:00
|
|
|
|
2021-07-04 14:44:27 +00:00
|
|
|
@functools.cached_property
|
2020-06-19 01:01:23 +00:00
|
|
|
def player_ids(self):
|
2021-07-04 20:21:53 +00:00
|
|
|
return tuple(range(1, self.players + 1))
|
2020-06-19 01:01:23 +00:00
|
|
|
|
2021-07-21 16:08:15 +00:00
|
|
|
@functools.lru_cache()
|
|
|
|
def get_game_players(self, game_name: str):
|
|
|
|
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
Minecraft Randomizer
Squash merge, original Commits:
* Minecraft locations, items, and generation without logic
* added id lookup for minecraft
* typing import fix in minecraft/Items.py
* fix 2
* implementing Minecraft options and hard/postgame advancement exclusion
* first logic pass (75/80)
* logic pass 2 and proper completion conditions
* added insane difficulty pool, modified method of excluding item pools for easier extension
* bump network_data_package version
* minecraft testing framework
* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item
* Testing now functions, split tests up by advancement pane, added some story tests
* Newer testing framework: every advancement gets its own function, for ease of testing
* fixed logic for The End... Again...
* changed option names to "include_hard_advancements" etc.
* village/pillager-related advancements now require can_adventure: weapon + food
* a few minecraft tests
* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name
* additional MC tests
* more tests, mostly nether-related tests
* more tests, removed anvil path for Two Birds One Arrow
* include Minecraft slot data, and a world seed for each Minecraft player slot
* Added new items: ender pearls, lapis, porkchops
* All remaining Minecraft tests
* formatting of Minecraft tests and logic for better readability
* require Wither kill for Monsters Hunted
* properly removed 8 Emeralds item from item pool
* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill
* Added 12 new advancements (ported from old achievement system)
* renamed "On a Rail" for consistency with modern advancements
* tests for the new advancements
* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data
* output minecraft options in the spoiler log
* modified advancement goal values for new advancements
* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars
* fixed glowstone block logic for Not Quite Nine Lives
* setup for shuffling MC structures: building ER world and shuffling regions/entrances
* ensured Nether Fortresses can't be placed in the End
* finished logic for structure randomization
* fixed nonnative items always showing up as Hammers in ALttP shops
* output minecraft structure info in the spoiler
* generate .apmc file for communication with MC client
* fixed structure rando always using the same seed
* move stuff to worlds/minecraft/Regions.py
* make output apmc file have consistent name with other files
* added minecraft bottle macro; fixed tests imports
* generalizing MC region generation
* restructured structure shuffling in preparation for structure plando
* only output structure rando info in spoiler if they are shuffled
* Force structure rando to always be off, for the stable release
* added Minecraft options to player settings
* formally added combat_difficulty as an option
* Added Ender Dragon into playthrough, cleaned up goal map
* Added new difficulties: Easy, Normal, Hard combat
* moved .apmc generation time to prevent outputs on failed generation
* updated tests for new combat logic
* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix
* moved all MC-specific functions into gen_minecraft
* renamed "logic_version" to "client_version"
* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures
* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching
* added seed_name, player_name, client_version to apmc file
* reenabled structure shuffle
* added entrance tests for minecraft
Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-08 11:38:57 +00:00
|
|
|
|
2021-08-30 14:31:56 +00:00
|
|
|
@functools.lru_cache()
|
|
|
|
def get_game_worlds(self, game_name: str):
|
2022-02-05 14:49:19 +00:00
|
|
|
return tuple(world for player, world in self.worlds.items() if
|
|
|
|
player not in self.groups and self.game[player] == game_name)
|
2021-08-30 14:31:56 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_name_string_for_object(self, obj) -> str:
|
2021-08-09 07:15:41 +00:00
|
|
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
2020-01-14 09:42:27 +00:00
|
|
|
|
2021-08-09 07:15:41 +00:00
|
|
|
def get_player_name(self, player: int) -> str:
|
|
|
|
return self.player_name[player]
|
2020-01-14 09:42:27 +00:00
|
|
|
|
2019-12-14 18:19:08 +00:00
|
|
|
def initialize_regions(self, regions=None):
|
|
|
|
for region in regions if regions else self.regions:
|
2017-10-28 22:34:37 +00:00
|
|
|
region.world = self
|
2019-12-14 18:19:08 +00:00
|
|
|
self._region_cache[region.player][region.name] = region
|
|
|
|
|
2021-02-20 01:30:55 +00:00
|
|
|
@functools.cached_property
|
|
|
|
def world_name_lookup(self):
|
2021-08-09 07:15:41 +00:00
|
|
|
return {self.player_name[player_id]: player_id for player_id in self.player_ids}
|
2021-02-20 01:30:55 +00:00
|
|
|
|
2020-09-08 13:02:37 +00:00
|
|
|
def _recache(self):
|
|
|
|
"""Rebuild world cache"""
|
|
|
|
for region in self.regions:
|
|
|
|
player = region.player
|
|
|
|
self._region_cache[player][region.name] = region
|
|
|
|
for exit in region.exits:
|
|
|
|
self._entrance_cache[exit.name, player] = exit
|
|
|
|
|
|
|
|
for r_location in region.locations:
|
|
|
|
self._location_cache[r_location.name, player] = r_location
|
|
|
|
|
2019-12-14 18:19:08 +00:00
|
|
|
def get_regions(self, player=None):
|
|
|
|
return self.regions if player is None else self._region_cache[player].values()
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_region(self, regionname: str, player: int) -> Region:
|
2017-05-15 18:28:04 +00:00
|
|
|
try:
|
2019-12-14 18:19:08 +00:00
|
|
|
return self._region_cache[player][regionname]
|
2017-05-15 18:28:04 +00:00
|
|
|
except KeyError:
|
2020-09-08 13:02:37 +00:00
|
|
|
self._recache()
|
|
|
|
return self._region_cache[player][regionname]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_entrance(self, entrance: str, player: int) -> Entrance:
|
2017-05-15 18:28:04 +00:00
|
|
|
try:
|
2020-09-08 13:02:37 +00:00
|
|
|
return self._entrance_cache[entrance, player]
|
2017-05-15 18:28:04 +00:00
|
|
|
except KeyError:
|
2020-09-08 13:02:37 +00:00
|
|
|
self._recache()
|
|
|
|
return self._entrance_cache[entrance, player]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_location(self, location: str, player: int) -> Location:
|
2017-05-15 18:28:04 +00:00
|
|
|
try:
|
2020-09-08 13:02:37 +00:00
|
|
|
return self._location_cache[location, player]
|
2017-05-15 18:28:04 +00:00
|
|
|
except KeyError:
|
2020-09-08 13:02:37 +00:00
|
|
|
self._recache()
|
|
|
|
return self._location_cache[location, player]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
2021-08-29 14:02:28 +00:00
|
|
|
try:
|
|
|
|
return self.dungeons[dungeonname, player]
|
|
|
|
except KeyError as e:
|
|
|
|
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
|
2020-04-10 18:54:18 +00:00
|
|
|
|
2021-09-01 19:01:54 +00:00
|
|
|
def get_all_state(self, use_cache: bool) -> CollectionState:
|
2021-09-01 00:19:26 +00:00
|
|
|
cached = getattr(self, "_all_state", None)
|
2021-09-01 19:01:54 +00:00
|
|
|
if use_cache and cached:
|
2021-08-09 04:33:26 +00:00
|
|
|
return cached.copy()
|
|
|
|
|
2017-06-17 12:40:37 +00:00
|
|
|
ret = CollectionState(self)
|
2017-11-04 18:23:57 +00:00
|
|
|
|
2017-06-17 12:40:37 +00:00
|
|
|
for item in self.itempool:
|
2021-07-04 13:47:11 +00:00
|
|
|
self.worlds[item.player].collect(ret, item)
|
2022-02-13 22:02:18 +00:00
|
|
|
for player in self.player_ids:
|
|
|
|
subworld = self.worlds[player]
|
|
|
|
for item in subworld.get_pre_fill_items():
|
2021-09-01 00:19:26 +00:00
|
|
|
subworld.collect(ret, item)
|
2017-07-17 21:14:31 +00:00
|
|
|
ret.sweep_for_events()
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2021-09-01 19:01:54 +00:00
|
|
|
if use_cache:
|
2021-09-01 18:20:43 +00:00
|
|
|
self._all_state = ret
|
2017-06-17 12:40:37 +00:00
|
|
|
return ret
|
|
|
|
|
2022-01-31 21:23:01 +00:00
|
|
|
def get_items(self) -> List[Item]:
|
2018-01-03 01:01:16 +00:00
|
|
|
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
|
|
|
|
2021-11-27 21:57:54 +00:00
|
|
|
def find_item_locations(self, item, player: int) -> List[Location]:
|
2020-03-02 23:12:14 +00:00
|
|
|
return [location for location in self.get_locations() if
|
2021-11-27 21:57:54 +00:00
|
|
|
location.item and location.item.name == item and location.item.player == player]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-03-13 23:27:06 +00:00
|
|
|
def find_item(self, item, player: int) -> Location:
|
|
|
|
return next(location for location in self.get_locations() if
|
|
|
|
location.item and location.item.name == item and location.item.player == player)
|
|
|
|
|
2021-11-27 21:57:54 +00:00
|
|
|
def find_items_in_locations(self, items: Set[str], player: int) -> List[Location]:
|
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
location.item and location.item.name in items and location.item.player == player]
|
|
|
|
|
2021-07-12 11:54:47 +00:00
|
|
|
def create_item(self, item_name: str, player: int) -> Item:
|
|
|
|
return self.worlds[player].create_item(item_name)
|
2021-03-13 23:27:06 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def push_precollected(self, item: Item):
|
2020-01-14 09:42:27 +00:00
|
|
|
item.world = self
|
2021-10-10 14:50:01 +00:00
|
|
|
self.precollected_items[item.player].append(item)
|
2019-08-10 19:30:14 +00:00
|
|
|
self.state.collect(item, True)
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def push_item(self, location: Location, item: Item, collect: bool = True):
|
2017-05-15 18:28:04 +00:00
|
|
|
if not isinstance(location, Location):
|
2020-08-13 22:34:41 +00:00
|
|
|
raise RuntimeError(
|
|
|
|
'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2018-01-02 05:39:53 +00:00
|
|
|
if location.can_fill(self.state, item, False):
|
2017-05-15 18:28:04 +00:00
|
|
|
location.item = item
|
|
|
|
item.location = location
|
2021-04-12 07:45:07 +00:00
|
|
|
item.world = self # try to not have this here anymore
|
2017-05-15 18:28:04 +00:00
|
|
|
if collect:
|
2018-01-01 20:55:13 +00:00
|
|
|
self.state.collect(item, location.event, location)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-08-13 22:34:41 +00:00
|
|
|
logging.debug('Placed %s at %s', item, location)
|
2017-05-15 18:28:04 +00:00
|
|
|
else:
|
|
|
|
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def get_entrances(self) -> List[Entrance]:
|
2019-01-20 07:01:02 +00:00
|
|
|
if self._cached_entrances is None:
|
2020-08-27 02:05:11 +00:00
|
|
|
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
|
2019-01-20 07:01:02 +00:00
|
|
|
return self._cached_entrances
|
|
|
|
|
|
|
|
def clear_entrance_cache(self):
|
|
|
|
self._cached_entrances = None
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def get_locations(self) -> List[Location]:
|
2017-05-15 18:28:04 +00:00
|
|
|
if self._cached_locations is None:
|
2020-08-27 02:05:11 +00:00
|
|
|
self._cached_locations = [location for region in self.regions for location in region.locations]
|
2017-05-15 18:28:04 +00:00
|
|
|
return self._cached_locations
|
|
|
|
|
2018-03-23 03:18:40 +00:00
|
|
|
def clear_location_cache(self):
|
|
|
|
self._cached_locations = None
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def get_unfilled_locations(self, player=None) -> List[Location]:
|
2020-06-03 00:19:16 +00:00
|
|
|
if player is not None:
|
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
location.player == player and not location.item]
|
|
|
|
return [location for location in self.get_locations() if not location.item]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-10-07 17:51:46 +00:00
|
|
|
def get_unfilled_dungeon_locations(self):
|
|
|
|
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def get_filled_locations(self, player=None) -> List[Location]:
|
2020-08-13 22:34:41 +00:00
|
|
|
if player is not None:
|
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
location.player == player and location.item is not None]
|
|
|
|
return [location for location in self.get_locations() if location.item is not None]
|
2017-06-17 12:40:37 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def get_reachable_locations(self, state=None, player=None) -> List[Location]:
|
2017-05-15 18:28:04 +00:00
|
|
|
if state is None:
|
|
|
|
state = self.state
|
2020-03-02 23:12:14 +00:00
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
(player is None or location.player == player) and location.can_reach(state)]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
|
2017-05-15 18:28:04 +00:00
|
|
|
if state is None:
|
|
|
|
state = self.state
|
2020-03-02 23:12:14 +00:00
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
(player is None or location.player == player) and location.item is None and location.can_reach(state)]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2022-01-20 18:34:17 +00:00
|
|
|
def get_unfilled_locations_for_players(self, locations, players: Iterable[int]):
|
2021-01-04 14:14:20 +00:00
|
|
|
for player in players:
|
2022-01-20 18:34:17 +00:00
|
|
|
if len(locations) == 0:
|
|
|
|
locations = [location.name for location in self.get_unfilled_locations(player)]
|
|
|
|
for location_name in locations:
|
|
|
|
location = self._location_cache.get((location_name, player), None)
|
|
|
|
if location is not None and location.item is None:
|
|
|
|
yield location
|
2021-01-04 14:14:20 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def unlocks_new_location(self, item) -> bool:
|
2017-05-15 18:28:04 +00:00
|
|
|
temp_state = self.state.copy()
|
2017-06-17 12:40:37 +00:00
|
|
|
temp_state.collect(item, True)
|
2017-05-26 07:55:49 +00:00
|
|
|
|
|
|
|
for location in self.get_unfilled_locations():
|
|
|
|
if temp_state.can_reach(location) and not self.state.can_reach(location):
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2017-11-19 01:43:37 +00:00
|
|
|
|
2020-08-25 17:45:33 +00:00
|
|
|
def has_beaten_game(self, state, player: Optional[int] = None):
|
2019-07-10 02:18:24 +00:00
|
|
|
if player:
|
2021-02-22 10:18:53 +00:00
|
|
|
return self.completion_condition[player](state)
|
2019-07-10 02:18:24 +00:00
|
|
|
else:
|
|
|
|
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
|
2017-06-23 20:15:29 +00:00
|
|
|
if starting_state:
|
2020-03-07 22:20:11 +00:00
|
|
|
if self.has_beaten_game(starting_state):
|
|
|
|
return True
|
2017-06-23 20:15:29 +00:00
|
|
|
state = starting_state.copy()
|
|
|
|
else:
|
2020-03-07 22:20:11 +00:00
|
|
|
if self.has_beaten_game(self.state):
|
|
|
|
return True
|
2017-06-23 20:15:29 +00:00
|
|
|
state = CollectionState(self)
|
2021-04-29 07:54:49 +00:00
|
|
|
prog_locations = {location for location in self.get_locations() if location.item
|
|
|
|
and location.item.advancement and location not in state.locations_checked}
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
while prog_locations:
|
2021-04-29 07:54:49 +00:00
|
|
|
sphere = set()
|
2021-03-07 21:05:07 +00:00
|
|
|
# build up spheres of collection radius.
|
|
|
|
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
2017-05-16 19:23:47 +00:00
|
|
|
for location in prog_locations:
|
2019-07-11 04:12:09 +00:00
|
|
|
if location.can_reach(state):
|
2021-04-29 07:54:49 +00:00
|
|
|
sphere.add(location)
|
2017-05-16 19:23:47 +00:00
|
|
|
|
|
|
|
if not sphere:
|
2019-07-11 04:12:09 +00:00
|
|
|
# ran out of places and did not finish yet, quit
|
2017-05-16 19:23:47 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
for location in sphere:
|
2018-01-01 20:55:13 +00:00
|
|
|
state.collect(location.item, True, location)
|
2021-04-29 07:54:49 +00:00
|
|
|
prog_locations -= sphere
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2019-07-11 04:12:09 +00:00
|
|
|
if self.has_beaten_game(state):
|
|
|
|
return True
|
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
return False
|
|
|
|
|
2021-01-17 21:58:52 +00:00
|
|
|
def get_spheres(self):
|
|
|
|
state = CollectionState(self)
|
2021-02-27 17:58:17 +00:00
|
|
|
locations = set(self.get_filled_locations())
|
2021-01-17 21:58:52 +00:00
|
|
|
|
|
|
|
while locations:
|
|
|
|
sphere = set()
|
|
|
|
|
|
|
|
for location in locations:
|
|
|
|
if location.can_reach(state):
|
|
|
|
sphere.add(location)
|
2021-02-27 17:58:17 +00:00
|
|
|
yield sphere
|
2021-01-17 21:58:52 +00:00
|
|
|
if not sphere:
|
|
|
|
if locations:
|
|
|
|
yield locations # unreachable locations
|
|
|
|
break
|
|
|
|
|
|
|
|
for location in sphere:
|
|
|
|
state.collect(location.item, True, location)
|
|
|
|
locations -= sphere
|
|
|
|
|
2021-01-13 13:27:17 +00:00
|
|
|
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
|
|
|
|
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
|
|
|
if not state:
|
|
|
|
state = CollectionState(self)
|
2021-10-06 09:32:49 +00:00
|
|
|
players = {"minimal": set(),
|
2021-01-13 13:58:40 +00:00
|
|
|
"items": set(),
|
|
|
|
"locations": set()}
|
2021-01-13 13:27:17 +00:00
|
|
|
for player, access in self.accessibility.items():
|
2021-09-16 22:17:54 +00:00
|
|
|
players[access.current_key].add(player)
|
2021-01-11 18:56:18 +00:00
|
|
|
|
|
|
|
beatable_fulfilled = False
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def location_conditition(location: Location):
|
2021-01-13 13:27:17 +00:00
|
|
|
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
2021-09-16 22:17:54 +00:00
|
|
|
if location.player in players["minimal"]:
|
2021-01-13 13:27:17 +00:00
|
|
|
return False
|
|
|
|
return True
|
2021-01-11 18:56:18 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def location_relevant(location: Location):
|
2021-01-13 13:27:17 +00:00
|
|
|
"""Determine if this location is relevant to sweep."""
|
|
|
|
if location.player in players["locations"] or location.event or \
|
|
|
|
(location.item and location.item.advancement):
|
|
|
|
return True
|
2021-01-11 18:56:18 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
def all_done():
|
|
|
|
"""Check if all access rules are fulfilled"""
|
|
|
|
if beatable_fulfilled:
|
2021-01-13 13:27:17 +00:00
|
|
|
if any(location_conditition(location) for location in locations):
|
|
|
|
return False # still locations required to be collected
|
2021-01-11 18:56:18 +00:00
|
|
|
return True
|
|
|
|
|
2021-01-13 13:27:17 +00:00
|
|
|
locations = {location for location in self.get_locations() if location_relevant(location)}
|
|
|
|
|
2021-01-11 18:56:18 +00:00
|
|
|
while locations:
|
|
|
|
sphere = set()
|
|
|
|
for location in locations:
|
|
|
|
if location.can_reach(state):
|
|
|
|
sphere.add(location)
|
|
|
|
|
|
|
|
if not sphere:
|
|
|
|
# ran out of places and did not finish yet, quit
|
2021-02-26 20:03:16 +00:00
|
|
|
logging.warning(f"Could not access required locations for accessibility check."
|
|
|
|
f" Missing: {locations}")
|
2021-01-11 18:56:18 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
for location in sphere:
|
|
|
|
locations.remove(location)
|
|
|
|
state.collect(location.item, True, location)
|
|
|
|
|
|
|
|
if self.has_beaten_game(state):
|
|
|
|
beatable_fulfilled = True
|
|
|
|
|
|
|
|
if all_done():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2021-01-13 13:27:17 +00:00
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
class CollectionState(object):
|
|
|
|
|
2020-10-24 03:38:56 +00:00
|
|
|
def __init__(self, parent: MultiWorld):
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items = Counter()
|
2017-05-15 18:28:04 +00:00
|
|
|
self.world = parent
|
2019-07-11 04:18:30 +00:00
|
|
|
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
2020-05-10 09:27:13 +00:00
|
|
|
self.blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
2020-08-22 17:19:29 +00:00
|
|
|
self.events = set()
|
2018-01-01 20:55:13 +00:00
|
|
|
self.path = {}
|
|
|
|
self.locations_checked = set()
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale = {player: True for player in range(1, parent.players + 1)}
|
2021-10-10 14:50:01 +00:00
|
|
|
for items in parent.precollected_items.values():
|
|
|
|
for item in items:
|
|
|
|
self.collect(item, True)
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def update_reachable_regions(self, player: int):
|
2021-02-21 19:37:43 +00:00
|
|
|
from worlds.alttp.EntranceShuffle import indirect_connections
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale[player] = False
|
|
|
|
rrp = self.reachable_regions[player]
|
2020-05-10 09:27:13 +00:00
|
|
|
bc = self.blocked_connections[player]
|
|
|
|
queue = deque(self.blocked_connections[player])
|
|
|
|
start = self.world.get_region('Menu', player)
|
|
|
|
|
|
|
|
# init on first call - this can't be done on construction since the regions don't exist yet
|
|
|
|
if not start in rrp:
|
|
|
|
rrp.add(start)
|
|
|
|
bc.update(start.exits)
|
|
|
|
queue.extend(start.exits)
|
|
|
|
|
|
|
|
# run BFS on all connections, and keep track of those blocked by missing items
|
2020-06-30 05:32:05 +00:00
|
|
|
while queue:
|
|
|
|
connection = queue.popleft()
|
|
|
|
new_region = connection.connected_region
|
|
|
|
if new_region in rrp:
|
|
|
|
bc.remove(connection)
|
|
|
|
elif connection.can_reach(self):
|
|
|
|
rrp.add(new_region)
|
|
|
|
bc.remove(connection)
|
|
|
|
bc.update(new_region.exits)
|
|
|
|
queue.extend(new_region.exits)
|
|
|
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
|
|
|
|
|
|
|
# Retry connections if the new region can unblock them
|
|
|
|
if new_region.name in indirect_connections:
|
|
|
|
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
|
|
|
if new_entrance in bc and new_entrance not in queue:
|
|
|
|
queue.append(new_entrance)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def copy(self) -> CollectionState:
|
2017-06-17 12:40:37 +00:00
|
|
|
ret = CollectionState(self.world)
|
2019-07-13 22:17:16 +00:00
|
|
|
ret.prog_items = self.prog_items.copy()
|
2020-03-02 23:12:14 +00:00
|
|
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
|
|
|
range(1, self.world.players + 1)}
|
2021-10-06 09:32:49 +00:00
|
|
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
|
|
|
|
range(1, self.world.players + 1)}
|
2017-06-17 12:40:37 +00:00
|
|
|
ret.events = copy.copy(self.events)
|
2018-01-01 20:55:13 +00:00
|
|
|
ret.path = copy.copy(self.path)
|
|
|
|
ret.locations_checked = copy.copy(self.locations_checked)
|
2017-05-15 18:28:04 +00:00
|
|
|
return ret
|
|
|
|
|
2020-06-30 05:32:05 +00:00
|
|
|
def can_reach(self, spot, resolution_hint=None, player=None) -> bool:
|
2020-03-08 04:41:56 +00:00
|
|
|
if not hasattr(spot, "spot_type"):
|
2017-05-15 18:28:04 +00:00
|
|
|
# try to resolve a name
|
|
|
|
if resolution_hint == 'Location':
|
2019-04-18 09:23:24 +00:00
|
|
|
spot = self.world.get_location(spot, player)
|
2017-05-15 18:28:04 +00:00
|
|
|
elif resolution_hint == 'Entrance':
|
2019-04-18 09:23:24 +00:00
|
|
|
spot = self.world.get_entrance(spot, player)
|
2017-05-15 18:28:04 +00:00
|
|
|
else:
|
|
|
|
# default to Region
|
2019-04-18 09:23:24 +00:00
|
|
|
spot = self.world.get_region(spot, player)
|
2019-07-09 02:48:16 +00:00
|
|
|
return spot.can_reach(self)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-06-30 05:32:05 +00:00
|
|
|
def sweep_for_events(self, key_only: bool = False, locations=None):
|
2019-12-13 21:37:52 +00:00
|
|
|
if locations is None:
|
|
|
|
locations = self.world.get_filled_locations()
|
2017-06-17 12:40:37 +00:00
|
|
|
new_locations = True
|
2021-02-14 16:52:01 +00:00
|
|
|
# since the loop has a good chance to run more than once, only filter the events once
|
|
|
|
locations = {location for location in locations if location.event}
|
2017-06-17 12:40:37 +00:00
|
|
|
while new_locations:
|
2021-02-14 16:52:01 +00:00
|
|
|
reachable_events = {location for location in locations if
|
2021-08-30 14:31:56 +00:00
|
|
|
(not key_only or getattr(location.item, "locked_dungeon_item", False))
|
2020-08-22 17:19:29 +00:00
|
|
|
and location.can_reach(self)}
|
|
|
|
new_locations = reachable_events - self.events
|
|
|
|
for event in new_locations:
|
|
|
|
self.events.add(event)
|
|
|
|
self.collect(event.item, True, event)
|
2019-07-13 22:17:16 +00:00
|
|
|
|
2020-06-30 05:32:05 +00:00
|
|
|
def has(self, item, player: int, count: int = 1):
|
2020-03-07 22:35:55 +00:00
|
|
|
return self.prog_items[item, player] >= count
|
2017-11-19 01:43:37 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def has_all(self, items: Set[str], player: int):
|
2021-07-15 06:50:08 +00:00
|
|
|
return all(self.prog_items[item, player] for item in items)
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def has_any(self, items: Set[str], player: int):
|
2021-07-15 06:50:08 +00:00
|
|
|
return any(self.prog_items[item, player] for item in items)
|
|
|
|
|
2021-07-21 07:45:15 +00:00
|
|
|
def has_group(self, item_name_group: str, player: int, count: int = 1):
|
|
|
|
found: int = 0
|
|
|
|
for item_name in self.world.worlds[player].item_name_groups[item_name_group]:
|
|
|
|
found += self.prog_items[item_name, player]
|
|
|
|
if found >= count:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def count_group(self, item_name_group: str, player: int):
|
|
|
|
found: int = 0
|
|
|
|
for item_name in self.world.worlds[player].item_name_groups[item_name_group]:
|
|
|
|
found += self.prog_items[item_name, player]
|
|
|
|
return found
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_buy_unlimited(self, item: str, player: int) -> bool:
|
2020-08-20 18:13:00 +00:00
|
|
|
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
|
|
|
|
shop in self.world.shops)
|
2018-02-17 23:38:54 +00:00
|
|
|
|
2020-08-23 19:38:21 +00:00
|
|
|
def can_buy(self, item: str, player: int) -> bool:
|
|
|
|
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for
|
|
|
|
shop in self.world.shops)
|
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def item_count(self, item: str, player: int) -> int:
|
2020-03-07 22:35:55 +00:00
|
|
|
return self.prog_items[item, player]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-06-26 14:18:53 +00:00
|
|
|
def has_triforce_pieces(self, count: int, player: int) -> bool:
|
|
|
|
return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_crystals(self, count: int, player: int) -> bool:
|
2020-08-13 22:34:41 +00:00
|
|
|
found: int = 0
|
2020-08-22 17:19:29 +00:00
|
|
|
for crystalnumber in range(1, 8):
|
|
|
|
found += self.prog_items[f"Crystal {crystalnumber}", player]
|
|
|
|
if found >= count:
|
|
|
|
return True
|
2020-08-13 22:34:41 +00:00
|
|
|
return False
|
2019-07-25 22:25:14 +00:00
|
|
|
|
2020-08-13 22:34:41 +00:00
|
|
|
def can_lift_rocks(self, player: int):
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def bottle_count(self, player: int) -> int:
|
2021-07-21 07:45:15 +00:00
|
|
|
return min(self.world.difficulty_requirements[player].progressive_bottle_limit,
|
|
|
|
self.count_group("Bottles", player))
|
2017-11-12 02:22:44 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_hearts(self, player: int, count: int) -> int:
|
2018-09-16 16:55:49 +00:00
|
|
|
# Warning: This only considers items that are marked as advancement items
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.heart_count(player) >= count
|
2018-01-06 18:39:22 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def heart_count(self, player: int) -> int:
|
2018-09-16 16:55:49 +00:00
|
|
|
# Warning: This only considers items that are marked as advancement items
|
2019-12-16 16:46:21 +00:00
|
|
|
diff = self.world.difficulty_requirements[player]
|
2020-03-02 23:12:14 +00:00
|
|
|
return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
|
|
|
+ self.item_count('Sanctuary Heart Container', player) \
|
|
|
|
+ min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
|
|
|
+ 3 # starting hearts
|
2018-01-06 18:39:22 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_lift_heavy_rocks(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Titans Mitts', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_extend_magic(self, player: int, smallmagic: int = 16,
|
|
|
|
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
2018-01-07 03:07:46 +00:00
|
|
|
basemagic = 8
|
2020-03-13 23:31:28 +00:00
|
|
|
if self.has('Magic Upgrade (1/4)', player):
|
2018-01-07 03:07:46 +00:00
|
|
|
basemagic = 32
|
2020-03-13 23:31:28 +00:00
|
|
|
elif self.has('Magic Upgrade (1/2)', player):
|
2018-01-07 03:07:46 +00:00
|
|
|
basemagic = 16
|
2019-04-18 09:23:24 +00:00
|
|
|
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
|
2021-02-10 06:01:03 +00:00
|
|
|
if self.world.item_functionality[player] == 'hard' and not fullrefill:
|
2019-04-18 09:23:24 +00:00
|
|
|
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
|
2021-02-10 06:01:03 +00:00
|
|
|
elif self.world.item_functionality[player] == 'expert' and not fullrefill:
|
2019-04-18 09:23:24 +00:00
|
|
|
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
|
2018-09-16 16:55:49 +00:00
|
|
|
else:
|
2019-04-18 09:23:24 +00:00
|
|
|
basemagic = basemagic + basemagic * self.bottle_count(player)
|
2018-02-17 23:38:54 +00:00
|
|
|
return basemagic >= smallmagic
|
2018-01-02 05:39:53 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_kill_most_things(self, player: int, enemies=5) -> bool:
|
2020-04-20 17:17:10 +00:00
|
|
|
return (self.has_melee_weapon(player)
|
2019-04-18 09:23:24 +00:00
|
|
|
or self.has('Cane of Somaria', player)
|
|
|
|
or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player)))
|
|
|
|
or self.can_shoot_arrows(player)
|
|
|
|
or self.has('Fire Rod', player)
|
2020-03-15 10:59:06 +00:00
|
|
|
or (self.has('Bombs (10)', player) and enemies < 6))
|
2018-01-02 05:39:53 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_shoot_arrows(self, player: int) -> bool:
|
2019-12-16 23:16:02 +00:00
|
|
|
if self.world.retro[player]:
|
2020-08-23 19:38:21 +00:00
|
|
|
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player)
|
2020-06-30 07:51:11 +00:00
|
|
|
return self.has('Bow', player) or self.has('Silver Bow', player)
|
2018-02-17 23:38:54 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_get_good_bee(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
cave = self.world.get_region('Good Bee Cave', player)
|
2018-09-23 02:51:54 +00:00
|
|
|
return (
|
2021-07-21 07:45:15 +00:00
|
|
|
self.has_group("Bottles", player) and
|
2020-03-02 23:12:14 +00:00
|
|
|
self.has('Bug Catching Net', player) and
|
2021-02-27 16:11:54 +00:00
|
|
|
(self.has('Pegasus Boots', player) or (self.has_sword(player) and self.has('Quake', player))) and
|
2020-03-02 23:12:14 +00:00
|
|
|
cave.can_reach(self) and
|
|
|
|
self.is_not_bunny(cave, player)
|
2018-09-23 02:51:54 +00:00
|
|
|
)
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def can_retrieve_tablet(self, player: int) -> bool:
|
2020-10-07 17:51:46 +00:00
|
|
|
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
2021-10-06 09:32:49 +00:00
|
|
|
(self.world.swordless[player] and
|
|
|
|
self.has("Hammer", player)))
|
2020-10-07 17:51:46 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_sword(self, player: int) -> bool:
|
2020-08-20 18:13:00 +00:00
|
|
|
return self.has('Fighter Sword', player) \
|
|
|
|
or self.has('Master Sword', player) \
|
|
|
|
or self.has('Tempered Sword', player) \
|
|
|
|
or self.has('Golden Sword', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_beam_sword(self, player: int) -> bool:
|
2021-10-06 09:32:49 +00:00
|
|
|
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
|
|
|
|
player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-20 17:17:10 +00:00
|
|
|
def has_melee_weapon(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has_sword(player) or self.has('Hammer', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_fire_source(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Fire Rod', player) or self.has('Lamp', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_melt_things(self, player: int) -> bool:
|
2020-10-07 17:51:46 +00:00
|
|
|
return self.has('Fire Rod', player) or \
|
|
|
|
(self.has('Bombos', player) and
|
2021-04-09 18:40:45 +00:00
|
|
|
(self.world.swordless[player] or
|
2020-10-07 17:51:46 +00:00
|
|
|
self.has_sword(player)))
|
2020-03-02 23:12:14 +00:00
|
|
|
|
|
|
|
def can_avoid_lasers(self, player: int) -> bool:
|
2019-07-27 13:13:13 +00:00
|
|
|
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def is_not_bunny(self, region: Region, player: int) -> bool:
|
2021-02-27 16:11:54 +00:00
|
|
|
if self.has('Moon Pearl', player):
|
2020-03-02 23:12:14 +00:00
|
|
|
return True
|
|
|
|
|
2019-12-16 15:54:46 +00:00
|
|
|
return region.is_light_world if self.world.mode[player] != 'inverted' else region.is_dark_world
|
2019-07-27 13:13:13 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_reach_light_world(self, player: int) -> bool:
|
2019-09-22 01:59:16 +00:00
|
|
|
if True in [i.is_light_world for i in self.reachable_regions[player]]:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_reach_dark_world(self, player: int) -> bool:
|
2019-09-22 01:59:16 +00:00
|
|
|
if True in [i.is_dark_world for i in self.reachable_regions[player]]:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_misery_mire_medallion(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has(self.world.required_medallions[player][0], player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_turtle_rock_medallion(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has(self.world.required_medallions[player][1], player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def can_boots_clip_lw(self, player: int):
|
2020-02-10 04:38:55 +00:00
|
|
|
if self.world.mode[player] == 'inverted':
|
2021-02-27 16:11:54 +00:00
|
|
|
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
|
|
|
|
return self.has('Pegasus Boots', player)
|
2020-02-10 04:38:55 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def can_boots_clip_dw(self, player: int):
|
2020-02-10 04:38:55 +00:00
|
|
|
if self.world.mode[player] != 'inverted':
|
2021-02-27 16:11:54 +00:00
|
|
|
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
|
|
|
|
return self.has('Pegasus Boots', player)
|
2020-02-10 04:38:55 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def can_get_glitched_speed_lw(self, player: int):
|
2021-02-27 16:11:54 +00:00
|
|
|
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
2020-02-11 03:54:35 +00:00
|
|
|
if self.world.mode[player] == 'inverted':
|
2021-02-27 16:11:54 +00:00
|
|
|
rules.append(self.has('Moon Pearl', player))
|
2020-02-11 03:54:35 +00:00
|
|
|
return all(rules)
|
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def can_superbunny_mirror_with_sword(self, player: int):
|
2021-02-27 16:11:54 +00:00
|
|
|
return self.has('Magic Mirror', player) and self.has_sword(player)
|
2020-02-12 23:48:36 +00:00
|
|
|
|
2021-02-27 16:11:54 +00:00
|
|
|
def can_get_glitched_speed_dw(self, player: int):
|
|
|
|
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
2020-02-11 03:54:35 +00:00
|
|
|
if self.world.mode[player] != 'inverted':
|
2021-02-27 16:11:54 +00:00
|
|
|
rules.append(self.has('Moon Pearl', player))
|
2020-02-11 03:54:35 +00:00
|
|
|
return all(rules)
|
2020-02-10 04:38:55 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def can_bomb_clip(self, region: Region, player: int) -> bool:
|
2021-06-07 06:19:27 +00:00
|
|
|
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
|
|
|
|
|
2021-02-27 16:11:54 +00:00
|
|
|
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
|
2018-01-01 20:55:13 +00:00
|
|
|
if location:
|
|
|
|
self.locations_checked.add(location)
|
2021-02-26 20:03:16 +00:00
|
|
|
|
2021-07-04 13:47:11 +00:00
|
|
|
changed = self.world.worlds[item.player].collect(self, item)
|
|
|
|
|
|
|
|
if not changed and event:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items[item.name, item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2020-03-02 23:12:14 +00:00
|
|
|
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale[item.player] = True
|
2017-05-26 07:55:49 +00:00
|
|
|
|
2021-02-14 16:52:01 +00:00
|
|
|
if changed and not event:
|
|
|
|
self.sweep_for_events()
|
|
|
|
|
|
|
|
return changed
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def remove(self, item: Item):
|
2021-08-10 07:47:28 +00:00
|
|
|
changed = self.world.worlds[item.player].remove(self, item)
|
|
|
|
if changed:
|
|
|
|
# invalidate caches, nothing can be trusted anymore now
|
|
|
|
self.reachable_regions[item.player] = set()
|
|
|
|
self.blocked_connections[item.player] = set()
|
|
|
|
self.stale[item.player] = True
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2018-01-19 02:51:43 +00:00
|
|
|
@unique
|
2021-07-15 06:50:08 +00:00
|
|
|
class RegionType(int, Enum):
|
|
|
|
Generic = 0
|
2018-01-19 02:51:43 +00:00
|
|
|
LightWorld = 1
|
|
|
|
DarkWorld = 2
|
2021-10-06 09:32:49 +00:00
|
|
|
Cave = 3 # Also includes Houses
|
2018-01-19 02:51:43 +00:00
|
|
|
Dungeon = 4
|
|
|
|
|
|
|
|
@property
|
2021-11-30 04:33:56 +00:00
|
|
|
def is_indoors(self) -> bool:
|
2018-01-19 02:51:43 +00:00
|
|
|
"""Shorthand for checking if Cave or Dungeon"""
|
|
|
|
return self in (RegionType.Cave, RegionType.Dungeon)
|
|
|
|
|
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
class Region(object):
|
2022-01-06 05:09:15 +00:00
|
|
|
def __init__(self, name: str, type_: RegionType, hint, player: int, world: Optional[MultiWorld] = None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
2022-01-06 05:09:15 +00:00
|
|
|
self.type = type_
|
2017-05-15 18:28:04 +00:00
|
|
|
self.entrances = []
|
|
|
|
self.exits = []
|
2022-01-20 03:19:07 +00:00
|
|
|
self.locations: List[Location] = []
|
2017-10-15 16:16:07 +00:00
|
|
|
self.dungeon = None
|
2018-02-17 23:38:54 +00:00
|
|
|
self.shop = None
|
2021-07-15 06:50:08 +00:00
|
|
|
self.world = world
|
2020-03-07 22:20:11 +00:00
|
|
|
self.is_light_world = False # will be set after making connections.
|
2018-01-27 22:17:03 +00:00
|
|
|
self.is_dark_world = False
|
2017-05-20 12:03:15 +00:00
|
|
|
self.spot_type = 'Region'
|
2019-01-20 07:01:02 +00:00
|
|
|
self.hint_text = hint
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def can_reach(self, state: CollectionState) -> bool:
|
2019-07-11 04:18:30 +00:00
|
|
|
if state.stale[self.player]:
|
|
|
|
state.update_reachable_regions(self.player)
|
|
|
|
return self in state.reachable_regions[self.player]
|
2019-07-09 02:48:16 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def can_reach_private(self, state: CollectionState) -> bool:
|
2017-05-15 18:28:04 +00:00
|
|
|
for entrance in self.entrances:
|
2019-07-11 04:18:30 +00:00
|
|
|
if entrance.can_reach(state):
|
2018-01-01 20:55:13 +00:00
|
|
|
if not self in state.path:
|
|
|
|
state.path[self] = (self.name, state.path.get(entrance, None))
|
2017-05-15 18:28:04 +00:00
|
|
|
return True
|
|
|
|
return False
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Entrance(object):
|
2021-07-16 10:23:05 +00:00
|
|
|
spot_type = 'Entrance'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def __init__(self, player: int, name: str = '', parent=None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
|
|
|
self.parent_region = parent
|
|
|
|
self.connected_region = None
|
2017-05-20 12:03:15 +00:00
|
|
|
self.target = None
|
2017-06-03 13:33:11 +00:00
|
|
|
self.addresses = None
|
2017-12-17 05:25:46 +00:00
|
|
|
self.access_rule = lambda state: True
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2020-05-10 09:27:13 +00:00
|
|
|
self.hide_path = False
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def can_reach(self, state: CollectionState) -> bool:
|
2019-07-11 04:18:30 +00:00
|
|
|
if self.parent_region.can_reach(state) and self.access_rule(state):
|
2020-05-10 09:27:13 +00:00
|
|
|
if not self.hide_path and not self in state.path:
|
2018-01-01 20:55:13 +00:00
|
|
|
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
|
2017-05-20 12:03:15 +00:00
|
|
|
return True
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2017-05-20 12:03:15 +00:00
|
|
|
return False
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-11-30 04:33:56 +00:00
|
|
|
def connect(self, region: Region, addresses=None, target = None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.connected_region = region
|
2017-05-20 12:03:15 +00:00
|
|
|
self.target = target
|
2017-06-03 13:33:11 +00:00
|
|
|
self.addresses = addresses
|
2017-05-15 18:28:04 +00:00
|
|
|
region.entrances.append(self)
|
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
world = self.parent_region.world if self.parent_region else None
|
|
|
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2017-10-15 16:16:07 +00:00
|
|
|
|
2021-08-30 14:31:56 +00:00
|
|
|
class Dungeon(object):
|
2020-06-04 01:30:59 +00:00
|
|
|
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
|
2017-10-15 16:16:07 +00:00
|
|
|
self.name = name
|
|
|
|
self.regions = regions
|
|
|
|
self.big_key = big_key
|
|
|
|
self.small_keys = small_keys
|
|
|
|
self.dungeon_items = dungeon_items
|
2018-09-26 17:12:20 +00:00
|
|
|
self.bosses = dict()
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2019-07-13 22:11:43 +00:00
|
|
|
self.world = None
|
2018-09-26 17:12:20 +00:00
|
|
|
|
|
|
|
@property
|
2021-11-30 04:33:56 +00:00
|
|
|
def boss(self) -> Optional[Boss]:
|
2018-09-26 17:12:20 +00:00
|
|
|
return self.bosses.get(None, None)
|
|
|
|
|
|
|
|
@boss.setter
|
2021-11-30 04:33:56 +00:00
|
|
|
def boss(self, value: Optional[Boss]):
|
2018-09-26 17:12:20 +00:00
|
|
|
self.bosses[None] = value
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2017-10-15 16:16:07 +00:00
|
|
|
@property
|
|
|
|
def keys(self):
|
|
|
|
return self.small_keys + ([self.big_key] if self.big_key else [])
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2017-10-15 16:16:07 +00:00
|
|
|
@property
|
|
|
|
def all_items(self):
|
2017-10-28 22:34:37 +00:00
|
|
|
return self.dungeon_items + self.keys
|
|
|
|
|
2020-06-04 01:30:59 +00:00
|
|
|
def is_dungeon_item(self, item: Item) -> bool:
|
2021-03-20 23:47:17 +00:00
|
|
|
return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items)
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
def __eq__(self, other: Dungeon) -> bool:
|
|
|
|
if not other:
|
|
|
|
return False
|
2020-06-04 01:30:59 +00:00
|
|
|
return self.name == other.name and self.player == other.player
|
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-10-15 16:16:07 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2021-01-10 18:23:57 +00:00
|
|
|
class Boss():
|
2021-11-30 04:33:56 +00:00
|
|
|
def __init__(self, name: str, enemizer_name: str, defeat_rule, player: int):
|
2018-09-26 17:12:20 +00:00
|
|
|
self.name = name
|
|
|
|
self.enemizer_name = enemizer_name
|
|
|
|
self.defeat_rule = defeat_rule
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_defeat(self, state) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.defeat_rule(state, self.player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-03-26 03:05:36 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return f"Boss({self.name})"
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
|
2022-01-20 03:19:07 +00:00
|
|
|
class LocationProgressType(Enum):
|
|
|
|
DEFAULT = 1
|
|
|
|
PRIORITY = 2
|
|
|
|
EXCLUDED = 3
|
|
|
|
|
2022-02-01 15:36:14 +00:00
|
|
|
|
2021-01-10 18:23:57 +00:00
|
|
|
class Location():
|
2021-08-27 12:52:33 +00:00
|
|
|
# If given as integer, then this is the shop's inventory index
|
|
|
|
shop_slot: Optional[int] = None
|
2021-01-22 13:40:50 +00:00
|
|
|
shop_slot_disabled: bool = False
|
2021-01-10 18:23:57 +00:00
|
|
|
event: bool = False
|
|
|
|
locked: bool = False
|
2021-02-14 16:52:01 +00:00
|
|
|
spot_type = 'Location'
|
2021-02-21 19:17:24 +00:00
|
|
|
game: str = "Generic"
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
show_in_spoiler: bool = True
|
2021-02-21 19:17:24 +00:00
|
|
|
crystal: bool = False
|
2022-01-20 03:19:07 +00:00
|
|
|
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
2021-07-15 06:50:08 +00:00
|
|
|
always_allow = staticmethod(lambda item, state: False)
|
|
|
|
access_rule = staticmethod(lambda state: True)
|
|
|
|
item_rule = staticmethod(lambda item: True)
|
2021-01-10 18:23:57 +00:00
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
2021-07-15 06:50:08 +00:00
|
|
|
self.name: str = name
|
|
|
|
self.address: Optional[int] = address
|
2021-03-20 23:47:17 +00:00
|
|
|
self.parent_region: Region = parent
|
2021-07-15 06:50:08 +00:00
|
|
|
self.player: int = player
|
|
|
|
self.item: Optional[Item] = None
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-07-09 14:16:31 +00:00
|
|
|
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
2021-08-30 14:31:56 +00:00
|
|
|
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-07-09 14:16:31 +00:00
|
|
|
def can_reach(self, state: CollectionState) -> bool:
|
2020-08-21 16:35:48 +00:00
|
|
|
# self.access_rule computes faster on average, so placing it first for faster abort
|
|
|
|
if self.access_rule(state) and self.parent_region.can_reach(state):
|
2017-05-20 12:03:15 +00:00
|
|
|
return True
|
|
|
|
return False
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-06-06 14:08:17 +00:00
|
|
|
def place_locked_item(self, item: Item):
|
|
|
|
if self.item:
|
|
|
|
raise Exception(f"Location {self} already filled.")
|
|
|
|
self.item = item
|
|
|
|
self.event = item.advancement
|
|
|
|
self.item.world = self.parent_region.world
|
|
|
|
self.locked = True
|
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
|
|
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-08-22 17:19:29 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash((self.name, self.player))
|
|
|
|
|
2021-01-17 21:58:52 +00:00
|
|
|
def __lt__(self, other):
|
|
|
|
return (self.player, self.name) < (other.player, other.name)
|
|
|
|
|
2021-07-12 12:10:49 +00:00
|
|
|
@property
|
|
|
|
def native_item(self) -> bool:
|
|
|
|
"""Returns True if the item in this location matches game."""
|
|
|
|
return self.item and self.item.game == self.game
|
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
@property
|
|
|
|
def hint_text(self):
|
2021-08-28 21:18:45 +00:00
|
|
|
hint_text = getattr(self, "_hint_text", None)
|
|
|
|
if hint_text:
|
|
|
|
return hint_text
|
|
|
|
return "at " + self.name.replace("_", " ").replace("-", " ")
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2021-07-20 16:23:06 +00:00
|
|
|
|
2021-02-21 19:17:24 +00:00
|
|
|
class Item():
|
2021-01-30 08:57:25 +00:00
|
|
|
location: Optional[Location] = None
|
2021-02-26 20:03:16 +00:00
|
|
|
world: Optional[MultiWorld] = None
|
2021-09-29 02:44:20 +00:00
|
|
|
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
|
|
|
|
name: str
|
2021-02-21 19:17:24 +00:00
|
|
|
game: str = "Generic"
|
|
|
|
type: str = None
|
2021-09-29 03:21:33 +00:00
|
|
|
# indicates if this is a negative impact item. Causes these to be handled differently by various games.
|
|
|
|
trap: bool = False
|
2021-08-28 10:56:52 +00:00
|
|
|
# change manually to ensure that a specific non-progression item never goes on an excluded location
|
|
|
|
never_exclude = False
|
|
|
|
|
|
|
|
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
2021-07-20 16:23:06 +00:00
|
|
|
pedestal_credit_text: str = "and the Unknown Item"
|
|
|
|
sickkid_credit_text: Optional[str] = None
|
|
|
|
magicshop_credit_text: Optional[str] = None
|
|
|
|
zora_credit_text: Optional[str] = None
|
|
|
|
fluteboy_credit_text: Optional[str] = None
|
2021-08-28 10:56:52 +00:00
|
|
|
|
|
|
|
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
|
|
|
|
smallkey: bool = False
|
|
|
|
bigkey: bool = False
|
|
|
|
map: bool = False
|
|
|
|
compass: bool = False
|
2021-02-21 19:17:24 +00:00
|
|
|
|
2021-06-06 14:17:07 +00:00
|
|
|
def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
|
|
|
self.advancement = advancement
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2021-02-21 19:17:24 +00:00
|
|
|
self.code = code
|
|
|
|
|
|
|
|
@property
|
|
|
|
def hint_text(self):
|
2021-06-14 00:20:13 +00:00
|
|
|
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
2021-02-21 19:17:24 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def pedestal_hint_text(self):
|
2021-06-14 00:23:41 +00:00
|
|
|
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2022-01-18 05:16:16 +00:00
|
|
|
@property
|
|
|
|
def flags(self) -> int:
|
|
|
|
return self.advancement + (self.never_exclude << 1) + (self.trap << 2)
|
|
|
|
|
2021-01-02 11:49:43 +00:00
|
|
|
def __eq__(self, other):
|
|
|
|
return self.name == other.name and self.player == other.player
|
|
|
|
|
2021-02-03 13:24:29 +00:00
|
|
|
def __lt__(self, other):
|
|
|
|
if other.player != self.player:
|
|
|
|
return other.player < self.player
|
|
|
|
return self.name < other.name
|
|
|
|
|
2021-01-02 11:59:19 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash((self.name, self.player))
|
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
2017-05-20 12:03:15 +00:00
|
|
|
|
|
|
|
|
2021-08-02 02:57:57 +00:00
|
|
|
class Spoiler():
|
2020-10-24 03:38:56 +00:00
|
|
|
world: MultiWorld
|
2022-02-06 23:26:44 +00:00
|
|
|
unreachables: Set[Location]
|
2020-08-25 12:31:20 +00:00
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
def __init__(self, world):
|
|
|
|
self.world = world
|
2020-01-14 09:42:27 +00:00
|
|
|
self.hashes = {}
|
2018-03-24 05:50:54 +00:00
|
|
|
self.entrances = OrderedDict()
|
2017-07-18 10:44:13 +00:00
|
|
|
self.medallions = {}
|
|
|
|
self.playthrough = {}
|
2022-02-06 23:26:44 +00:00
|
|
|
self.unreachables = set()
|
2017-07-18 10:44:13 +00:00
|
|
|
self.locations = {}
|
2018-01-01 20:55:13 +00:00
|
|
|
self.paths = {}
|
2018-03-24 05:43:10 +00:00
|
|
|
self.shops = []
|
2018-09-26 17:12:20 +00:00
|
|
|
self.bosses = OrderedDict()
|
2017-07-18 10:44:13 +00:00
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
def set_entrance(self, entrance, exit, direction, player):
|
2019-07-13 22:11:43 +00:00
|
|
|
if self.world.players == 1:
|
2021-10-06 09:32:49 +00:00
|
|
|
self.entrances[(entrance, direction, player)] = OrderedDict(
|
|
|
|
[('entrance', entrance), ('exit', exit), ('direction', direction)])
|
2019-07-13 22:11:43 +00:00
|
|
|
else:
|
2021-10-06 09:32:49 +00:00
|
|
|
self.entrances[(entrance, direction, player)] = OrderedDict(
|
|
|
|
[('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
|
2017-07-18 10:44:13 +00:00
|
|
|
|
|
|
|
def parse_data(self):
|
2019-04-18 09:23:24 +00:00
|
|
|
self.medallions = OrderedDict()
|
2021-07-21 16:08:15 +00:00
|
|
|
for player in self.world.get_game_players("A Link to the Past"):
|
2021-10-06 09:32:49 +00:00
|
|
|
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]
|
2018-03-23 15:03:38 +00:00
|
|
|
|
|
|
|
self.locations = OrderedDict()
|
|
|
|
listed_locations = set()
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
lw_locations = [loc for loc in self.world.get_locations() if
|
|
|
|
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
|
|
|
|
self.locations['Light World'] = OrderedDict(
|
|
|
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
|
|
lw_locations])
|
2018-03-23 15:03:38 +00:00
|
|
|
listed_locations.update(lw_locations)
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
dw_locations = [loc for loc in self.world.get_locations() if
|
|
|
|
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
|
|
|
|
self.locations['Dark World'] = OrderedDict(
|
|
|
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
|
|
dw_locations])
|
2018-03-23 15:03:38 +00:00
|
|
|
listed_locations.update(dw_locations)
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
cave_locations = [loc for loc in self.world.get_locations() if
|
|
|
|
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
|
|
|
|
self.locations['Caves'] = OrderedDict(
|
|
|
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
|
|
cave_locations])
|
2018-03-23 15:03:38 +00:00
|
|
|
listed_locations.update(cave_locations)
|
|
|
|
|
2021-08-29 14:02:28 +00:00
|
|
|
for dungeon in self.world.dungeons.values():
|
2021-10-06 09:32:49 +00:00
|
|
|
dungeon_locations = [loc for loc in self.world.get_locations() if
|
|
|
|
loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
|
|
|
|
self.locations[str(dungeon)] = OrderedDict(
|
|
|
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
|
|
dungeon_locations])
|
2018-03-23 15:03:38 +00:00
|
|
|
listed_locations.update(dungeon_locations)
|
|
|
|
|
2021-10-06 09:32:49 +00:00
|
|
|
other_locations = [loc for loc in self.world.get_locations() if
|
|
|
|
loc not in listed_locations and loc.show_in_spoiler]
|
2018-03-23 15:03:38 +00:00
|
|
|
if other_locations:
|
2021-10-06 09:32:49 +00:00
|
|
|
self.locations['Other Locations'] = OrderedDict(
|
|
|
|
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
|
|
other_locations])
|
2018-03-23 15:03:38 +00:00
|
|
|
listed_locations.update(other_locations)
|
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
self.shops = []
|
2021-09-12 21:09:13 +00:00
|
|
|
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
|
2018-03-24 05:43:10 +00:00
|
|
|
for shop in self.world.shops:
|
2020-01-10 10:41:22 +00:00
|
|
|
if not shop.custom:
|
2018-03-24 05:43:10 +00:00
|
|
|
continue
|
2021-09-12 18:25:08 +00:00
|
|
|
shopdata = {
|
|
|
|
'location': str(shop.region),
|
|
|
|
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
|
|
|
}
|
2018-03-24 05:43:10 +00:00
|
|
|
for index, item in enumerate(shop.inventory):
|
|
|
|
if item is None:
|
|
|
|
continue
|
2021-09-12 21:09:13 +00:00
|
|
|
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
|
2021-10-06 09:32:49 +00:00
|
|
|
shopdata['item_{}'.format(
|
|
|
|
index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}"
|
2020-11-24 02:05:04 +00:00
|
|
|
|
|
|
|
if item['player'] > 0:
|
2021-10-06 09:32:49 +00:00
|
|
|
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—',
|
|
|
|
'(Player {}) — '.format(
|
|
|
|
item['player']))
|
2020-11-24 02:05:04 +00:00
|
|
|
|
2020-09-02 04:23:43 +00:00
|
|
|
if item['max'] == 0:
|
|
|
|
continue
|
|
|
|
shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
|
|
|
|
|
|
|
|
if item['replacement'] is None:
|
|
|
|
continue
|
2021-10-06 09:32:49 +00:00
|
|
|
shopdata['item_{}'.format(
|
|
|
|
index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
|
2018-03-24 05:43:10 +00:00
|
|
|
self.shops.append(shopdata)
|
|
|
|
|
2021-07-21 16:08:15 +00:00
|
|
|
for player in self.world.get_game_players("A Link to the Past"):
|
2019-04-18 09:23:24 +00:00
|
|
|
self.bosses[str(player)] = OrderedDict()
|
|
|
|
self.bosses[str(player)]["Eastern Palace"] = self.world.get_dungeon("Eastern Palace", player).boss.name
|
|
|
|
self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name
|
|
|
|
self.bosses[str(player)]["Tower Of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name
|
|
|
|
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
|
2021-10-06 09:32:49 +00:00
|
|
|
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness",
|
|
|
|
player).boss.name
|
2019-04-18 09:23:24 +00:00
|
|
|
self.bosses[str(player)]["Swamp Palace"] = self.world.get_dungeon("Swamp Palace", player).boss.name
|
|
|
|
self.bosses[str(player)]["Skull Woods"] = self.world.get_dungeon("Skull Woods", player).boss.name
|
|
|
|
self.bosses[str(player)]["Thieves Town"] = self.world.get_dungeon("Thieves Town", player).boss.name
|
|
|
|
self.bosses[str(player)]["Ice Palace"] = self.world.get_dungeon("Ice Palace", player).boss.name
|
|
|
|
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
|
|
|
|
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
|
2019-12-16 15:54:46 +00:00
|
|
|
if self.world.mode[player] != 'inverted':
|
2021-10-06 09:32:49 +00:00
|
|
|
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
|
|
|
self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
|
|
|
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses[
|
|
|
|
'middle'].name
|
|
|
|
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses[
|
|
|
|
'top'].name
|
2019-07-27 13:13:13 +00:00
|
|
|
else:
|
2021-10-06 09:32:49 +00:00
|
|
|
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
|
|
|
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
|
|
|
|
self.bosses[str(player)]["Ganons Tower Middle"] = \
|
|
|
|
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
|
|
|
|
self.bosses[str(player)]["Ganons Tower Top"] = \
|
|
|
|
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
|
2019-07-27 13:13:13 +00:00
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
|
|
|
self.bosses[str(player)]["Ganon"] = "Ganon"
|
2018-09-26 17:12:20 +00:00
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
def to_json(self):
|
|
|
|
self.parse_data()
|
|
|
|
out = OrderedDict()
|
2018-03-27 01:39:48 +00:00
|
|
|
out['Entrances'] = list(self.entrances.values())
|
2017-07-18 10:44:13 +00:00
|
|
|
out.update(self.locations)
|
2018-03-27 01:39:48 +00:00
|
|
|
out['Special'] = self.medallions
|
2020-01-14 09:42:27 +00:00
|
|
|
if self.hashes:
|
2021-08-09 07:15:41 +00:00
|
|
|
out['Hashes'] = self.hashes
|
2018-03-27 01:39:48 +00:00
|
|
|
if self.shops:
|
|
|
|
out['Shops'] = self.shops
|
2017-07-18 10:44:13 +00:00
|
|
|
out['playthrough'] = self.playthrough
|
2018-01-01 20:55:13 +00:00
|
|
|
out['paths'] = self.paths
|
2019-12-17 14:55:53 +00:00
|
|
|
out['Bosses'] = self.bosses
|
2018-09-26 17:12:20 +00:00
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
return json.dumps(out)
|
|
|
|
|
|
|
|
def to_file(self, filename):
|
2021-11-02 11:29:29 +00:00
|
|
|
from worlds.AutoWorld import call_all, call_single, call_stage
|
2017-07-18 10:44:13 +00:00
|
|
|
self.parse_data()
|
2020-08-19 21:24:17 +00:00
|
|
|
|
2020-08-20 18:13:00 +00:00
|
|
|
def bool_to_text(variable: Union[bool, str]) -> str:
|
|
|
|
if type(variable) == str:
|
|
|
|
return variable
|
2020-08-19 21:24:17 +00:00
|
|
|
return 'Yes' if variable else 'No'
|
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
def write_option(option_key: str, option_obj: type(Options.Option)):
|
|
|
|
res = getattr(self.world, option_key)[player]
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = getattr(option_obj, "display_name", option_key)
|
2021-09-16 22:17:54 +00:00
|
|
|
try:
|
2022-02-02 15:29:29 +00:00
|
|
|
outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n')
|
2021-09-16 22:17:54 +00:00
|
|
|
except:
|
|
|
|
raise Exception
|
|
|
|
|
2020-03-09 23:36:26 +00:00
|
|
|
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
|
|
|
outfile.write(
|
2021-01-03 13:32:32 +00:00
|
|
|
'Archipelago Version %s - Seed: %s\n\n' % (
|
2021-09-13 00:01:15 +00:00
|
|
|
Utils.__version__, self.world.seed))
|
2019-08-24 19:53:21 +00:00
|
|
|
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Players: %d\n' % self.world.players)
|
2021-11-02 11:29:29 +00:00
|
|
|
call_stage(self.world, "write_spoiler_header", outfile)
|
2021-08-09 07:15:41 +00:00
|
|
|
|
2020-01-14 09:42:27 +00:00
|
|
|
for player in range(1, self.world.players + 1):
|
|
|
|
if self.world.players > 1:
|
2021-08-09 07:15:41 +00:00
|
|
|
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
|
2021-08-10 18:40:44 +00:00
|
|
|
outfile.write('Game: %s\n' % self.world.game[player])
|
2021-09-16 22:17:54 +00:00
|
|
|
for f_option, option in Options.per_game_common_options.items():
|
|
|
|
write_option(f_option, option)
|
2021-06-25 21:32:13 +00:00
|
|
|
options = self.world.worlds[player].options
|
|
|
|
if options:
|
2021-08-02 02:57:57 +00:00
|
|
|
for f_option, option in options.items():
|
2021-09-16 22:17:54 +00:00
|
|
|
write_option(f_option, option)
|
2021-11-02 11:29:29 +00:00
|
|
|
call_single(self.world, "write_spoiler_header", player, outfile)
|
2021-06-03 22:29:59 +00:00
|
|
|
|
2021-07-21 16:08:15 +00:00
|
|
|
if player in self.world.get_game_players("A Link to the Past"):
|
2021-08-09 07:15:41 +00:00
|
|
|
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
|
2021-02-21 19:17:24 +00:00
|
|
|
|
2021-09-13 00:01:15 +00:00
|
|
|
outfile.write('Logic: %s\n' % self.world.logic[player])
|
|
|
|
outfile.write('Dark Room Logic: %s\n' % self.world.dark_room_logic[player])
|
|
|
|
outfile.write('Mode: %s\n' % self.world.mode[player])
|
|
|
|
outfile.write('Goal: %s\n' % self.world.goal[player])
|
|
|
|
if "triforce" in self.world.goal[player]: # triforce hunt
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write("Pieces available for Triforce: %s\n" %
|
2021-09-13 00:01:15 +00:00
|
|
|
self.world.triforce_pieces_available[player])
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write("Pieces required for Triforce: %s\n" %
|
2021-09-13 00:01:15 +00:00
|
|
|
self.world.triforce_pieces_required[player])
|
2021-09-13 00:03:59 +00:00
|
|
|
outfile.write('Difficulty: %s\n' % self.world.difficulty[player])
|
2021-09-13 00:01:15 +00:00
|
|
|
outfile.write('Item Functionality: %s\n' % self.world.item_functionality[player])
|
|
|
|
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
|
|
|
|
if self.world.shuffle[player] != "vanilla":
|
2021-10-07 02:31:03 +00:00
|
|
|
outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed)
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
2021-09-13 00:01:15 +00:00
|
|
|
'Yes' if self.world.open_pyramid[player] else 'No'))
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write('Shop inventory shuffle: %s\n' %
|
2021-09-13 00:01:15 +00:00
|
|
|
bool_to_text("i" in self.world.shop_shuffle[player]))
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write('Shop price shuffle: %s\n' %
|
2021-09-13 00:01:15 +00:00
|
|
|
bool_to_text("p" in self.world.shop_shuffle[player]))
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write('Shop upgrade shuffle: %s\n' %
|
2021-09-13 00:01:15 +00:00
|
|
|
bool_to_text("u" in self.world.shop_shuffle[player]))
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write('New Shop inventory: %s\n' %
|
2021-09-13 00:01:15 +00:00
|
|
|
bool_to_text("g" in self.world.shop_shuffle[player] or
|
|
|
|
"f" in self.world.shop_shuffle[player]))
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write('Custom Potion Shop: %s\n' %
|
2021-09-13 00:01:15 +00:00
|
|
|
bool_to_text("w" in self.world.shop_shuffle[player]))
|
|
|
|
outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player])
|
|
|
|
outfile.write('Enemy health: %s\n' % self.world.enemy_health[player])
|
|
|
|
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
|
2021-02-21 19:17:24 +00:00
|
|
|
outfile.write('Prize shuffle %s\n' %
|
2021-09-13 00:01:15 +00:00
|
|
|
self.world.shuffle_prizes[player])
|
2017-07-18 10:44:13 +00:00
|
|
|
if self.entrances:
|
|
|
|
outfile.write('\n\nEntrances:\n\n')
|
2021-08-09 07:15:41 +00:00
|
|
|
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_name(entry["player"])}: '
|
2020-08-23 19:38:21 +00:00
|
|
|
if self.world.players > 1 else '', entry['entrance'],
|
|
|
|
'<=>' if entry['direction'] == 'both' else
|
|
|
|
'<=' if entry['direction'] == 'exit' else '=>',
|
|
|
|
entry['exit']) for entry in self.entrances.values()]))
|
2021-06-06 15:13:34 +00:00
|
|
|
|
|
|
|
if self.medallions:
|
2021-05-22 14:42:15 +00:00
|
|
|
outfile.write('\n\nMedallions:\n')
|
|
|
|
for dungeon, medallion in self.medallions.items():
|
|
|
|
outfile.write(f'\n{dungeon}: {medallion}')
|
2021-11-02 11:29:29 +00:00
|
|
|
|
|
|
|
call_all(self.world, "write_spoiler", outfile)
|
2021-07-07 08:14:58 +00:00
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
outfile.write('\n\nLocations:\n\n')
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write('\n'.join(
|
|
|
|
['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in
|
|
|
|
grouping.items()]))
|
2021-06-06 15:13:34 +00:00
|
|
|
|
|
|
|
if self.shops:
|
|
|
|
outfile.write('\n\nShops:\n\n')
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(
|
|
|
|
item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if
|
|
|
|
item)) for shop in self.shops))
|
2021-06-06 15:13:34 +00:00
|
|
|
|
2021-07-21 16:08:15 +00:00
|
|
|
for player in self.world.get_game_players("A Link to the Past"):
|
2020-05-20 20:21:05 +00:00
|
|
|
if self.world.boss_shuffle[player] != 'none':
|
|
|
|
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write(
|
|
|
|
f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
|
|
|
|
outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
2017-07-18 10:44:13 +00:00
|
|
|
outfile.write('\n\nPlaythrough:\n\n')
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
|
|
|
|
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [
|
|
|
|
f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
2019-12-21 12:33:07 +00:00
|
|
|
if self.unreachables:
|
|
|
|
outfile.write('\n\nUnreachable Items:\n\n')
|
2021-10-06 09:32:49 +00:00
|
|
|
outfile.write(
|
|
|
|
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2021-06-06 15:13:34 +00:00
|
|
|
if self.paths:
|
|
|
|
outfile.write('\n\nPaths:\n\n')
|
|
|
|
path_listings = []
|
|
|
|
for location, path in sorted(self.paths.items()):
|
|
|
|
path_lines = []
|
|
|
|
for region, exit in path:
|
|
|
|
if exit is not None:
|
|
|
|
path_lines.append("{} -> {}".format(region, exit))
|
|
|
|
else:
|
|
|
|
path_lines.append(region)
|
|
|
|
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
|
|
|
|
|
|
|
|
outfile.write('\n'.join(path_listings))
|
2021-11-02 11:29:29 +00:00
|
|
|
call_all(self.world, "write_spoiler_end", outfile)
|
2021-10-06 09:32:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
seeddigits = 20
|
|
|
|
|
|
|
|
|
|
|
|
def get_seed(seed=None):
|
|
|
|
if seed is None:
|
|
|
|
random.seed(None)
|
|
|
|
return random.randint(0, pow(10, seeddigits) - 1)
|
|
|
|
return seed
|