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
							 |