Skip to content

Instantly share code, notes, and snippets.

@huderlem
Created July 28, 2020 22:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save huderlem/e6d95cd8bcaf6430edc4757d70730d54 to your computer and use it in GitHub Desktop.
Save huderlem/e6d95cd8bcaf6430edc4757d70730d54 to your computer and use it in GitHub Desktop.
Region map generator
# 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