diff --git a/BaseClasses.py b/BaseClasses.py index 350c28c0..91bdc097 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -867,6 +867,7 @@ class Door(object): 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.landing = False def getAddress(self): if self.type == DoorType.Normal: @@ -908,22 +909,32 @@ class Sector(object): def __init__(self): self.regions = [] - self.oustandings_doors = [] + self.outstanding_doors = [] + # todo: make these lazy init? - when do you invalidate them def polarity(self): polarity = [0, 0, 0] - for door in self.oustandings_doors: - idx, inc = pol_idx[door.direction] - polarity[idx] = pol_inc[inc](polarity[idx]) + for door in self.outstanding_doors: + if not door.landing: + idx, inc = pol_idx[door.direction] + polarity[idx] = pol_inc[inc](polarity[idx]) return polarity def magnitude(self): magnitude = [0, 0, 0] - for door in self.oustandings_doors: - idx, inc = pol_idx[door.direction] - magnitude[idx] = magnitude[idx] + 1 + for door in self.outstanding_doors: + if not door.landing: + 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 + class Boss(object): def __init__(self, name, enemizer_name, defeat_rule, player): diff --git a/DoorShuffle.py b/DoorShuffle.py index 2908c8b0..f4066576 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -11,8 +11,8 @@ def link_doors(world, player): for exitName, regionName in mandatory_connections: connect_simple_door(world, exitName, regionName, player) # These should all be connected for now as normal connections - for edge_a, edge_b in intratile_doors: - connect_intertile_door(world, edge_a, edge_b, player) + for edge_a, edge_b in interior_doors: + connect_two_way(world, edge_a, edge_b, player) # These connection are here because they are currently unable to be shuffled if world.doorShuffle not in ['basic', 'experimental']: # these modes supports spirals @@ -110,20 +110,6 @@ def connect_simple_door(world, exit_name, region_name, player): d.dest = region -def connect_intertile_door(world, edge_1, edge_2, player): - ent_a = world.get_entrance(edge_1, player) - ent_b = world.get_entrance(edge_2, player) - - # if these were already connected somewhere, remove the backreference - if ent_a.connected_region is not None: - ent_a.connected_region.entrances.remove(ent_a) - if ent_b.connected_region is not None: - ent_b.connected_region.entrances.remove(ent_b) - - ent_a.connect(ent_b.parent_region) - ent_b.connect(ent_a.parent_region) - - def connect_two_way(world, entrancename, exitname, player): entrance = world.get_entrance(entrancename, player) ext = world.get_entrance(exitname, player) @@ -134,7 +120,6 @@ def connect_two_way(world, entrancename, exitname, player): if ext.connected_region is not None: ext.connected_region.entrances.remove(ext) - # todo - access rules for the doors... entrance.connect(ext.parent_region) ext.connect(entrance.parent_region) if entrance.parent_region.dungeon: @@ -237,7 +222,7 @@ def shuffle_dungeon(world, player, start_region_names, dungeon_region_names): available_doors.remove(connect_door) else: # If there's no available region with a door, use an internal connection - connect_door = find_compatible_door_in_list(world, door, available_doors, player) + connect_door = find_compatible_door_in_list(ugly_regions, world, door, available_doors, player) if connect_door is not None: logger.info(' Adding loop via %s', connect_door.name) maybe_connect_two_way(world, door, connect_door, player) @@ -735,22 +720,23 @@ def experiment(world, player): hc = convert_to_sectors(hyrule_castle_regions, world, player) ep = convert_to_sectors(eastern_regions, world, player) dp = convert_to_sectors(desert_regions, world, player) - dungeon_sectors = [hc, ep, dp] + dungeon_sectors = [hc, ep] + dp_split = split_up_sectors(dp, desert_default_entrance_sets) + dungeon_sectors.extend(dp_split) + for sector_list in dungeon_sectors: for sector in sector_list: for region in sector.regions: print(region.name) - for door in sector.oustandings_doors: + for door in sector.outstanding_doors: print(door.name) print('pol: ' + str(sector.polarity())) print('mag: ' + str(sector.magnitude())) print() print() - split_up_sectors(dp, desert_default_entrance_sets) - dungeon_region_lists = [hyrule_castle_regions, eastern_regions, desert_regions] - for region_list in dungeon_region_lists: - shuffle_dungeon_no_repeats(world, player, region_list) + for sector_list in dungeon_sectors: + shuffle_dungeon_no_repeats(world, player, sector_list) # for ent, ext in experimental_connections: # if world.get_door(ent, player).blocked: # connect_one_way(world, ext, ent, player) @@ -792,7 +778,7 @@ def convert_to_sectors(region_names, world, player): outstanding_doors.append(door) sector = Sector() sector.regions.extend(region_chunk) - sector.oustandings_doors.extend(outstanding_doors) + sector.outstanding_doors.extend(outstanding_doors) sectors.append(sector) return sectors @@ -817,18 +803,17 @@ def split_up_sectors(sector_list, entrance_sets): print('mag:'+str(sum_vector(s_list, lambda s: s.magnitude()))) print('pol:'+str(sum_vector(leftover_sectors, lambda s: s.polarity()))) print('mag:'+str(sum_vector(leftover_sectors, lambda s: s.magnitude()))) - - + assignment(new_sector_grid, leftover_sectors) return new_sector_grid def sum_vector(sector_list, func): - sum = [0, 0, 0] + result = [0, 0, 0] for sector in sector_list: vector = func(sector) - for i in range(len(sum)): - sum[i] = sum[i] + vector[i] - return sum + for i in range(len(result)): + result[i] = result[i] + vector[i] + return result # def add_vectors(vector_one, vector_two): @@ -839,10 +824,10 @@ def sum_vector(sector_list, func): def is_polarity_neutral(polarity): - neutral = 0 for value in polarity: - neutral = neutral + value - return neutral == 0 + if value != 0: + return False + return True def is_proposal_valid(proposal, buckets, candidates): @@ -850,9 +835,9 @@ def is_proposal_valid(proposal, buckets, candidates): for i in range(len(proposal)): if proposal[i] is -1: return False # indicates an incomplete proposal - test_bucket = [[]]*len(buckets) + test_bucket = [] for bucket_idx in range(len(buckets)): - test_bucket[bucket_idx].extend(buckets[bucket_idx]) + test_bucket.append(list(buckets[bucket_idx])) for i in range(len(proposal)): test_bucket[proposal[i]].append(candidates[i]) for test in test_bucket: @@ -863,28 +848,41 @@ def is_proposal_valid(proposal, buckets, candidates): def assignment(buckets, candidates): - random.shuffle(buckets) + # for a faster search - instead of random - put the most likely culprits to cause problems at the end, least likely at the front + # unless we start checking for failures earlier in the algo random.shuffle(candidates) proposal = [-1]*len(candidates) - solution = next_proposal(proposal, buckets, candidates) + solution = find_proposal(proposal, buckets, candidates) if solution is None: raise Exception('Unable to find a proposal') - # todo: use solution to assign to buckets and candidates - # buckets = + for i in range(len(solution)): + buckets[solution[i]].append(candidates[i]) -def next_proposal(proposal, buckets, candidates): - if is_proposal_valid(proposal, buckets, candidates): - return proposal +# this is a DFS search +def find_proposal(proposal, buckets, candidates): + size = len(candidates) + combination_grid = [] + for i in range(size): + combination_grid.append(list(range(len(buckets)))) + # randomize which bucket + for possible_buckets in combination_grid: + random.shuffle(possible_buckets) - next_candidate_idx = proposal.index(-1) - for i in range(len(buckets)): # todo: this produces a weighted solution unfortunately, good for a mode called OneBigHappyDungeon in crossed, not good for a balanced, or random approach - proposal[next_candidate_idx] = i - found_proposal = next_proposal(proposal, buckets, candidates) - if found_proposal is not None: - return found_proposal - return None # there was no valid assignment + idx = 0 + while idx != size or not is_proposal_valid(proposal, buckets, candidates): + if idx == size: + idx = idx - 1 + while len(combination_grid[idx]) == 0: + if idx == -1: # this is the failure case - we shouldn't hit it + return None + combination_grid[idx] = list(range(len(buckets))) + idx = idx - 1 + proposal[idx] = combination_grid[idx].pop() + # can we detect a bad choice at this stage + idx = idx + 1 + return proposal # code below is for an algorithm without restarts @@ -898,23 +896,19 @@ def next_proposal(proposal, buckets, candidates): # Nuts, normal loop -def shuffle_dungeon_no_repeats(world, player, dungeon_region_names): +def shuffle_dungeon_no_repeats(world, player, available_sectors): logger = logging.getLogger('') - available_regions = [] - for name in dungeon_region_names: - available_regions.append(world.get_region(name, player)) - random.shuffle(available_regions) + + random.shuffle(available_sectors) available_doors = [] # this is interesting but I want to ensure connectedness # Except for Desert1/2 and Skull 1/2/3 - treat them as separate dungeons? - while len(available_regions) > 0: + while len(available_sectors) > 0: # Pick a random region and make its doors the open set - region = available_regions.pop() - logger.info("Starting in %s", region.name) - regions = find_connected_regions(region, available_regions, logger) - for region in regions: - available_doors.extend(get_doors_ex(world, region, player)) + sector = available_sectors.pop() + current_sector = sector + available_doors.extend(sector.outstanding_doors) # Loop until all available doors are used while len(available_doors) > 0: @@ -923,62 +917,75 @@ def shuffle_dungeon_no_repeats(world, player, dungeon_region_names): door = available_doors.pop() logger.info('Linking %s', door.name) # Find an available region that has a compatible door - connect_region, connect_door = find_compatible_door_in_regions_ex(world, door, available_regions, player) - if connect_region is not None: - logger.info(' Found new region %s via %s', connect_region.name, connect_door.name) + connect_sector, connect_door = find_compatible_door_in_sectors_ex(world, door, available_sectors, player) + if connect_sector is not None: + logger.info(' Found new sector via %s', connect_door.name) + if door not in current_sector.outstanding_doors: + raise Exception('Door %s was not in sector', door.name) + # Check if valid + if not is_valid(door, connect_door, current_sector, connect_sector, len(available_sectors) == 1): + logger.info(' Not Linking %s to %s', door.name, connect_door.name) + available_doors.insert(0, door) + continue # Apply connection and add the new region's doors to the available list maybe_connect_two_way(world, door, connect_door, player) - c_regions = find_connected_regions(connect_region, available_regions, logger) - for region in c_regions: - available_doors.extend(get_doors_ex(world, region, player)) - # We've used this region and door, so don't use them again - available_doors.remove(connect_door) - available_regions.remove(connect_region) + current_sector.outstanding_doors.remove(door) + connect_sector.outstanding_doors.remove(connect_door) + available_doors.extend(connect_sector.outstanding_doors) + available_sectors.remove(connect_sector) + current_sector.outstanding_doors.extend(connect_sector.outstanding_doors) + current_sector.regions.extend(connect_sector.regions) else: # If there's no available region with a door, use an internal connection - connect_door = find_compatible_door_in_list(world, door, available_doors, player) + connect_door = find_compatible_door_in_list_old(world, door, available_doors, player) if connect_door is not None: logger.info(' Adding loop via %s', connect_door.name) + if door not in current_sector.outstanding_doors: + raise Exception('Door %s was not in sector', door.name) + # Check if valid + if not is_loop_valid(door, connect_door, current_sector): + logger.info(' Not Linking %s to %s', door.name, connect_door.name) + available_doors.insert(0, door) + continue maybe_connect_two_way(world, door, connect_door, player) - available_doors.remove(connect_door) + current_sector.outstanding_doors.remove(door) + current_sector.outstanding_doors.remove(connect_door) + else: + raise Exception ('Something has gone terribly wrong') # Check that we used everything, we failed otherwise - if len(available_regions) > 0 or len(available_doors) > 0: + if len(available_sectors) > 0 or len(available_doors) > 0: logger.warning('Failed to add all regions/doors to dungeon, generation will likely fail.') -def find_connected_regions(region, available_regions, logger): - region_chunk = [region] - exits = [] - exits.extend(region.exits) - while len(exits) > 0: - ext = exits.pop() - if ext.connected_region is not None: - connect_region = ext.connected_region - if connect_region not in region_chunk and connect_region in available_regions: - # door = world.check_for_door(ext.name, player) - # if door is None or door.type not in [DoorType.Normal, DoorType.SpiralStairs]: - logger.info(' Found new region %s via %s', connect_region.name, ext.name) - available_regions.remove(connect_region) - region_chunk.append(connect_region) - exits.extend(connect_region.exits) - return region_chunk - - -def find_compatible_door_in_regions_ex(world, door, regions, player): - for region in regions: - for proposed_door in get_doors_ex(world, region, player): +def find_compatible_door_in_sectors_ex(world, door, sectors, player): + for sector in sectors: + for proposed_door in sector.outstanding_doors: if doors_compatible(door, proposed_door): - return region, proposed_door + return sector, proposed_door return None, None -def get_doors_ex(world, region, player): - res = [] - for exit in region.exits: - door = world.check_for_door(exit.name, player) - if door is not None and door.type in [DoorType.Normal, DoorType.SpiralStairs]: - res.append(door) - return res +def find_compatible_door_in_list_old(world, door, doors, player): + for proposed_door in doors: + if doors_compatible(door, proposed_door): + return proposed_door + + +# todo: path checking needed? +def is_valid(door_a, door_b, sector_a, sector_b, last_sector): + if last_sector: + return True + elif door_a.blocked and door_b.blocked: # todo, I can't see this going well unless we are in loop generation... + return False + elif not door_a.blocked and not door_b.blocked: + return sector_a.outflow() + sector_b.outflow() - 2 > 0 + elif door_a.blocked or door_b.blocked: + return sector_a.outflow() + sector_b.outflow() - 1 > 0 + return False # not sure how we got here, but it's a bad idea + + +def is_loop_valid(door_a, door_b, sector): + return True # todo: is this always true? # DATA GOES DOWN HERE diff --git a/Doors.py b/Doors.py index a5ee7ee2..6c75bf2c 100644 --- a/Doors.py +++ b/Doors.py @@ -103,9 +103,9 @@ def create_doors(world, player): create_dir_door(player, 'Eastern Courtyard EN', DoorType.Normal, Direction.East, 0xa9, Top, Low), big_key(create_dir_door(player, 'Eastern Courtyard N', DoorType.Normal, Direction.North, 0xa9, Mid, High)), create_door(player, 'Eastern Courtyard Potholes', DoorType.Hole), - create_door(player, 'Eastern Fairy Landing', DoorType.Hole), + landing(create_door(player, 'Eastern Fairy Landing', DoorType.Hole)), create_door(player, 'Eastern Fairies\' Warp', DoorType.Warp), - create_door(player, 'Eastern Courtyard Warp End', DoorType.Warp), + landing(create_door(player, 'Eastern Courtyard Warp End', DoorType.Warp)), create_dir_door(player, 'Eastern Map Valley WN', DoorType.Normal, Direction.West, 0xaa, Top, Low), create_dir_door(player, 'Eastern Map Valley SW', DoorType.Normal, Direction.South, 0xaa, Left, High), create_dir_door(player, 'Eastern Dark Square NW', DoorType.Normal, Direction.North, 0xba, Left, High), @@ -226,3 +226,8 @@ def toggle(door): def blocked(door): door.blocked = True return door + + +def landing(door): + door.landing = True + return door diff --git a/Dungeons.py b/Dungeons.py index 3afa86f3..969ec010 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -171,10 +171,10 @@ hyrule_castle_regions = [ 'Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Hyrule Castle East Hall', 'Hyrule Castle West Hall', 'Hyrule Castle Back Hall', 'Hyrule Castle Throne Room', 'Hyrule Dungeon Map Room', 'Hyrule Dungeon North Abyss', 'Hyrule Dungeon North Abyss Catwalk', 'Hyrule Dungeon South Abyss', - 'Hyrule Dungeon South Abyss Catwalk', 'Hyrule Dungeon Guardroom', 'Hyrule Dungeon Armory', - 'Hyrule Dungeon Staircase', 'Hyrule Dungeon Cellblock', 'Sewers Behind Tapestry', 'Sewers Rope Room', - 'Sewers Dark Cross', 'Sewers Water', 'Sewers Key Rat', 'Sewers Secret Room', 'Sewers Secret Room Blocked Path', - 'Sewers Pull Switch', 'Sanctuary' + 'Hyrule Dungeon South Abyss Catwalk', 'Hyrule Dungeon Guardroom', 'Hyrule Dungeon Armory Main', + 'Hyrule Dungeon Armory North Branch', 'Hyrule Dungeon Staircase', 'Hyrule Dungeon Cellblock', + 'Sewers Behind Tapestry', 'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Key Rat', + 'Sewers Secret Room', 'Sewers Secret Room Blocked Path', 'Sewers Pull Switch', 'Sanctuary' ] eastern_regions = [