Archipelago/BaseClasses.py

1540 lines
72 KiB
Python
Raw Normal View History

2020-03-02 23:12:14 +00:00
from __future__ import annotations
import copy
from enum import Enum, unique
import logging
import json
2021-02-20 01:30:55 +00:00
import functools
from collections import OrderedDict, Counter, deque
from typing import *
import secrets
import random
2020-10-24 03:38:56 +00:00
class MultiWorld():
2020-04-10 18:54:18 +00:00
debug_types = False
2021-01-02 11:49:43 +00:00
player_names: Dict[int, List[str]]
2020-03-02 23:12:14 +00:00
_region_cache: dict
difficulty_requirements: dict
required_medallions: dict
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]]
plando_items: List[PlandoItem]
plando_connections: List[PlandoConnection]
er_seeds: Dict[int, str]
2021-06-11 16:02:48 +00:00
worlds: Dict[int, "AutoWorld.World"]
class AttributeProxy():
def __init__(self, rule):
self.rule = rule
def __getitem__(self, player) -> bool:
return self.rule(player)
2021-03-14 07:38:02 +00:00
def __init__(self, players: int):
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
self.players = players
self.teams = 1
self.glitch_triforce = False
2021-03-14 07:38:02 +00:00
self.algorithm = 'balanced'
self.dungeons = []
self.regions = []
self.shops = []
self.itempool = []
2017-05-20 12:03:15 +00:00
self.seed = None
self.seed_name: str = "Unavailable"
self.precollected_items = []
self.state = CollectionState(self)
self._cached_entrances = None
self._cached_locations = None
self._entrance_cache = {}
self._location_cache = {}
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
self.rupoor_cost = 10
self.aga_randomness = True
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
2018-03-23 03:18:40 +00:00
self.dynamic_regions = []
self.dynamic_locations = []
self.spoiler = Spoiler(self)
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'])
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
self.remote_items = self.AttributeProxy(lambda player: self.game[player] != "A Link to the Past")
for player in range(1, players + 1):
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('tech_tree_layout_prerequisites', {})
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')
2021-04-09 18:40:45 +00:00
set_player_attr('swordless', False)
2021-03-14 07:38:02 +00:00
set_player_attr('difficulty', 'normal')
set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False)
set_player_attr('goal', 'ganon')
set_player_attr('progressive', 'on')
set_player_attr('accessibility', 'items')
set_player_attr('retro', False)
set_player_attr('hints', True)
set_player_attr('player_names', [])
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)
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)
set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', False)
set_player_attr('bigkeyshuffle', False)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_shuffle', False)
set_player_attr('enemy_health', 'default')
set_player_attr('enemy_damage', 'default')
set_player_attr('killable_thieves', False)
set_player_attr('tile_shuffle', False)
set_player_attr('bush_shuffle', False)
2019-12-30 02:03:53 +00:00
set_player_attr('beemizer', 0)
set_player_attr('escape_assist', [])
set_player_attr('open_pyramid', False)
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)
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)
set_player_attr('can_take_damage', True)
set_player_attr('glitch_boots', True)
set_player_attr('progression_balancing', True)
set_player_attr('local_items', set())
2020-11-22 21:53:31 +00:00
set_player_attr('non_local_items', set())
2020-06-17 08:02:54 +00:00
set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off')
2020-09-20 02:35:45 +00:00
set_player_attr('shuffle_prizes', "g")
set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp")
set_player_attr('restrict_dungeon_item_on_boss', False)
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', [])
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)
self.custom_data = {}
self.worlds = {}
2021-06-11 16:02:48 +00:00
def set_options(self, args):
from worlds import AutoWorld
for player in self.player_ids:
self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option in world_type.options:
setattr(self, option, getattr(args, option, {}))
self.worlds[player] = world_type(self, player)
2021-03-20 23:47:17 +00:00
def secure(self):
self.random = secrets.SystemRandom()
2021-07-04 14:44:27 +00:00
@functools.cached_property
def player_ids(self):
2021-07-04 20:21:53 +00:00
return tuple(range(1, self.players + 1))
2021-07-04 14:44:27 +00:00
# Todo: make these automatic, or something like get_players_for_game(game_name)
@functools.cached_property
def alttp_player_ids(self):
2021-07-04 20:21:53 +00:00
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
2021-07-04 14:44:27 +00:00
@functools.cached_property
def hk_player_ids(self):
2021-07-04 20:21:53 +00:00
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
2021-07-04 14:44:27 +00:00
@functools.cached_property
2021-04-01 09:40:58 +00:00
def factorio_player_ids(self):
2021-07-04 20:21:53 +00:00
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
2021-04-01 09:40:58 +00:00
2021-07-04 14:44:27 +00:00
@functools.cached_property
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
def minecraft_player_ids(self):
2021-07-04 20:21:53 +00:00
return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
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
2020-03-02 23:12:14 +00:00
def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
2020-08-20 01:57:09 +00:00
def get_player_names(self, player: int) -> str:
return ", ".join(self.player_names[player])
def initialize_regions(self, regions=None):
for region in regions if regions else self.regions:
region.world = self
self._region_cache[region.player][region.name] = region
2021-02-20 01:30:55 +00:00
@functools.cached_property
def world_name_lookup(self):
return {self.player_names[player_id][0]: player_id for player_id in self.player_ids}
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
def get_regions(self, player=None):
return self.regions if player is None else self._region_cache[player].values()
def get_region(self, regionname: str, player: int) -> Region:
try:
return self._region_cache[player][regionname]
except KeyError:
2020-09-08 13:02:37 +00:00
self._recache()
return self._region_cache[player][regionname]
2020-04-10 18:54:18 +00:00
def get_entrance(self, entrance: str, player: int) -> Entrance:
try:
2020-09-08 13:02:37 +00:00
return self._entrance_cache[entrance, player]
except KeyError:
2020-09-08 13:02:37 +00:00
self._recache()
return self._entrance_cache[entrance, player]
2020-04-10 18:54:18 +00:00
def get_location(self, location: str, player: int) -> Location:
try:
2020-09-08 13:02:37 +00:00
return self._location_cache[location, player]
except KeyError:
2020-09-08 13:02:37 +00:00
self._recache()
return self._location_cache[location, player]
2020-04-10 18:54:18 +00:00
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
for dungeon in self.dungeons:
if dungeon.name == dungeonname and dungeon.player == player:
return dungeon
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player))
2020-04-10 18:54:18 +00:00
2020-03-02 23:12:14 +00:00
def get_all_state(self, keys=False) -> CollectionState:
ret = CollectionState(self)
2017-11-04 18:23:57 +00:00
for item in self.itempool:
2021-07-04 13:47:11 +00:00
self.worlds[item.player].collect(ret, item)
2017-08-05 15:52:18 +00:00
if keys:
2021-03-20 23:47:17 +00:00
for p in self.alttp_player_ids:
2021-07-04 13:47:11 +00:00
world = self.worlds[p]
2020-10-24 03:38:56 +00:00
from worlds.alttp.Items import ItemFactory
for item in ItemFactory(
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)',
'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + [
'Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + [
'Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + [
'Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)',
'Big Key (Ganons Tower)'] + [
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
'Small Key (Ganons Tower)'] * 4,
p):
2021-07-04 13:47:11 +00:00
world.collect(ret, item)
ret.sweep_for_events()
return ret
2020-03-02 23:12:14 +00:00
def get_items(self) -> list:
return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_items(self, item, player: int) -> List[Location]:
2020-03-02 23:12:14 +00:00
return [location for location in self.get_locations() if
location.item is not None and location.item.name == item and location.item.player == player]
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-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)
2020-03-02 23:12:14 +00:00
def push_precollected(self, item: Item):
item.world = self
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
item.advancement = True
self.precollected_items.append(item)
self.state.collect(item, True)
2020-03-02 23:12:14 +00:00
def push_item(self, location: Location, item: Item, collect: bool = True):
if not isinstance(location, Location):
raise RuntimeError(
'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player))
2018-01-02 05:39:53 +00:00
if location.can_fill(self.state, item, False):
location.item = item
item.location = location
item.world = self # try to not have this here anymore
if collect:
self.state.collect(item, location.event, location)
logging.debug('Placed %s at %s', item, location)
else:
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
2020-03-02 23:12:14 +00:00
def get_entrances(self) -> list:
if self._cached_entrances is None:
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
return self._cached_entrances
def clear_entrance_cache(self):
self._cached_entrances = None
2020-03-02 23:12:14 +00:00
def get_locations(self) -> list:
if self._cached_locations is None:
self._cached_locations = [location for region in self.regions for location in region.locations]
return self._cached_locations
2018-03-23 03:18:40 +00:00
def clear_location_cache(self):
self._cached_locations = None
2020-03-02 23:12:14 +00:00
def get_unfilled_locations(self, player=None) -> list:
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]
def get_unfilled_dungeon_locations(self):
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
2020-03-02 23:12:14 +00:00
def get_filled_locations(self, player=None) -> list:
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]
2020-03-02 23:12:14 +00:00
def get_reachable_locations(self, state=None, player=None) -> list:
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)]
2020-03-02 23:12:14 +00:00
def get_placeable_locations(self, state=None, player=None) -> list:
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)]
def get_unfilled_locations_for_players(self, location_name: str, players: Iterable[int]):
for player in players:
location = self.get_location(location_name, player)
if location.item is None:
yield location
2020-03-02 23:12:14 +00:00
def unlocks_new_location(self, item) -> bool:
temp_state = self.state.copy()
temp_state.collect(item, True)
for location in self.get_unfilled_locations():
if temp_state.can_reach(location) and not self.state.can_reach(location):
return True
return False
def has_beaten_game(self, state, player: Optional[int] = None):
if player:
2021-02-22 10:18:53 +00:00
return self.completion_condition[player](state)
else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
def can_beat_game(self, starting_state : Optional[CollectionState]=None):
if starting_state:
2020-03-07 22:20:11 +00:00
if self.has_beaten_game(starting_state):
return True
state = starting_state.copy()
else:
2020-03-07 22:20:11 +00:00
if self.has_beaten_game(self.state):
return True
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}
while prog_locations:
2021-04-29 07:54:49 +00:00
sphere = set()
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations:
if location.can_reach(state):
2021-04-29 07:54:49 +00:00
sphere.add(location)
if not sphere:
# ran out of places and did not finish yet, quit
return False
for location in sphere:
state.collect(location.item, True, location)
2021-04-29 07:54:49 +00:00
prog_locations -= sphere
if self.has_beaten_game(state):
return True
return False
def get_spheres(self):
state = CollectionState(self)
2021-02-27 17:58:17 +00:00
locations = set(self.get_filled_locations())
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
if not sphere:
if locations:
yield locations # unreachable locations
break
for location in sphere:
state.collect(location.item, True, location)
locations -= sphere
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)
players = {"none" : set(),
"items": set(),
"locations": set()}
for player, access in self.accessibility.items():
players[access].add(player)
beatable_fulfilled = False
def location_conditition(location : Location):
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
if location.player in players["none"]:
return False
return True
def location_relevant(location : Location):
"""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
return False
def all_done():
"""Check if all access rules are fulfilled"""
if beatable_fulfilled:
if any(location_conditition(location) for location in locations):
return False # still locations required to be collected
return True
locations = {location for location in self.get_locations() if location_relevant(location)}
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
logging.warning(f"Could not access required locations for accessibility check."
f" Missing: {locations}")
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
class CollectionState(object):
2020-10-24 03:38:56 +00:00
def __init__(self, parent: MultiWorld):
self.prog_items = Counter()
self.world = parent
2019-07-11 04:18:30 +00:00
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.blocked_connections = {player: set() for player in range(1, parent.players + 1)}
self.events = set()
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)}
for item in parent.precollected_items:
self.collect(item, True)
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]
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)
2020-03-02 23:12:14 +00:00
def copy(self) -> CollectionState:
ret = CollectionState(self.world)
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)}
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked)
return ret
2020-06-30 05:32:05 +00:00
def can_reach(self, spot, resolution_hint=None, player=None) -> bool:
if not hasattr(spot, "spot_type"):
# try to resolve a name
if resolution_hint == 'Location':
spot = self.world.get_location(spot, player)
elif resolution_hint == 'Entrance':
spot = self.world.get_entrance(spot, player)
else:
# default to Region
spot = self.world.get_region(spot, player)
return spot.can_reach(self)
2020-06-30 05:32:05 +00:00
def sweep_for_events(self, key_only: bool = False, locations=None):
if locations is None:
locations = self.world.get_filled_locations()
new_locations = True
# 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}
while new_locations:
reachable_events = {location for location in locations if
(not key_only or
(not self.world.keyshuffle[location.item.player] and location.item.smallkey)
or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
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)
2020-06-30 05:32:05 +00:00
def has(self, item, player: int, count: int = 1):
return self.prog_items[item, player] >= count
def has_essence(self, player: int, count: int):
return self.prog_items["Dream_Nail", player]
# return self.prog_items["Essence", player] >= count
def has_grubs(self, player: int, count: int):
from worlds.hk import Items as HKItems
found = 0
for item_name in HKItems.lookup_type_to_names["Grub"]:
found += self.prog_items[item_name, player]
if found >= count:
return True
2021-02-24 05:02:51 +00:00
return False
2021-02-27 15:27:35 +00:00
def has_flames(self, player: int, count: int):
from worlds.hk import Items as HKItems
found = 0
for item_name in HKItems.lookup_type_to_names["Flame"]:
found += self.prog_items[item_name, player]
if found >= count:
return True
return False
2020-06-30 05:32:05 +00:00
def has_key(self, item, player, count: int = 1):
if self.world.logic[player] == 'nologic':
return True
if self.world.keyshuffle[player] == "universal":
return self.can_buy_unlimited('Small Key (Universal)', player)
return self.prog_items[item, player] >= count
2020-03-02 23:12:14 +00:00
def can_buy_unlimited(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
shop in self.world.shops)
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)
2020-03-02 23:12:14 +00:00
def item_count(self, item, player: int) -> int:
return self.prog_items[item, player]
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:
found: int = 0
for crystalnumber in range(1, 8):
found += self.prog_items[f"Crystal {crystalnumber}", player]
if found >= count:
return True
return False
def can_lift_rocks(self, player: int):
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
2020-03-02 23:12:14 +00:00
def has_bottle(self, player: int) -> bool:
return self.has_bottles(1, player)
2020-03-02 23:12:14 +00:00
def bottle_count(self, player: int) -> int:
found: int = 0
for bottlename in item_name_groups["Bottles"]:
found += self.prog_items[bottlename, player]
return min(self.world.difficulty_requirements[player].progressive_bottle_limit, found)
def has_bottles(self, bottles: int, player: int) -> bool:
"""Version of bottle_count that allows fast abort"""
if bottles > self.world.difficulty_requirements[player].progressive_bottle_limit:
return False
found: int = 0
for bottlename in item_name_groups["Bottles"]:
found += self.prog_items[bottlename, player]
if found >= bottles:
return True
return False
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
return self.heart_count(player) >= count
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
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
2020-03-02 23:12:14 +00:00
def can_lift_heavy_rocks(self, player: int) -> bool:
return self.has('Titans Mitts', player)
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.
basemagic = 8
2020-03-13 23:31:28 +00:00
if self.has('Magic Upgrade (1/4)', player):
basemagic = 32
2020-03-13 23:31:28 +00:00
elif self.has('Magic Upgrade (1/2)', player):
basemagic = 16
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
if self.world.item_functionality[player] == 'hard' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
elif self.world.item_functionality[player] == 'expert' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
2018-09-16 16:55:49 +00:00
else:
basemagic = basemagic + basemagic * self.bottle_count(player)
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:
return (self.has_melee_weapon(player)
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)
return self.has('Bow', player) or self.has('Silver Bow', player)
2020-03-02 23:12:14 +00:00
def can_get_good_bee(self, player: int) -> bool:
cave = self.world.get_region('Good Bee Cave', player)
2018-09-23 02:51:54 +00:00
return (
2020-03-02 23:12:14 +00:00
self.has_bottle(player) and
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
)
def can_retrieve_tablet(self, player:int) -> bool:
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
2021-04-09 18:40:45 +00:00
(self.world.swordless[player] and
self.has("Hammer", player)))
2020-03-02 23:12:14 +00:00
def has_sword(self, player: int) -> bool:
return self.has('Fighter Sword', player) \
or self.has('Master Sword', player) \
or self.has('Tempered Sword', player) \
or self.has('Golden Sword', player)
2020-03-02 23:12:14 +00:00
def has_beam_sword(self, player: int) -> bool:
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
def has_melee_weapon(self, player: int) -> bool:
return self.has_sword(player) or self.has('Hammer', player)
2020-03-02 23:12:14 +00:00
def has_fire_source(self, player: int) -> bool:
return self.has('Fire Rod', player) or self.has('Lamp', player)
2020-03-02 23:12:14 +00:00
def can_melt_things(self, player: int) -> bool:
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
self.has_sword(player)))
2020-03-02 23:12:14 +00:00
def can_avoid_lasers(self, player: int) -> bool:
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
2020-03-02 23:12:14 +00:00
def can_reach_light_world(self, player: int) -> bool:
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:
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:
return self.has(self.world.required_medallions[player][0], player)
2020-03-02 23:12:14 +00:00
def has_turtle_rock_medallion(self, player: int) -> bool:
return self.has(self.world.required_medallions[player][1], player)
2020-02-10 04:38:55 +00:00
def can_boots_clip_lw(self, player):
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
def can_boots_clip_dw(self, player):
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
def can_get_glitched_speed_lw(self, player):
2021-02-27 16:11:54 +00:00
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
if self.world.mode[player] == 'inverted':
2021-02-27 16:11:54 +00:00
rules.append(self.has('Moon Pearl', player))
return all(rules)
2020-02-12 23:48:36 +00:00
def can_superbunny_mirror_with_sword(self, player):
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)])]
if self.world.mode[player] != 'inverted':
2021-02-27 16:11:54 +00:00
rules.append(self.has('Moon Pearl', player))
return all(rules)
2020-02-10 04:38:55 +00:00
def can_bomb_clip(self, region: Region, player: int) -> bool:
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
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
# Minecraft logic functions
def has_iron_ingots(self, player: int):
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
def has_gold_ingots(self, player: int):
return self.has('Ingot Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
def has_diamond_pickaxe(self, player: int):
return self.has('Progressive Tools', player, 3) and self.has_iron_ingots(player)
def craft_crossbow(self, player: int):
return self.has('Archery', player) and self.has_iron_ingots(player)
def has_bottle_mc(self, player: int):
return self.has('Bottles', player) and self.has('Ingot Crafting', player)
def can_enchant(self, player: int):
return self.has('Enchanting', player) and self.has_diamond_pickaxe(player) # mine obsidian and lapis
def can_use_anvil(self, player: int):
return self.has('Enchanting', player) and self.has('Resource Blocks', player) and self.has_iron_ingots(player)
def fortress_loot(self, player: int): # saddles, blaze rods, wither skulls
return self.can_reach('Nether Fortress', 'Region', player) and self.basic_combat(player)
def can_brew_potions(self, player: int):
return self.fortress_loot(player) and self.has('Brewing', player) and self.has_bottle_mc(player)
def can_piglin_trade(self, player: int):
return self.has_gold_ingots(player) and (self.can_reach('The Nether', 'Region', player) or self.can_reach('Bastion Remnant', 'Region', player))
def enter_stronghold(self, player: int):
return self.fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
# Difficulty-dependent functions
def combat_difficulty(self, player: int):
return self.world.combat_difficulty[player].get_option_name()
def can_adventure(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard':
return True
return self.has('Progressive Weapons', player) and (self.has('Ingot Crafting', player) or self.has('Campfire', player))
def basic_combat(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \
self.has('Shield', player) and self.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard':
return True
return self.has('Progressive Weapons', player) and (self.has('Progressive Armor', player) or self.has('Shield', player)) and self.has_iron_ingots(player)
def complete_raid(self, player: int):
reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player)
if self.combat_difficulty(player) == 'easy':
return reach_regions and \
self.has('Progressive Weapons', player, 3) and self.has('Progressive Armor', player, 2) and \
self.has('Shield', player) and self.has('Archery', player) and \
self.has('Progressive Tools', player, 2) and self.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard': # might be too hard?
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
(self.has('Progressive Armor', player) or self.has('Shield', player))
return reach_regions and self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player) and \
self.has('Progressive Armor', player) and self.has('Shield', player)
def can_kill_wither(self, player: int):
normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.can_brew_potions(player) and self.can_enchant(player)
if self.combat_difficulty(player) == 'easy':
Minecraft updates (#13) * 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 * Minecraft logic updates Wither kill now considers nether fortresses as a valid source of soul sand A Furious Cocktail now requires beacons for resistance and village access for carrots Uneasy Alliance now requires fishing rod to pull the ghast through the portal On a Rail now requires iron pickaxe to make powered rails Overkill now may require strength II without stone axe, which needs nether access * embed all apmc info into slot_data * updated MC tests for logic changes * put apmc into zipfile Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-15 22:49:58 +00:00
return self.fortress_loot(player) and normal_kill and self.has('Archery', player)
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
elif self.combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings
Minecraft updates (#13) * 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 * Minecraft logic updates Wither kill now considers nether fortresses as a valid source of soul sand A Furious Cocktail now requires beacons for resistance and village access for carrots Uneasy Alliance now requires fishing rod to pull the ghast through the portal On a Rail now requires iron pickaxe to make powered rails Overkill now may require strength II without stone axe, which needs nether access * embed all apmc info into slot_data * updated MC tests for logic changes * put apmc into zipfile Co-authored-by: achuang <alexander.w.chuang@gmail.com>
2021-05-15 22:49:58 +00:00
return self.fortress_loot(player) and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player))
return self.fortress_loot(player) and normal_kill
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
def can_kill_ender_dragon(self, player: int):
# Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned.
respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Ingot Crafting', player)
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
if self.combat_difficulty(player) == 'easy':
return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \
self.has('Archery', player) and self.can_brew_potions(player) and self.can_enchant(player)
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
if self.combat_difficulty(player) == 'hard':
return respawn_dragon and ((self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player)))
return respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
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-02-27 16:11:54 +00:00
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
if location:
self.locations_checked.add(location)
2021-07-04 13:47:11 +00:00
changed = self.world.worlds[item.player].collect(self, item)
if not changed and event:
self.prog_items[item.name, item.player] += 1
changed = True
2020-03-02 23:12:14 +00:00
2019-07-11 04:18:30 +00:00
self.stale[item.player] = True
if changed and not event:
self.sweep_for_events()
return changed
def remove(self, item):
if item.advancement:
to_remove = item.name
if item.game == "A Link to the Past" and to_remove.startswith('Progressive '):
if 'Sword' in to_remove:
if self.has('Golden Sword', item.player):
to_remove = 'Golden Sword'
elif self.has('Tempered Sword', item.player):
to_remove = 'Tempered Sword'
elif self.has('Master Sword', item.player):
to_remove = 'Master Sword'
elif self.has('Fighter Sword', item.player):
to_remove = 'Fighter Sword'
else:
to_remove = None
elif 'Glove' in item.name:
if self.has('Titans Mitts', item.player):
to_remove = 'Titans Mitts'
elif self.has('Power Glove', item.player):
to_remove = 'Power Glove'
else:
to_remove = None
elif 'Shield' in item.name:
if self.has('Mirror Shield', item.player):
to_remove = 'Mirror Shield'
elif self.has('Red Shield', item.player):
to_remove = 'Red Shield'
elif self.has('Blue Shield', item.player):
to_remove = 'Blue Shield'
else:
to_remove = None
elif 'Bow' in item.name:
if self.has('Silver Bow', item.player):
to_remove = 'Silver Bow'
elif self.has('Bow', item.player):
to_remove = 'Bow'
else:
to_remove = None
if to_remove:
self.prog_items[to_remove, item.player] -= 1
if self.prog_items[to_remove, item.player] < 1:
del (self.prog_items[to_remove, item.player])
# invalidate caches, nothing can be trusted anymore now
2019-07-11 04:18:30 +00:00
self.reachable_regions[item.player] = set()
self.blocked_connections[item.player] = set()
2019-07-11 04:18:30 +00:00
self.stale[item.player] = True
@unique
class RegionType(Enum):
LightWorld = 1
DarkWorld = 2
Cave = 3 # Also includes Houses
Dungeon = 4
@property
def is_indoors(self):
"""Shorthand for checking if Cave or Dungeon"""
return self in (RegionType.Cave, RegionType.Dungeon)
class Region(object):
2020-03-02 23:12:14 +00:00
def __init__(self, name: str, type, hint, player: int):
self.name = name
self.type = type
self.entrances = []
self.exits = []
self.locations = []
self.dungeon = None
self.shop = None
self.world = None
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'
self.hint_text = hint
self.recursion_count = 0
self.player = player
def can_reach(self, state):
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]
2020-03-07 22:20:11 +00:00
def can_reach_private(self, state: CollectionState):
for entrance in self.entrances:
2019-07-11 04:18:30 +00:00
if entrance.can_reach(state):
if not self in state.path:
state.path[self] = (self.name, state.path.get(entrance, None))
return True
return False
2020-03-07 22:20:11 +00:00
def can_fill(self, item: Item):
2021-03-20 23:47:17 +00:00
inside_dungeon_item = item.locked_dungeon_item
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)'
if sewer_hack or inside_dungeon_item:
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
return True
def __repr__(self):
return self.__str__()
def __str__(self):
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
class Entrance(object):
2020-03-02 23:12:14 +00:00
def __init__(self, player: int, name: str = '', parent=None):
self.name = name
self.parent_region = parent
self.connected_region = None
2017-05-20 12:03:15 +00:00
self.target = None
self.addresses = None
2017-05-20 12:03:15 +00:00
self.spot_type = 'Entrance'
self.recursion_count = 0
self.vanilla = None
self.access_rule = lambda state: True
self.player = player
self.hide_path = False
def can_reach(self, state):
2019-07-11 04:18:30 +00:00
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
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-20 12:03:15 +00:00
return False
def connect(self, region, addresses=None, target=None, vanilla=None):
self.connected_region = region
2017-05-20 12:03:15 +00:00
self.target = target
self.addresses = addresses
self.vanilla = vanilla
region.entrances.append(self)
def __repr__(self):
return self.__str__()
def __str__(self):
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})'
class Dungeon(object):
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
self.name = name
self.regions = regions
self.big_key = big_key
self.small_keys = small_keys
self.dungeon_items = dungeon_items
self.bosses = dict()
self.player = player
self.world = None
@property
def boss(self):
return self.bosses.get(None, None)
@boss.setter
def boss(self, value):
self.bosses[None] = value
@property
def keys(self):
return self.small_keys + ([self.big_key] if self.big_key else [])
@property
def all_items(self):
return self.dungeon_items + self.keys
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)
def __eq__(self, other: Dungeon) -> bool:
if not other:
return False
return self.name == other.name and self.player == other.player
def __repr__(self):
return self.__str__()
def __str__(self):
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
2021-01-10 18:23:57 +00:00
class Boss():
2020-03-02 23:12:14 +00:00
def __init__(self, name, enemizer_name, defeat_rule, player: int):
self.name = name
self.enemizer_name = enemizer_name
self.defeat_rule = defeat_rule
self.player = player
2020-03-02 23:12:14 +00:00
def can_defeat(self, state) -> bool:
return self.defeat_rule(state, self.player)
def __repr__(self):
return f"Boss({self.name})"
2021-01-10 18:23:57 +00:00
class Location():
shop_slot: bool = False
shop_slot_disabled: bool = False
2021-01-10 18:23:57 +00:00
event: bool = False
locked: bool = False
spot_type = 'Location'
game: str = "Generic"
crystal: bool = False
2021-01-10 18:23:57 +00:00
def __init__(self, player: int, name: str = '', address:int = None, parent=None):
self.name = name
self.address = address
2021-03-20 23:47:17 +00:00
self.parent_region: Region = parent
self.recursion_count = 0
self.player = player
self.item = None
2018-01-02 05:39:53 +00:00
self.always_allow = lambda item, state: False
self.access_rule = lambda state: True
2018-01-02 05:39:53 +00:00
self.item_rule = lambda item: True
2020-07-09 14:16:31 +00:00
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
2020-07-09 14:16:31 +00:00
def can_reach(self, state: CollectionState) -> bool:
# 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
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
def __repr__(self):
return self.__str__()
def __str__(self):
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})'
def __hash__(self):
return hash((self.name, self.player))
def __lt__(self, other):
return (self.player, self.name) < (other.player, other.name)
@property
def hint_text(self):
2021-04-03 18:02:15 +00:00
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
class Item():
location: Optional[Location] = None
world: Optional[MultiWorld] = None
game: str = "Generic"
type: str = None
pedestal_credit_text = "and the Unknown Item"
sickkid_credit_text = None
magicshop_credit_text = None
zora_credit_text = None
fluteboy_credit_text = None
2021-06-06 14:17:07 +00:00
def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
self.name = name
self.advancement = advancement
self.player = player
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("-", " "))
@property
def pedestal_hint_text(self):
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
2021-01-02 11:49:43 +00:00
def __eq__(self, other):
return self.name == other.name and self.player == other.player
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))
@property
2020-03-02 23:12:14 +00:00
def crystal(self) -> bool:
return self.type == 'Crystal'
@property
2020-03-02 23:12:14 +00:00
def smallkey(self) -> bool:
return self.type == 'SmallKey'
@property
2020-03-02 23:12:14 +00:00
def bigkey(self) -> bool:
return self.type == 'BigKey'
@property
2020-03-02 23:12:14 +00:00
def map(self) -> bool:
return self.type == 'Map'
@property
2020-03-02 23:12:14 +00:00
def compass(self) -> bool:
return self.type == 'Compass'
2021-03-20 23:47:17 +00:00
@property
def dungeon_item(self) -> Optional[str]:
if self.game == "A Link to the Past" and self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
return self.type
@property
def shuffled_dungeon_item(self) -> bool:
dungeon_item_type = self.dungeon_item
if dungeon_item_type:
return {"SmallKey" : self.world.keyshuffle,
"BigKey": self.world.bigkeyshuffle,
"Map": self.world.mapshuffle,
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
return False
@property
def locked_dungeon_item(self) -> bool:
dungeon_item_type = self.dungeon_item
if dungeon_item_type:
return not {"SmallKey" : self.world.keyshuffle,
"BigKey": self.world.bigkeyshuffle,
"Map": self.world.mapshuffle,
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
return False
def __repr__(self):
return self.__str__()
def __str__(self):
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
class Spoiler(object):
2020-10-24 03:38:56 +00:00
world: MultiWorld
2020-08-25 12:31:20 +00:00
def __init__(self, world):
self.world = world
self.hashes = {}
2018-03-24 05:50:54 +00:00
self.entrances = OrderedDict()
self.medallions = {}
self.playthrough = {}
self.unreachables = []
self.startinventory = []
self.locations = {}
self.paths = {}
self.metadata = {}
self.shops = []
self.bosses = OrderedDict()
def set_entrance(self, entrance, exit, direction, player):
if self.world.players == 1:
self.entrances[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
else:
self.entrances[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
def parse_data(self):
self.medallions = OrderedDict()
2021-03-21 04:19:29 +00:00
for player in self.world.alttp_player_ids:
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
2018-03-23 15:03:38 +00:00
self.startinventory = list(map(str, self.world.precollected_items))
2018-03-23 15:03:38 +00:00
self.locations = OrderedDict()
listed_locations = set()
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]
self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
listed_locations.update(lw_locations)
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]
self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
listed_locations.update(dw_locations)
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]
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
listed_locations.update(cave_locations)
for dungeon in self.world.dungeons:
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]
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)
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations]
if other_locations:
self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
listed_locations.update(other_locations)
self.shops = []
2021-01-30 22:43:15 +00:00
from worlds.alttp.Shops import ShopType
for shop in self.world.shops:
if not shop.custom:
continue
shopdata = {'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
}
for index, item in enumerate(shop.inventory):
if item is None:
continue
shopdata['item_{}'.format(index)] = "{}{}".format(item['item'], item['price']) if item['price'] else item['item']
if item['player'] > 0:
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('', '(Player {}) — '.format(item['player']))
if item['max'] == 0:
continue
shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
if item['replacement'] is None:
continue
shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
self.shops.append(shopdata)
for player in self.world.alttp_player_ids:
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"
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness", player).boss.name
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':
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
else:
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
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
self.bosses[str(player)]["Ganon"] = "Ganon"
from Utils import __version__ as APVersion
self.metadata = {'version': APVersion,
'logic': self.world.logic,
'dark_room_logic': self.world.dark_room_logic,
'mode': self.world.mode,
2019-12-16 23:16:02 +00:00
'retro': self.world.retro,
2021-04-09 18:40:45 +00:00
'swordless': self.world.swordless,
'goal': self.world.goal,
'shuffle': self.world.shuffle,
2019-08-24 19:35:58 +00:00
'item_pool': self.world.difficulty,
'item_functionality': self.world.item_functionality,
'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility,
2019-08-24 19:35:58 +00:00
'hints': self.world.hints,
'mapshuffle': self.world.mapshuffle,
'compassshuffle': self.world.compassshuffle,
'keyshuffle': self.world.keyshuffle,
'bigkeyshuffle': self.world.bigkeyshuffle,
'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health,
'enemy_damage': self.world.enemy_damage,
'killable_thieves': self.world.killable_thieves,
'tile_shuffle': self.world.tile_shuffle,
'bush_shuffle': self.world.bush_shuffle,
'beemizer': self.world.beemizer,
'progressive': self.world.progressive,
'shufflepots': self.world.shufflepots,
'players': self.world.players,
2020-05-20 20:30:21 +00:00
'teams': self.world.teams,
'progression_balancing': self.world.progression_balancing,
2020-06-17 08:02:54 +00:00
'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required,
2020-09-20 02:35:45 +00:00
'shop_shuffle': self.world.shop_shuffle,
'shuffle_prizes': self.world.shuffle_prizes,
2020-10-07 20:48:18 +00:00
'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
'game': self.world.game,
'er_seeds': self.world.er_seeds
2019-08-24 19:35:58 +00:00
}
def to_json(self):
self.parse_data()
out = OrderedDict()
out['Entrances'] = list(self.entrances.values())
out.update(self.locations)
out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions
if self.hashes:
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
if self.shops:
out['Shops'] = self.shops
out['playthrough'] = self.playthrough
out['paths'] = self.paths
out['Bosses'] = self.bosses
out['meta'] = self.metadata
return json.dumps(out)
def to_file(self, filename):
2021-06-03 22:29:59 +00:00
import Options
self.parse_data()
def bool_to_text(variable: Union[bool, str]) -> str:
if type(variable) == str:
return variable
return 'Yes' if variable else 'No'
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' % (
self.metadata['version'], self.world.seed))
2019-08-24 19:53:21 +00:00
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Players: %d\n' % self.world.players)
outfile.write('Teams: %d\n' % self.world.teams)
for player in range(1, self.world.players + 1):
if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
outfile.write('Game: %s\n' % self.metadata['game'][player])
2020-05-20 20:30:21 +00:00
if self.world.players > 1:
outfile.write('Progression Balanced: %s\n' % (
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
options = self.world.worlds[player].options
if options:
for f_option in options:
2021-06-03 22:29:59 +00:00
res = getattr(self.world, f_option)[player]
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
if player in self.world.alttp_player_ids:
for team in range(self.world.teams):
outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
(player in self.world.alttp_player_ids and self.world.teams > 1) else 'Hash: ',
self.hashes[player, team]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
outfile.write('Restricted Boss Drops: %s\n' %
bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player]))
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No'))
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
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if "triforce" in self.metadata["goal"][player]: # triforce hunt
outfile.write("Pieces available for Triforce: %s\n" %
self.metadata['triforce_pieces_available'][player])
outfile.write("Pieces required for Triforce: %s\n" %
self.metadata["triforce_pieces_required"][player])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Map shuffle: %s\n' %
('Yes' if self.metadata['mapshuffle'][player] else 'No'))
outfile.write('Compass shuffle: %s\n' %
('Yes' if self.metadata['compassshuffle'][player] else 'No'))
outfile.write(
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
outfile.write('Big Key shuffle: %s\n' % (
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop price shuffle: %s\n' %
bool_to_text("p" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop upgrade shuffle: %s\n' %
bool_to_text("u" in self.metadata["shop_shuffle"][player]))
outfile.write('New Shop inventory: %s\n' %
bool_to_text("g" in self.metadata["shop_shuffle"][player] or
"f" in self.metadata["shop_shuffle"][player]))
outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write(
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write(f'Killable thieves: {bool_to_text(self.metadata["killable_thieves"][player])}\n')
outfile.write(f'Shuffled tiles: {bool_to_text(self.metadata["tile_shuffle"][player])}\n')
outfile.write(f'Shuffled bushes: {bool_to_text(self.metadata["bush_shuffle"][player])}\n')
outfile.write(
'Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
outfile.write('Beemizer: %s\n' % self.metadata['beemizer'][player])
outfile.write('Pot shuffle %s\n'
% ('Yes' if self.metadata['shufflepots'][player] else 'No'))
outfile.write('Prize shuffle %s\n' %
self.metadata['shuffle_prizes'][player])
if self.entrances:
outfile.write('\n\nEntrances:\n\n')
2020-08-23 19:38:21 +00:00
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: '
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:
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
2021-06-06 15:13:34 +00:00
if self.world.factorio_player_ids:
outfile.write('\n\nRecipes:\n')
for player in self.world.factorio_player_ids:
name = self.world.get_player_names(player)
for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))
2021-06-06 15:13:34 +00:00
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
2021-06-06 15:13:34 +00:00
if self.shops:
outfile.write('\n\nShops:\n\n')
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))
for player in self.world.alttp_player_ids:
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
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n')
outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
outfile.write('\n\nPlaythrough:\n\n')
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()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
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-01-02 11:49:43 +00:00
2021-02-21 19:37:43 +00:00
from worlds.alttp.Items import item_name_groups
from worlds.generic import PlandoItem, PlandoConnection