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
 |