2020-03-02 23:12:14 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
import copy
|
2018-01-19 02:51:43 +00:00
|
|
|
from enum import Enum, unique
|
2017-05-16 19:23:47 +00:00
|
|
|
import logging
|
2017-07-18 10:44:13 +00:00
|
|
|
import json
|
2020-05-10 09:27:13 +00:00
|
|
|
from collections import OrderedDict, Counter, deque
|
2020-08-16 14:49:48 +00:00
|
|
|
from typing import Union, Optional
|
2020-07-14 05:01:51 +00:00
|
|
|
import secrets
|
|
|
|
import random
|
|
|
|
|
2020-08-22 17:19:29 +00:00
|
|
|
from EntranceShuffle import door_addresses, indirect_connections
|
|
|
|
from Utils import int16_as_bytes
|
|
|
|
from Items import item_name_groups
|
|
|
|
|
2020-07-14 05:01:51 +00:00
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
class World(object):
|
2020-04-10 18:54:18 +00:00
|
|
|
debug_types = False
|
2020-02-23 16:06:44 +00:00
|
|
|
player_names: list
|
2020-03-02 23:12:14 +00:00
|
|
|
_region_cache: dict
|
|
|
|
difficulty_requirements: dict
|
|
|
|
required_medallions: dict
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-08-16 14:49:48 +00:00
|
|
|
def __init__(self, players: int, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer,
|
|
|
|
progressive,
|
2020-03-02 23:12:14 +00:00
|
|
|
goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
2020-04-10 18:54:18 +00:00
|
|
|
if self.debug_types:
|
|
|
|
import inspect
|
|
|
|
methods = inspect.getmembers(self, predicate=inspect.ismethod)
|
|
|
|
|
|
|
|
for name, method in methods:
|
|
|
|
if name.startswith("_debug_"):
|
|
|
|
setattr(self, name[7:], method)
|
2020-04-10 18:58:52 +00:00
|
|
|
logging.debug(f"Set {self}.{name[7:]} to {method}")
|
2020-04-10 18:54:18 +00:00
|
|
|
self.get_location = self._debug_get_location
|
2020-07-14 05:01:51 +00:00
|
|
|
self.random = random.Random() # world-local random state is saved in case of future use a
|
|
|
|
# persistently running program with multiple worlds rolling concurrently
|
2019-04-18 09:23:24 +00:00
|
|
|
self.players = players
|
2020-01-14 09:42:27 +00:00
|
|
|
self.teams = 1
|
2019-12-16 17:24:34 +00:00
|
|
|
self.shuffle = shuffle.copy()
|
2019-12-16 12:26:07 +00:00
|
|
|
self.logic = logic.copy()
|
2019-12-16 15:54:46 +00:00
|
|
|
self.mode = mode.copy()
|
2019-12-16 13:31:47 +00:00
|
|
|
self.swords = swords.copy()
|
2019-12-16 16:46:21 +00:00
|
|
|
self.difficulty = difficulty.copy()
|
|
|
|
self.difficulty_adjustments = difficulty_adjustments.copy()
|
2020-02-03 01:10:56 +00:00
|
|
|
self.timer = timer.copy()
|
2017-11-10 10:11:40 +00:00
|
|
|
self.progressive = progressive
|
2019-12-16 14:27:20 +00:00
|
|
|
self.goal = goal.copy()
|
2017-06-04 13:02:27 +00:00
|
|
|
self.algorithm = algorithm
|
2017-10-15 16:16:07 +00:00
|
|
|
self.dungeons = []
|
2017-05-15 18:28:04 +00:00
|
|
|
self.regions = []
|
2018-02-17 23:38:54 +00:00
|
|
|
self.shops = []
|
2017-05-15 18:28:04 +00:00
|
|
|
self.itempool = []
|
2017-05-20 12:03:15 +00:00
|
|
|
self.seed = None
|
2019-08-10 19:30:14 +00:00
|
|
|
self.precollected_items = []
|
2017-05-15 18:28:04 +00:00
|
|
|
self.state = CollectionState(self)
|
2019-01-20 07:01:02 +00:00
|
|
|
self._cached_entrances = None
|
2017-05-15 18:28:04 +00:00
|
|
|
self._cached_locations = None
|
|
|
|
self._entrance_cache = {}
|
|
|
|
self._location_cache = {}
|
2017-06-04 14:15:59 +00:00
|
|
|
self.required_locations = []
|
2017-07-14 12:38:13 +00:00
|
|
|
self.shuffle_bonk_prizes = False
|
2017-06-03 19:27:34 +00:00
|
|
|
self.light_world_light_cone = False
|
2017-06-03 13:46:05 +00:00
|
|
|
self.dark_world_light_cone = False
|
2018-01-22 02:43:44 +00:00
|
|
|
self.rupoor_cost = 10
|
2017-08-01 16:58:42 +00:00
|
|
|
self.aga_randomness = True
|
2017-06-04 11:10:22 +00:00
|
|
|
self.lock_aga_door_in_escape = False
|
2018-09-23 02:51:54 +00:00
|
|
|
self.save_and_quit_from_boss = True
|
2019-12-17 11:14:29 +00:00
|
|
|
self.accessibility = accessibility.copy()
|
2017-07-17 20:28:29 +00:00
|
|
|
self.shuffle_ganon = shuffle_ganon
|
|
|
|
self.fix_gtower_exit = self.shuffle_ganon
|
2019-12-16 23:16:02 +00:00
|
|
|
self.retro = retro.copy()
|
2018-01-22 02:43:44 +00:00
|
|
|
self.custom = custom
|
|
|
|
self.customitemarray = customitemarray
|
2019-12-17 11:22:55 +00:00
|
|
|
self.hints = hints.copy()
|
2018-03-23 03:18:40 +00:00
|
|
|
self.dynamic_regions = []
|
|
|
|
self.dynamic_locations = []
|
2017-07-18 10:44:13 +00:00
|
|
|
self.spoiler = Spoiler(self)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2019-12-17 20:09:33 +00:00
|
|
|
for player in range(1, players + 1):
|
|
|
|
def set_player_attr(attr, val):
|
|
|
|
self.__dict__.setdefault(attr, {})[player] = val
|
|
|
|
set_player_attr('_region_cache', {})
|
2020-01-14 09:42:27 +00:00
|
|
|
set_player_attr('player_names', [])
|
2020-01-18 08:50:12 +00:00
|
|
|
set_player_attr('remote_items', False)
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
|
|
|
set_player_attr('swamp_patch_required', False)
|
|
|
|
set_player_attr('powder_patch_required', False)
|
|
|
|
set_player_attr('ganon_at_pyramid', True)
|
|
|
|
set_player_attr('ganonstower_vanilla', True)
|
|
|
|
set_player_attr('sewer_light_cone', self.mode[player] == 'standard')
|
|
|
|
set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
|
|
|
set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
|
|
set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
|
|
set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
2019-12-18 19:45:51 +00:00
|
|
|
set_player_attr('can_access_trock_eyebridge', None)
|
|
|
|
set_player_attr('can_access_trock_front', None)
|
|
|
|
set_player_attr('can_access_trock_big_chest', None)
|
|
|
|
set_player_attr('can_access_trock_middle', None)
|
|
|
|
set_player_attr('fix_fake_world', True)
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('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')
|
2020-08-19 21:24:17 +00:00
|
|
|
set_player_attr('enemy_shuffle', False)
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('enemy_health', 'default')
|
|
|
|
set_player_attr('enemy_damage', 'default')
|
2020-08-19 21:24:17 +00:00
|
|
|
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)
|
2019-12-17 20:09:33 +00:00
|
|
|
set_player_attr('escape_assist', [])
|
|
|
|
set_player_attr('crystals_needed_for_ganon', 7)
|
|
|
|
set_player_attr('crystals_needed_for_gt', 7)
|
|
|
|
set_player_attr('open_pyramid', False)
|
2019-12-21 09:42:59 +00:00
|
|
|
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
|
|
|
set_player_attr('treasure_hunt_count', 0)
|
2020-03-04 12:55:03 +00:00
|
|
|
set_player_attr('clock_mode', False)
|
2020-02-03 02:52:57 +00:00
|
|
|
set_player_attr('can_take_damage', True)
|
2020-04-16 09:02:16 +00:00
|
|
|
set_player_attr('glitch_boots', True)
|
2020-05-18 01:54:29 +00:00
|
|
|
set_player_attr('progression_balancing', True)
|
2020-06-03 20:13:58 +00:00
|
|
|
set_player_attr('local_items', set())
|
2020-06-17 08:02:54 +00:00
|
|
|
set_player_attr('triforce_pieces_available', 30)
|
2020-06-07 13:22:24 +00:00
|
|
|
set_player_attr('triforce_pieces_required', 20)
|
2020-08-23 13:03:06 +00:00
|
|
|
set_player_attr('shop_shuffle', 'off')
|
2019-12-17 20:09:33 +00:00
|
|
|
|
2020-07-14 05:01:51 +00:00
|
|
|
def secure(self):
|
|
|
|
self.random = secrets.SystemRandom()
|
|
|
|
|
2020-06-19 01:01:23 +00:00
|
|
|
@property
|
|
|
|
def player_ids(self):
|
|
|
|
yield from range(1, self.players + 1)
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_name_string_for_object(self, obj) -> str:
|
2020-01-14 09:42:27 +00:00
|
|
|
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])
|
2020-01-14 09:42:27 +00:00
|
|
|
|
2019-12-14 18:19:08 +00:00
|
|
|
def initialize_regions(self, regions=None):
|
|
|
|
for region in regions if regions else self.regions:
|
2017-10-28 22:34:37 +00:00
|
|
|
region.world = self
|
2019-12-14 18:19:08 +00:00
|
|
|
self._region_cache[region.player][region.name] = region
|
|
|
|
|
|
|
|
def get_regions(self, player=None):
|
|
|
|
return self.regions if player is None else self._region_cache[player].values()
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_region(self, regionname: str, player: int) -> Region:
|
2017-05-15 18:28:04 +00:00
|
|
|
try:
|
2019-12-14 18:19:08 +00:00
|
|
|
return self._region_cache[player][regionname]
|
2017-05-15 18:28:04 +00:00
|
|
|
except KeyError:
|
|
|
|
for region in self.regions:
|
2019-04-18 09:23:24 +00:00
|
|
|
if region.name == regionname and region.player == player:
|
2020-03-02 23:12:14 +00:00
|
|
|
assert not region.world # this should only happen before initialization
|
2017-05-15 18:28:04 +00:00
|
|
|
return region
|
2019-04-18 09:23:24 +00:00
|
|
|
raise RuntimeError('No such region %s for player %d' % (regionname, player))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 18:54:18 +00:00
|
|
|
def _debug_get_region(self, regionname: str, player: int) -> Region:
|
|
|
|
if type(regionname) != str:
|
|
|
|
raise TypeError(f"expected str, got {type(regionname)} instead")
|
|
|
|
try:
|
|
|
|
return self._region_cache[player][regionname]
|
|
|
|
except KeyError:
|
|
|
|
for region in self.regions:
|
|
|
|
if region.name == regionname and region.player == player:
|
|
|
|
assert not region.world # this should only happen before initialization
|
|
|
|
return region
|
|
|
|
raise RuntimeError('No such region %s for player %d' % (regionname, player))
|
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_entrance(self, entrance: str, player: int) -> Entrance:
|
2017-05-15 18:28:04 +00:00
|
|
|
try:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self._entrance_cache[(entrance, player)]
|
2017-05-15 18:28:04 +00:00
|
|
|
except KeyError:
|
|
|
|
for region in self.regions:
|
|
|
|
for exit in region.exits:
|
2019-04-18 09:23:24 +00:00
|
|
|
if exit.name == entrance and exit.player == player:
|
|
|
|
self._entrance_cache[(entrance, player)] = exit
|
2017-05-15 18:28:04 +00:00
|
|
|
return exit
|
2019-04-18 09:23:24 +00:00
|
|
|
raise RuntimeError('No such entrance %s for player %d' % (entrance, player))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 18:54:18 +00:00
|
|
|
def _debug_get_entrance(self, entrance: str, player: int) -> Entrance:
|
|
|
|
if type(entrance) != str:
|
|
|
|
raise TypeError(f"expected str, got {type(entrance)} instead")
|
|
|
|
try:
|
|
|
|
return self._entrance_cache[(entrance, player)]
|
|
|
|
except KeyError:
|
|
|
|
for region in self.regions:
|
|
|
|
for exit in region.exits:
|
|
|
|
if exit.name == entrance and exit.player == player:
|
|
|
|
self._entrance_cache[(entrance, player)] = exit
|
|
|
|
return exit
|
|
|
|
raise RuntimeError('No such entrance %s for player %d' % (entrance, player))
|
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_location(self, location: str, player: int) -> Location:
|
2017-05-15 18:28:04 +00:00
|
|
|
try:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self._location_cache[(location, player)]
|
2017-05-15 18:28:04 +00:00
|
|
|
except KeyError:
|
|
|
|
for region in self.regions:
|
|
|
|
for r_location in region.locations:
|
2019-04-18 09:23:24 +00:00
|
|
|
if r_location.name == location and r_location.player == player:
|
|
|
|
self._location_cache[(location, player)] = r_location
|
2017-05-15 18:28:04 +00:00
|
|
|
return r_location
|
2019-04-18 09:23:24 +00:00
|
|
|
raise RuntimeError('No such location %s for player %d' % (location, player))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 18:54:18 +00:00
|
|
|
def _debug_get_location(self, location: str, player: int) -> Location:
|
|
|
|
if type(location) != str:
|
|
|
|
raise TypeError(f"expected str, got {type(location)} instead")
|
|
|
|
try:
|
|
|
|
return self._location_cache[(location, player)]
|
|
|
|
except KeyError:
|
|
|
|
for region in self.regions:
|
|
|
|
for r_location in region.locations:
|
|
|
|
if r_location.name == location and r_location.player == player:
|
|
|
|
self._location_cache[(location, player)] = r_location
|
|
|
|
return r_location
|
|
|
|
raise RuntimeError('No such location %s for player %d' % (location, player))
|
|
|
|
|
2020-03-02 23:17:36 +00:00
|
|
|
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
2018-09-26 17:12:20 +00:00
|
|
|
for dungeon in self.dungeons:
|
2019-04-18 09:23:24 +00:00
|
|
|
if dungeon.name == dungeonname and dungeon.player == player:
|
2018-09-26 17:12:20 +00:00
|
|
|
return dungeon
|
2020-04-10 18:54:18 +00:00
|
|
|
raise RuntimeError('No such dungeon %s for player %d' % (dungeonname, player))
|
|
|
|
|
|
|
|
def _debug_get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
|
|
|
if type(dungeonname) != str:
|
|
|
|
raise TypeError(f"expected str, got {type(dungeonname)} instead")
|
|
|
|
for dungeon in self.dungeons:
|
|
|
|
if dungeon.name == dungeonname and dungeon.player == player:
|
|
|
|
return dungeon
|
2019-04-18 09:23:24 +00:00
|
|
|
raise RuntimeError('No such dungeon %s for player %d' % (dungeonname, player))
|
2018-09-26 17:12:20 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_all_state(self, keys=False) -> CollectionState:
|
2017-06-17 12:40:37 +00:00
|
|
|
ret = CollectionState(self)
|
2017-11-04 18:23:57 +00:00
|
|
|
|
2017-06-17 12:40:37 +00:00
|
|
|
def soft_collect(item):
|
|
|
|
if item.name.startswith('Progressive '):
|
|
|
|
if 'Sword' in item.name:
|
2019-04-18 09:23:24 +00:00
|
|
|
if ret.has('Golden Sword', item.player):
|
2017-06-17 12:40:37 +00:00
|
|
|
pass
|
2020-03-02 23:12:14 +00:00
|
|
|
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[
|
|
|
|
item.player].progressive_sword_limit >= 4:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Golden Sword', item.player] += 1
|
2020-03-02 23:12:14 +00:00
|
|
|
elif ret.has('Master Sword', item.player) and self.difficulty_requirements[
|
|
|
|
item.player].progressive_sword_limit >= 3:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Tempered Sword', item.player] += 1
|
2019-12-16 16:46:21 +00:00
|
|
|
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Master Sword', item.player] += 1
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Fighter Sword', item.player] += 1
|
2017-06-17 12:40:37 +00:00
|
|
|
elif 'Glove' in item.name:
|
2019-04-18 09:23:24 +00:00
|
|
|
if ret.has('Titans Mitts', item.player):
|
2017-06-17 12:40:37 +00:00
|
|
|
pass
|
2019-04-18 09:23:24 +00:00
|
|
|
elif ret.has('Power Glove', item.player):
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Titans Mitts', item.player] += 1
|
2017-06-17 12:40:37 +00:00
|
|
|
else:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Power Glove', item.player] += 1
|
2017-12-16 20:38:48 +00:00
|
|
|
elif 'Shield' in item.name:
|
2019-04-18 09:23:24 +00:00
|
|
|
if ret.has('Mirror Shield', item.player):
|
2017-12-16 20:38:48 +00:00
|
|
|
pass
|
2019-12-16 16:46:21 +00:00
|
|
|
elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Mirror Shield', item.player] += 1
|
2019-12-16 16:46:21 +00:00
|
|
|
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Red Shield', item.player] += 1
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Blue Shield', item.player] += 1
|
2019-08-04 16:32:35 +00:00
|
|
|
elif 'Bow' in item.name:
|
2020-06-30 07:51:11 +00:00
|
|
|
if ret.has('Silver', item.player):
|
2019-08-04 16:32:35 +00:00
|
|
|
pass
|
2019-12-16 16:46:21 +00:00
|
|
|
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
2020-06-30 07:51:11 +00:00
|
|
|
ret.prog_items['Silver Bow', item.player] += 1
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items['Bow', item.player] += 1
|
2018-01-04 06:06:22 +00:00
|
|
|
elif item.name.startswith('Bottle'):
|
2019-12-16 16:46:21 +00:00
|
|
|
if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items[item.name, item.player] += 1
|
2019-12-13 21:37:52 +00:00
|
|
|
elif item.advancement or item.smallkey or item.bigkey:
|
2020-03-07 22:35:55 +00:00
|
|
|
ret.prog_items[item.name, item.player] += 1
|
2017-06-17 12:40:37 +00:00
|
|
|
|
|
|
|
for item in self.itempool:
|
|
|
|
soft_collect(item)
|
2019-04-18 09:23:24 +00:00
|
|
|
|
2017-08-05 15:52:18 +00:00
|
|
|
if keys:
|
2019-04-18 09:23:24 +00:00
|
|
|
for p in range(1, self.players + 1):
|
|
|
|
from Items import ItemFactory
|
2020-06-24 14:22:49 +00:00
|
|
|
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):
|
2019-04-18 09:23:24 +00:00
|
|
|
soft_collect(item)
|
2017-07-17 21:14:31 +00:00
|
|
|
ret.sweep_for_events()
|
2017-06-17 12:40:37 +00:00
|
|
|
return ret
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_items(self) -> list:
|
2018-01-03 01:01:16 +00:00
|
|
|
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def find_items(self, item, player: int) -> list:
|
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
location.item is not None and location.item.name == item and location.item.player == player]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def push_precollected(self, item: Item):
|
2020-01-14 09:42:27 +00:00
|
|
|
item.world = self
|
2020-01-09 07:31:49 +00:00
|
|
|
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
|
|
|
item.advancement = True
|
2019-08-10 19:30:14 +00:00
|
|
|
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):
|
2017-05-15 18:28:04 +00:00
|
|
|
if not isinstance(location, Location):
|
2020-08-13 22:34:41 +00:00
|
|
|
raise RuntimeError(
|
|
|
|
'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2018-01-02 05:39:53 +00:00
|
|
|
if location.can_fill(self.state, item, False):
|
2017-05-15 18:28:04 +00:00
|
|
|
location.item = item
|
|
|
|
item.location = location
|
2020-01-14 09:42:27 +00:00
|
|
|
item.world = self
|
2017-05-15 18:28:04 +00:00
|
|
|
if collect:
|
2018-01-01 20:55:13 +00:00
|
|
|
self.state.collect(item, location.event, location)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-08-13 22:34:41 +00:00
|
|
|
logging.debug('Placed %s at %s', item, location)
|
2017-05-15 18:28:04 +00:00
|
|
|
else:
|
|
|
|
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_entrances(self) -> list:
|
2019-01-20 07:01:02 +00:00
|
|
|
if self._cached_entrances is None:
|
|
|
|
self._cached_entrances = []
|
|
|
|
for region in self.regions:
|
|
|
|
self._cached_entrances.extend(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:
|
2017-05-15 18:28:04 +00:00
|
|
|
if self._cached_locations is None:
|
|
|
|
self._cached_locations = []
|
|
|
|
for region in self.regions:
|
|
|
|
self._cached_locations.extend(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:
|
2020-06-03 00:19:16 +00:00
|
|
|
if player is not None:
|
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
location.player == player and not location.item]
|
|
|
|
return [location for location in self.get_locations() if not location.item]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_filled_locations(self, player=None) -> list:
|
2020-08-13 22:34:41 +00:00
|
|
|
if player is not None:
|
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
location.player == player and location.item is not None]
|
|
|
|
return [location for location in self.get_locations() if location.item is not None]
|
2017-06-17 12:40:37 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_reachable_locations(self, state=None, player=None) -> list:
|
2017-05-15 18:28:04 +00:00
|
|
|
if state is None:
|
|
|
|
state = self.state
|
2020-03-02 23:12:14 +00:00
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
(player is None or location.player == player) and location.can_reach(state)]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def get_placeable_locations(self, state=None, player=None) -> list:
|
2017-05-15 18:28:04 +00:00
|
|
|
if state is None:
|
|
|
|
state = self.state
|
2020-03-02 23:12:14 +00:00
|
|
|
return [location for location in self.get_locations() if
|
|
|
|
(player is None or location.player == player) and location.item is None and location.can_reach(state)]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def unlocks_new_location(self, item) -> bool:
|
2017-05-15 18:28:04 +00:00
|
|
|
temp_state = self.state.copy()
|
2017-06-17 12:40:37 +00:00
|
|
|
temp_state.collect(item, True)
|
2017-05-26 07:55:49 +00:00
|
|
|
|
|
|
|
for location in self.get_unfilled_locations():
|
|
|
|
if temp_state.can_reach(location) and not self.state.can_reach(location):
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
2017-11-19 01:43:37 +00:00
|
|
|
|
2020-03-07 22:20:11 +00:00
|
|
|
def has_beaten_game(self, state, player: Union[None, int] = None):
|
2019-07-10 02:18:24 +00:00
|
|
|
if player:
|
2020-07-12 10:19:45 +00:00
|
|
|
return state.has('Triforce', player) or state.world.logic[player] == 'nologic'
|
2019-07-10 02:18:24 +00:00
|
|
|
else:
|
|
|
|
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2017-06-23 20:15:29 +00:00
|
|
|
def can_beat_game(self, starting_state=None):
|
|
|
|
if starting_state:
|
2020-03-07 22:20:11 +00:00
|
|
|
if self.has_beaten_game(starting_state):
|
|
|
|
return True
|
2017-06-23 20:15:29 +00:00
|
|
|
state = starting_state.copy()
|
|
|
|
else:
|
2020-03-07 22:20:11 +00:00
|
|
|
if self.has_beaten_game(self.state):
|
|
|
|
return True
|
2017-06-23 20:15:29 +00:00
|
|
|
state = CollectionState(self)
|
2020-03-07 22:20:11 +00:00
|
|
|
prog_locations = {location for location in self.get_locations() if location.item is not None and (
|
|
|
|
location.item.advancement or location.event) and location not in state.locations_checked}
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
while prog_locations:
|
|
|
|
sphere = []
|
|
|
|
# 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:
|
2019-07-11 04:12:09 +00:00
|
|
|
if location.can_reach(state):
|
2017-05-16 19:23:47 +00:00
|
|
|
sphere.append(location)
|
|
|
|
|
|
|
|
if not sphere:
|
2019-07-11 04:12:09 +00:00
|
|
|
# ran out of places and did not finish yet, quit
|
2017-05-16 19:23:47 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
for location in sphere:
|
|
|
|
prog_locations.remove(location)
|
2018-01-01 20:55:13 +00:00
|
|
|
state.collect(location.item, True, location)
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2019-07-11 04:12:09 +00:00
|
|
|
if self.has_beaten_game(state):
|
|
|
|
return True
|
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
return False
|
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
class CollectionState(object):
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def __init__(self, parent: World):
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items = Counter()
|
2017-05-15 18:28:04 +00:00
|
|
|
self.world = parent
|
2019-07-11 04:18:30 +00:00
|
|
|
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
2020-05-10 09:27:13 +00:00
|
|
|
self.blocked_connections = {player: set() for player in range(1, parent.players + 1)}
|
2020-08-22 17:19:29 +00:00
|
|
|
self.events = set()
|
2018-01-01 20:55:13 +00:00
|
|
|
self.path = {}
|
|
|
|
self.locations_checked = set()
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale = {player: True for player in range(1, parent.players + 1)}
|
2019-08-10 19:30:14 +00:00
|
|
|
for item in parent.precollected_items:
|
|
|
|
self.collect(item, True)
|
2018-01-01 20:55:13 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def update_reachable_regions(self, player: int):
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale[player] = False
|
|
|
|
rrp = self.reachable_regions[player]
|
2020-05-10 09:27:13 +00:00
|
|
|
bc = self.blocked_connections[player]
|
|
|
|
queue = deque(self.blocked_connections[player])
|
|
|
|
start = self.world.get_region('Menu', player)
|
|
|
|
|
|
|
|
# init on first call - this can't be done on construction since the regions don't exist yet
|
|
|
|
if not start in rrp:
|
|
|
|
rrp.add(start)
|
|
|
|
bc.update(start.exits)
|
|
|
|
queue.extend(start.exits)
|
|
|
|
|
|
|
|
# run BFS on all connections, and keep track of those blocked by missing items
|
2020-06-30 05:32:05 +00:00
|
|
|
while queue:
|
|
|
|
connection = queue.popleft()
|
|
|
|
new_region = connection.connected_region
|
|
|
|
if new_region in rrp:
|
|
|
|
bc.remove(connection)
|
|
|
|
elif connection.can_reach(self):
|
|
|
|
rrp.add(new_region)
|
|
|
|
bc.remove(connection)
|
|
|
|
bc.update(new_region.exits)
|
|
|
|
queue.extend(new_region.exits)
|
|
|
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
|
|
|
|
|
|
|
# Retry connections if the new region can unblock them
|
|
|
|
if new_region.name in indirect_connections:
|
|
|
|
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
|
|
|
if new_entrance in bc and new_entrance not in queue:
|
|
|
|
queue.append(new_entrance)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def copy(self) -> CollectionState:
|
2017-06-17 12:40:37 +00:00
|
|
|
ret = CollectionState(self.world)
|
2019-07-13 22:17:16 +00:00
|
|
|
ret.prog_items = self.prog_items.copy()
|
2020-03-02 23:12:14 +00:00
|
|
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
|
|
|
|
range(1, self.world.players + 1)}
|
2020-05-10 09:27:13 +00:00
|
|
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
2017-06-17 12:40:37 +00:00
|
|
|
ret.events = copy.copy(self.events)
|
2018-01-01 20:55:13 +00:00
|
|
|
ret.path = copy.copy(self.path)
|
|
|
|
ret.locations_checked = copy.copy(self.locations_checked)
|
2017-05-15 18:28:04 +00:00
|
|
|
return ret
|
|
|
|
|
2020-06-30 05:32:05 +00:00
|
|
|
def can_reach(self, spot, resolution_hint=None, player=None) -> bool:
|
2020-03-08 04:41:56 +00:00
|
|
|
if not hasattr(spot, "spot_type"):
|
2017-05-15 18:28:04 +00:00
|
|
|
# try to resolve a name
|
|
|
|
if resolution_hint == 'Location':
|
2019-04-18 09:23:24 +00:00
|
|
|
spot = self.world.get_location(spot, player)
|
2017-05-15 18:28:04 +00:00
|
|
|
elif resolution_hint == 'Entrance':
|
2019-04-18 09:23:24 +00:00
|
|
|
spot = self.world.get_entrance(spot, player)
|
2017-05-15 18:28:04 +00:00
|
|
|
else:
|
|
|
|
# default to Region
|
2019-04-18 09:23:24 +00:00
|
|
|
spot = self.world.get_region(spot, player)
|
2019-07-09 02:48:16 +00:00
|
|
|
return spot.can_reach(self)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-06-30 05:32:05 +00:00
|
|
|
def sweep_for_events(self, key_only: bool = False, locations=None):
|
2019-12-13 21:37:52 +00:00
|
|
|
if locations is None:
|
|
|
|
locations = self.world.get_filled_locations()
|
2017-06-17 12:40:37 +00:00
|
|
|
new_locations = True
|
|
|
|
while new_locations:
|
2020-08-22 17:19:29 +00:00
|
|
|
reachable_events = {location for location in locations if location.event and
|
2020-06-30 05:32:05 +00:00
|
|
|
(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))
|
2020-08-22 17:19:29 +00:00
|
|
|
and location.can_reach(self)}
|
|
|
|
new_locations = reachable_events - self.events
|
|
|
|
for event in new_locations:
|
|
|
|
self.events.add(event)
|
|
|
|
self.collect(event.item, True, event)
|
2019-07-13 22:17:16 +00:00
|
|
|
|
2020-06-30 05:32:05 +00:00
|
|
|
def has(self, item, player: int, count: int = 1):
|
2020-03-07 22:35:55 +00:00
|
|
|
return self.prog_items[item, player] >= count
|
2017-11-19 01:43:37 +00:00
|
|
|
|
2020-06-30 05:32:05 +00:00
|
|
|
def has_key(self, item, player, count: int = 1):
|
2020-07-16 08:59:23 +00:00
|
|
|
if self.world.logic[player] == 'nologic':
|
|
|
|
return True
|
2020-08-20 18:13:00 +00:00
|
|
|
if self.world.keyshuffle[player] == "universal":
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.can_buy_unlimited('Small Key (Universal)', player)
|
2020-03-07 22:35:55 +00:00
|
|
|
return self.prog_items[item, player] >= count
|
2018-03-15 21:23:02 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_buy_unlimited(self, item: str, player: int) -> bool:
|
2020-08-20 18:13:00 +00:00
|
|
|
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
|
|
|
|
shop in self.world.shops)
|
2018-02-17 23:38:54 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def item_count(self, item, player: int) -> int:
|
2020-03-07 22:35:55 +00:00
|
|
|
return self.prog_items[item, player]
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-06-26 14:18:53 +00:00
|
|
|
def has_triforce_pieces(self, count: int, player: int) -> bool:
|
|
|
|
return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_crystals(self, count: int, player: int) -> bool:
|
2020-08-13 22:34:41 +00:00
|
|
|
found: int = 0
|
2020-08-22 17:19:29 +00:00
|
|
|
for crystalnumber in range(1, 8):
|
|
|
|
found += self.prog_items[f"Crystal {crystalnumber}", player]
|
|
|
|
if found >= count:
|
|
|
|
return True
|
2020-08-13 22:34:41 +00:00
|
|
|
return False
|
2019-07-25 22:25:14 +00:00
|
|
|
|
2020-08-13 22:34:41 +00:00
|
|
|
def can_lift_rocks(self, player: int):
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_bottle(self, player: int) -> bool:
|
2020-08-13 22:34:41 +00:00
|
|
|
return self.has_bottles(1, player)
|
2018-01-04 06:06:22 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def bottle_count(self, player: int) -> int:
|
2020-08-22 17:19:29 +00:00
|
|
|
found: int = 0
|
|
|
|
for bottlename in item_name_groups["Bottles"]:
|
|
|
|
found += self.prog_items[bottlename, player]
|
|
|
|
return found
|
2020-08-13 22:34:41 +00:00
|
|
|
|
2020-08-22 17:19:29 +00:00
|
|
|
def has_bottles(self, bottles: int, player: int) -> bool:
|
2020-08-13 22:34:41 +00:00
|
|
|
"""Version of bottle_count that allows fast abort"""
|
|
|
|
found: int = 0
|
2020-08-22 17:19:29 +00:00
|
|
|
for bottlename in item_name_groups["Bottles"]:
|
|
|
|
found += self.prog_items[bottlename, player]
|
|
|
|
if found >= bottles:
|
|
|
|
return True
|
2020-08-13 22:34:41 +00:00
|
|
|
return False
|
2017-11-12 02:22:44 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_hearts(self, player: int, count: int) -> int:
|
2018-09-16 16:55:49 +00:00
|
|
|
# Warning: This only considers items that are marked as advancement items
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.heart_count(player) >= count
|
2018-01-06 18:39:22 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def heart_count(self, player: int) -> int:
|
2018-09-16 16:55:49 +00:00
|
|
|
# Warning: This only considers items that are marked as advancement items
|
2019-12-16 16:46:21 +00:00
|
|
|
diff = self.world.difficulty_requirements[player]
|
2020-03-02 23:12:14 +00:00
|
|
|
return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
|
|
|
+ self.item_count('Sanctuary Heart Container', player) \
|
|
|
|
+ min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
|
|
|
+ 3 # starting hearts
|
2018-01-06 18:39:22 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_lift_heavy_rocks(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Titans Mitts', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_extend_magic(self, player: int, smallmagic: int = 16,
|
|
|
|
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
2018-01-07 03:07:46 +00:00
|
|
|
basemagic = 8
|
2020-03-13 23:31:28 +00:00
|
|
|
if self.has('Magic Upgrade (1/4)', player):
|
2018-01-07 03:07:46 +00:00
|
|
|
basemagic = 32
|
2020-03-13 23:31:28 +00:00
|
|
|
elif self.has('Magic Upgrade (1/2)', player):
|
2018-01-07 03:07:46 +00:00
|
|
|
basemagic = 16
|
2019-04-18 09:23:24 +00:00
|
|
|
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
|
2019-12-16 16:46:21 +00:00
|
|
|
if self.world.difficulty_adjustments[player] == 'hard' and not fullrefill:
|
2019-04-18 09:23:24 +00:00
|
|
|
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.world.difficulty_adjustments[player] == 'expert' and not fullrefill:
|
2019-04-18 09:23:24 +00:00
|
|
|
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
|
2018-09-16 16:55:49 +00:00
|
|
|
else:
|
2019-04-18 09:23:24 +00:00
|
|
|
basemagic = basemagic + basemagic * self.bottle_count(player)
|
2018-02-17 23:38:54 +00:00
|
|
|
return basemagic >= smallmagic
|
2018-01-02 05:39:53 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_kill_most_things(self, player: int, enemies=5) -> bool:
|
2020-04-20 17:17:10 +00:00
|
|
|
return (self.has_melee_weapon(player)
|
2019-04-18 09:23:24 +00:00
|
|
|
or self.has('Cane of Somaria', player)
|
|
|
|
or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player)))
|
|
|
|
or self.can_shoot_arrows(player)
|
|
|
|
or self.has('Fire Rod', player)
|
2020-03-15 10:59:06 +00:00
|
|
|
or (self.has('Bombs (10)', player) and enemies < 6))
|
2018-01-02 05:39:53 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_shoot_arrows(self, player: int) -> bool:
|
2019-12-16 23:16:02 +00:00
|
|
|
if self.world.retro[player]:
|
2020-04-04 07:16:39 +00:00
|
|
|
# TODO: Progressive and Non-Progressive silvers work differently (progressive is not usable until the shop arrow is bought)
|
2020-06-30 07:51:11 +00:00
|
|
|
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy_unlimited('Single Arrow', player)
|
|
|
|
return self.has('Bow', player) or self.has('Silver Bow', player)
|
2018-02-17 23:38:54 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_get_good_bee(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
cave = self.world.get_region('Good Bee Cave', player)
|
2018-09-23 02:51:54 +00:00
|
|
|
return (
|
2020-03-02 23:12:14 +00:00
|
|
|
self.has_bottle(player) and
|
|
|
|
self.has('Bug Catching Net', player) and
|
|
|
|
(self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and
|
|
|
|
cave.can_reach(self) and
|
|
|
|
self.is_not_bunny(cave, player)
|
2018-09-23 02:51:54 +00:00
|
|
|
)
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_sword(self, player: int) -> bool:
|
2020-08-20 18:13:00 +00:00
|
|
|
return self.has('Fighter Sword', player) \
|
|
|
|
or self.has('Master Sword', player) \
|
|
|
|
or self.has('Tempered Sword', player) \
|
|
|
|
or self.has('Golden Sword', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_beam_sword(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-20 17:17:10 +00:00
|
|
|
def has_melee_weapon(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has_sword(player) or self.has('Hammer', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_Mirror(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Magic Mirror', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_Boots(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Pegasus Boots', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_Pearl(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Moon Pearl', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_fire_source(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has('Fire Rod', player) or self.has('Lamp', player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_flute(self, player: int) -> bool:
|
2019-07-27 13:13:13 +00:00
|
|
|
lw = self.world.get_region('Light World', player)
|
2020-02-17 09:08:03 +00:00
|
|
|
return self.has('Flute', player) and lw.can_reach(self) and self.is_not_bunny(lw, player)
|
2019-07-27 13:13:13 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_melt_things(self, player: int) -> bool:
|
2019-07-27 13:13:13 +00:00
|
|
|
return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player))
|
2020-03-02 23:12:14 +00:00
|
|
|
|
|
|
|
def can_avoid_lasers(self, player: int) -> bool:
|
2019-07-27 13:13:13 +00:00
|
|
|
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def is_not_bunny(self, region: Region, player: int) -> bool:
|
2019-07-27 13:13:13 +00:00
|
|
|
if self.has_Pearl(player):
|
2020-03-02 23:12:14 +00:00
|
|
|
return True
|
|
|
|
|
2019-12-16 15:54:46 +00:00
|
|
|
return region.is_light_world if self.world.mode[player] != 'inverted' else region.is_dark_world
|
2019-07-27 13:13:13 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_reach_light_world(self, player: int) -> bool:
|
2019-09-22 01:59:16 +00:00
|
|
|
if True in [i.is_light_world for i in self.reachable_regions[player]]:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_reach_dark_world(self, player: int) -> bool:
|
2019-09-22 01:59:16 +00:00
|
|
|
if True in [i.is_dark_world for i in self.reachable_regions[player]]:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_misery_mire_medallion(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has(self.world.required_medallions[player][0], player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def has_turtle_rock_medallion(self, player: int) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.has(self.world.required_medallions[player][1], player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-02-10 04:38:55 +00:00
|
|
|
def can_boots_clip_lw(self, player):
|
|
|
|
if self.world.mode[player] == 'inverted':
|
|
|
|
return self.has_Boots(player) and self.has_Pearl(player)
|
|
|
|
return self.has_Boots(player)
|
|
|
|
|
|
|
|
def can_boots_clip_dw(self, player):
|
|
|
|
if self.world.mode[player] != 'inverted':
|
|
|
|
return self.has_Boots(player) and self.has_Pearl(player)
|
|
|
|
return self.has_Boots(player)
|
|
|
|
|
2020-02-11 03:54:35 +00:00
|
|
|
def can_get_glitched_speed_lw(self, player):
|
|
|
|
rules = [self.has_Boots(player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
|
|
|
if self.world.mode[player] == 'inverted':
|
|
|
|
rules.append(self.has_Pearl(player))
|
|
|
|
return all(rules)
|
|
|
|
|
2020-02-12 23:48:36 +00:00
|
|
|
def can_superbunny_mirror_with_sword(self, player):
|
|
|
|
return self.has_Mirror(player) and self.has_sword(player)
|
|
|
|
|
2020-02-11 03:54:35 +00:00
|
|
|
def can_get_glitched_speed_dw(self, player):
|
|
|
|
rules = [self.has_Boots(player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
|
|
|
if self.world.mode[player] != 'inverted':
|
|
|
|
rules.append(self.has_Pearl(player))
|
|
|
|
return all(rules)
|
2020-02-10 04:38:55 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def collect(self, item: Item, event=False, location=None):
|
2018-01-01 20:55:13 +00:00
|
|
|
if location:
|
|
|
|
self.locations_checked.add(location)
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = False
|
2017-05-15 18:28:04 +00:00
|
|
|
if item.name.startswith('Progressive '):
|
|
|
|
if 'Sword' in item.name:
|
2019-04-18 09:23:24 +00:00
|
|
|
if self.has('Golden Sword', item.player):
|
2017-05-26 07:55:49 +00:00
|
|
|
pass
|
2020-03-02 23:12:14 +00:00
|
|
|
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
|
|
|
|
item.player].progressive_sword_limit >= 4:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Golden Sword', item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Tempered Sword', item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Master Sword', item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Fighter Sword', item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2017-05-15 18:28:04 +00:00
|
|
|
elif 'Glove' in item.name:
|
2019-04-18 09:23:24 +00:00
|
|
|
if self.has('Titans Mitts', item.player):
|
2017-05-26 07:55:49 +00:00
|
|
|
pass
|
2019-04-18 09:23:24 +00:00
|
|
|
elif self.has('Power Glove', item.player):
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Titans Mitts', item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2017-05-15 18:28:04 +00:00
|
|
|
else:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Power Glove', item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2017-10-14 18:45:59 +00:00
|
|
|
elif 'Shield' in item.name:
|
2019-04-18 09:23:24 +00:00
|
|
|
if self.has('Mirror Shield', item.player):
|
2017-10-14 18:45:59 +00:00
|
|
|
pass
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Mirror Shield', item.player] += 1
|
2017-10-14 18:45:59 +00:00
|
|
|
changed = True
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Red Shield', item.player] += 1
|
2017-10-14 18:45:59 +00:00
|
|
|
changed = True
|
2019-12-16 16:46:21 +00:00
|
|
|
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Blue Shield', item.player] += 1
|
2017-10-14 18:45:59 +00:00
|
|
|
changed = True
|
2019-08-04 16:32:35 +00:00
|
|
|
elif 'Bow' in item.name:
|
2020-06-30 07:51:11 +00:00
|
|
|
if self.has('Silver Bow', item.player):
|
2019-08-04 16:32:35 +00:00
|
|
|
pass
|
|
|
|
elif self.has('Bow', item.player):
|
2020-06-30 07:51:11 +00:00
|
|
|
self.prog_items['Silver Bow', item.player] += 1
|
2019-08-04 16:32:35 +00:00
|
|
|
changed = True
|
|
|
|
else:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items['Bow', item.player] += 1
|
2019-08-04 16:32:35 +00:00
|
|
|
changed = True
|
2018-01-04 06:06:22 +00:00
|
|
|
elif item.name.startswith('Bottle'):
|
2019-12-16 16:46:21 +00:00
|
|
|
if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items[item.name, item.player] += 1
|
2018-01-04 06:06:22 +00:00
|
|
|
changed = True
|
2017-06-17 12:40:37 +00:00
|
|
|
elif event or item.advancement:
|
2020-03-07 22:35:55 +00:00
|
|
|
self.prog_items[item.name, item.player] += 1
|
2017-05-26 07:55:49 +00:00
|
|
|
changed = True
|
2020-03-02 23:12:14 +00:00
|
|
|
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale[item.player] = True
|
2017-05-26 07:55:49 +00:00
|
|
|
|
|
|
|
if changed:
|
2017-06-17 12:40:37 +00:00
|
|
|
if not event:
|
|
|
|
self.sweep_for_events()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2017-05-16 19:23:47 +00:00
|
|
|
def remove(self, item):
|
|
|
|
if item.advancement:
|
|
|
|
to_remove = item.name
|
|
|
|
if to_remove.startswith('Progressive '):
|
|
|
|
if 'Sword' in to_remove:
|
2019-04-18 09:23:24 +00:00
|
|
|
if self.has('Golden Sword', item.player):
|
2017-05-16 19:23:47 +00:00
|
|
|
to_remove = 'Golden Sword'
|
2019-04-18 09:23:24 +00:00
|
|
|
elif self.has('Tempered Sword', item.player):
|
2017-05-16 19:23:47 +00:00
|
|
|
to_remove = 'Tempered Sword'
|
2019-04-18 09:23:24 +00:00
|
|
|
elif self.has('Master Sword', item.player):
|
2017-05-16 19:23:47 +00:00
|
|
|
to_remove = 'Master Sword'
|
2019-04-18 09:23:24 +00:00
|
|
|
elif self.has('Fighter Sword', item.player):
|
2017-05-16 19:23:47 +00:00
|
|
|
to_remove = 'Fighter Sword'
|
|
|
|
else:
|
|
|
|
to_remove = None
|
|
|
|
elif 'Glove' in item.name:
|
2019-04-18 09:23:24 +00:00
|
|
|
if self.has('Titans Mitts', item.player):
|
2017-05-16 19:23:47 +00:00
|
|
|
to_remove = 'Titans Mitts'
|
2019-04-18 09:23:24 +00:00
|
|
|
elif self.has('Power Glove', item.player):
|
2017-05-16 19:23:47 +00:00
|
|
|
to_remove = 'Power Glove'
|
|
|
|
else:
|
|
|
|
to_remove = None
|
2019-08-04 16:32:35 +00:00
|
|
|
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:
|
2020-06-30 07:51:11 +00:00
|
|
|
if self.has('Silver Bow', item.player):
|
|
|
|
to_remove = 'Silver Bow'
|
2019-08-04 16:32:35 +00:00
|
|
|
elif self.has('Bow', item.player):
|
|
|
|
to_remove = 'Bow'
|
|
|
|
else:
|
|
|
|
to_remove = None
|
2017-05-16 19:23:47 +00:00
|
|
|
|
|
|
|
if to_remove is not None:
|
|
|
|
|
2020-03-07 22:35:55 +00:00
|
|
|
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])
|
2017-05-16 19:23:47 +00:00
|
|
|
# invalidate caches, nothing can be trusted anymore now
|
2019-07-11 04:18:30 +00:00
|
|
|
self.reachable_regions[item.player] = set()
|
2020-05-10 09:27:13 +00:00
|
|
|
self.blocked_connections[item.player] = set()
|
2019-07-11 04:18:30 +00:00
|
|
|
self.stale[item.player] = True
|
2017-05-16 19:23:47 +00:00
|
|
|
|
2018-01-19 02:51:43 +00:00
|
|
|
@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)
|
|
|
|
|
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
class Region(object):
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def __init__(self, name: str, type, hint, player: int):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
2018-01-19 02:51:43 +00:00
|
|
|
self.type = type
|
2017-05-15 18:28:04 +00:00
|
|
|
self.entrances = []
|
|
|
|
self.exits = []
|
|
|
|
self.locations = []
|
2017-10-15 16:16:07 +00:00
|
|
|
self.dungeon = None
|
2018-02-17 23:38:54 +00:00
|
|
|
self.shop = None
|
2017-10-28 22:34:37 +00:00
|
|
|
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'
|
2019-01-20 07:01:02 +00:00
|
|
|
self.hint_text = hint
|
2017-05-26 07:55:49 +00:00
|
|
|
self.recursion_count = 0
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2017-05-15 18:28:04 +00:00
|
|
|
|
|
|
|
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]
|
2019-07-09 02:48:16 +00:00
|
|
|
|
2020-03-07 22:20:11 +00:00
|
|
|
def can_reach_private(self, state: CollectionState):
|
2017-05-15 18:28:04 +00:00
|
|
|
for entrance in self.entrances:
|
2019-07-11 04:18:30 +00:00
|
|
|
if entrance.can_reach(state):
|
2018-01-01 20:55:13 +00:00
|
|
|
if not self in state.path:
|
|
|
|
state.path[self] = (self.name, state.path.get(entrance, None))
|
2017-05-15 18:28:04 +00:00
|
|
|
return True
|
|
|
|
return False
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-03-07 22:20:11 +00:00
|
|
|
def can_fill(self, item: Item):
|
2019-12-16 20:46:47 +00:00
|
|
|
inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player])
|
|
|
|
or (item.bigkey and not self.world.bigkeyshuffle[item.player])
|
|
|
|
or (item.map and not self.world.mapshuffle[item.player])
|
|
|
|
or (item.compass and not self.world.compassshuffle[item.player]))
|
2020-06-24 14:22:49 +00:00
|
|
|
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)'
|
2019-12-13 21:37:52 +00:00
|
|
|
if sewer_hack or inside_dungeon_item:
|
2019-07-10 02:18:24 +00:00
|
|
|
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2017-10-15 16:16:07 +00:00
|
|
|
return True
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Entrance(object):
|
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def __init__(self, player: int, name: str = '', parent=None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
|
|
|
self.parent_region = parent
|
|
|
|
self.connected_region = None
|
2017-05-20 12:03:15 +00:00
|
|
|
self.target = None
|
2017-06-03 13:33:11 +00:00
|
|
|
self.addresses = None
|
2017-05-20 12:03:15 +00:00
|
|
|
self.spot_type = 'Entrance'
|
2017-05-26 07:55:49 +00:00
|
|
|
self.recursion_count = 0
|
2017-06-17 11:16:13 +00:00
|
|
|
self.vanilla = None
|
2017-12-17 05:25:46 +00:00
|
|
|
self.access_rule = lambda state: True
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2020-05-10 09:27:13 +00:00
|
|
|
self.hide_path = False
|
2017-05-15 18:28:04 +00:00
|
|
|
|
|
|
|
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):
|
2020-05-10 09:27:13 +00:00
|
|
|
if not self.hide_path and not self in state.path:
|
2018-01-01 20:55:13 +00:00
|
|
|
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
|
2017-05-20 12:03:15 +00:00
|
|
|
return True
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2017-05-20 12:03:15 +00:00
|
|
|
return False
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2017-06-17 11:16:13 +00:00
|
|
|
def connect(self, region, addresses=None, target=None, vanilla=None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.connected_region = region
|
2017-05-20 12:03:15 +00:00
|
|
|
self.target = target
|
2017-06-03 13:33:11 +00:00
|
|
|
self.addresses = addresses
|
2017-06-17 11:16:13 +00:00
|
|
|
self.vanilla = vanilla
|
2017-05-15 18:28:04 +00:00
|
|
|
region.entrances.append(self)
|
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
world = self.parent_region.world if self.parent_region else None
|
|
|
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2017-10-15 16:16:07 +00:00
|
|
|
class Dungeon(object):
|
|
|
|
|
2020-06-04 01:30:59 +00:00
|
|
|
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
|
2017-10-15 16:16:07 +00:00
|
|
|
self.name = name
|
|
|
|
self.regions = regions
|
|
|
|
self.big_key = big_key
|
|
|
|
self.small_keys = small_keys
|
|
|
|
self.dungeon_items = dungeon_items
|
2018-09-26 17:12:20 +00:00
|
|
|
self.bosses = dict()
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2019-07-13 22:11:43 +00:00
|
|
|
self.world = None
|
2018-09-26 17:12:20 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def boss(self):
|
|
|
|
return self.bosses.get(None, None)
|
|
|
|
|
|
|
|
@boss.setter
|
|
|
|
def boss(self, value):
|
|
|
|
self.bosses[None] = value
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2017-10-15 16:16:07 +00:00
|
|
|
@property
|
|
|
|
def keys(self):
|
|
|
|
return self.small_keys + ([self.big_key] if self.big_key else [])
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2017-10-15 16:16:07 +00:00
|
|
|
@property
|
|
|
|
def all_items(self):
|
2017-10-28 22:34:37 +00:00
|
|
|
return self.dungeon_items + self.keys
|
|
|
|
|
2020-06-04 01:30:59 +00:00
|
|
|
def is_dungeon_item(self, item: Item) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items]
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-06-04 01:30:59 +00:00
|
|
|
def __eq__(self, other: Item) -> bool:
|
|
|
|
return self.name == other.name and self.player == other.player
|
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-10-15 16:16:07 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2018-09-26 17:12:20 +00:00
|
|
|
class Boss(object):
|
2020-03-02 23:12:14 +00:00
|
|
|
def __init__(self, name, enemizer_name, defeat_rule, player: int):
|
2018-09-26 17:12:20 +00:00
|
|
|
self.name = name
|
|
|
|
self.enemizer_name = enemizer_name
|
|
|
|
self.defeat_rule = defeat_rule
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-03-02 23:12:14 +00:00
|
|
|
def can_defeat(self, state) -> bool:
|
2019-04-18 09:23:24 +00:00
|
|
|
return self.defeat_rule(state, self.player)
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2018-09-26 17:12:20 +00:00
|
|
|
class Location(object):
|
2020-08-16 14:49:48 +00:00
|
|
|
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
|
|
|
|
hint_text: Optional[str] = None, parent=None,
|
2020-03-06 22:08:46 +00:00
|
|
|
player_address=None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
|
|
|
self.parent_region = parent
|
|
|
|
self.item = None
|
2017-05-25 15:47:15 +00:00
|
|
|
self.crystal = crystal
|
|
|
|
self.address = address
|
2019-12-09 18:27:56 +00:00
|
|
|
self.player_address = player_address
|
2017-05-20 12:03:15 +00:00
|
|
|
self.spot_type = 'Location'
|
2020-03-06 22:08:46 +00:00
|
|
|
self.hint_text: str = hint_text if hint_text else name
|
2017-05-26 07:55:49 +00:00
|
|
|
self.recursion_count = 0
|
2017-06-03 19:28:02 +00:00
|
|
|
self.staleness_count = 0
|
2017-06-17 12:40:37 +00:00
|
|
|
self.event = False
|
2019-12-10 18:21:05 +00:00
|
|
|
self.locked = False
|
2018-01-02 05:39:53 +00:00
|
|
|
self.always_allow = lambda item, state: False
|
2017-12-17 05:25:46 +00:00
|
|
|
self.access_rule = lambda state: True
|
2018-01-02 05:39:53 +00:00
|
|
|
self.item_rule = lambda item: True
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2017-10-28 22:34:37 +00:00
|
|
|
|
2020-07-09 14:16:31 +00:00
|
|
|
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
2020-07-16 08:59:23 +00:00
|
|
|
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)))
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-07-09 14:16:31 +00:00
|
|
|
def can_reach(self, state: CollectionState) -> bool:
|
2020-08-21 16:35:48 +00:00
|
|
|
# self.access_rule computes faster on average, so placing it first for faster abort
|
|
|
|
if self.access_rule(state) and self.parent_region.can_reach(state):
|
2017-05-20 12:03:15 +00:00
|
|
|
return True
|
|
|
|
return False
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
|
|
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-08-22 17:19:29 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash((self.name, self.player))
|
|
|
|
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2017-10-28 22:34:37 +00:00
|
|
|
class Item(object):
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
def __init__(self, name='', advancement=False, priority=False, type=None, code=None, pedestal_hint=None, pedestal_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None, hint_text=None, player=None):
|
2017-05-15 18:28:04 +00:00
|
|
|
self.name = name
|
|
|
|
self.advancement = advancement
|
2017-06-04 12:43:13 +00:00
|
|
|
self.priority = priority
|
2017-10-15 16:16:07 +00:00
|
|
|
self.type = type
|
2017-10-29 03:42:35 +00:00
|
|
|
self.pedestal_hint_text = pedestal_hint
|
|
|
|
self.pedestal_credit_text = pedestal_credit
|
2017-05-25 13:58:35 +00:00
|
|
|
self.sickkid_credit_text = sickkid_credit
|
|
|
|
self.zora_credit_text = zora_credit
|
|
|
|
self.magicshop_credit_text = witch_credit
|
|
|
|
self.fluteboy_credit_text = fluteboy_credit
|
2019-04-09 01:11:33 +00:00
|
|
|
self.hint_text = hint_text
|
2017-05-20 12:03:15 +00:00
|
|
|
self.code = code
|
2017-05-25 13:58:35 +00:00
|
|
|
self.location = None
|
2020-01-14 09:42:27 +00:00
|
|
|
self.world = None
|
2019-04-18 09:23:24 +00:00
|
|
|
self.player = player
|
2017-10-28 22:34:37 +00:00
|
|
|
|
|
|
|
@property
|
2020-03-02 23:12:14 +00:00
|
|
|
def crystal(self) -> bool:
|
2017-10-15 16:16:07 +00:00
|
|
|
return self.type == 'Crystal'
|
2017-10-28 22:34:37 +00:00
|
|
|
|
|
|
|
@property
|
2020-03-02 23:12:14 +00:00
|
|
|
def smallkey(self) -> bool:
|
2019-12-13 21:37:52 +00:00
|
|
|
return self.type == 'SmallKey'
|
2017-10-28 22:34:37 +00:00
|
|
|
|
|
|
|
@property
|
2020-03-02 23:12:14 +00:00
|
|
|
def bigkey(self) -> bool:
|
2019-12-13 21:37:52 +00:00
|
|
|
return self.type == 'BigKey'
|
|
|
|
|
2017-10-28 22:34:37 +00:00
|
|
|
@property
|
2020-03-02 23:12:14 +00:00
|
|
|
def map(self) -> bool:
|
2017-10-15 16:16:07 +00:00
|
|
|
return self.type == 'Map'
|
2017-10-28 22:34:37 +00:00
|
|
|
|
|
|
|
@property
|
2020-03-02 23:12:14 +00:00
|
|
|
def compass(self) -> bool:
|
2017-10-15 16:16:07 +00:00
|
|
|
return self.type == 'Compass'
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
2017-05-15 18:28:04 +00:00
|
|
|
|
2020-04-10 19:31:15 +00:00
|
|
|
def __str__(self):
|
2020-01-14 09:42:27 +00:00
|
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
2017-05-20 12:03:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
# have 6 address that need to be filled
|
|
|
|
class Crystal(Item):
|
|
|
|
pass
|
2017-07-18 10:44:13 +00:00
|
|
|
|
2018-02-17 23:38:54 +00:00
|
|
|
@unique
|
|
|
|
class ShopType(Enum):
|
|
|
|
Shop = 0
|
|
|
|
TakeAny = 1
|
2018-09-23 02:51:54 +00:00
|
|
|
UpgradeShop = 2
|
2018-02-17 23:38:54 +00:00
|
|
|
|
|
|
|
class Shop(object):
|
2020-08-23 13:03:06 +00:00
|
|
|
slots = 3
|
2020-01-10 10:41:22 +00:00
|
|
|
def __init__(self, region, room_id, type, shopkeeper_config, custom, locked):
|
2018-02-17 23:38:54 +00:00
|
|
|
self.region = region
|
|
|
|
self.room_id = room_id
|
|
|
|
self.type = type
|
|
|
|
self.inventory = [None, None, None]
|
|
|
|
self.shopkeeper_config = shopkeeper_config
|
2020-01-10 10:41:22 +00:00
|
|
|
self.custom = custom
|
|
|
|
self.locked = locked
|
2018-02-17 23:38:54 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def item_count(self):
|
|
|
|
return (3 if self.inventory[2] else
|
|
|
|
2 if self.inventory[1] else
|
|
|
|
1 if self.inventory[0] else
|
|
|
|
0)
|
|
|
|
|
|
|
|
def get_bytes(self):
|
|
|
|
# [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
|
|
|
|
entrances = self.region.entrances
|
|
|
|
config = self.item_count
|
2020-01-02 21:27:22 +00:00
|
|
|
if len(entrances) == 1 and entrances[0].name in door_addresses:
|
|
|
|
door_id = door_addresses[entrances[0].name][0]+1
|
2018-02-17 23:38:54 +00:00
|
|
|
else:
|
|
|
|
door_id = 0
|
|
|
|
config |= 0x40 # ignore door id
|
|
|
|
if self.type == ShopType.TakeAny:
|
|
|
|
config |= 0x80
|
2018-09-23 02:51:54 +00:00
|
|
|
if self.type == ShopType.UpgradeShop:
|
|
|
|
config |= 0x10 # Alt. VRAM
|
2018-02-17 23:38:54 +00:00
|
|
|
return [0x00]+int16_as_bytes(self.room_id)+[door_id, 0x00, config, self.shopkeeper_config, 0x00]
|
|
|
|
|
|
|
|
def has_unlimited(self, item):
|
|
|
|
for inv in self.inventory:
|
|
|
|
if inv is None:
|
|
|
|
continue
|
|
|
|
if inv['max'] != 0 and inv['replacement'] is not None and inv['replacement'] == item:
|
|
|
|
return True
|
|
|
|
elif inv['item'] is not None and inv['item'] == item:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def clear_inventory(self):
|
|
|
|
self.inventory = [None, None, None]
|
|
|
|
|
|
|
|
def add_inventory(self, slot, item, price, max=0, replacement=None, replacement_price=0, create_location=False):
|
|
|
|
self.inventory[slot] = {
|
|
|
|
'item': item,
|
|
|
|
'price': price,
|
|
|
|
'max': max,
|
|
|
|
'replacement': replacement,
|
|
|
|
'replacement_price': replacement_price,
|
|
|
|
'create_location': create_location
|
|
|
|
}
|
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
|
|
|
|
class Spoiler(object):
|
2020-02-16 14:32:40 +00:00
|
|
|
world: World
|
2017-07-18 10:44:13 +00:00
|
|
|
def __init__(self, world):
|
|
|
|
self.world = world
|
2020-01-14 09:42:27 +00:00
|
|
|
self.hashes = {}
|
2018-03-24 05:50:54 +00:00
|
|
|
self.entrances = OrderedDict()
|
2017-07-18 10:44:13 +00:00
|
|
|
self.medallions = {}
|
|
|
|
self.playthrough = {}
|
2019-12-21 12:33:07 +00:00
|
|
|
self.unreachables = []
|
2020-01-09 07:31:49 +00:00
|
|
|
self.startinventory = []
|
2017-07-18 10:44:13 +00:00
|
|
|
self.locations = {}
|
2018-01-01 20:55:13 +00:00
|
|
|
self.paths = {}
|
2017-07-18 10:44:13 +00:00
|
|
|
self.metadata = {}
|
2018-03-24 05:43:10 +00:00
|
|
|
self.shops = []
|
2018-09-26 17:12:20 +00:00
|
|
|
self.bosses = OrderedDict()
|
2017-07-18 10:44:13 +00:00
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
def set_entrance(self, entrance, exit, direction, player):
|
2019-07-13 22:11:43 +00:00
|
|
|
if self.world.players == 1:
|
|
|
|
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)])
|
2017-07-18 10:44:13 +00:00
|
|
|
|
|
|
|
def parse_data(self):
|
2019-04-18 09:23:24 +00:00
|
|
|
self.medallions = OrderedDict()
|
2019-07-13 22:11:43 +00:00
|
|
|
if self.world.players == 1:
|
|
|
|
self.medallions['Misery Mire'] = self.world.required_medallions[1][0]
|
|
|
|
self.medallions['Turtle Rock'] = self.world.required_medallions[1][1]
|
|
|
|
else:
|
|
|
|
for player in range(1, self.world.players + 1):
|
2020-01-14 09:42:27 +00:00
|
|
|
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
|
|
|
|
2020-01-10 06:02:44 +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]
|
2019-04-18 09:23:24 +00:00
|
|
|
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)
|
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
self.shops = []
|
2018-03-24 05:43:10 +00:00
|
|
|
for shop in self.world.shops:
|
2020-01-10 10:41:22 +00:00
|
|
|
if not shop.custom:
|
2018-03-24 05:43:10 +00:00
|
|
|
continue
|
2019-04-18 09:23:24 +00:00
|
|
|
shopdata = {'location': str(shop.region),
|
2018-03-24 05:43:10 +00:00
|
|
|
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
|
|
|
}
|
|
|
|
for index, item in enumerate(shop.inventory):
|
|
|
|
if item is None:
|
|
|
|
continue
|
2018-03-27 01:39:48 +00:00
|
|
|
shopdata['item_{}'.format(index)] = "{} — {}".format(item['item'], item['price']) if item['price'] else item['item']
|
2018-03-24 05:43:10 +00:00
|
|
|
self.shops.append(shopdata)
|
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
for player in range(1, self.world.players + 1):
|
|
|
|
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':
|
2019-07-27 13:13:13 +00:00
|
|
|
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
|
|
|
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses['middle'].name
|
|
|
|
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses['top'].name
|
|
|
|
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
|
|
|
|
|
2019-04-18 09:23:24 +00:00
|
|
|
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
|
|
|
self.bosses[str(player)]["Ganon"] = "Ganon"
|
2018-09-26 17:12:20 +00:00
|
|
|
|
2019-07-13 22:11:43 +00:00
|
|
|
if self.world.players == 1:
|
|
|
|
self.bosses = self.bosses["1"]
|
2018-03-24 05:43:10 +00:00
|
|
|
|
2020-04-20 12:50:49 +00:00
|
|
|
from Utils import __version__ as ERVersion
|
2017-07-18 10:44:13 +00:00
|
|
|
self.metadata = {'version': ERVersion,
|
|
|
|
'logic': self.world.logic,
|
|
|
|
'mode': self.world.mode,
|
2019-12-16 23:16:02 +00:00
|
|
|
'retro': self.world.retro,
|
2019-08-24 19:35:58 +00:00
|
|
|
'weapons': self.world.swords,
|
2017-07-18 10:44:13 +00:00
|
|
|
'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.difficulty_adjustments,
|
2019-12-16 18:09:15 +00:00
|
|
|
'gt_crystals': self.world.crystals_needed_for_gt,
|
|
|
|
'ganon_crystals': self.world.crystals_needed_for_ganon,
|
|
|
|
'open_pyramid': self.world.open_pyramid,
|
2019-08-04 21:40:13 +00:00
|
|
|
'accessibility': self.world.accessibility,
|
2019-08-24 19:35:58 +00:00
|
|
|
'hints': self.world.hints,
|
2019-12-13 21:37:52 +00:00
|
|
|
'mapshuffle': self.world.mapshuffle,
|
|
|
|
'compassshuffle': self.world.compassshuffle,
|
|
|
|
'keyshuffle': self.world.keyshuffle,
|
|
|
|
'bigkeyshuffle': self.world.bigkeyshuffle,
|
2019-12-17 14:55:53 +00:00
|
|
|
'boss_shuffle': self.world.boss_shuffle,
|
|
|
|
'enemy_shuffle': self.world.enemy_shuffle,
|
|
|
|
'enemy_health': self.world.enemy_health,
|
|
|
|
'enemy_damage': self.world.enemy_damage,
|
2020-08-19 21:24:17 +00:00
|
|
|
'killable_thieves': self.world.killable_thieves,
|
|
|
|
'tile_shuffle': self.world.tile_shuffle,
|
|
|
|
'bush_shuffle': self.world.bush_shuffle,
|
2020-01-18 17:51:10 +00:00
|
|
|
'beemizer': self.world.beemizer,
|
2020-01-22 05:28:58 +00:00
|
|
|
'progressive': self.world.progressive,
|
2020-01-18 17:51:10 +00:00
|
|
|
'shufflepots': self.world.shufflepots,
|
2020-01-14 09:42:27 +00:00
|
|
|
'players': self.world.players,
|
2020-05-20 20:30:21 +00:00
|
|
|
'teams': self.world.teams,
|
2020-06-07 13:22:24 +00:00
|
|
|
'progression_balancing': self.world.progression_balancing,
|
2020-06-17 08:02:54 +00:00
|
|
|
'triforce_pieces_available': self.world.triforce_pieces_available,
|
2020-06-07 13:22:24 +00:00
|
|
|
'triforce_pieces_required': self.world.triforce_pieces_required,
|
2019-08-24 19:35:58 +00:00
|
|
|
}
|
2017-07-18 10:44:13 +00:00
|
|
|
|
|
|
|
def to_json(self):
|
|
|
|
self.parse_data()
|
|
|
|
out = OrderedDict()
|
2018-03-27 01:39:48 +00:00
|
|
|
out['Entrances'] = list(self.entrances.values())
|
2017-07-18 10:44:13 +00:00
|
|
|
out.update(self.locations)
|
2020-01-09 07:31:49 +00:00
|
|
|
out['Starting Inventory'] = self.startinventory
|
2018-03-27 01:39:48 +00:00
|
|
|
out['Special'] = self.medallions
|
2020-01-14 09:42:27 +00:00
|
|
|
if self.hashes:
|
|
|
|
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
|
2018-03-27 01:39:48 +00:00
|
|
|
if self.shops:
|
|
|
|
out['Shops'] = self.shops
|
2017-07-18 10:44:13 +00:00
|
|
|
out['playthrough'] = self.playthrough
|
2018-01-01 20:55:13 +00:00
|
|
|
out['paths'] = self.paths
|
2019-12-17 14:55:53 +00:00
|
|
|
out['Bosses'] = self.bosses
|
2017-07-18 10:44:13 +00:00
|
|
|
out['meta'] = self.metadata
|
2018-09-26 17:12:20 +00:00
|
|
|
|
2017-07-18 10:44:13 +00:00
|
|
|
return json.dumps(out)
|
|
|
|
|
|
|
|
def to_file(self, filename):
|
|
|
|
self.parse_data()
|
2020-08-19 21:24:17 +00:00
|
|
|
|
2020-08-20 18:13:00 +00:00
|
|
|
def bool_to_text(variable: Union[bool, str]) -> str:
|
|
|
|
if type(variable) == str:
|
|
|
|
return variable
|
2020-08-19 21:24:17 +00:00
|
|
|
return 'Yes' if variable else 'No'
|
|
|
|
|
2020-03-09 23:36:26 +00:00
|
|
|
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
|
|
|
outfile.write(
|
2020-08-19 21:24:17 +00:00
|
|
|
'ALttP Berserker\'s Multiworld Version %s - Seed: %s\n\n' % (
|
2020-08-20 18:13:00 +00:00
|
|
|
self.metadata['version'], self.world.seed))
|
2019-08-24 19:53:21 +00:00
|
|
|
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Players: %d\n' % self.world.players)
|
|
|
|
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)))
|
|
|
|
for team in range(self.world.teams):
|
2020-03-09 23:36:26 +00:00
|
|
|
outfile.write('%s%s\n' % (
|
2020-06-07 13:22:24 +00:00
|
|
|
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if self.world.teams > 1 else 'Hash: ',
|
|
|
|
self.hashes[player, team]))
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
|
2020-05-20 20:30:21 +00:00
|
|
|
if self.world.players > 1:
|
2020-06-07 13:22:24 +00:00
|
|
|
outfile.write('Progression Balanced: %s\n' % (
|
|
|
|
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
2020-06-07 13:22:24 +00:00
|
|
|
outfile.write(
|
|
|
|
'Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No'))
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
|
|
|
|
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
2020-06-07 13:22:24 +00:00
|
|
|
if "triforce" in self.metadata["goal"][player]: # triforce hunt
|
2020-06-17 08:02:54 +00:00
|
|
|
outfile.write(
|
|
|
|
"Pieces available for Triforce: %s\n" % self.metadata['triforce_pieces_available'][player])
|
2020-06-07 13:22:24 +00:00
|
|
|
outfile.write(
|
|
|
|
"Pieces required for Triforce: %s\n" % self.metadata["triforce_pieces_required"][player])
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
|
|
|
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
2020-01-22 17:08:56 +00:00
|
|
|
outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player])
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
|
|
|
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
|
|
|
|
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
|
2020-06-07 13:22:24 +00:00
|
|
|
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
|
|
|
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
2020-06-07 13:22:24 +00:00
|
|
|
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'))
|
2020-08-19 21:24:17 +00:00
|
|
|
outfile.write(
|
2020-08-20 18:13:00 +00:00
|
|
|
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
|
2020-08-19 21:24:17 +00:00
|
|
|
outfile.write('Big Key shuffle: %s\n' % (
|
|
|
|
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
2020-08-19 21:24:17 +00:00
|
|
|
outfile.write(
|
|
|
|
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
|
2020-01-14 09:42:27 +00:00
|
|
|
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
|
|
|
|
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
|
2020-08-19 21:24:17 +00:00
|
|
|
outfile.write(f'Killable thieves: {bool_to_text(self.metadata["killable_thieves"])}\n')
|
|
|
|
outfile.write(f'Shuffled tiles: {bool_to_text(self.metadata["tile_shuffle"])}\n')
|
|
|
|
outfile.write(f'Shuffled bushes: {bool_to_text(self.metadata["bush_shuffle"])}\n')
|
|
|
|
outfile.write(
|
|
|
|
'Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
|
2020-01-18 17:51:10 +00:00
|
|
|
outfile.write('Beemizer: %s\n' % self.metadata['beemizer'][player])
|
2020-08-19 21:24:17 +00:00
|
|
|
outfile.write(
|
|
|
|
'Pot shuffle %s\n' % ('Yes' if self.metadata['shufflepots'][player] else 'No'))
|
2017-07-18 10:44:13 +00:00
|
|
|
if self.entrances:
|
|
|
|
outfile.write('\n\nEntrances:\n\n')
|
2020-01-14 09:42:27 +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()]))
|
|
|
|
outfile.write('\n\nMedallions:\n')
|
|
|
|
for dungeon, medallion in self.medallions.items():
|
|
|
|
outfile.write(f'\n{dungeon}: {medallion}')
|
|
|
|
if self.startinventory:
|
|
|
|
outfile.write('\n\nStarting Inventory:\n\n')
|
|
|
|
outfile.write('\n'.join(self.startinventory))
|
2017-07-18 10:44:13 +00:00
|
|
|
outfile.write('\n\nLocations:\n\n')
|
2018-03-24 05:43:10 +00:00
|
|
|
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
|
|
|
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))
|
2020-05-20 20:21:05 +00:00
|
|
|
for player in range(1, self.world.players + 1):
|
|
|
|
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()]))
|
2017-07-18 10:44:13 +00:00
|
|
|
outfile.write('\n\nPlaythrough:\n\n')
|
2020-01-09 07:31:49 +00:00
|
|
|
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
2019-12-21 12:33:07 +00:00
|
|
|
if self.unreachables:
|
|
|
|
outfile.write('\n\nUnreachable Items:\n\n')
|
|
|
|
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
2018-01-01 20:55:13 +00:00
|
|
|
outfile.write('\n\nPaths:\n\n')
|
|
|
|
|
|
|
|
path_listings = []
|
2018-01-07 06:31:56 +00:00
|
|
|
for location, path in sorted(self.paths.items()):
|
2018-01-01 20:55:13 +00:00
|
|
|
path_lines = []
|
2018-01-06 21:25:14 +00:00
|
|
|
for region, exit in path:
|
2018-01-01 20:55:13 +00:00
|
|
|
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))
|