Key Door Shuffle algorithm added

This commit is contained in:
aerinon
2019-10-03 16:27:44 -06:00
parent 5cc4fdfa1f
commit 92ad60bedf
5 changed files with 375 additions and 84 deletions

View File

@@ -1,7 +1,9 @@
import random
import collections
import logging
import operator as op
from functools import reduce
from BaseClasses import RegionType, DoorType, Direction, Sector, pol_idx
from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions
from Dungeons import dungeon_regions
@@ -91,6 +93,14 @@ def create_door_spoiler(world, player):
logger.debug('Door not found in queue: %s connected to %s', door_b.name, door_a.name)
else:
logger.warning('Door not connected: %s', door_a.name)
for room in world.rooms:
if room.modified:
logger.debug('Room %s changed (p%d)', room.index, player)
for dp in world.paired_doors[player]:
if dp.pair:
logger.debug('Paired Doors: %s with %s (p%d)', dp.door_a, dp.door_b, player)
else:
logger.debug('Unpaired Doors: %s not paired with %s (p%d)', dp.door_a, dp.door_b, player)
# some useful functions
@@ -414,8 +424,28 @@ def experiment(world, player):
for idx, sector_list in enumerate(dp_split):
dungeon_sectors.append((sector_list, desert_default_entrance_sets[idx]))
dungeon_layouts = []
for sector_list, entrance_list in dungeon_sectors:
shuffle_dungeon_no_repeats(world, player, sector_list, entrance_list)
ds = shuffle_dungeon_no_repeats(world, player, sector_list, entrance_list)
dungeon_layouts.append((ds, entrance_list))
desert_combined = None
desert_entrances = []
queue = collections.deque(dungeon_layouts)
while len(queue) > 0:
sector, entrance_list = queue.pop()
if entrance_list in desert_default_entrance_sets:
dungeon_layouts.remove((sector, entrance_list))
desert_entrances.extend(entrance_list)
if desert_combined is None:
desert_combined = sector
else:
desert_combined.regions.extend(sector.regions)
dungeon_layouts.append((desert_combined, desert_entrances))
# shuffle_key_doors for dungeons
for layout in dungeon_layouts:
shuffle_key_doors(layout[0], layout[1], world, player)
def convert_regions(region_names, world, player):
@@ -642,7 +672,7 @@ def shuffle_dungeon_no_repeats(world, player, available_sectors, entrance_region
connect_door = compatibles.pop()
logger.info(' Adding loop via %s', connect_door.name)
# Check if valid
if is_loop_valid(door, connect_door, sector, len(available_sectors) == 1):
if is_loop_valid(door, connect_door, sector, available_sectors):
maybe_connect_two_way(world, door, connect_door, player)
reachable_doors.remove(door)
reachable_doors.remove(connect_door)
@@ -739,8 +769,10 @@ def is_valid(door_a, door_b, sector_a, sector_b, available_sectors):
return False # not sure how we got here, but it's a bad idea
def is_loop_valid(door_a, door_b, sector, no_more_sectors):
if no_more_sectors:
def is_loop_valid(door_a, door_b, sector, available_sectors):
if len(available_sectors) == 1:
return True
if len(available_sectors) == 2 and door_b not in sector.outstanding_doors:
return True
elif not door_a.blocked and not door_b.blocked:
return sector.outflow() - 1 > 0
@@ -775,6 +807,254 @@ def are_there_outstanding_doors_of_type(door_a, door_b, sector_a, sector_b, avai
return True
def shuffle_key_doors(dungeon_sector, entrances, world, player):
start_regions = convert_regions(entrances, world, player)
# count number of key doors
num_key_doors = 0
current_doors = []
skips = []
for region in dungeon_sector.regions:
for ext in region.exits:
d = world.check_for_door(ext.name, player)
if d is not None and d.smallKey:
current_doors.append(d)
if d not in skips:
if d.type == DoorType.Interior:
skips.append(d.dest)
if d.type == DoorType.Normal:
for dp in world.paired_doors[player]:
if d.name == dp.door_a:
skips.append(world.get_door(dp.door_b, player))
break
elif d.name == dp.door_b:
skips.append(world.get_door(dp.door_a, player))
break
num_key_doors += 1
# traverse dungeon and find candidates
candidates = []
checked_doors = set()
for region in start_regions:
possible, checked = find_key_door_candidates(region, checked_doors, world, player)
candidates.extend(possible)
checked_doors.update(checked)
flat_candidates = []
for candidate in candidates:
# not valid if: Normal 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:
flat_candidates.append(candidate)
# find valid combination of candidates
paired_candidates = build_pair_list(flat_candidates)
if len(paired_candidates) < num_key_doors:
raise Exception('Not enough candidates')
random.shuffle(paired_candidates)
combinations = ncr(len(paired_candidates), num_key_doors)
itr = 0
proposal = kth_combination(itr, paired_candidates, num_key_doors)
while not validate_key_layout(start_regions, proposal, world, player):
itr += 1
proposal = kth_combination(itr, paired_candidates, num_key_doors)
if itr > combinations:
raise Exception('No valid key layouts!')
# make changes
reassign_key_doors(current_doors, proposal, world, player)
def build_pair_list(flat_list):
paired_list = []
queue = collections.deque(flat_list)
while len(queue) > 0:
d = queue.pop()
if d.dest in queue:
paired_list.append((d, d.dest))
queue.remove(d.dest)
else:
paired_list.append(d)
return paired_list
def flatten_pair_list(paired_list):
flat_list = []
for d in paired_list:
if type(d) is tuple:
flat_list.append(d[0])
flat_list.append(d[1])
else:
flat_list.append(d)
return flat_list
def find_key_door_candidates(region, checked, world, player):
candidates = []
checked_doors = list(checked)
queue = collections.deque([(region, None)])
while len(queue) > 0:
current, last_door = queue.pop()
for ext in current.exits:
d = world.check_for_door(ext.name, player)
if d is not None and not d.blocked and d.dest is not last_door and d not in checked_doors:
valid = False
if 0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs]:
room = world.get_room(d.roomIndex, player)
position, kind = room.doorList[d.doorListPos]
if d.type == DoorType.Interior:
valid = kind in [DoorKind.Normal, DoorKind.NormalLow, DoorKind.SmallKey, DoorKind.Bombable,
DoorKind.Dashable, DoorKind.NormalLow2]
elif d.type == DoorType.SpiralStairs:
valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow]
elif d.type == DoorType.Normal:
d2 = d.dest
if d2 not in candidates:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
okay_normals = [DoorKind.Normal, DoorKind.NormalLow, DoorKind.SmallKey, DoorKind.Bombable,
DoorKind.Dashable, DoorKind.NormalLow2, DoorKind.Warp, DoorKind.DungeonChanger]
valid = kind in okay_normals and kind_b in okay_normals
else:
valid = True
if valid:
candidates.append(d)
queue.append((ext.connected_region, d))
if d is not None:
checked_doors.append(d)
return candidates, checked_doors
def kth_combination(k, l, r):
if r == 0:
return []
elif len(l) == r:
return l
else:
i = ncr(len(l)-1, r-1)
if k < i:
return l[0:1] + kth_combination(k, l[1:], r-1)
else:
return kth_combination(k-i, l[1:], r)
def ncr(n, r):
r = min(r, n-r)
numerator = reduce(op.mul, range(n, n-r, -1), 1)
denominator = reduce(op.mul, range(1, r+1), 1)
return numerator / denominator
def validate_key_layout(start_regions, key_door_proposal, world, player):
flat_proposal = flatten_pair_list(key_door_proposal)
available_doors = [] # Doors to explore
big_key_doors = []
small_key_doors = []
big_key_opened = False
visited_regions = set() # Regions we've been to and don't need to expand
ttl_locations = 0
used_locations = 0
# Everything in a start region is in key region 0.
for region in start_regions:
visited_regions.add(region)
ttl_locations += len(region.locations)
add_doors_to_lists(region, flat_proposal, available_doors, small_key_doors,
big_key_doors, big_key_opened, world, player)
while len(available_doors) > 0:
door = available_doors.pop()
connect_region = world.get_entrance(door.name, player).connected_region
if not door.blocked and connect_region not in visited_regions:
visited_regions.add(connect_region)
ttl_locations += len(connect_region.locations)
add_doors_to_lists(connect_region, flat_proposal, available_doors, small_key_doors,
big_key_doors, big_key_opened, world, player)
if len(available_doors) == 0:
num_smalls = 0
for small in small_key_doors:
if small.dest in small_key_doors:
num_smalls += 0.5 # half now, half with the dest
else:
num_smalls += 1
num_bigs = 1 if len(big_key_doors) > 0 else 0 # all or nothing
if num_smalls == 0 and num_bigs == 0:
return True # I think that's the end
available_locations = ttl_locations - used_locations
if available_locations >= num_smalls > 0: # todo: this not lenient at all - need a recursive function maybe
available_doors.extend(small_key_doors)
small_key_doors.clear()
used_locations += num_smalls
elif not big_key_opened and available_locations >= num_bigs > 0:
big_key_opened = True
used_locations += 1 # todo: this does not handle hc big key in crossed modes
available_doors.extend(big_key_doors)
big_key_doors.clear()
else:
return False
return len(small_key_doors) == 0 and len(big_key_doors) == 0
def add_doors_to_lists(region, key_door_proposal, available_doors, small_key_doors,
big_key_doors, big_key_opened, world, player):
for door in get_doors(world, region, player):
if not door.blocked:
if door in key_door_proposal and door not in small_key_doors:
small_key_doors.append(door)
elif door.bigKey and not big_key_opened and door not in big_key_doors:
big_key_doors.append(door)
elif door not in available_doors:
available_doors.append(door)
def reassign_key_doors(current_doors, proposal, world, player):
flat_proposal = flatten_pair_list(proposal)
queue = collections.deque(current_doors)
while len(queue) > 0:
d = queue.pop()
if d.type is DoorType.SpiralStairs and d not in proposal:
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
d.smallKey = False
elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal:
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
d.smallKey = False
d.dest.smallKey = False
queue.remove(d.dest)
elif d.type is DoorType.Normal and d not in flat_proposal:
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
d.smallKey = False
for dp in world.paired_doors[player]:
if dp.door_a == d.name or dp.door_b == d.name:
dp.pair = False
for obj in proposal:
if type(obj) is tuple:
d1 = obj[0]
d2 = obj[1]
if d1.type is DoorType.Interior:
change_door_to_small_key(d1, world, player)
else:
names = [d1.name, d2.name]
found = False
for dp in world.paired_doors[player]:
if dp.door_a in names and dp.door_b in names:
dp.pair = True
found = True
if not found:
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
change_door_to_small_key(d1, world, player)
change_door_to_small_key(d2, world, player)
else:
d = obj
if d.type is DoorType.Interior:
change_door_to_small_key(d, world, player)
elif d.type is DoorType.SpiralStairs:
pass # we don't have spiral stairs candidates yet that aren't already key doors
elif d.type is DoorType.Normal:
change_door_to_small_key(d, world, player)
def change_door_to_small_key(d, world, player):
d.smallKey = True
room = world.get_room(d.roomIndex, player)
if room.doorList[d.doorListPos][1] != DoorKind.SmallKey:
room.change(d.doorListPos, DoorKind.SmallKey)
# DATA GOES DOWN HERE
@@ -894,7 +1174,7 @@ interior_doors = [
('Tower Lone Statue WN', 'Tower Dark Maze EN'),
('Tower Dark Maze ES', 'Tower Dark Chargers WS'),
('Tower Dual Statues WS', 'Tower Dark Pits ES'),
('Tower Dark Pits EN', 'Tower Dark Archers WS'),
('Tower Dark Pits EN', 'Tower Dark Archers WN'),
('Tower Red Spears WN', 'Tower Red Guards EN'),
('Tower Red Guards SW', 'Tower Circle of Pots NW'),
('Tower Circle of Pots WS', 'Tower Pacifist Run ES'),
@@ -1027,7 +1307,7 @@ desert_default_entrance_sets = [
# 'Skull': ['Skull 1 Lobby', 'Skull 2 Mummy Lobby', 'Skull 2 Key Lobby', 'Skull 3 Lobby'],
entrance_sets = [
['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Secret Room'],
['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Secret Room', 'Sanctuary'],
['Eastern Lobby'],
['Hera Lobby'],
['Tower Lobby'],