Init work on decoupling doors
This commit is contained in:
@@ -142,6 +142,7 @@ class World(object):
|
|||||||
set_player_attr('collection_rate', False)
|
set_player_attr('collection_rate', False)
|
||||||
set_player_attr('colorizepots', False)
|
set_player_attr('colorizepots', False)
|
||||||
set_player_attr('pot_pool', {})
|
set_player_attr('pot_pool', {})
|
||||||
|
set_player_attr('decoupledoors', False)
|
||||||
|
|
||||||
set_player_attr('shopsanity', False)
|
set_player_attr('shopsanity', False)
|
||||||
set_player_attr('mixed_travel', 'prevent')
|
set_player_attr('mixed_travel', 'prevent')
|
||||||
@@ -2462,6 +2463,7 @@ class Spoiler(object):
|
|||||||
'shufflelinks': self.world.shufflelinks,
|
'shufflelinks': self.world.shufflelinks,
|
||||||
'door_shuffle': self.world.doorShuffle,
|
'door_shuffle': self.world.doorShuffle,
|
||||||
'intensity': self.world.intensity,
|
'intensity': self.world.intensity,
|
||||||
|
'decoupledoors': self.world.decoupledoors,
|
||||||
'item_pool': self.world.difficulty,
|
'item_pool': self.world.difficulty,
|
||||||
'item_functionality': self.world.difficulty_adjustments,
|
'item_functionality': self.world.difficulty_adjustments,
|
||||||
'gt_crystals': self.world.crystals_needed_for_gt,
|
'gt_crystals': self.world.crystals_needed_for_gt,
|
||||||
@@ -2544,6 +2546,7 @@ class Spoiler(object):
|
|||||||
outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n")
|
outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n")
|
||||||
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
|
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
|
||||||
outfile.write('Intensity: %s\n' % self.metadata['intensity'][player])
|
outfile.write('Intensity: %s\n' % self.metadata['intensity'][player])
|
||||||
|
outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n")
|
||||||
outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n")
|
outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n")
|
||||||
outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n")
|
outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n")
|
||||||
outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n")
|
outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n")
|
||||||
@@ -2759,7 +2762,7 @@ goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'cryst
|
|||||||
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
|
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||||
func_mode = {"normal": 0, "hard": 1, "expert": 2}
|
func_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||||
|
|
||||||
# byte 3: S?MM PIII (shop, unused, mixed, palettes, intensity)
|
# byte 3: SDMM PIII (shop, decouple doors, mixed, palettes, intensity)
|
||||||
# keydrop now has it's own byte
|
# keydrop now has it's own byte
|
||||||
mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
|
mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
|
||||||
# intensity is 3 bits (reserves 4-7 levels)
|
# intensity is 3 bits (reserves 4-7 levels)
|
||||||
@@ -2813,7 +2816,8 @@ class Settings(object):
|
|||||||
(goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3)
|
(goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3)
|
||||||
| (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0),
|
| (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0),
|
||||||
|
|
||||||
(0x80 if w.shopsanity[p] else 0) | (mixed_travel_mode[w.mixed_travel[p]] << 4)
|
(0x80 if w.shopsanity[p] else 0) | (0x40 if w.decoupledoors[p] else 0)
|
||||||
|
| (mixed_travel_mode[w.mixed_travel[p]] << 4)
|
||||||
| (0x8 if w.standardize_palettes[p] == "original" else 0)
|
| (0x8 if w.standardize_palettes[p] == "original" else 0)
|
||||||
| (0 if w.intensity[p] == "random" else w.intensity[p]),
|
| (0 if w.intensity[p] == "random" else w.intensity[p]),
|
||||||
|
|
||||||
@@ -2861,7 +2865,7 @@ class Settings(object):
|
|||||||
args.retro[p] = True if settings[1] & 0x01 else False
|
args.retro[p] = True if settings[1] & 0x01 else False
|
||||||
args.hints[p] = True if settings[2] & 0x01 else False
|
args.hints[p] = True if settings[2] & 0x01 else False
|
||||||
args.shopsanity[p] = True if settings[3] & 0x80 else False
|
args.shopsanity[p] = True if settings[3] & 0x80 else False
|
||||||
# args.keydropshuffle[p] = True if settings[3] & 0x40 else False
|
args.decoupledoors[p] = True if settings[3] & 0x40 else False
|
||||||
args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4]
|
args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4]
|
||||||
args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize"
|
args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize"
|
||||||
intensity = settings[3] & 0x7
|
intensity = settings[3] & 0x7
|
||||||
|
|||||||
3
CLI.py
3
CLI.py
@@ -125,7 +125,7 @@ def parse_cli(argv, no_defaults=False):
|
|||||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||||
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
|
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
|
||||||
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
|
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
|
||||||
'msu_resume', 'collection_rate', 'colorizepots']:
|
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors']:
|
||||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||||
if player == 1:
|
if player == 1:
|
||||||
setattr(ret, name, {1: value})
|
setattr(ret, name, {1: value})
|
||||||
@@ -192,6 +192,7 @@ def parse_settings():
|
|||||||
"keysanity": False,
|
"keysanity": False,
|
||||||
"door_shuffle": "basic",
|
"door_shuffle": "basic",
|
||||||
"intensity": 2,
|
"intensity": 2,
|
||||||
|
'decoupledoors': False,
|
||||||
"experimental": False,
|
"experimental": False,
|
||||||
"dungeon_counters": "default",
|
"dungeon_counters": "default",
|
||||||
"mixed_travel": "prevent",
|
"mixed_travel": "prevent",
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ from Dungeons import dungeon_regions, region_starts, standard_starts, split_regi
|
|||||||
from Dungeons import dungeon_bigs, dungeon_hints
|
from Dungeons import dungeon_bigs, dungeon_hints
|
||||||
from Items import ItemFactory
|
from Items import ItemFactory
|
||||||
from RoomData import DoorKind, PairedDoor, reset_rooms
|
from RoomData import DoorKind, PairedDoor, reset_rooms
|
||||||
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances
|
from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon
|
||||||
|
# from DungeonGenerator import generate_dungeon
|
||||||
|
from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances
|
||||||
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
|
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
|
||||||
from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException
|
from DungeonGenerator import dungeon_portals, dungeon_drops
|
||||||
from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim
|
from DungeonGenerator import valid_region_to_explore as valid_region_to_explore_lim
|
||||||
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
|
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
|
||||||
from Utils import ncr, kth_combination
|
from Utils import ncr, kth_combination
|
||||||
@@ -780,6 +782,7 @@ def within_dungeon(world, player):
|
|||||||
for builder in world.dungeon_layouts[player].values():
|
for builder in world.dungeon_layouts[player].values():
|
||||||
shuffle_key_doors(builder, world, player)
|
shuffle_key_doors(builder, world, player)
|
||||||
logging.getLogger('').info('%s: %s', world.fish.translate("cli", "cli", "keydoor.shuffle.time"), time.process_time()-start)
|
logging.getLogger('').info('%s: %s', world.fish.translate("cli", "cli", "keydoor.shuffle.time"), time.process_time()-start)
|
||||||
|
if not world.decoupledoors[player]:
|
||||||
smooth_door_pairs(world, player)
|
smooth_door_pairs(world, player)
|
||||||
|
|
||||||
if world.intensity[player] >= 3:
|
if world.intensity[player] >= 3:
|
||||||
@@ -1059,6 +1062,7 @@ def cross_dungeon(world, player):
|
|||||||
target_items += 19 # 19 pot keys
|
target_items += 19 # 19 pot keys
|
||||||
d_items = target_items - all_dungeon_items_cnt
|
d_items = target_items - all_dungeon_items_cnt
|
||||||
world.pool_adjustment[player] = d_items
|
world.pool_adjustment[player] = d_items
|
||||||
|
if not world.decoupledoors[player]:
|
||||||
smooth_door_pairs(world, player)
|
smooth_door_pairs(world, player)
|
||||||
|
|
||||||
# Re-assign dungeon bosses
|
# Re-assign dungeon bosses
|
||||||
@@ -1567,8 +1571,9 @@ def find_small_key_door_candidates(builder, start_regions, world, player):
|
|||||||
checked_doors.update(checked)
|
checked_doors.update(checked)
|
||||||
flat_candidates = []
|
flat_candidates = []
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
# not valid if: Normal and Pair in is Checked and Pair is not in Candidates
|
# not valid if: Normal Coupled and Pair in is Checked and Pair is not in Candidates
|
||||||
if candidate.type != DoorType.Normal or candidate.dest not in checked_doors or candidate.dest in candidates:
|
if (world.decoupledoors[player] or candidate.type != DoorType.Normal
|
||||||
|
or candidate.dest not in checked_doors or candidate.dest in candidates):
|
||||||
flat_candidates.append(candidate)
|
flat_candidates.append(candidate)
|
||||||
|
|
||||||
paired_candidates = build_pair_list(flat_candidates)
|
paired_candidates = build_pair_list(flat_candidates)
|
||||||
@@ -1609,8 +1614,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
|
|||||||
combinations = ncr(len(key_door_pool), key_doors_needed)
|
combinations = ncr(len(key_door_pool), key_doors_needed)
|
||||||
itr = 0
|
itr = 0
|
||||||
start = time.process_time()
|
start = time.process_time()
|
||||||
sample_list = list(range(0, int(combinations)))
|
sample_list = build_sample_list(combinations)
|
||||||
random.shuffle(sample_list)
|
|
||||||
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
|
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
|
||||||
proposal.extend(custom_key_doors)
|
proposal.extend(custom_key_doors)
|
||||||
# eliminate start region if portal marked as destination
|
# eliminate start region if portal marked as destination
|
||||||
@@ -1625,12 +1629,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
|
|||||||
determine_prize_lock(key_layout, world, player)
|
determine_prize_lock(key_layout, world, player)
|
||||||
while not validate_key_layout(key_layout, world, player):
|
while not validate_key_layout(key_layout, world, player):
|
||||||
itr += 1
|
itr += 1
|
||||||
stop_early = False
|
if itr >= len(sample_list):
|
||||||
if itr % 1000 == 0:
|
|
||||||
mark = time.process_time()-start
|
|
||||||
if (mark > 10 and itr*100/combinations > 50) or (mark > 20 and itr*100/combinations > 25) or mark > 30:
|
|
||||||
stop_early = True
|
|
||||||
if itr >= combinations or stop_early:
|
|
||||||
if not drop_keys:
|
if not drop_keys:
|
||||||
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
|
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
|
||||||
return False
|
return False
|
||||||
@@ -1640,8 +1639,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
|
|||||||
if builder.key_doors_num < 0:
|
if builder.key_doors_num < 0:
|
||||||
raise Exception('Bad dungeon %s - less than 0 key doors not valid' % builder.name)
|
raise Exception('Bad dungeon %s - less than 0 key doors not valid' % builder.name)
|
||||||
combinations = ncr(len(key_door_pool), max(0, key_doors_needed))
|
combinations = ncr(len(key_door_pool), max(0, key_doors_needed))
|
||||||
sample_list = list(range(0, int(combinations)))
|
sample_list = build_sample_list(combinations)
|
||||||
random.shuffle(sample_list)
|
|
||||||
itr = 0
|
itr = 0
|
||||||
start = time.process_time() # reset time since itr reset
|
start = time.process_time() # reset time since itr reset
|
||||||
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
|
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
|
||||||
@@ -1660,6 +1658,20 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def build_sample_list(combinations):
|
||||||
|
if combinations <= 1000000:
|
||||||
|
sample_list = list(range(0, int(combinations)))
|
||||||
|
|
||||||
|
else:
|
||||||
|
num_set = set()
|
||||||
|
while len(num_set) < 1000000:
|
||||||
|
num_set.add(random.randint(0, combinations))
|
||||||
|
sample_list = list(num_set)
|
||||||
|
sample_list.sort()
|
||||||
|
random.shuffle(sample_list)
|
||||||
|
return sample_list
|
||||||
|
|
||||||
|
|
||||||
def log_key_logic(d_name, key_logic):
|
def log_key_logic(d_name, key_logic):
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
@@ -1693,7 +1705,8 @@ def build_pair_list(flat_list):
|
|||||||
queue = deque(flat_list)
|
queue = deque(flat_list)
|
||||||
while len(queue) > 0:
|
while len(queue) > 0:
|
||||||
d = queue.pop()
|
d = queue.pop()
|
||||||
if d.dest in queue and d.type != DoorType.SpiralStairs:
|
paired = d.dest.dest == d
|
||||||
|
if d.dest in queue and d.type != DoorType.SpiralStairs and paired:
|
||||||
paired_list.append((d, d.dest))
|
paired_list.append((d, d.dest))
|
||||||
queue.remove(d.dest)
|
queue.remove(d.dest)
|
||||||
else:
|
else:
|
||||||
@@ -1716,6 +1729,7 @@ okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.
|
|||||||
|
|
||||||
|
|
||||||
def find_key_door_candidates(region, checked, world, player):
|
def find_key_door_candidates(region, checked, world, player):
|
||||||
|
decoupled = world.decoupledoors[player]
|
||||||
dungeon_name = region.dungeon.name
|
dungeon_name = region.dungeon.name
|
||||||
candidates = []
|
candidates = []
|
||||||
checked_doors = list(checked)
|
checked_doors = list(checked)
|
||||||
@@ -1740,6 +1754,9 @@ def find_key_door_candidates(region, checked, world, player):
|
|||||||
elif d.type == DoorType.SpiralStairs:
|
elif d.type == DoorType.SpiralStairs:
|
||||||
valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow]
|
valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow]
|
||||||
elif d.type == DoorType.Normal:
|
elif d.type == DoorType.Normal:
|
||||||
|
if decoupled:
|
||||||
|
valid = kind in okay_normals
|
||||||
|
else:
|
||||||
d2 = d.dest
|
d2 = d.dest
|
||||||
if d2 not in candidates:
|
if d2 not in candidates:
|
||||||
if d2.type == DoorType.Normal:
|
if d2.type == DoorType.Normal:
|
||||||
|
|||||||
1
Main.py
1
Main.py
@@ -443,6 +443,7 @@ def copy_world(world):
|
|||||||
ret.enemy_damage = world.enemy_damage.copy()
|
ret.enemy_damage = world.enemy_damage.copy()
|
||||||
ret.beemizer = world.beemizer.copy()
|
ret.beemizer = world.beemizer.copy()
|
||||||
ret.intensity = world.intensity.copy()
|
ret.intensity = world.intensity.copy()
|
||||||
|
ret.decoupledoors = world.decoupledoors.copy()
|
||||||
ret.experimental = world.experimental.copy()
|
ret.experimental = world.experimental.copy()
|
||||||
ret.shopsanity = world.shopsanity.copy()
|
ret.shopsanity = world.shopsanity.copy()
|
||||||
ret.dropshuffle = world.dropshuffle.copy()
|
ret.dropshuffle = world.dropshuffle.copy()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
1: 2
|
1: 2
|
||||||
2: 2
|
2: 2
|
||||||
3: 4
|
3: 4
|
||||||
|
decoupledoors: off
|
||||||
dropshuffle:
|
dropshuffle:
|
||||||
on: 1
|
on: 1
|
||||||
off: 1
|
off: 1
|
||||||
|
|||||||
@@ -164,6 +164,10 @@
|
|||||||
"3", "2", "1", "random"
|
"3", "2", "1", "random"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"deoupledoors": {
|
||||||
|
"action": "store_true",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"action": "store_true",
|
"action": "store_true",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
|||||||
@@ -215,6 +215,7 @@
|
|||||||
"3: And shuffles dungeon lobbies",
|
"3: And shuffles dungeon lobbies",
|
||||||
"random: Picks one of those at random"
|
"random: Picks one of those at random"
|
||||||
],
|
],
|
||||||
|
"decoupledoors" : [ "Door entrances and exits are decoupled" ],
|
||||||
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
|
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
|
||||||
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
|
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
|
||||||
"crystals_ganon": [
|
"crystals_ganon": [
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"randomizer.dungeon.smallkeyshuffle": "Small Keys",
|
"randomizer.dungeon.smallkeyshuffle": "Small Keys",
|
||||||
"randomizer.dungeon.bigkeyshuffle": "Big Keys",
|
"randomizer.dungeon.bigkeyshuffle": "Big Keys",
|
||||||
"randomizer.dungeon.keydropshuffle": "Key Drop Shuffle (Legacy)",
|
"randomizer.dungeon.keydropshuffle": "Key Drop Shuffle (Legacy)",
|
||||||
|
"randomizer.dungeon.decoupledoors": "Decouple Doors",
|
||||||
"randomizer.dungeon.dropshuffle": "Shuffle Enemy Key Drops",
|
"randomizer.dungeon.dropshuffle": "Shuffle Enemy Key Drops",
|
||||||
"randomizer.dungeon.potshuffle": "Pot Shuffle (Legacy)",
|
"randomizer.dungeon.potshuffle": "Pot Shuffle (Legacy)",
|
||||||
"randomizer.dungeon.pottery": "Pottery",
|
"randomizer.dungeon.pottery": "Pottery",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"width": 45
|
"width": 45
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"decoupledoors": { "type": "checkbox" },
|
||||||
"keydropshuffle": { "type": "checkbox" },
|
"keydropshuffle": { "type": "checkbox" },
|
||||||
"pottery": {
|
"pottery": {
|
||||||
"type": "selectbox",
|
"type": "selectbox",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class CustomSettings(object):
|
|||||||
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
|
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
|
||||||
args.standardize_palettes[p])
|
args.standardize_palettes[p])
|
||||||
args.intensity[p] = get_setting(settings['intensity'], args.intensity[p])
|
args.intensity[p] = get_setting(settings['intensity'], args.intensity[p])
|
||||||
|
args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p])
|
||||||
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p])
|
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p])
|
||||||
args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p])
|
args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p])
|
||||||
args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
|
args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
|
||||||
@@ -181,6 +182,7 @@ class CustomSettings(object):
|
|||||||
settings_dict[p]['shuffle'] = world.shuffle[p]
|
settings_dict[p]['shuffle'] = world.shuffle[p]
|
||||||
settings_dict[p]['door_shuffle'] = world.doorShuffle[p]
|
settings_dict[p]['door_shuffle'] = world.doorShuffle[p]
|
||||||
settings_dict[p]['intensity'] = world.intensity[p]
|
settings_dict[p]['intensity'] = world.intensity[p]
|
||||||
|
settings_dict[p]['decoupledoors'] = world.decoupledoors[p]
|
||||||
settings_dict[p]['logic'] = world.logic[p]
|
settings_dict[p]['logic'] = world.logic[p]
|
||||||
settings_dict[p]['mode'] = world.mode[p]
|
settings_dict[p]['mode'] = world.mode[p]
|
||||||
settings_dict[p]['swords'] = world.swords[p]
|
settings_dict[p]['swords'] = world.swords[p]
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ SETTINGSTOPROCESS = {
|
|||||||
"bigkeyshuffle": "bigkeyshuffle",
|
"bigkeyshuffle": "bigkeyshuffle",
|
||||||
"dungeondoorshuffle": "door_shuffle",
|
"dungeondoorshuffle": "door_shuffle",
|
||||||
"dungeonintensity": "intensity",
|
"dungeonintensity": "intensity",
|
||||||
|
"decoupledoors": "decoupledoors",
|
||||||
"keydropshuffle": "keydropshuffle",
|
"keydropshuffle": "keydropshuffle",
|
||||||
"dropshuffle": "dropshuffle",
|
"dropshuffle": "dropshuffle",
|
||||||
"pottery": "pottery",
|
"pottery": "pottery",
|
||||||
|
|||||||
848
source/dungeon/DungeonStitcher.py
Normal file
848
source/dungeon/DungeonStitcher.py
Normal file
@@ -0,0 +1,848 @@
|
|||||||
|
import RaceRandom as random
|
||||||
|
import collections
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from BaseClasses import CrystalBarrier, DoorType, Hook, RegionType, Sector
|
||||||
|
from BaseClasses import hook_from_door, flooded_keys
|
||||||
|
from Regions import dungeon_events, flooded_keys_reverse
|
||||||
|
|
||||||
|
|
||||||
|
def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
|
||||||
|
pass
|
||||||
|
# todo: determine the part of check_valid that are necessary here
|
||||||
|
|
||||||
|
|
||||||
|
def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
|
||||||
|
if builder.valid_proposal: # we made this earlier in gen, just use it
|
||||||
|
proposed_map = builder.valid_proposal
|
||||||
|
else:
|
||||||
|
proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player)
|
||||||
|
builder.valid_proposal = proposed_map
|
||||||
|
queue = collections.deque(proposed_map.items())
|
||||||
|
while len(queue) > 0:
|
||||||
|
a, b = queue.popleft()
|
||||||
|
if world.decoupledoors[player]:
|
||||||
|
connect_doors_one_way(a, b)
|
||||||
|
else:
|
||||||
|
connect_doors(a, b)
|
||||||
|
queue.remove((b, a))
|
||||||
|
if len(builder.sectors) == 0:
|
||||||
|
return Sector()
|
||||||
|
available_sectors = list(builder.sectors)
|
||||||
|
master_sector = available_sectors.pop()
|
||||||
|
for sub_sector in available_sectors:
|
||||||
|
master_sector.regions.extend(sub_sector.regions)
|
||||||
|
master_sector.outstanding_doors.clear()
|
||||||
|
master_sector.r_name_set = None
|
||||||
|
return master_sector
|
||||||
|
|
||||||
|
|
||||||
|
def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player):
|
||||||
|
logger = logging.getLogger('')
|
||||||
|
name = builder.name
|
||||||
|
logger.debug(f'Generating Dungeon: {name}')
|
||||||
|
entrance_regions = convert_regions(entrance_region_names, world, player)
|
||||||
|
excluded = {}
|
||||||
|
for region in entrance_regions:
|
||||||
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
||||||
|
if portal:
|
||||||
|
if portal.destination:
|
||||||
|
excluded[region] = None
|
||||||
|
elif len(entrance_regions) > 1:
|
||||||
|
p_region = portal.door.entrance.connected_region
|
||||||
|
access_region = next(x.parent_region for x in p_region.entrances
|
||||||
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||||
|
if (access_region.name in world.inaccessible_regions[player] and
|
||||||
|
region.name not in world.enabled_entrances[player]):
|
||||||
|
excluded[region] = None
|
||||||
|
elif len(region.entrances) == 1: # for holes
|
||||||
|
access_region = next(x.parent_region for x in region.entrances
|
||||||
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
||||||
|
or x.parent_region.name == 'Sewer Drop')
|
||||||
|
if access_region.name == 'Sewer Drop':
|
||||||
|
access_region = next(x.parent_region for x in access_region.entrances)
|
||||||
|
if (access_region.name in world.inaccessible_regions[player] and
|
||||||
|
region.name not in world.enabled_entrances[player]):
|
||||||
|
excluded[region] = None
|
||||||
|
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
||||||
|
doors_to_connect, idx = {}, 0
|
||||||
|
all_regions = set()
|
||||||
|
bk_special = False
|
||||||
|
for sector in builder.sectors:
|
||||||
|
for door in sector.outstanding_doors:
|
||||||
|
doors_to_connect[door.name] = door, idx
|
||||||
|
idx += 1
|
||||||
|
all_regions.update(sector.regions)
|
||||||
|
bk_special |= check_for_special(sector.regions)
|
||||||
|
bk_needed = False
|
||||||
|
for sector in builder.sectors:
|
||||||
|
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
|
||||||
|
finished = False
|
||||||
|
# flag if standard and this is hyrule castle
|
||||||
|
paths = determine_paths_for_dungeon(world, player, all_regions, name)
|
||||||
|
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||||
|
itr = 0
|
||||||
|
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||||
|
hash_code_set = set()
|
||||||
|
start = time.time()
|
||||||
|
while not finished:
|
||||||
|
if itr > 1000:
|
||||||
|
elasped = time.time() - start
|
||||||
|
raise GenerationException(f'Generation taking too long. {elasped}. Ref {name}')
|
||||||
|
if hash_code in hash_code_set:
|
||||||
|
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||||
|
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||||
|
if hash_code not in hash_code_set:
|
||||||
|
hash_code_set.add(hash_code)
|
||||||
|
explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect,
|
||||||
|
bk_needed, bk_special, world, player)
|
||||||
|
if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions,
|
||||||
|
bk_needed, bk_special, paths, entrance_regions, world, player):
|
||||||
|
finished = True
|
||||||
|
else:
|
||||||
|
proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect,
|
||||||
|
hash_code_set, world, player)
|
||||||
|
itr += 1
|
||||||
|
return proposed_map
|
||||||
|
|
||||||
|
|
||||||
|
def create_random_proposal(doors_to_connect, world, player):
|
||||||
|
logger = logging.getLogger('')
|
||||||
|
hooks = [Hook.North, Hook.South, Hook.East, Hook.West, Hook.Stairs]
|
||||||
|
primary_bucket = collections.defaultdict(list)
|
||||||
|
secondary_bucket = collections.defaultdict(list)
|
||||||
|
for name, door in doors_to_connect.items():
|
||||||
|
door, idx = door
|
||||||
|
primary_bucket[hook_from_door(door)].append(door)
|
||||||
|
secondary_bucket[hook_from_door(door)].append(door)
|
||||||
|
proposal = {}
|
||||||
|
while True:
|
||||||
|
hooks_left, left = [], 0
|
||||||
|
for hook in hooks:
|
||||||
|
hook_len = len(primary_bucket[hook])
|
||||||
|
if hook_len > 0:
|
||||||
|
hooks_left.append(hook)
|
||||||
|
left += hook_len
|
||||||
|
if left == 0:
|
||||||
|
return proposal
|
||||||
|
next_hook = random.choice(hooks_left)
|
||||||
|
primary_door = random.choice(primary_bucket[next_hook])
|
||||||
|
opp_hook, secondary_door = type_map[next_hook], None
|
||||||
|
while (secondary_door is None or secondary_door == primary_door
|
||||||
|
or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook],
|
||||||
|
primary_door, secondary_door, world, player)):
|
||||||
|
secondary_door = random.choice(secondary_bucket[opp_hook])
|
||||||
|
proposal[primary_door] = secondary_door
|
||||||
|
primary_bucket[next_hook].remove(primary_door)
|
||||||
|
secondary_bucket[opp_hook].remove(secondary_door)
|
||||||
|
if not world.decoupledoors[player]:
|
||||||
|
proposal[secondary_door] = primary_door
|
||||||
|
primary_bucket[opp_hook].remove(secondary_door)
|
||||||
|
secondary_bucket[next_hook].remove(primary_door)
|
||||||
|
logger.debug(f'Linking {primary_door.name} <-> {secondary_door.name}')
|
||||||
|
else:
|
||||||
|
logger.debug(f'Linking {primary_door.name} -> {secondary_door.name}')
|
||||||
|
|
||||||
|
|
||||||
|
def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player):
|
||||||
|
if world.decoupledoors[player] and len(primary_list) == 2 and len(secondary_list) == 2:
|
||||||
|
primary_alone = next(d for d in primary_list if d != primary_door)
|
||||||
|
secondary_alone = next(d for d in secondary_list if d != secondary_door)
|
||||||
|
return primary_alone == secondary_alone
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def proposal_hash(doors_to_connect, proposed_map):
|
||||||
|
hash_code = ''
|
||||||
|
for name, door_pair in doors_to_connect.items():
|
||||||
|
door, idx = door_pair
|
||||||
|
hash_code += str(idx) + str(doors_to_connect[proposed_map[door].name][1])
|
||||||
|
return hash_code
|
||||||
|
|
||||||
|
|
||||||
|
def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_set, world, player):
|
||||||
|
logger = logging.getLogger('')
|
||||||
|
hash_code, itr = None, 0
|
||||||
|
while hash_code is None or hash_code in hash_code_set:
|
||||||
|
if itr > 10:
|
||||||
|
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||||
|
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||||
|
return proposed_map, hash_code
|
||||||
|
visited_bucket = collections.defaultdict(list)
|
||||||
|
unvisted_bucket = collections.defaultdict(list)
|
||||||
|
visited_choices = []
|
||||||
|
unvisted_count = 0
|
||||||
|
for door_one, door_two in proposed_map.items():
|
||||||
|
if door_one in explored_state.visited_doors:
|
||||||
|
visited_bucket[hook_from_door(door_one)].append(door_one)
|
||||||
|
visited_choices.append(door_one)
|
||||||
|
else:
|
||||||
|
unvisted_bucket[hook_from_door(door_one)].append(door_one)
|
||||||
|
unvisted_count += 1
|
||||||
|
if unvisted_count == 0:
|
||||||
|
# something is wrong beyond connectedness, crystal switch puzzle or bk layout - reshuffle
|
||||||
|
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||||
|
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||||
|
return proposed_map, hash_code
|
||||||
|
|
||||||
|
attempt, opp_hook = None, None
|
||||||
|
opp_hook_len = 0
|
||||||
|
while opp_hook_len == 0:
|
||||||
|
attempt = random.choice(visited_choices)
|
||||||
|
opp_hook = type_map[hook_from_door(attempt)]
|
||||||
|
opp_hook_len = len(unvisted_bucket[opp_hook])
|
||||||
|
unvisted_bucket[opp_hook].sort(key=lambda d: d.name)
|
||||||
|
new_door = random.choice(unvisted_bucket[opp_hook])
|
||||||
|
old_target = proposed_map[attempt]
|
||||||
|
proposed_map[attempt] = new_door
|
||||||
|
if not world.decoupledoors[player]:
|
||||||
|
old_attempt = proposed_map[new_door]
|
||||||
|
else:
|
||||||
|
old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door)
|
||||||
|
proposed_map[old_attempt] = old_target
|
||||||
|
if not world.decoupledoors[player]:
|
||||||
|
proposed_map[old_target] = old_attempt
|
||||||
|
proposed_map[new_door] = attempt
|
||||||
|
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||||
|
itr += 1
|
||||||
|
|
||||||
|
if not world.decoupledoors[player]:
|
||||||
|
logger.debug(f'Re-linking {attempt.name} <-> {new_door.name}')
|
||||||
|
logger.debug(f'Re-linking {old_attempt.name} <-> {old_target.name}')
|
||||||
|
else:
|
||||||
|
logger.debug(f'Re-Linking {attempt.name} -> {new_door.name}')
|
||||||
|
logger.debug(f'Re-Linking {old_attempt.name} -> {old_target.name}')
|
||||||
|
hash_code_set.add(hash_code)
|
||||||
|
return proposed_map, hash_code
|
||||||
|
|
||||||
|
|
||||||
|
def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors,
|
||||||
|
bk_needed, bk_special, world, player):
|
||||||
|
start = ExplorationState(dungeon=name)
|
||||||
|
start.big_key_special = bk_special
|
||||||
|
|
||||||
|
bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed
|
||||||
|
|
||||||
|
def exception(d):
|
||||||
|
return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS'
|
||||||
|
original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions,
|
||||||
|
valid_doors, bk_flag, world, player, exception)
|
||||||
|
return original_state
|
||||||
|
|
||||||
|
|
||||||
|
def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions,
|
||||||
|
bk_needed, bk_special, paths, entrance_regions, world, player):
|
||||||
|
all_visited = set()
|
||||||
|
all_visited.update(exploration_state.visited_blue)
|
||||||
|
all_visited.update(exploration_state.visited_orange)
|
||||||
|
if len(all_regions.difference(all_visited)) > 0:
|
||||||
|
return False
|
||||||
|
if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map,
|
||||||
|
bk_needed, bk_special, world, player):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
|
||||||
|
if not split_dungeon or bk_special:
|
||||||
|
for region in sector.regions:
|
||||||
|
for ext in region.exits:
|
||||||
|
door = world.check_for_door(ext.name, player)
|
||||||
|
if door is not None and door.bigKey:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_special(regions):
|
||||||
|
for region in regions:
|
||||||
|
for loc in region.locations:
|
||||||
|
if loc.forced_big_key():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map,
|
||||||
|
bk_needed, bk_special, world, player):
|
||||||
|
for path in paths:
|
||||||
|
if type(path) is tuple:
|
||||||
|
target = path[1]
|
||||||
|
start_regions = []
|
||||||
|
for region in all_regions:
|
||||||
|
if path[0] == region.name:
|
||||||
|
start_regions.append(region)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
target = path
|
||||||
|
start_regions = entrance_regions
|
||||||
|
if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions,
|
||||||
|
bk_needed, bk_special, world, player):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions,
|
||||||
|
bk_needed, bk_special, world, player):
|
||||||
|
target_regions = set()
|
||||||
|
if type(target) is not list:
|
||||||
|
for region in all_regions:
|
||||||
|
if target == region.name:
|
||||||
|
target_regions.add(region)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for region in all_regions:
|
||||||
|
if region.name in target:
|
||||||
|
target_regions.add(region)
|
||||||
|
|
||||||
|
start = ExplorationState(dungeon=name)
|
||||||
|
start.big_key_special = bk_special
|
||||||
|
bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed
|
||||||
|
|
||||||
|
def exception(d):
|
||||||
|
return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS'
|
||||||
|
original_state = extend_reachable_state_improved(starting_regions, start, proposed_map, all_regions,
|
||||||
|
valid_doors, bk_flag, world, player, exception)
|
||||||
|
|
||||||
|
for exp_door in original_state.unattached_doors:
|
||||||
|
if not exp_door.door.blocked:
|
||||||
|
return True # outstanding connection possible
|
||||||
|
for target in target_regions:
|
||||||
|
if original_state.visited_at_all(target):
|
||||||
|
return True
|
||||||
|
return False # couldn't find an outstanding door or the target
|
||||||
|
|
||||||
|
|
||||||
|
boss_path_checks = ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1', 'PoD Boss', 'Swamp Boss',
|
||||||
|
'Skull Boss', 'Ice Boss', 'Mire Boss', 'TR Boss', 'GT Agahnim 2']
|
||||||
|
|
||||||
|
# pinball is allowed to orphan you
|
||||||
|
drop_path_checks = ['Skull Pot Circle', 'Skull Left Drop', 'Skull Back Drop', 'Sewers Rat Path']
|
||||||
|
|
||||||
|
|
||||||
|
def determine_paths_for_dungeon(world, player, all_regions, name):
|
||||||
|
all_r_names = set(x.name for x in all_regions)
|
||||||
|
paths = []
|
||||||
|
non_hole_portals = []
|
||||||
|
for portal in world.dungeon_portals[player]:
|
||||||
|
if portal.door.entrance.parent_region in all_regions:
|
||||||
|
non_hole_portals.append(portal.door.entrance.parent_region.name)
|
||||||
|
if portal.destination:
|
||||||
|
paths.append(portal.door.entrance.parent_region.name)
|
||||||
|
if world.mode[player] == 'standard' and name == 'Hyrule Castle':
|
||||||
|
paths.append('Hyrule Dungeon Cellblock')
|
||||||
|
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
|
||||||
|
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
|
||||||
|
paths.append('Thieves Attic Window')
|
||||||
|
elif 'Thieves Attic Window' in all_r_names:
|
||||||
|
paths.append('Thieves Attic Window')
|
||||||
|
for boss in boss_path_checks:
|
||||||
|
if boss in all_r_names:
|
||||||
|
paths.append(boss)
|
||||||
|
if 'Thieves Boss' in all_r_names:
|
||||||
|
paths.append('Thieves Boss')
|
||||||
|
if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
|
||||||
|
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
|
||||||
|
for drop_check in drop_path_checks:
|
||||||
|
if drop_check in all_r_names:
|
||||||
|
paths.append((drop_check, non_hole_portals))
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def convert_regions(region_names, world, player):
|
||||||
|
region_list = []
|
||||||
|
for name in region_names:
|
||||||
|
region_list.append(world.get_region(name, player))
|
||||||
|
return region_list
|
||||||
|
|
||||||
|
|
||||||
|
type_map = {
|
||||||
|
Hook.Stairs: Hook.Stairs,
|
||||||
|
Hook.North: Hook.South,
|
||||||
|
Hook.South: Hook.North,
|
||||||
|
Hook.West: Hook.East,
|
||||||
|
Hook.East: Hook.West
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def connect_doors(a, b):
|
||||||
|
# Return on unsupported types.
|
||||||
|
if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
|
||||||
|
return
|
||||||
|
# Connect supported types
|
||||||
|
if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
||||||
|
if a.blocked:
|
||||||
|
connect_one_way(b.entrance, a.entrance)
|
||||||
|
elif b.blocked:
|
||||||
|
connect_one_way(a.entrance, b.entrance)
|
||||||
|
else:
|
||||||
|
connect_two_way(a.entrance, b.entrance)
|
||||||
|
dep_doors, target = [], None
|
||||||
|
if len(a.dependents) > 0:
|
||||||
|
dep_doors, target = a.dependents, b
|
||||||
|
elif len(b.dependents) > 0:
|
||||||
|
dep_doors, target = b.dependents, a
|
||||||
|
if target is not None:
|
||||||
|
target_region = target.entrance.parent_region
|
||||||
|
for dep in dep_doors:
|
||||||
|
connect_simple_door(dep, target_region)
|
||||||
|
return
|
||||||
|
# If we failed to account for a type, panic
|
||||||
|
raise RuntimeError('Unknown door type ' + a.type.name)
|
||||||
|
|
||||||
|
|
||||||
|
def connect_doors_one_way(a, b):
|
||||||
|
# Return on unsupported types.
|
||||||
|
if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
|
||||||
|
return
|
||||||
|
# Connect supported types
|
||||||
|
if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
||||||
|
if not a.blocked:
|
||||||
|
connect_one_way(a.entrance, b.entrance)
|
||||||
|
dep_doors, target = [], None
|
||||||
|
if len(a.dependents) > 0:
|
||||||
|
dep_doors, target = a.dependents, b
|
||||||
|
if target is not None:
|
||||||
|
target_region = target.entrance.parent_region
|
||||||
|
for dep in dep_doors:
|
||||||
|
connect_simple_door(dep, target_region)
|
||||||
|
return
|
||||||
|
# If we failed to account for a type, panic
|
||||||
|
raise RuntimeError('Unknown door type ' + a.type.name)
|
||||||
|
|
||||||
|
|
||||||
|
def connect_two_way(entrance, ext):
|
||||||
|
|
||||||
|
# if these were already connected somewhere, remove the backreference
|
||||||
|
if entrance.connected_region is not None:
|
||||||
|
entrance.connected_region.entrances.remove(entrance)
|
||||||
|
if ext.connected_region is not None:
|
||||||
|
ext.connected_region.entrances.remove(ext)
|
||||||
|
|
||||||
|
entrance.connect(ext.parent_region)
|
||||||
|
ext.connect(entrance.parent_region)
|
||||||
|
if entrance.parent_region.dungeon:
|
||||||
|
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
||||||
|
x = entrance.door
|
||||||
|
y = ext.door
|
||||||
|
if x is not None:
|
||||||
|
x.dest = y
|
||||||
|
if y is not None:
|
||||||
|
y.dest = x
|
||||||
|
|
||||||
|
|
||||||
|
def connect_one_way(entrance, ext):
|
||||||
|
|
||||||
|
# if these were already connected somewhere, remove the backreference
|
||||||
|
if entrance.connected_region is not None:
|
||||||
|
entrance.connected_region.entrances.remove(entrance)
|
||||||
|
|
||||||
|
entrance.connect(ext.parent_region)
|
||||||
|
if entrance.parent_region.dungeon:
|
||||||
|
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
||||||
|
x = entrance.door
|
||||||
|
if x is not None:
|
||||||
|
x.dest = ext.door
|
||||||
|
|
||||||
|
|
||||||
|
def connect_simple_door(exit_door, region):
|
||||||
|
exit_door.entrance.connect(region)
|
||||||
|
exit_door.dest = region
|
||||||
|
|
||||||
|
|
||||||
|
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
|
||||||
|
|
||||||
|
|
||||||
|
class ExplorationState(object):
|
||||||
|
|
||||||
|
def __init__(self, init_crystal=CrystalBarrier.Orange, dungeon=None):
|
||||||
|
|
||||||
|
self.unattached_doors = []
|
||||||
|
self.avail_doors = []
|
||||||
|
self.event_doors = []
|
||||||
|
|
||||||
|
self.visited_orange = []
|
||||||
|
self.visited_blue = []
|
||||||
|
self.visited_doors = set()
|
||||||
|
self.events = set()
|
||||||
|
self.crystal = init_crystal
|
||||||
|
|
||||||
|
# key region stuff
|
||||||
|
self.door_krs = {}
|
||||||
|
|
||||||
|
# key validation stuff
|
||||||
|
self.small_doors = []
|
||||||
|
self.big_doors = []
|
||||||
|
self.opened_doors = []
|
||||||
|
self.big_key_opened = False
|
||||||
|
self.big_key_special = False
|
||||||
|
|
||||||
|
self.found_locations = []
|
||||||
|
self.ttl_locations = 0
|
||||||
|
self.used_locations = 0
|
||||||
|
self.key_locations = 0
|
||||||
|
self.used_smalls = 0
|
||||||
|
self.bk_found = set()
|
||||||
|
|
||||||
|
self.non_door_entrances = []
|
||||||
|
self.dungeon = dungeon
|
||||||
|
self.pinball_used = False
|
||||||
|
|
||||||
|
self.prize_door_set = {}
|
||||||
|
self.prize_doors = []
|
||||||
|
self.prize_doors_opened = False
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
ret = ExplorationState(dungeon=self.dungeon)
|
||||||
|
ret.unattached_doors = list(self.unattached_doors)
|
||||||
|
ret.avail_doors = list(self.avail_doors)
|
||||||
|
ret.event_doors = list(self.event_doors)
|
||||||
|
ret.visited_orange = list(self.visited_orange)
|
||||||
|
ret.visited_blue = list(self.visited_blue)
|
||||||
|
ret.events = set(self.events)
|
||||||
|
ret.crystal = self.crystal
|
||||||
|
ret.door_krs = self.door_krs.copy()
|
||||||
|
|
||||||
|
ret.small_doors = list(self.small_doors)
|
||||||
|
ret.big_doors = list(self.big_doors)
|
||||||
|
ret.opened_doors = list(self.opened_doors)
|
||||||
|
ret.big_key_opened = self.big_key_opened
|
||||||
|
ret.big_key_special = self.big_key_special
|
||||||
|
ret.ttl_locations = self.ttl_locations
|
||||||
|
ret.key_locations = self.key_locations
|
||||||
|
ret.used_locations = self.used_locations
|
||||||
|
ret.used_smalls = self.used_smalls
|
||||||
|
ret.found_locations = list(self.found_locations)
|
||||||
|
ret.bk_found = set(self.bk_found)
|
||||||
|
|
||||||
|
ret.non_door_entrances = list(self.non_door_entrances)
|
||||||
|
ret.dungeon = self.dungeon
|
||||||
|
ret.pinball_used = self.pinball_used
|
||||||
|
|
||||||
|
ret.prize_door_set = dict(self.prize_door_set)
|
||||||
|
ret.prize_doors = list(self.prize_doors)
|
||||||
|
ret.prize_doors_opened = self.prize_doors_opened
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def next_avail_door(self):
|
||||||
|
self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2)
|
||||||
|
exp_door = self.avail_doors.pop()
|
||||||
|
self.crystal = exp_door.crystal
|
||||||
|
return exp_door
|
||||||
|
|
||||||
|
def visit_region(self, region, key_region=None, key_checks=False, bk_flag=False):
|
||||||
|
if region.type != RegionType.Dungeon:
|
||||||
|
self.crystal = CrystalBarrier.Orange
|
||||||
|
if self.crystal == CrystalBarrier.Either:
|
||||||
|
if region not in self.visited_blue:
|
||||||
|
self.visited_blue.append(region)
|
||||||
|
if region not in self.visited_orange:
|
||||||
|
self.visited_orange.append(region)
|
||||||
|
elif self.crystal == CrystalBarrier.Orange:
|
||||||
|
self.visited_orange.append(region)
|
||||||
|
elif self.crystal == CrystalBarrier.Blue:
|
||||||
|
self.visited_blue.append(region)
|
||||||
|
if region.type == RegionType.Dungeon:
|
||||||
|
for location in region.locations:
|
||||||
|
if key_checks and location not in self.found_locations:
|
||||||
|
if location.forced_item and 'Small Key' in location.item.name:
|
||||||
|
self.key_locations += 1
|
||||||
|
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
|
||||||
|
self.ttl_locations += 1
|
||||||
|
if location not in self.found_locations: # todo: special logic for TT Boss?
|
||||||
|
self.found_locations.append(location)
|
||||||
|
if not bk_flag:
|
||||||
|
self.bk_found.add(location)
|
||||||
|
if location.name in dungeon_events and location.name not in self.events:
|
||||||
|
if self.flooded_key_check(location):
|
||||||
|
self.perform_event(location.name, key_region)
|
||||||
|
if location.name in flooded_keys_reverse.keys() and self.location_found(
|
||||||
|
flooded_keys_reverse[location.name]):
|
||||||
|
self.perform_event(flooded_keys_reverse[location.name], key_region)
|
||||||
|
if '- Prize' in location.name:
|
||||||
|
self.prize_received = True
|
||||||
|
|
||||||
|
def flooded_key_check(self, location):
|
||||||
|
if location.name not in flooded_keys.keys():
|
||||||
|
return True
|
||||||
|
return flooded_keys[location.name] in [x.name for x in self.found_locations]
|
||||||
|
|
||||||
|
def location_found(self, location_name):
|
||||||
|
for l in self.found_locations:
|
||||||
|
if l.name == location_name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def perform_event(self, location_name, key_region):
|
||||||
|
self.events.add(location_name)
|
||||||
|
queue = collections.deque(self.event_doors)
|
||||||
|
while len(queue) > 0:
|
||||||
|
exp_door = queue.popleft()
|
||||||
|
if exp_door.door.req_event == location_name:
|
||||||
|
self.avail_doors.append(exp_door)
|
||||||
|
self.event_doors.remove(exp_door)
|
||||||
|
if key_region is not None:
|
||||||
|
d_name = exp_door.door.name
|
||||||
|
if d_name not in self.door_krs.keys():
|
||||||
|
self.door_krs[d_name] = key_region
|
||||||
|
|
||||||
|
def add_all_entrance_doors_check_unattached(self, region, world, player):
|
||||||
|
door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]]
|
||||||
|
door_list.extend(get_entrance_doors(world, region, player))
|
||||||
|
for door in door_list:
|
||||||
|
if self.can_traverse(door):
|
||||||
|
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
|
||||||
|
self.append_door_to_list(door, self.unattached_doors)
|
||||||
|
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
|
||||||
|
self.event_doors):
|
||||||
|
self.append_door_to_list(door, self.event_doors)
|
||||||
|
elif not self.in_door_list(door, self.avail_doors):
|
||||||
|
self.append_door_to_list(door, self.avail_doors)
|
||||||
|
for entrance in region.entrances:
|
||||||
|
door = world.check_for_door(entrance.name, player)
|
||||||
|
if door is None:
|
||||||
|
self.non_door_entrances.append(entrance)
|
||||||
|
|
||||||
|
def add_all_doors_check_unattached(self, region, world, player):
|
||||||
|
for door in get_doors(world, region, player):
|
||||||
|
if self.can_traverse(door):
|
||||||
|
if door.controller is not None:
|
||||||
|
door = door.controller
|
||||||
|
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
|
||||||
|
self.append_door_to_list(door, self.unattached_doors)
|
||||||
|
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
|
||||||
|
self.event_doors):
|
||||||
|
self.append_door_to_list(door, self.event_doors)
|
||||||
|
elif not self.in_door_list(door, self.avail_doors):
|
||||||
|
self.append_door_to_list(door, self.avail_doors)
|
||||||
|
|
||||||
|
def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, flag, world, player, exception):
|
||||||
|
for door in get_doors(world, region, player):
|
||||||
|
if door in proposed_map and door.name in valid_doors:
|
||||||
|
self.visited_doors.add(door)
|
||||||
|
if door.blocked and exception(door):
|
||||||
|
self.pinball_used = True
|
||||||
|
if self.can_traverse(door, exception):
|
||||||
|
if door.controller is not None:
|
||||||
|
door = door.controller
|
||||||
|
if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors:
|
||||||
|
if not self.in_door_list_ic(door, self.unattached_doors):
|
||||||
|
self.append_door_to_list(door, self.unattached_doors, flag)
|
||||||
|
else:
|
||||||
|
other = self.find_door_in_list(door, self.unattached_doors)
|
||||||
|
if self.crystal != other.crystal:
|
||||||
|
other.crystal = CrystalBarrier.Either
|
||||||
|
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
|
||||||
|
self.event_doors):
|
||||||
|
self.append_door_to_list(door, self.event_doors, flag)
|
||||||
|
elif not self.in_door_list(door, self.avail_doors):
|
||||||
|
self.append_door_to_list(door, self.avail_doors, flag)
|
||||||
|
|
||||||
|
def add_all_doors_check_key_region(self, region, key_region, world, player):
|
||||||
|
for door in get_doors(world, region, player):
|
||||||
|
if self.can_traverse(door):
|
||||||
|
if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
|
||||||
|
self.event_doors):
|
||||||
|
self.append_door_to_list(door, self.event_doors)
|
||||||
|
elif not self.in_door_list(door, self.avail_doors):
|
||||||
|
self.append_door_to_list(door, self.avail_doors)
|
||||||
|
if door.name not in self.door_krs.keys():
|
||||||
|
self.door_krs[door.name] = key_region
|
||||||
|
else:
|
||||||
|
if door.name not in self.door_krs.keys():
|
||||||
|
self.door_krs[door.name] = key_region
|
||||||
|
|
||||||
|
def add_all_doors_check_keys(self, region, key_door_proposal, world, player):
|
||||||
|
for door in get_doors(world, region, player):
|
||||||
|
if self.can_traverse(door):
|
||||||
|
if door.controller:
|
||||||
|
door = door.controller
|
||||||
|
if door in key_door_proposal and door not in self.opened_doors:
|
||||||
|
if not self.in_door_list(door, self.small_doors):
|
||||||
|
self.append_door_to_list(door, self.small_doors)
|
||||||
|
elif (door.bigKey or door.name in special_big_key_doors) and not self.big_key_opened:
|
||||||
|
if not self.in_door_list(door, self.big_doors):
|
||||||
|
self.append_door_to_list(door, self.big_doors)
|
||||||
|
elif door.req_event is not None and door.req_event not in self.events:
|
||||||
|
if not self.in_door_list(door, self.event_doors):
|
||||||
|
self.append_door_to_list(door, self.event_doors)
|
||||||
|
elif not self.in_door_list(door, self.avail_doors):
|
||||||
|
self.append_door_to_list(door, self.avail_doors)
|
||||||
|
|
||||||
|
def visited(self, region):
|
||||||
|
if self.crystal == CrystalBarrier.Either:
|
||||||
|
return region in self.visited_blue and region in self.visited_orange
|
||||||
|
elif self.crystal == CrystalBarrier.Orange:
|
||||||
|
return region in self.visited_orange
|
||||||
|
elif self.crystal == CrystalBarrier.Blue:
|
||||||
|
return region in self.visited_blue
|
||||||
|
return False
|
||||||
|
|
||||||
|
def visited_at_all(self, region):
|
||||||
|
return region in self.visited_blue or region in self.visited_orange
|
||||||
|
|
||||||
|
def found_forced_bk(self):
|
||||||
|
for location in self.found_locations:
|
||||||
|
if location.forced_big_key():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_traverse(self, door, exception=None):
|
||||||
|
if door.blocked:
|
||||||
|
return exception(door) if exception else False
|
||||||
|
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
|
||||||
|
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
|
||||||
|
return True
|
||||||
|
|
||||||
|
def count_locations_exclude_specials(self, world, player):
|
||||||
|
return count_locations_exclude_big_chest(self.found_locations, world, player)
|
||||||
|
|
||||||
|
def validate(self, door, region, world, player):
|
||||||
|
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
|
||||||
|
world, player)
|
||||||
|
|
||||||
|
def in_door_list(self, door, door_list):
|
||||||
|
for d in door_list:
|
||||||
|
if d.door == door and d.crystal == self.crystal:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def in_door_list_ic(door, door_list):
|
||||||
|
for d in door_list:
|
||||||
|
if d.door == door:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_door_in_list(door, door_list):
|
||||||
|
for d in door_list:
|
||||||
|
if d.door == door:
|
||||||
|
return d
|
||||||
|
return None
|
||||||
|
|
||||||
|
def append_door_to_list(self, door, door_list, flag=False):
|
||||||
|
if door.crystal == CrystalBarrier.Null:
|
||||||
|
door_list.append(ExplorableDoor(door, self.crystal, flag))
|
||||||
|
else:
|
||||||
|
door_list.append(ExplorableDoor(door, door.crystal, flag))
|
||||||
|
|
||||||
|
def key_door_sort(self, d):
|
||||||
|
if d.door.smallKey:
|
||||||
|
if d.door in self.opened_doors:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def count_locations_exclude_big_chest(locations, world, player):
|
||||||
|
cnt = 0
|
||||||
|
for loc in locations:
|
||||||
|
if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player)
|
||||||
|
and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)):
|
||||||
|
cnt += 1
|
||||||
|
return cnt
|
||||||
|
|
||||||
|
|
||||||
|
def prize_or_event(loc):
|
||||||
|
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
||||||
|
|
||||||
|
|
||||||
|
def reserved_location(loc, world, player):
|
||||||
|
return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player]
|
||||||
|
|
||||||
|
|
||||||
|
def blind_boss_unavail(loc, locations, world, player):
|
||||||
|
if loc.name == "Thieves' Town - Boss":
|
||||||
|
return (loc.parent_region.dungeon.boss.name == 'Blind' and
|
||||||
|
(not any(x for x in locations if x.name == 'Suspicious Maiden') or
|
||||||
|
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
|
||||||
|
not any(x for x in locations if x.name == 'Attic Cracked Floor'))))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ExplorableDoor(object):
|
||||||
|
|
||||||
|
def __init__(self, door, crystal, flag):
|
||||||
|
self.door = door
|
||||||
|
self.crystal = crystal
|
||||||
|
self.flag = flag
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.__unicode__())
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return '%s (%s)' % (self.door.name, self.crystal.name)
|
||||||
|
|
||||||
|
|
||||||
|
def extend_reachable_state_improved(search_regions, state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception):
|
||||||
|
local_state = state.copy()
|
||||||
|
for region in search_regions:
|
||||||
|
local_state.visit_region(region)
|
||||||
|
local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player, exception)
|
||||||
|
while len(local_state.avail_doors) > 0:
|
||||||
|
explorable_door = local_state.next_avail_door()
|
||||||
|
if explorable_door.door.bigKey:
|
||||||
|
if bk_flag:
|
||||||
|
big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special
|
||||||
|
else local_state.count_locations_exclude_specials(world, player) == 0)
|
||||||
|
if big_not_found:
|
||||||
|
continue # we can't open this door
|
||||||
|
if explorable_door.door in proposed_map:
|
||||||
|
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
|
||||||
|
else:
|
||||||
|
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
|
||||||
|
if connect_region is not None:
|
||||||
|
if valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(
|
||||||
|
connect_region):
|
||||||
|
flag = explorable_door.flag or explorable_door.door.bigKey
|
||||||
|
local_state.visit_region(connect_region, bk_flag=flag)
|
||||||
|
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception)
|
||||||
|
return local_state
|
||||||
|
|
||||||
|
|
||||||
|
def special_big_key_found(state):
|
||||||
|
for location in state.found_locations:
|
||||||
|
if location.forced_item and location.forced_item.bigkey:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def valid_region_to_explore_in_regions(region, all_regions, world, player):
|
||||||
|
if region is None:
|
||||||
|
return False
|
||||||
|
return ((region.type == RegionType.Dungeon and region in all_regions)
|
||||||
|
or region.name in world.inaccessible_regions[player]
|
||||||
|
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
|
||||||
|
|
||||||
|
|
||||||
|
def valid_region_to_explore(region, name, world, player):
|
||||||
|
if region is None:
|
||||||
|
return False
|
||||||
|
return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
|
||||||
|
or region.name in world.inaccessible_regions[player]
|
||||||
|
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_doors(world, region, player):
|
||||||
|
res = []
|
||||||
|
for ext in region.exits:
|
||||||
|
door = world.check_for_door(ext.name, player)
|
||||||
|
if door is not None:
|
||||||
|
res.append(door)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def get_entrance_doors(world, region, player):
|
||||||
|
res = []
|
||||||
|
for ext in region.entrances:
|
||||||
|
door = world.check_for_door(ext.name, player)
|
||||||
|
if door is not None:
|
||||||
|
res.append(door)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class GenerationException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ def roll_settings(weights):
|
|||||||
door_shuffle = get_choice('door_shuffle')
|
door_shuffle = get_choice('door_shuffle')
|
||||||
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
|
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
|
||||||
ret.intensity = get_choice('intensity')
|
ret.intensity = get_choice('intensity')
|
||||||
|
ret.decoupledoors = get_choice('decoupledoors') == 'on'
|
||||||
ret.experimental = get_choice('experimental') == 'on'
|
ret.experimental = get_choice('experimental') == 'on'
|
||||||
ret.collection_rate = get_choice('collection_rate') == 'on'
|
ret.collection_rate = get_choice('collection_rate') == 'on'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user