def add_center_duchy(size_list, allowable_chunks, a_dist, b_dist, ranking):
    '''Given a list of necessary sizes (size_list), and a list of list of hexes (allowable_chunks), 
    attempt to create a center duchy, where counties are adjacent to both a and b (has a hex with 1 a_dist and 1 b_dist).
    Ranking is a dictionary of all (base) elements in allowable_chunks.
    Returns False if it doesn't find a solution in time.'''
    size = sum(size_list)
    while len(allowable_chunks) > 0:
        chunk = allowable_chunks.pop(0)
        if len(chunk) >= size:
            poss_centers = sort_hexlist([
                el for el in chunk
                if all([nel in chunk for nel in el.neighbors()])
            ], ranking)
            a_adj = sort_hexlist([el for el in chunk if a_dist[el] == 1],
                                 ranking)
            b_adj = sort_hexlist([el for el in chunk if b_dist[el] == 1],
                                 ranking)
            for center in poss_centers:
                duchy = Tile(hex_list=[],
                             tile_list=[
                                 make_capital_county(size_list[0],
                                                     origin=center,
                                                     coastal=False)
                             ],
                             rgb=d_col())
                c_nbrs = [
                    el for el in duchy.tile_list[0].neighbors() if el in chunk
                ]
                drhl = duchy.real_hex_list()
                a_county = add_center_county(
                    size_list[1], c_nbrs, a_adj,
                    [el for el in chunk if el not in drhl])
                if a_county:
                    duchy.add_tile(a_county)
                else:
                    continue
                drhl = duchy.real_hex_list()
                c_nbrs = [
                    el for el in duchy.tile_list[0].neighbors()
                    if el in chunk and el not in drhl
                ]
                b_county = add_center_county(size_list[2],
                                             duchy.tile_list[0].neighbors(),
                                             b_adj, chunk)
                if b_county:
                    duchy.add_tile(b_county)
                else:
                    continue
                for _ in range(20):
                    try:
                        duchy.add_bordering_tile(size_list[3],
                                                 rgb=c_col(),
                                                 only=chunk,
                                                 ranking=ranking)
                        break
                    except:
                        continue
                return duchy
    return False
def duchy_from_snake(snake, size_list):
    duchy = Tile(rgb=d_col(), hex_list=[])
    ind = 0
    for c_size in size_list:
        duchy.add_tile(Tile(rgb=c_col(), hex_list=snake[ind:ind + c_size]))
        ind += c_size
    #The capital is assumed to come first, but it should be in the middle.
    if len(size_list) > 2:
        duchy.tile_list.insert(0, duchy.tile_list.pop(1))
    return duchy
def make_world(cont_size_list=[3, 3, 3],
               island_size_list=[1, 1, 1],
               angles=[2, 4, 0]):
    '''Create three continents, with number of continental kingdoms determined by cont_size_list, and arrange them around an inner sea.
    angles determines where the continents go; 2,4,0 is northwest, northeast, south; 3,5,1 is north, southeast, southwest.'''
    assert len(cont_size_list) == 3
    assert len(cont_size_list) == len(angles)
    assert len(cont_size_list) == len(island_size_list)
    world = Tile(hex_list=[])
    #Continents
    for cont_idx, cont_size in enumerate(cont_size_list):
        cont = new_continent_gen(num_kingdoms=cont_size)
        bounding_hex = BoundingHex(cont)
        cont.origin, cont.rotation = bounding_hex.best_corner(angles[cont_idx])
        world.add_tile(cont)
    #Inner sea
    if False:
        bounding_hex = BoundingHex(world)
        inner_sea = set()
        to_search = set(Cube())
        while len(to_search) > 0:
            curr = to_search.pop()
            inner_sea.add(curr)
            to_search.update([
                el for el in curr.neighbors() if el in bounding_hex
                and bounding_hex.dist(el) >= 2 and el not in inner_sea
            ])
        if len(inner_sea) > 20:
            #We have enough to make a kingdom here
            water_height = {el: bounding_hex.dist(el) for el in inner_sea}
            inner_kingdom = make_island_kingdom(water_height)
            if inner_kingdom:
                world.add_tile(inner_kingdom)
                inner_sea -= set(inner_kingdom.inclusive_neighbors())
        #We should just drop some sicily-esque islands.
    #Outer islands
    for cont_idx, island_size in enumerate(island_size_list):
        for _ in range(island_size):
            land = world.real_hex_list()
            land_dist = calculate_distances(world, land, 3)[0]
            cont_dist = calculate_distances(world.tile_list[cont_idx], land,
                                            6)[0]
            for k, v in cont_dist.items():
                if k in land_dist:
                    cont_dist[k] = min(v, land_dist[k])
            new_island = make_island_kingdom(cont_dist)
            if new_island:
                world.tile_list[cont_idx].add_tile(new_island)
            else:
                print("Failed to add an island!", cont_idx)
    return world
def inner_continent_gen(center, kingdoms, cen_nbrs, k_r_bnds, port_locs):
    continent = Tile(hex_list=[])
    continent.add_tile(center)
    for k_idx in range(3):
        if not move_kingdom_into_place(continent, kingdoms[k_idx], cen_nbrs):
            return True, 'move into place'
    if not (check_water_access(continent.real_hex_list(),
                               continent.real_water_list())):
        return True, 'water access 0'
    assigned = continent.real_total_list()
    c_dist = calculate_distances(center, assigned, 10)[0]
    k_dists = calculate_distances(kingdoms[:3], assigned,
                                  sum(BORDER_SIZE_LIST))
    for a_idx, b_idx in combinations(range(3), r=2):
        allowable = [el for el in k_dists[a_idx] if el in k_dists[b_idx]]
        ranking = {
            el: c_dist[el] if el in c_dist else el.mag()
            for el in allowable
        }
        new_duchies = divide_into_duchies(BORDER_SIZE_LIST, 2,
                                          get_chunks(allowable),
                                          k_dists[a_idx], k_dists[b_idx],
                                          ranking)
        if new_duchies:
            for duchy in new_duchies:
                continent.add_tile(duchy)
        else:
            return True, 'border duchies 3'
    if not (check_water_access(continent.real_hex_list(),
                               continent.real_water_list())):
        return True, 'water access bd 3'
    print('Added 3 kingdoms!')
    if len(kingdoms) > 3:
        if not inner_add_triangle(continent, kingdoms, 3):
            return True, 'add fourth kingdom'
        print('Added 4 kingdoms!')
    if len(kingdoms) > 4:
        if not inner_add_triangle(continent, kingdoms, 4):
            return True, 'add fifth kingdom'
        print('Added 5 kingdoms!')
    return False, continent  #(continent, kingdoms)
def make_kingdom(
    origin=Cube(0, 0, 0),
    size_list=KINGDOM_SIZE_LIST,
    rgb_tuple=None,
    coastal=True,
):
    """rgb_tuple is complicated. For each level, the left element is the rgb of the title for that tile,
    and the right element is a list of rgb_tuples for the tiles the next element below 
    (or a list of rgb tuples for baronies)."""
    rgb_tuple = rgb_tuple or random_rgb_tuple(size_list)
    kingdom = Tile(origin=origin,
                   tile_list=[
                       make_capital_duchy(size_list=size_list[0],
                                          coastal=coastal,
                                          rgb_tuple=rgb_tuple[1][0])
                   ],
                   hex_list=[],
                   rgb=rgb_tuple[0])
    d_idx = 1
    while d_idx < len(size_list):
        duchy_size_list = size_list[d_idx]
        krhl = kingdom.relative_hex_list()
        krwl = kingdom.relative_water_list()
        new_county = Tile.new_tile(duchy_size_list[0],
                                   rgb=rgb_tuple[1][d_idx][1][0])
        if new_county.move_into_place([kingdom.relative_neighbors()], krhl,
                                      krwl):
            new_duchy = Tile(origin=Cube(0, 0, 0),
                             tile_list=[new_county],
                             hex_list=[],
                             rgb=rgb_tuple[1][d_idx][0])
            for c_idx, county_size_list in enumerate(duchy_size_list[1:]):
                new_duchy.add_new_tile(county_size_list,
                                       cant=krhl + krwl,
                                       rgb=rgb_tuple[1][d_idx][1][c_idx])
            if check_water_access(krhl + new_duchy.relative_hex_list(), krwl):
                kingdom.add_tile(new_duchy)
                d_idx += 1
    return kingdom
def divide_into_duchies(size_list, num_duchies, allowable_chunks, a_dist,
                        b_dist, ranking):
    '''Given a list of necessary sizes (size_list), and a list of list of hexes (allowable_chunks), 
    attempt to create num_duchies duchies with size hexes each, where each is adjacent to both a and b (has a hex with 1 a_dist and 1 b_dist).
    Ranking is a dictionary of all (base) elements in allowable_chunks.
    Returns False if it doesn't find a solution in time.'''
    assert num_duchies > 0
    size = sum(size_list)
    possible_tiles = []
    while len(allowable_chunks) > 0:
        tile_split = [[]] * len(size_list)
        chunk = allowable_chunks.pop(0)
        if len(chunk) < size:
            continue  # We can't make one, so don't bother trying.
        sorted_chunk = [
            pair[1] for pair in sorted([(ranking.get(el, el.mag()) +
                                         a_dist[el] + b_dist[el], el)
                                        for el in chunk])
        ]
        if len(get_chunks(sorted_chunk[:size])) == 1:
            possible_tiles, allowable_chunks = salvage_remainder(
                possible_tiles, duchy_from_snake(sorted_chunk[:size],
                                                 size_list), chunk,
                allowable_chunks, size)
        else:
            sorted_chunk = [
                pair[1] for pair in sorted([(ranking.get(el, 999), el)
                                            for el in chunk])
            ]
            a_adj = [el for el in sorted_chunk if a_dist[el] == 1]
            b_adj = [el for el in sorted_chunk if b_dist[el] == 1]
            if len(a_adj) == 0 or len(b_adj) == 0:
                continue  # We're not going to get adjacency to both.
            closest_a = a_adj[0]
            snake = [closest_a]
            disconnected = True
            while disconnected:
                closer_nbrs = [
                    el for el in snake[-1].neighbors()
                    if el in chunk and b_dist.get(el, 999) < b_dist[snake[-1]]
                ]
                if len(closer_nbrs) == 0:
                    break
                sorted_nbrs = [
                    pair[1] for pair in sorted([(ranking.get(el, 999), el)
                                                for el in closer_nbrs])
                ]
                snake.append(sorted_nbrs[0])
                if b_dist[snake[-1]] == 1:
                    disconnected = False
            if not disconnected and len(
                    snake
            ) >= size:  # I'm not sure why I thought this case was possible.
                overage = len(snake) - size
                if a_dist[snake[overage]] == 1:
                    snake = snake[overage:]
                    possible_tiles, allowable_chunks = salvage_remainder(
                        possible_tiles, duchy_from_snake(snake, size_list),
                        chunk, allowable_chunks, size)
            elif not disconnected:  # We have a valid snake, but too few.
                underage = size - len(snake)
                extendable = True
                while underage > 0 and extendable:
                    start_nbrs = [
                        el for el in snake[0].neighbors()
                        if el in chunk and ranking.get(el, 999) <= ranking.get(
                            snake[0]) and el not in snake
                    ]
                    if len(start_nbrs) > 0:
                        snake.insert(0, random.choice(start_nbrs))
                        underage -= 1
                    if underage > 0:
                        end_nbrs = [
                            el for el in snake[-1].neighbors()
                            if el in chunk and ranking.get(el, 999) <=
                            ranking.get(snake[-1]) and el not in snake
                        ]
                        if len(end_nbrs) > 0:
                            snake.append(random.choice(end_nbrs))
                            underage -= 1
                    if len(start_nbrs) == 0 and len(end_nbrs) == 0:
                        # Now we have to grow in the middle.
                        extendable = False
                if underage == 0:
                    possible_tiles, allowable_chunks = salvage_remainder(
                        possible_tiles, duchy_from_snake(snake, size_list),
                        chunk, allowable_chunks, size)
                else:
                    duchy = Tile(rgb=d_col(), hex_list=[])
                    for c_size in size_list:
                        duchy.add_tile(Tile(rgb=c_col(), hex_list=[]))
                    assigned = []
                    ind = 0
                    while len(snake) > 0 and underage > 0 and ind < len(
                            size_list):
                        el_nbrs = [
                            el for el in snake[0].neighbors() if el in chunk
                            and el not in snake and el not in assigned
                        ]
                        assigned.append(snake.pop(0))
                        duchy.tile_list[ind].hex_list.append(assigned[-1])
                        if len(duchy.tile_list[ind].hex_list
                               ) == size_list[ind]:
                            ind += 1
                            if ind == len(size_list):
                                break
                        if len(el_nbrs) > 0:
                            num_to_take = min(
                                underage, size_list[ind] -
                                len(duchy.tile_list[ind].hex_list))
                            added_now = random.sample(
                                el_nbrs, min(num_to_take, len(el_nbrs)))
                            assigned.extend(added_now)
                            duchy.tile_list[ind].hex_list.extend(added_now)
                            if len(duchy.tile_list[ind].hex_list
                                   ) == size_list[ind]:
                                ind += 1
                            # Note that we could keep going here, and check the neighbors of these neighbors, but I'm going to skip this for now.
                    if underage == 0:
                        if len(size_list) > 2:
                            duchy.tile_list.insert(0, duchy.tile_list.pop(1))
                        possible_tiles, allowable_chunks = salvage_remainder(
                            possible_tiles, duchy, chunk, allowable_chunks,
                            size)
        if len(possible_tiles) == num_duchies:
            return possible_tiles
    return False
def make_island_kingdom(water_height,
                        origin=None,
                        size_list=[6, 4, 4, 3],
                        banned=[],
                        weighted=True,
                        min_mag=6,
                        min_capital_coast=3,
                        min_coast=2,
                        max_tries=1000,
                        strait_prob=0.5,
                        center_bias=0.5,
                        coast_bias=0.125):
    '''Given a dictionary from cubes to distance from shore, return a tile with duchies whose size are from duchy_size_list,
    and which are connected either directly or by straits (with probability strait_prob), and doesn't have any hexes in banned.
    The probability that a hex is selected as the origin is proportional to np.exp(-el.mag() * center_bias) * np.exp(water_height[el] * coast_bias),
    so high values of center_bias will make it closer to the center and high values of coast_bias will make it further from the shore.
    Tries max_tries times and returns False if it fails.'''
    assert min_capital_coast >= 3
    assert min_coast >= 2
    for _ in range(max_tries):
        island = Tile(hex_list=[],
                      tile_list=[make_capital_duchy(d_size=size_list[0])])
        if origin:
            island.tile_list[0].origin = origin
        else:
            opts = [
                k for k, v in water_height.items()
                if v >= min_capital_coast and k.mag() >= min_mag
            ]  #center-coast-water-land means center has to be at least 3.
            probs = [
                np.exp(-el.mag() * center_bias) *
                np.exp(water_height[el] * coast_bias) for el in opts
            ]
            probs /= sum(probs)
            island.tile_list[0].origin = np.random.choice(opts, p=probs)
        allowable = [
            k for k, v in water_height.items()
            if v >= min_coast and k not in banned
        ]
        if any([el not in allowable for el in island.real_hex_list()]):
            break
        for size in size_list[1:]:
            allocated = island.real_total_list()
            land_nbrs = island.neighbors()
            allowable = [el for el in allowable if el not in allocated]
            if np.random.rand() <= strait_prob:
                opts = [
                    el for el in island.strait_neighbors() if el in allowable
                ]
                new_origin = random.choice(opts)
                water_hexes = set()
                new_origin_nbrs = new_origin.neighbors()
                land = island.real_hex_list()
                for strait_neighbor in new_origin.strait_neighbors():
                    if strait_neighbor in land:
                        water_hexes.update([
                            el for el in strait_neighbor.neighbors()
                            if el in new_origin_nbrs
                        ])
                new_tile = Tile(hex_list=[new_origin],
                                water_list=list(water_hexes))
                while len(new_tile.hex_list) < size:
                    new_neighbors = new_tile.relative_neighbors(weighted)
                    new_neighbors = [
                        x for x in new_neighbors
                        if x not in land_nbrs and x in allowable
                    ]
                    if len(new_neighbors) > 0:
                        new_tile.add_hex(random.choice(new_neighbors))
                    else:
                        break
                if len(new_tile.hex_list) == size:
                    island.add_tile(new_tile)
            else:
                opts = [el for el in island.neighbors() if el in allowable]
                new_origin = random.choice(opts)
                new_tile = Tile(hex_list=[new_origin])
                while len(new_tile.hex_list) < size:
                    new_neighbors = new_tile.relative_neighbors(weighted)
                    new_neighbors = [
                        x for x in new_neighbors
                        if x not in allocated and x in allowable
                    ]
                    if len(new_neighbors) > 0:
                        new_tile.add_hex(random.choice(new_neighbors))
                    else:
                        break
                if len(new_tile.hex_list) == size:
                    island.add_tile(new_tile)
        if len(island.real_hex_list()) == sum(
                size_list) and check_water_access(
                    island.real_hex_list(), island.real_water_list(),
                    max([el.mag() for el in island.real_hex_list()])):
            return island
    return False