204 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			204 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from .tileset import entrance_tiles, solid_tiles, walkable_tiles | ||
|  | from .map import Map | ||
|  | from .util import xyrange | ||
|  | from .locations.entrance import Entrance | ||
|  | from .locations.chest import Chest, FloorItem | ||
|  | from .locations.seashell import HiddenSeashell, DigSeashell, BonkSeashell | ||
|  | import random | ||
|  | from typing import List | ||
|  | 
 | ||
|  | all_location_constructors = (Chest, FloorItem, HiddenSeashell, DigSeashell, BonkSeashell) | ||
|  | 
 | ||
|  | 
 | ||
|  | def remove_duplicate_tile(tiles, to_find): | ||
|  |     try: | ||
|  |         idx0 = tiles.index(to_find) | ||
|  |         idx1 = tiles.index(to_find, idx0 + 1) | ||
|  |         tiles[idx1] = 0x04 | ||
|  |     except ValueError: | ||
|  |         return | ||
|  | 
 | ||
|  | 
 | ||
|  | class Dijkstra: | ||
|  |     def __init__(self, the_map: Map): | ||
|  |         self.map = the_map | ||
|  |         self.w = the_map.w * 10 | ||
|  |         self.h = the_map.h * 8 | ||
|  |         self.area = [-1] * (self.w * self.h) | ||
|  |         self.distance = [0] * (self.w * self.h) | ||
|  |         self.area_size = [] | ||
|  |         self.next_area_id = 0 | ||
|  | 
 | ||
|  |     def fill(self, start_x, start_y): | ||
|  |         size = 0 | ||
|  |         todo = [(start_x, start_y, 0)] | ||
|  |         while todo: | ||
|  |             x, y, distance = todo.pop(0) | ||
|  |             room = self.map.get(x // 10, y // 8) | ||
|  |             tile_idx = (x % 10) + (y % 8) * 10 | ||
|  |             area_idx = x + y * self.w | ||
|  |             if room.tiles[tile_idx] not in solid_tiles and self.area[area_idx] == -1: | ||
|  |                 size += 1 | ||
|  |                 self.area[area_idx] = self.next_area_id | ||
|  |                 self.distance[area_idx] = distance | ||
|  |                 todo += [(x - 1, y, distance + 1), (x + 1, y, distance + 1), (x, y - 1, distance + 1), (x, y + 1, distance + 1)] | ||
|  |         self.next_area_id += 1 | ||
|  |         self.area_size.append(size) | ||
|  |         return self.next_area_id - 1 | ||
|  | 
 | ||
|  |     def dump(self): | ||
|  |         print(self.area_size) | ||
|  |         for y in range(self.map.h * 8): | ||
|  |             for x in range(self.map.w * 10): | ||
|  |                 n = self.area[x + y * self.map.w * 10] | ||
|  |                 if n < 0: | ||
|  |                     print(' ', end='') | ||
|  |                 else: | ||
|  |                     print(n, end='') | ||
|  |             print() | ||
|  | 
 | ||
|  | 
 | ||
|  | class EntranceInfo: | ||
|  |     def __init__(self, room, x, y): | ||
|  |         self.room = room | ||
|  |         self.x = x | ||
|  |         self.y = y | ||
|  |         self.tile = room.tiles[x + y * 10] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def map_x(self): | ||
|  |         return self.room.x * 10 + self.x | ||
|  | 
 | ||
|  |     @property | ||
|  |     def map_y(self): | ||
|  |         return self.room.y * 8 + self.y | ||
|  | 
 | ||
|  | 
 | ||
|  | class LocationGenerator: | ||
|  |     def __init__(self, the_map: Map): | ||
|  |         # Find all entrances | ||
|  |         entrances: List[EntranceInfo] = [] | ||
|  |         for room in the_map: | ||
|  |             # Prevent more then one chest or hole-entrance per map | ||
|  |             remove_duplicate_tile(room.tiles, 0xA0) | ||
|  |             remove_duplicate_tile(room.tiles, 0xC6) | ||
|  |             for x, y in xyrange(10, 8): | ||
|  |                 if room.tiles[x + y * 10] in entrance_tiles: | ||
|  |                     entrances.append(EntranceInfo(room, x, y)) | ||
|  |                 if room.tiles[x + y * 10] == 0xA0: | ||
|  |                     Chest(room, x, y) | ||
|  |         todo_entrances = entrances.copy() | ||
|  | 
 | ||
|  |         # Find a place to put the start position | ||
|  |         start_entrances = [info for info in todo_entrances if info.room.tileset_id == "town"] | ||
|  |         if not start_entrances: | ||
|  |             start_entrances = entrances | ||
|  |         start_entrance = random.choice(start_entrances) | ||
|  |         todo_entrances.remove(start_entrance) | ||
|  | 
 | ||
|  |         # Setup the start position and fill the basic dijkstra flood fill from there. | ||
|  |         Entrance(start_entrance.room, start_entrance.x, start_entrance.y, "start_house") | ||
|  |         reachable_map = Dijkstra(the_map) | ||
|  |         reachable_map.fill(start_entrance.map_x, start_entrance.map_y) | ||
|  | 
 | ||
|  |         # Find each entrance that is not reachable from any other spot, and flood fill from that entrance | ||
|  |         for info in entrances: | ||
|  |             if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == -1: | ||
|  |                 reachable_map.fill(info.map_x, info.map_y) | ||
|  | 
 | ||
|  |         disabled_entrances = ["boomerang_cave", "seashell_mansion"] | ||
|  |         house_entrances = ["rooster_house", "writes_house", "photo_house", "raft_house", "crazy_tracy", "witch", "dream_hut", "shop", "madambowwow", "kennel", "library", "ulrira", "trendy_shop", "armos_temple", "banana_seller", "ghost_house", "animal_house1", "animal_house2", "animal_house3", "animal_house4", "animal_house5"] | ||
|  |         cave_entrances = ["madbatter_taltal", "bird_cave", "right_fairy", "moblin_cave", "hookshot_cave", "forest_madbatter", "castle_jump_cave", "rooster_grave", "prairie_left_cave1", "prairie_left_cave2", "prairie_left_fairy", "mamu", "armos_fairy", "armos_maze_cave", "prairie_madbatter", "animal_cave", "desert_cave"] | ||
|  |         water_entrances = ["mambo", "heartpiece_swim_cave"] | ||
|  |         phone_entrances = ["phone_d8", "writes_phone", "castle_phone", "mabe_phone", "prairie_left_phone", "prairie_right_phone", "prairie_low_phone", "animal_phone"] | ||
|  |         dungeon_entrances = ["d7", "d8", "d6", "d5", "d4", "d3", "d2", "d1", "d0"] | ||
|  |         connector_entrances = [("fire_cave_entrance", "fire_cave_exit"), ("left_to_right_taltalentrance", "left_taltal_entrance"), ("obstacle_cave_entrance", "obstacle_cave_outside_chest", "obstacle_cave_exit"), ("papahl_entrance", "papahl_exit"), ("multichest_left", "multichest_right", "multichest_top"), ("right_taltal_connector1", "right_taltal_connector2"), ("right_taltal_connector3", "right_taltal_connector4"), ("right_taltal_connector5", "right_taltal_connector6"), ("writes_cave_left", "writes_cave_right"), ("raft_return_enter", "raft_return_exit"), ("toadstool_entrance", "toadstool_exit"), ("graveyard_cave_left", "graveyard_cave_right"), ("castle_main_entrance", "castle_upper_left", "castle_upper_right"), ("castle_secret_entrance", "castle_secret_exit"), ("papahl_house_left", "papahl_house_right"), ("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high"), ("prairie_to_animal_connector", "animal_to_prairie_connector"), ("d6_connector_entrance", "d6_connector_exit"), ("richard_house", "richard_maze"), ("prairie_madbatter_connector_entrance", "prairie_madbatter_connector_exit")] | ||
|  | 
 | ||
|  |         # For each area that is not yet reachable from the start area: | ||
|  |         # add a connector cave from a reachable area to this new area. | ||
|  |         reachable_areas = [0] | ||
|  |         unreachable_areas = list(range(1, reachable_map.next_area_id)) | ||
|  |         retry_count = 10000 | ||
|  |         while unreachable_areas: | ||
|  |             source = random.choice(reachable_areas) | ||
|  |             target = random.choice(unreachable_areas) | ||
|  | 
 | ||
|  |             source_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == source] | ||
|  |             target_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == target] | ||
|  |             if not source_entrances: | ||
|  |                 retry_count -= 1 | ||
|  |                 if retry_count < 1: | ||
|  |                     raise RuntimeError("Failed to add connectors...") | ||
|  |                 continue | ||
|  | 
 | ||
|  |             source_info = random.choice(source_entrances) | ||
|  |             target_info = random.choice(target_entrances) | ||
|  | 
 | ||
|  |             connector = random.choice(connector_entrances) | ||
|  |             connector_entrances.remove(connector) | ||
|  |             Entrance(source_info.room, source_info.x, source_info.y, connector[0]) | ||
|  |             todo_entrances.remove(source_info) | ||
|  |             Entrance(target_info.room, target_info.x, target_info.y, connector[1]) | ||
|  |             todo_entrances.remove(target_info) | ||
|  | 
 | ||
|  |             for extra_exit in connector[2:]: | ||
|  |                 info = random.choice(todo_entrances) | ||
|  |                 todo_entrances.remove(info) | ||
|  |                 Entrance(info.room, info.x, info.y, extra_exit) | ||
|  | 
 | ||
|  |             unreachable_areas.remove(target) | ||
|  |             reachable_areas.append(target) | ||
|  | 
 | ||
|  |         # Find areas that only have a single entrance, and try to force something in there. | ||
|  |         #   As else we have useless dead ends, and that is no fun. | ||
|  |         for area_id in range(reachable_map.next_area_id): | ||
|  |             area_entrances = [info for info in entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == area_id] | ||
|  |             if len(area_entrances) != 1: | ||
|  |                 continue | ||
|  |             cells = [] | ||
|  |             for y in range(reachable_map.h): | ||
|  |                 for x in range(reachable_map.w): | ||
|  |                     if reachable_map.area[x + y * reachable_map.w] == area_id: | ||
|  |                         if the_map.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10] in walkable_tiles: | ||
|  |                             cells.append((reachable_map.distance[x + y * reachable_map.w], x, y)) | ||
|  |             cells.sort(reverse=True) | ||
|  |             d, x, y = random.choice(cells[:10]) | ||
|  |             FloorItem(the_map.get(x // 10, y // 8), x % 10, y % 8) | ||
|  | 
 | ||
|  |         # Find potential dungeon entrances | ||
|  |         # Assign some dungeons | ||
|  |         for n in range(4): | ||
|  |             if not todo_entrances: | ||
|  |                 break | ||
|  |             info = random.choice(todo_entrances) | ||
|  |             todo_entrances.remove(info) | ||
|  |             dungeon = random.choice(dungeon_entrances) | ||
|  |             dungeon_entrances.remove(dungeon) | ||
|  |             Entrance(info.room, info.x, info.y, dungeon) | ||
|  | 
 | ||
|  |         # Assign something to all other entrances | ||
|  |         for info in todo_entrances: | ||
|  |             options = house_entrances if info.tile == 0xE2 else cave_entrances | ||
|  |             entrance = random.choice(options) | ||
|  |             options.remove(entrance) | ||
|  |             Entrance(info.room, info.x, info.y, entrance) | ||
|  | 
 | ||
|  |         # Go over each room, and assign something if nothing is assigned yet | ||
|  |         todo_list = [room for room in the_map if not room.locations] | ||
|  |         random.shuffle(todo_list) | ||
|  |         done_count = {} | ||
|  |         for room in todo_list: | ||
|  |             options = [] | ||
|  |             # figure out what things could potentially be placed here | ||
|  |             for constructor in all_location_constructors: | ||
|  |                 if done_count.get(constructor, 0) >= constructor.MAX_COUNT: | ||
|  |                     continue | ||
|  |                 xy = constructor.check_possible(room, reachable_map) | ||
|  |                 if xy is not None: | ||
|  |                     options.append((*xy, constructor)) | ||
|  | 
 | ||
|  |             if options: | ||
|  |                 x, y, constructor = random.choice(options) | ||
|  |                 constructor(room, x, y) | ||
|  |                 done_count[constructor] = done_count.get(constructor, 0) + 1 |