Created
July 28, 2020 22:34
-
-
Save huderlem/e6d95cd8bcaf6430edc4757d70730d54 to your computer and use it in GitHub Desktop.
Region map generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is a hacky thrown-together script that procedurally | |
# generates Gen-3 Pokemon region map images. I intend to build | |
# an online tool to facilitate this, but this is the first | |
# proof of concept. | |
# | |
# Requires the opensimplix, Pillow, and scikit-learn: | |
# pip install opensimplex | |
# pip install Pillow | |
# pip install -U scikit-learn | |
from opensimplex import OpenSimplex | |
from PIL import Image, ImageDraw | |
from sklearn.cluster import KMeans | |
import random | |
COLOR_WATER_0 = (152, 208, 248) | |
COLOR_WATER_1 = (160, 176, 248) | |
COLOR_LAND_0 = (0, 112, 0) | |
COLOR_LAND_1 = (56, 168, 8) | |
COLOR_LAND_2 = (96, 208, 0) | |
COLOR_LAND_3 = (168, 232, 48) | |
COLOR_LAND_4 = (208, 248, 120) | |
COLOR_ROUTE_WATER_0 = (72, 152, 224) | |
COLOR_ROUTE_WATER_1 = (40, 128, 224) | |
COLOR_ROUTE_LAND_0 = (224, 160, 0) | |
COLOR_ROUTE_LAND_1 = (232, 184, 56) | |
COLOR_ROUTE_LAND_2 = (240, 208, 80) | |
COLOR_ROUTE_LAND_3 = (232, 224, 112) | |
COLOR_ROUTE_LAND_4 = (232, 224, 168) | |
COLOR_ROUTE_CONVERSION = { | |
COLOR_WATER_0: COLOR_ROUTE_WATER_0, | |
COLOR_WATER_1: COLOR_ROUTE_WATER_1, | |
COLOR_LAND_0: COLOR_ROUTE_LAND_0, | |
COLOR_LAND_1: COLOR_ROUTE_LAND_1, | |
COLOR_LAND_2: COLOR_ROUTE_LAND_2, | |
COLOR_LAND_3: COLOR_ROUTE_LAND_3, | |
COLOR_LAND_4: COLOR_ROUTE_LAND_4, | |
} | |
def get_elevation_color(elevation): | |
if elevation > 1.10: | |
return COLOR_LAND_4 | |
if elevation > 0.85: | |
return COLOR_LAND_3 | |
if elevation > 0.60: | |
return COLOR_LAND_2 | |
if elevation > 0.35: | |
return COLOR_LAND_1 | |
return COLOR_LAND_0 | |
def clamp(val, minVal, maxVal): | |
return max(minVal, min(maxVal, val)) | |
def partition_tiles(tiles, partition_width, partition_height): | |
partitions = {} | |
for tile in tiles: | |
px = int(tile[0] / partition_width) | |
py = int(tile[1] / partition_height) | |
key = (px, py) | |
if key not in partitions: | |
partitions[key] = [] | |
partitions[key].append(tile) | |
return partitions | |
def is_valid_landmark_tile(tile): | |
if tile[0] < 1 or tile[1] < 2 or tile[0] > 28 or tile[1] > 16: | |
return False | |
if tile[0] > 14 and tile[1] > 14: | |
return False | |
if tile[0] > 19 and tile[1] < 5: | |
return False | |
return True | |
def draw_horizontal_route(start, end, special_tiles): | |
inc = 1 | |
if start[0] > end[0]: | |
inc = -1 | |
for i in range(start[0], end[0], inc): | |
tile = (i, start[1]) | |
special_tile = special_tiles.get(tile) | |
if special_tile == None or special_tile['type'] != "city": | |
special_tiles[tile] = {'type': 'route'} | |
return (end[0], start[1]) | |
def draw_vertical_route(start, end, special_tiles): | |
inc = 1 | |
if start[1] > end[1]: | |
inc = -1 | |
for j in range(start[1], end[1], inc): | |
tile = (start[0], j) | |
special_tile = special_tiles.get(tile) | |
if special_tile == None or special_tile['type'] != "city": | |
special_tiles[tile] = {'type': 'route'} | |
return (start[0], end[1]) | |
def main(): | |
num = 5 | |
num_city_clusters = 2 | |
spacing = 30 | |
w = 240 | |
h = 160 | |
width = w | |
height = (h + spacing) * num | |
img = Image.new('RGB', (width, height)) | |
draw = ImageDraw.Draw(img) | |
for n in range(num): | |
print("Generating region map...", n) | |
num_cities = 16 | |
noise = OpenSimplex(random.randint(0, 100000)) | |
noise2 = OpenSimplex(random.randint(0, 100000)) | |
noise3 = OpenSimplex(random.randint(0, 100000)) | |
noise4 = OpenSimplex(random.randint(0, 100000)) | |
noise5 = OpenSimplex(random.randint(0, 100000)) | |
# Generate an elevation for every pixel using additive simplex noise. | |
elevations = [] | |
for i in range(w): | |
elevations.append([]) | |
for j in range(h): | |
val = noise.noise2d(x=i/100.0, y=j/100.0) + 0.2 | |
val2 = noise2.noise2d(x=i/20.0, y=j/20.0) * 0.15 | |
elevation = val + val2 | |
elevation += noise3.noise2d(x=i/15.0, y=j/15.0) * abs(noise4.noise2d(x=i/50.0, y=j/50.0)) * 0.6 | |
if abs(elevation - 0.7) < 0.1: | |
elevation += noise5.noise2d(x=i/10.0, y=j/10.0) * 0.1 | |
elevations[i].append(elevation) | |
# Find the non-water tiles. | |
open_tiles = {} | |
for i in range(int(w / 8)): | |
for j in range(int(h / 8)): | |
found = False | |
count = 0 | |
for x in range(8): | |
for y in range(8): | |
if elevations[i * 8 + x][j * 8 + y] > 0: | |
count += 1 | |
if count > 20: | |
open_tiles[(i, j)] = "open" | |
found = True | |
break | |
if found: | |
break | |
# Place cities. | |
special_tiles = {} | |
partitions = partition_tiles(open_tiles, 7, 5) | |
partition_order = list(partitions) | |
random.shuffle(partition_order) | |
for c in range(num_cities): | |
partition = partition_order[c % len(partitions)] | |
for i in range(50): | |
tile = random.choice(partitions[partition]) | |
for j in range(50): | |
if tile[0] % 2 == 1 and tile[1] % 2 == 1: | |
break | |
tile = random.choice(partitions[partition]) | |
if j == 49: | |
break | |
dist = 999 | |
if len(special_tiles) > 0: | |
dist = min([abs(t[0] - tile[0]) + abs(t[1] - tile[1]) for t in special_tiles]) | |
if is_valid_landmark_tile(tile): | |
special_tiles[tile] = {'type': 'city'} | |
break | |
# Form clusters of cities. | |
coords = list(special_tiles) | |
kmeans = KMeans(n_clusters=num_city_clusters).fit(coords) | |
for i, tile in enumerate(coords): | |
special_tiles[tile]['cluster'] = kmeans.labels_[i] | |
cluster_cities = [[], []] | |
for i, tile in enumerate(coords): | |
cluster_cities[kmeans.labels_[i]].append(tile) | |
# Connect cities in respective clusters. | |
for cluster in range(num_city_clusters): | |
cities = {} | |
for i, tile in enumerate(coords): | |
if kmeans.labels_[i] == cluster: | |
cities[tile] = {} | |
first_city = None | |
last_city = None | |
for city in cities: | |
# Find closest unconnected city, and connect it. | |
if first_city == None: | |
first_city = city | |
last_city = city | |
min_dist = 99999 | |
closest_city = None | |
closest_valid_city = None | |
for other in cities: | |
if other == city: | |
continue | |
if cities[other].get('connected'): | |
continue | |
horiz_dist = abs(city[0] - other[0]) | |
vert_dist = abs(city[1] - other[1]) | |
dist = horiz_dist + vert_dist | |
if dist < min_dist: | |
min_dist = dist | |
closest_city = other | |
if horiz_dist < 7 and vert_dist < 7: | |
closest_valid_city = other | |
if closest_valid_city == None: | |
if closest_city == None: | |
continue | |
closest_valid_city = closest_city | |
draw_funcs = [draw_horizontal_route, draw_vertical_route] | |
random.shuffle(draw_funcs) | |
start = draw_funcs[0](city, closest_city, special_tiles) | |
start = draw_funcs[1](start, closest_city, special_tiles) | |
cities[other]['connected'] = True | |
cities[city]['connected'] = True | |
draw_funcs = [draw_horizontal_route, draw_vertical_route] | |
random.shuffle(draw_funcs) | |
start = draw_funcs[0](last_city, first_city, special_tiles) | |
start = draw_funcs[1](start, first_city, special_tiles) | |
# Find the pair of cities that are closest together from the two clusters. | |
closest_pair = None | |
closest_dist = 9999 | |
for cityA in cluster_cities[0]: | |
for cityB in cluster_cities[1]: | |
dist = abs(cityA[0] - cityB[0]) + abs(cityA[1] - cityB[1]) | |
if dist < closest_dist: | |
closest_dist = dist | |
closest_pair = (cityA, cityB) | |
# Connect the pair of cities. | |
if closest_pair != None: | |
draw_funcs = [draw_horizontal_route, draw_vertical_route] | |
random.shuffle(draw_funcs) | |
start = draw_funcs[0](closest_pair[0], closest_pair[1], special_tiles) | |
start = draw_funcs[1](start, closest_pair[1], special_tiles) | |
# Draw land and water. | |
for i in range(w): | |
for j in range(h): | |
val = elevations[i][j] | |
if val > 0.0: | |
color = get_elevation_color(val) | |
draw.point([i, j + ((h + spacing) * n)], color) | |
else: | |
if j % 2 == 0: | |
color = COLOR_WATER_0 | |
else: | |
color = COLOR_WATER_1 | |
draw.point([i, j + ((h + spacing) * n)], color) | |
# Draw cities. | |
for tile in special_tiles: | |
x = tile[0] * 8 | |
y = tile[1] * 8 + ((h + spacing) * n) | |
if special_tiles[tile]['type'] == "city": | |
draw.rectangle([(x, y), (x + 7, y + 7)], (255, 0, 0)) | |
if special_tiles[tile]['type'] == "route": | |
for i in range(x, x + 8): | |
for j in range(y, y + 8): | |
new_color = COLOR_ROUTE_CONVERSION[img.getpixel((i, j))] | |
draw.point([i, j], new_color) | |
img.save('output.png') | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment