Skip to content

Instantly share code, notes, and snippets.

@caseman
Created January 22, 2014 06:01
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save caseman/8554090 to your computer and use it in GitHub Desktop.
"""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])
@DouglasEKnapp
Copy link

Trying to get your email. I am at magick_crow@gmail.com. Long time no chat! Hope all is well. Best Douglas E Knapp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment