Created
January 22, 2014 06:01
-
-
Save caseman/8554090 to your computer and use it in GitHub Desktop.
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
"""Create a map and render it as a pgm image. | |
(see http://netpbm.sourceforge.net/doc/pgm.html) | |
""" | |
import sys | |
import math | |
import json | |
from functools import lru_cache | |
from noise import snoise2 | |
MAP_SIZE = 1024 | |
MAP_SEED = 3 | |
FAULT_SCALE = 3.5 | |
FAULT_OCTAVES = 5 | |
FAULT_THRESHOLD = 0.95 | |
FAULT_EROSION_SCALE = 10 | |
FAULT_EROSION_OCTAVES = 8 | |
FAULT_SCALE_F = FAULT_SCALE / MAP_SIZE | |
ERODE_SCALE_F = FAULT_EROSION_SCALE / MAP_SIZE | |
LAND_MASS_SCALE = 1.5 | |
EQUITORIAL_MULTIPLIER = 2.5 | |
COAST_COMPLEXITY = 12 | |
WATER_LEVEL = 0.025 | |
COAST_THRESHOLD = 0.02 | |
MOUNTAIN_FAULT_THRESHOLD = 0.08 | |
MOUNTAIN_HILL_THRESHOLD = 0.4 | |
LAND_SCALE_F = LAND_MASS_SCALE / MAP_SIZE | |
HILL_SCALE = 5.0 | |
HILL_OCTAVES = 8 | |
HILL_THRESHOLD = 0.19 | |
HILL_SCALE_F = HILL_SCALE / MAP_SIZE | |
MOISTURE_REACH = 0.1 | |
MOISTURE_REACH_TILES = round(MOISTURE_REACH * MAP_SIZE) | |
RAINFALL_INFLUENCE = 0.03 | |
RAINFALL_INFLUENCE_TILES = round(RAINFALL_INFLUENCE * MAP_SIZE) | |
RAINFALL_HILL_FACTOR = 0.03 | |
RAINFALL_MOUNTAIN_FACTOR = 0.1 | |
RAINFALL_KERNEL_RADIUS = 3 | |
ICE_ALT = 1.0 | |
RAINFALL_MAX = 0 | |
if len(sys.argv) != 2 or '--help' in sys.argv or '-h' in sys.argv: | |
print('%s FILE' % sys.argv[0]) | |
print() | |
print(__doc__) | |
raise SystemExit | |
class reify(object): | |
""" Use as a class method decorator. It operates almost exactly like the | |
Python ``@property`` decorator, but it puts the result of the method it | |
decorates into the instance dict after the first call, effectively | |
replacing the function it decorates with an instance variable. It is, in | |
Python parlance, a non-data descriptor. An example: | |
.. code-block:: python | |
class Foo(object): | |
@reify | |
def jammy(self): | |
print 'jammy called' | |
return 1 | |
And usage of Foo: | |
.. code-block:: text | |
>>> f = Foo() | |
>>> v = f.jammy | |
'jammy called' | |
>>> print v | |
1 | |
>>> f.jammy | |
1 | |
>>> # jammy func not called the second time; it replaced itself with 1 | |
""" | |
def __init__(self, wrapped): | |
self.wrapped = wrapped | |
try: | |
self.__doc__ = wrapped.__doc__ | |
except: # pragma: no cover | |
pass | |
def __get__(self, inst, objtype=None): | |
if inst is None: | |
return self | |
val = self.wrapped(inst) | |
setattr(inst, self.wrapped.__name__, val) | |
return val | |
def fault_level(x, y, seed): | |
FL = 1.0 - abs(snoise2(x * FAULT_SCALE_F, y * FAULT_SCALE_F, FAULT_OCTAVES, | |
base=seed + 10, repeatx=FAULT_SCALE)) | |
thold = max(0.0, (FL - FAULT_THRESHOLD) / (1.0 - FAULT_THRESHOLD)) | |
FL *= abs(snoise2(x * ERODE_SCALE_F, y * ERODE_SCALE_F, FAULT_EROSION_OCTAVES, 0.85, | |
base=seed, repeatx=FAULT_EROSION_SCALE)) | |
FL *= math.log10(thold * 9.0 + 1.0) | |
return FL | |
def equator_distance(y): | |
return abs(MAP_SIZE - y * 2.0) / MAP_SIZE | |
def base_height(x, y, seed): | |
height = snoise2(x * LAND_SCALE_F, y * LAND_SCALE_F, COAST_COMPLEXITY, base=seed, repeatx=LAND_MASS_SCALE) | |
return (height + height * math.log10(10.0 * (1.01 - equator_distance(y))) * EQUITORIAL_MULTIPLIER) / (EQUITORIAL_MULTIPLIER + 1.0) | |
def hilliness(x, y, seed): | |
return abs(snoise2(x * HILL_SCALE_F, y * HILL_SCALE_F, HILL_OCTAVES, 0.9, base=seed, repeatx=HILL_SCALE)) | |
TILE_COLORS = { | |
"ocean": (0, 0, 150), | |
"coast": (64, 64, 255), | |
"plain": (32, 150, 64), | |
"hill": (100, 100, 0), | |
"mountain": (150, 150, 180), | |
"ice": (220, 220, 255), | |
"tundra": (150, 120, 130), | |
} | |
class Tile: | |
def __init__(self, **attrs): | |
self.__dict__.update(attrs) | |
@reify | |
def terrain(self): | |
alt = self.base_height + self.fault * 0.5 | |
if alt + alt * self.ruggedness > (1.0 - self.equator_distance) * ICE_ALT: | |
return "ice" | |
if self.base_height < WATER_LEVEL and alt < WATER_LEVEL + COAST_THRESHOLD: | |
if WATER_LEVEL - self.base_height > COAST_THRESHOLD: | |
return "ocean" | |
else: | |
return "coast" | |
else: | |
if self.ruggedness > MOUNTAIN_HILL_THRESHOLD: | |
return "mountain" | |
if (self.ruggedness + self.fault > HILL_THRESHOLD | |
and self.ruggedness > self.fault): | |
return "hill" | |
if self.fault > MOUNTAIN_FAULT_THRESHOLD: | |
return "mountain" | |
return "plain" | |
def color(self): | |
color = TILE_COLORS[self.terrain] | |
rainfall = self.rainfall | |
return (color[0] - rainfall, color[1] - rainfall, color[2] - rainfall) | |
return TILE_COLORS[self.terrain] | |
def as_dict(self): | |
return | |
def line(dx, dy): | |
"""Bresenham's line algorithm""" | |
line = [] | |
dx = round(dx); dy = round(dy) | |
x = 0; end_x = x + dx | |
y = 0; end_y = y + dy | |
sx = -1 if dx < 0 else 1 | |
sy = -1 if dy < 0 else 1 | |
adx = abs(dx); ady = abs(dy) | |
err = (adx if adx > ady else -ady) // 2 | |
while x != end_x or y != end_y: | |
err2 = err | |
if err2 > -adx: | |
err -= ady | |
x += sx | |
if err2 < ady: | |
err += adx | |
y += sy | |
line.append((x, y)) | |
line.append((x, y)) | |
return line | |
def prevailing_wind(y): | |
angle = -equator_distance(y) * 2.0 * math.pi | |
return math.cos(angle), math.sin(angle) | |
@lru_cache() | |
def prevailing_wind_line(y): | |
dx, dy = prevailing_wind(y) | |
return line(dx * -MOISTURE_REACH_TILES, dy * -MOISTURE_REACH_TILES) | |
class Map: | |
def __init__(self, width, height, seed): | |
self.width = width | |
self.height = height | |
self.seed = seed | |
self.tiles = {} | |
self.create_tiles() | |
def tile_pass(self, func): | |
tiles = self.tiles | |
for y in range(self.height): | |
for x in range(self.width): | |
func(x, y, tiles) | |
def tile(self, x, y): | |
try: | |
return self.tiles[x, y] | |
except IndexError: | |
while x < 0: # wrap x | |
x += self.width | |
while x >= self.width: | |
x -= self.width | |
if y < 0: # bounce y | |
y = -y | |
elif y >= self.height: | |
y = (self.height - 1) - (y - self.height) | |
return self.tiles[x, y] | |
def tile_factory(self, x, y, tiles): | |
bh = base_height(x, y, self.seed) | |
f = fault_level(x, y, self.seed) | |
r = hilliness(x, y, self.seed) | |
el = bh + f * 0.5 | |
is_land = bh >= WATER_LEVEL or el >= WATER_LEVEL + COAST_THRESHOLD; | |
tile = Tile( | |
equator_distance = equator_distance(y), | |
base_height = bh, | |
fault = f, | |
elevation = el, | |
ruggedness = r, | |
is_land = is_land, | |
air_moisture = 0.0, | |
rainfall = 0.0, | |
) | |
# pre-wrap x, pre-bounce y | |
for tx in (x - self.width, x, x + self.width): | |
for ty in (-y, y, (self.height - 1) - (y - self.height)): | |
tiles[tx, ty] = tile | |
def set_rainfall(self, x, y, tiles): | |
global RAINFALL_MAX | |
if (x - RAINFALL_KERNEL_RADIUS) % RAINFALL_KERNEL_RADIUS != 0 or (y - RAINFALL_KERNEL_RADIUS) % RAINFALL_KERNEL_RADIUS != 0: | |
return | |
tile = tiles[x, y] | |
if not tile.is_land: | |
return | |
clear_line = True | |
moisture = 0.0 | |
rain_factor = 0.5 | |
rainfall_reach = RAINFALL_INFLUENCE_TILES | |
for wx, wy in prevailing_wind_line(y): | |
if clear_line: | |
nearby_tile = tiles[x + wx, y + wy] | |
terrain = nearby_tile.terrain | |
if terrain == 'coast' or terrain == 'ocean': | |
moisture += 1.0 | |
elif terrain == 'mountain': | |
clear_line = False | |
elif terrain != 'ice': | |
moisture += (1.0 - nearby_tile.equator_distance) * (1.0 - nearby_tile.ruggedness) | |
else: | |
moisture *= 0.25 | |
if rainfall_reach: | |
terrain = tiles[x - wx, y - wy].terrain | |
if terrain == 'hill': | |
rain_factor += RAINFALL_HILL_FACTOR | |
elif terrain == 'mountain': | |
rain_factor += RAINFALL_MOUNTAIN_FACTOR | |
rainfall_reach -= 1 | |
elif not clear_line: | |
break | |
rainfall = rain_factor * moisture | |
if rainfall > RAINFALL_MAX: | |
RAINFALL_MAX = rainfall | |
for tx in range(x - RAINFALL_KERNEL_RADIUS, x + RAINFALL_KERNEL_RADIUS): | |
for ty in range(y - RAINFALL_KERNEL_RADIUS, y + RAINFALL_KERNEL_RADIUS): | |
tiles[tx, ty].rainfall = rainfall | |
def create_tiles(self): | |
self.tile_pass(self.tile_factory) | |
self.tile_pass(self.set_rainfall) | |
def write_image(self, filename): | |
tiles = self.tiles | |
f = open(filename, 'wt') | |
f.write('P3\n') | |
f.write('%s %s\n' % (self.width, self.height)) | |
f.write('255\n') | |
for y in range(self.height): | |
for x in range(self.width): | |
c = tiles[x, y].color() | |
if isinstance(c, tuple): | |
f.write("%s %s %s\n" % c) | |
else: | |
c = int(c * 255) | |
f.write("%s %s %s\n" % (c, c, c)) | |
f.close() | |
def write_json(self, filename): | |
f = open(sys.argv[1], 'wt') | |
terrain_array = [ | |
[self.tiles[x, y].terrain | |
for x in range(self.width)] | |
for y in range(self.height)] | |
json.dump(terrain_array, f) | |
def tile_type(x, y, seed): | |
f = fault_level(x, y, seed) | |
bh = base_height(x, y, seed) | |
hills = hilliness(x, y, seed) | |
alt = bh + f * 0.5 | |
eq_dist = equator_distance(y) | |
if alt + alt * hills > (1.0 - eq_dist) * ICE_ALT: | |
return "ice" | |
if bh < WATER_LEVEL and alt < WATER_LEVEL + COAST_THRESHOLD: | |
if WATER_LEVEL - bh > COAST_THRESHOLD: | |
return "ocean" | |
else: | |
return "coast" | |
else: | |
if hills > MOUNTAIN_HILL_THRESHOLD: | |
return "mountain" | |
if hills + f > HILL_THRESHOLD and hills > f: | |
return "hill" | |
if f > MOUNTAIN_FAULT_THRESHOLD: | |
return "mountain" | |
return "plain" | |
def tile_color(x, y, seed): | |
return TILE_COLORS[tile_type(x, y, seed)] | |
def wind_dir(x, y, seed): | |
dx, dy = prevailing_wind(y) | |
r = dy | |
g = .866 * dx + -.5 * dy | |
b = -.866 * dx + -.5 * dy | |
if r<0 and g<0 and b<0: print((dx,dy)) | |
return r > 0 and r * 255, g > 0 and g * 255, b > 0 and b * 255 | |
world_map = Map(MAP_SIZE, MAP_SIZE, MAP_SEED) | |
world_map.write_image(sys.argv[1]) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Trying to get your email. I am at [email protected]. Long time no chat! Hope all is well. Best Douglas E Knapp