Merge remote-tracking branch 'remotes/door_rando/DoorDev' into Dev

This commit is contained in:
compiling
2020-01-04 21:33:42 +11:00
35 changed files with 10375 additions and 548 deletions

View File

@@ -6,12 +6,15 @@ from collections import OrderedDict
from _vendor.collections_extended import bag
from EntranceShuffle import door_addresses
from Utils import int16_as_bytes
from Tables import normal_offset_table, spiral_offset_table
from RoomData import Room
class World(object):
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, hints):
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, hints):
self.players = players
self.shuffle = shuffle.copy()
self.doorShuffle = doorShuffle.copy()
self.logic = logic.copy()
self.mode = mode.copy()
self.swords = swords.copy()
@@ -42,6 +45,9 @@ class World(object):
self.lock_aga_door_in_escape = False
self.save_and_quit_from_boss = True
self.accessibility = accessibility.copy()
self.fix_skullwoods_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle not in ['vanilla']
self.fix_palaceofdarkness_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']
self.fix_trock_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']
self.shuffle_ganon = shuffle_ganon
self.fix_gtower_exit = self.shuffle_ganon
self.quickswap = quickswap
@@ -56,6 +62,14 @@ class World(object):
self.dynamic_locations = []
self.spoiler = Spoiler(self)
self.lamps_needed_for_dark_rooms = 1
self.doors = []
self._door_cache = {}
self.paired_doors = {}
self.rooms = []
self._room_cache = {}
self.dungeon_layouts = {}
self.inaccessible_regions = {}
self.key_logic = {}
for player in range(1, players + 1):
def set_player_attr(attr, val):
@@ -148,6 +162,42 @@ class World(object):
return dungeon
raise RuntimeError('No such dungeon %s for player %d' % (dungeonname, player))
def get_door(self, doorname, player):
if isinstance(doorname, Door):
return doorname
try:
return self._door_cache[(doorname, player)]
except KeyError:
for door in self.doors:
if door.name == doorname and door.player == player:
self._door_cache[(doorname, player)] = door
return door
raise RuntimeError('No such door %s for player %d' % (doorname, player))
def check_for_door(self, doorname, player):
if isinstance(doorname, Door):
return doorname
try:
return self._door_cache[(doorname, player)]
except KeyError:
for door in self.doors:
if door.name == doorname and door.player == player:
self._door_cache[(doorname, player)] = door
return door
return None
def get_room(self, room_idx, player):
if isinstance(room_idx, Room):
return room_idx
try:
return self._room_cache[(room_idx, player)]
except KeyError:
for room in self.rooms:
if room.index == room_idx and room.player == player:
self._room_cache[(room_idx, player)] = room
return room
raise RuntimeError('No such room %s for player %d' % (room_idx, player))
def get_all_state(self, keys=False):
ret = CollectionState(self)
@@ -198,11 +248,15 @@ class World(object):
if keys:
for p in range(1, self.players + 1):
key_list = []
player_dungeons = [x for x in self.dungeons if x.player == p]
for dungeon in player_dungeons:
if dungeon.big_key is not None:
key_list += [dungeon.big_key.name]
if len(dungeon.small_keys) > 0:
key_list += [x.name for x in dungeon.small_keys]
from Items import ItemFactory
for item in ItemFactory(['Small Key (Escape)', '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):
for item in ItemFactory(key_list, p):
soft_collect(item)
ret.sweep_for_events()
return ret
@@ -298,7 +352,7 @@ class World(object):
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:
if location.can_reach(state):
if location.can_reach(state) and state.not_flooding_a_key(state.world, location):
sphere.append(location)
if not sphere:
@@ -362,7 +416,7 @@ class CollectionState(object):
else:
# default to Region
spot = self.world.get_region(spot, player)
return spot.can_reach(self)
def sweep_for_events(self, key_only=False, locations=None):
@@ -375,6 +429,7 @@ class CollectionState(object):
reachable_events = [location for location in locations if location.event and
(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)]
reachable_events = self._do_not_flood_the_keys(reachable_events)
for event in reachable_events:
if (event.name, event.player) not in self.events:
self.events.append((event.name, event.player))
@@ -382,6 +437,20 @@ class CollectionState(object):
new_locations = len(reachable_events) > checked_locations
checked_locations = len(reachable_events)
def _do_not_flood_the_keys(self, reachable_events):
adjusted_checks = list(reachable_events)
for event in reachable_events:
if event.name in flooded_keys.keys() and self.world.get_location(flooded_keys[event.name], event.player) not in reachable_events:
adjusted_checks.remove(event)
if len(adjusted_checks) < len(reachable_events):
return adjusted_checks
return reachable_events
def not_flooding_a_key(self, world, location):
if location.name in flooded_keys.keys():
return world.get_location(flooded_keys[location.name], location.player) in self.locations_checked
return True
def has(self, item, player, count=1):
if count == 1:
return (item, player) in self.prog_items
@@ -500,14 +569,14 @@ class CollectionState(object):
def can_melt_things(self, player):
return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player))
def can_avoid_lasers(self, player):
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
def is_not_bunny(self, region, player):
if self.has_Pearl(player):
return True
return True
return region.is_light_world if self.world.mode[player] != 'inverted' else region.is_dark_world
def can_reach_light_world(self, player):
@@ -583,7 +652,7 @@ class CollectionState(object):
elif event or item.advancement:
self.prog_items.add((item.name, item.player))
changed = True
self.stale[item.player] = True
if changed:
@@ -765,6 +834,8 @@ class Dungeon(object):
self.player = player
self.world = None
self.entrance_regions = []
@property
def boss(self):
return self.bosses.get(None, None)
@@ -784,6 +855,16 @@ class Dungeon(object):
def is_dungeon_item(self, item):
return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items]
def count_dungeon_item(self):
return len(self.dungeon_items) + 1 if self.big_key_required else 0 + self.key_number
def incomplete_paths(self):
ret = 0
for path in self.paths:
if not self.path_completion[path]:
ret += 1
return ret
def __str__(self):
return str(self.__unicode__())
@@ -793,6 +874,293 @@ class Dungeon(object):
else:
return '%s (Player %d)' % (self.name, self.player)
@unique
class DoorType(Enum):
Normal = 1
SpiralStairs = 2
StraightStairs = 3
Ladder = 4
Open = 5
Hole = 6
Warp = 7
Interior = 8
Logical = 9
@unique
class Direction(Enum):
North = 0
West = 1
South = 2
East = 3
Up = 4
Down = 5
class Polarity:
def __init__(self):
self.vector = [0, 0, 0]
def __len__(self):
return len(self.vector)
def __add__(self, other):
result = Polarity()
for i in range(len(self.vector)):
result.vector[i] = pol_add[pol_idx_2[i]](self.vector[i], other.vector[i])
return result
def __iadd__(self, other):
for i in range(len(self.vector)):
self.vector[i] = pol_add[pol_idx_2[i]](self.vector[i], other.vector[i])
return self
def __getitem__(self, item):
return self.vector[item]
def __eq__(self, other):
for i in range(len(self.vector)):
if self.vector[i] != other.vector[i]:
return False
return True
def is_neutral(self):
for i in range(len(self.vector)):
if self.vector[i] != 0:
return False
return True
def complement(self):
result = Polarity()
for i in range(len(self.vector)):
result.vector[i] = pol_comp[pol_idx_2[i]](self.vector[i])
return result
def charge(self):
result = 0
for i in range(len(self.vector)):
result += abs(self.vector[i])
return result
pol_idx = {
Direction.North: (0, 'Pos'),
Direction.South: (0, 'Neg'),
Direction.East: (1, 'Pos'),
Direction.West: (1, 'Neg'),
Direction.Up: (2, 'Mod'),
Direction.Down: (2, 'Mod')
}
pol_idx_2 = {
0: 'Add',
1: 'Add',
2: 'Mod'
}
pol_inc = {
'Pos': lambda x: x + 1,
'Neg': lambda x: x - 1,
'Mod': lambda x: (x + 1) % 2
}
pol_add = {
'Add': lambda x, y: x + y,
'Mod': lambda x, y: (x + y) % 2
}
pol_comp = {
'Add': lambda x: -x,
'Mod': lambda x: 0 if x == 0 else 1
}
@unique
class CrystalBarrier(Enum):
Null = 1 # no special requirement
Blue = 2 # blue must be down and explore state set to Blue
Orange = 3 # orange must be down and explore state set to Orange
Either = 4 # you choose to leave this room in Either state
class Door(object):
def __init__(self, player, name, type):
self.player = player
self.name = name
self.type = type
self.direction = None
# rom properties
self.roomIndex = -1
# 0,1,2 + Direction (N:0, W:3, S:6, E:9) for normal
# 0-4 for spiral offset thing
self.doorIndex = -1
self.layer = -1 # 0 for normal floor, 1 for the inset layer
self.toggle = False
self.trapFlag = 0x0
self.quadrant = 2
self.shiftX = 78
self.shiftY = 78
self.zeroHzCam = False
self.zeroVtCam = False
self.doorListPos = -1
# logical properties
# self.connected = False # combine with Dest?
self.dest = None
self.blocked = False # Indicates if the door is normally blocked off as an exit. (Sanc door or always closed)
self.stonewall = False # Indicate that the door cannot be enter until exited (Desert Torches, PoD Eye Statue)
self.smallKey = False # There's a small key door on this side
self.bigKey = False # There's a big key door on this side
self.ugly = False # Indicates that it can't be seen from the front (e.g. back of a big key door)
self.crystal = CrystalBarrier.Null # How your crystal state changes if you use this door
self.req_event = None # if a dungeon event is required for this door - swamp palace mostly
self.controller = None
self.dependents = []
self.dead = False
def getAddress(self):
if self.type == DoorType.Normal:
return 0x13A000 + normal_offset_table[self.roomIndex] * 24 + (self.doorIndex + self.direction.value * 3) * 2
elif self.type == DoorType.SpiralStairs:
return 0x13B000 + (spiral_offset_table[self.roomIndex] + self.doorIndex) * 4
def getTarget(self, toggle):
if self.type == DoorType.Normal:
bitmask = 4 * (self.layer ^ 1 if toggle else self.layer)
bitmask += 0x08 * int(self.trapFlag)
return [self.roomIndex, bitmask + self.doorIndex]
if self.type == DoorType.SpiralStairs:
bitmask = int(self.layer) << 2
bitmask += 0x10 * int(self.zeroHzCam)
bitmask += 0x20 * int(self.zeroVtCam)
bitmask += 0x80 if self.direction == Direction.Up else 0
return [self.roomIndex, bitmask + self.quadrant, self.shiftX, self.shiftY]
def dir(self, direction, room, doorIndex, layer):
self.direction = direction
self.roomIndex = room
self.doorIndex = doorIndex
self.layer = layer
return self
def ss(self, quadrant, shift_y, shift_x, zero_hz_cam=False, zero_vt_cam=False):
self.quadrant = quadrant
self.shiftY = shift_y
self.shiftX = shift_x
self.zeroHzCam = zero_hz_cam
self.zeroVtCam = zero_vt_cam
return self
def small_key(self):
self.smallKey = True
return self
def big_key(self):
self.bigKey = True
return self
def toggler(self):
self.toggle = True
return self
def no_exit(self):
self.blocked = True
return self
def no_entrance(self):
self.stonewall = True
return self
def trap(self, trapFlag):
self.trapFlag = trapFlag
return self
def pos(self, pos):
self.doorListPos = pos
return self
def event(self, event):
self.req_event = event
return self
def barrier(self, crystal):
self.crystal = crystal
return self
def c_switch(self):
self.crystal = CrystalBarrier.Either
return self
def kill(self):
self.dead = True
return self
def __eq__(self, other):
return isinstance(other, self.__class__) and self.name == other.name
def __hash__(self):
return hash(self.name)
def __str__(self):
return str(self.__unicode__())
def __unicode__(self):
return '%s' % self.name
class Sector(object):
def __init__(self):
self.regions = []
self.outstanding_doors = []
self.name = None
self.r_name_set = None
self.chest_locations = 0
self.key_only_locations = 0
self.c_switch = False
self.orange_barrier = False
self.blue_barrier = False
self.bk_required = False
self.bk_provided = False
def region_set(self):
if self.r_name_set is None:
self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions))
return self.r_name_set.keys()
def polarity(self):
pol = Polarity()
for door in self.outstanding_doors:
idx, inc = pol_idx[door.direction]
pol.vector[idx] = pol_inc[inc](pol.vector[idx])
return pol
def magnitude(self):
magnitude = [0, 0, 0]
for door in self.outstanding_doors:
idx, inc = pol_idx[door.direction]
magnitude[idx] = magnitude[idx] + 1
return magnitude
def outflow(self):
outflow = 0
for door in self.outstanding_doors:
if not door.blocked:
outflow = outflow + 1
return outflow
def adj_outflow(self):
outflow = 0
for door in self.outstanding_doors:
if not door.blocked and not door.dead:
outflow = outflow + 1
return outflow
def __str__(self):
return str(self.__unicode__())
def __unicode__(self):
return '%s' % next(iter(self.region_set()))
class Boss(object):
def __init__(self, name, enemizer_name, defeat_rule, player):
self.name = name
@@ -804,10 +1172,19 @@ class Boss(object):
return self.defeat_rule(state, self.player)
class Location(object):
def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, player_address=None):
def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, forced_item=None, player_address=None):
self.name = name
self.parent_region = parent
self.item = None
if forced_item is not None:
from Items import ItemFactory
self.forced_item = ItemFactory([forced_item], player)[0]
self.item = self.forced_item
self.item.location = self
self.event = True
else:
self.forced_item = None
self.item = None
self.event = False
self.crystal = crystal
self.address = address
self.player_address = player_address
@@ -815,7 +1192,6 @@ class Location(object):
self.hint_text = hint_text if hint_text is not None else 'Hyrule'
self.recursion_count = 0
self.staleness_count = 0
self.event = False
self.locked = False
self.always_allow = lambda item, state: False
self.access_rule = lambda state: True
@@ -899,9 +1275,10 @@ class ShopType(Enum):
UpgradeShop = 2
class Shop(object):
def __init__(self, region, room_id, type, shopkeeper_config, replaceable):
def __init__(self, region, room_id, default_door_id, type, shopkeeper_config, replaceable):
self.region = region
self.room_id = room_id
self.default_door_id = default_door_id
self.type = type
self.inventory = [None, None, None]
self.shopkeeper_config = shopkeeper_config
@@ -921,6 +1298,8 @@ class Shop(object):
config = self.item_count
if len(entrances) == 1 and entrances[0].name in door_addresses:
door_id = door_addresses[entrances[0].name][0]+1
elif self.default_door_id is not None:
door_id = self.default_door_id
else:
door_id = 0
config |= 0x40 # ignore door id
@@ -959,6 +1338,8 @@ class Spoiler(object):
def __init__(self, world):
self.world = world
self.entrances = OrderedDict()
self.doors = OrderedDict()
self.doorTypes = OrderedDict()
self.medallions = {}
self.playthrough = {}
self.unreachables = []
@@ -974,6 +1355,18 @@ class Spoiler(object):
else:
self.entrances[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
def set_door(self, entrance, exit, direction, player):
if self.world.players == 1:
self.doors[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
else:
self.doors[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
def set_door_type(self, doorNames, type, player):
if self.world.players == 1:
self.doorTypes[(doorNames, player)] = OrderedDict([('doorNames', doorNames), ('type', type)])
else:
self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)])
def parse_data(self):
self.medallions = OrderedDict()
if self.world.players == 1:
@@ -1035,14 +1428,9 @@ class Spoiler(object):
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
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 Basement"] = [x for x in self.world.dungeons if x.player == player and 'bottom' in x.bosses.keys()][0].bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = [x for x in self.world.dungeons if x.player == player and 'middle' in x.bosses.keys()][0].bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = [x for x in self.world.dungeons if x.player == player and 'top' in x.bosses.keys()][0].bosses['top'].name
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
self.bosses[str(player)]["Ganon"] = "Ganon"
@@ -1080,6 +1468,8 @@ class Spoiler(object):
self.parse_data()
out = OrderedDict()
out['Entrances'] = list(self.entrances.values())
out['Doors'] = list(self.doors.values())
out['DoorTypes'] = list(self.doorTypes.values())
out.update(self.locations)
out['Special'] = self.medallions
if self.shops:
@@ -1120,9 +1510,15 @@ class Spoiler(object):
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()})
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No'))
outfile.write('Menu speed: %s' % self.world.fastmenu)
if self.doors:
outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(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.doors.values()]))
if self.doorTypes:
outfile.write('\n\nDoor Types:\n\n')
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()]))
if self.entrances:
outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(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'.join(['%s%s %s %s' % ('Player {0}: '.format(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')
if self.world.players == 1:
outfile.write('\nMisery Mire Medallion: %s' % (self.medallions['Misery Mire']))
@@ -1153,3 +1549,9 @@ class Spoiler(object):
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings))
flooded_keys = {
'Trench 1 Switch': 'Swamp Palace - Trench 1 Pot Key',
'Trench 2 Switch': 'Swamp Palace - Trench 2 Pot Key'
}