Ooof! Well someone asked for an example of map procedural generation, so i've taken a bit of time to excavate and refactor a module I made awhile ago. The good news: It will generate a map of any size or dimensions you want, but bigger = longer, and the terrain consists only of water, shore, and land. You can set the density of the land mass, or adjust the number of land nodes, and when finished it will spit out a dandy little PNG image you can look at or edit with BrushTone. However, be warned that BrushTone is a 32 bit app, so can't handle anything over 4000 by 4000. Anyhoo:
import pyglet
from pyglet.window import key
from pyglet.gl import *
import numpy
import random
import sys
class Prototype(pyglet.window.Window):
def __init__(self):
super(Prototype, self).__init__(640, 480, resizable=False, fullscreen=False, caption="Test")
self.clear()
self.map = Map_Gen(128,128)
self.map.generate()
pyglet.clock.schedule_interval(self.update, .01)
def update(self,dt):
#draw screen
self.draw()
def draw(self):
self.clear()
self.map.minimap['map'].blit(32,0)
def on_key_press(self,symbol,modifiers):
if symbol == key.ESCAPE:
self.close()
#Map Generator
#/---------------------------------------------------------------------------/
class Map_Gen(object):
def __init__(self,mx=64,my=64,density=0.5,r_density=0.5,node=10):
#Map Dimensions
self.mx = mx
self.my = my
#density of terrain: 0.0 to 1.0
self.density = density
#number of terrain nodes
self.node = node
#random map seed
self.seed = None
#minimap main texture
self.minimap = {'map_image':pyglet.image.create(1,1),'map':pyglet.image.create(1,1)}
#contains the ground layer data that makes up the map
self.map = [] #28
#contains the object layer information IE: vehicles, resources, objects, components, (buildings?),etc.
self.resource = []
#The tileset is an indexed list of tile types based on their shape and purpose relative to terrain placement.
#In this case, the total is 16 tiles with the order being:
#0: Land
#1: Water
#2: Water with lower right corner Land
#3: Water with bottom half Land
#4: Water with lower left Land
#5: Water with right side Land
#6: Water with left side Land
#7: Water with upper right Land
#8: Water with upper half Land
#9: Water with upper left Land
#10: Land with Bottom Right Water
#11: Land with bottom left water
#12: Land with upper right Water
#13: Land with upper left water
#14: Land with Upper Right and Lower Left water
#15: Land with upper left and lower right water
self.tileset = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
#Tile properties determines whether the tiles are passable or solid, the length equals number
#of different types of tile, 16 in all: #0 passable, 1 impassable, 2 overhead
#tiles that have water are impassable.
self.properties = [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
#The update table is a quick reference 2D array to change adjacent tiles during a change based on
#what tiles are adjacent to the new ground tile. So if the tile above the new ground tile is solid water, it would be
#changed to water with the bottom being land, or self.table[8_direction][current_land_type] = new_land_type.
#update table
self.table = [[0,2,2,3,3,5,12,5,11,14,0,11,12,13,14,13],#upper left
[0,3,3,3,3,13,12,13,0,12,0,0,12,13,12,13],#top
[0,4,3,3,4,13,6,15,10,6,10,0,12,13,12,15],#upper right
[0,6,12,12,6,0,6,10,10,6,10,0,12,0,12,10],#right
[0,9,14,12,6,11,6,8,8,9,10,11,12,0,14,10],#lower right
[0,8,11,0,10,11,10,8,8,8,10,11,8,8,11,10],#bottom
[0,7,5,13,15,5,10,7,8,8,10,11,0,13,11,15],#lower left
[0,5,5,13,13,5,0,5,11,11,0,11,0,13,11,13]]#left
#Toggle map wrap around on/off
self.wrap_toggle = False
#toggle terrain update
self.update = False
#total solid water count
self.w_count = self.mx * self.my
#total shoreline count
self.s_count = 0
#total solid ground count
self.g_count = 0
#wander variables
self.x = 0
self.y = 0
self.counter = 0
#Generate map data and map image
#/-----------------------------------------------------------------------------/
def generate(self,seed=None):
#generate seed
if seed == None:
self.seed = random.randint(-999999999999,999999999999) #move to window?
print("SEED: ", self.seed)
random.seed(self.seed)
#generate water
print("Initializing Environment")
self.map = numpy.zeros((self.my,self.mx),dtype='uint8')
self.map[:] = 1
#generate land masses
print("Generating Land Masses")
dx = random.randrange(0,len(self.map[0])-1,1)#incorporate random positions to help map saturation.
dy = random.randrange(0,len(self.map)-1,1)
direction = 0
count = 0
#randomize direction and change terrain accordingly
while self.g_count < (self.mx*self.my)*self.density:
direction = random.randrange(0,4,1)
#right
if direction == 0 and (dx + 1) < len(self.map[dy])-1:
dx += 1
#left
elif direction == 1 and (dx - 1) > 0:
dx -= 1
#up
elif direction == 2 and (dy + 1) < len(self.map)-1:
dy += 1
#down
elif direction == 3 and (dy - 1) > 0:
dy -= 1
#count number of water/shore/land tiles
#if water
if self.map[dy][dx] == 1:
self.w_count = self.w_count - 1
self.g_count = self.g_count + 1
#if shore
elif self.map[dy][dx] != 0 and self.map[dy][dx] != 1:
self.s_count = self.s_count - 1
self.g_count = self.g_count + 1
#set tile to land and update surrounding tiles with flux
self.map[dy][dx] = 0
self.flux(dx,dy)
#calculate number of nodes and change position based on density threshold NOTE: use self.g_count instead of count?
count = count + 1
if count >= ((self.mx * self.my)*self.density)/(self.density*self.node):
dx = random.randrange(0,len(self.map[0])-1,1)
dy = random.randrange(0,len(self.map)-1,1)
count = 0
#generate minimap
print("Generating Map")
self.minimap['map_image'].width = len(self.map[0])
self.minimap['map_image'].height = len(self.map)
tmp = numpy.zeros((self.my,self.mx,3),dtype='uint8')
tmp = numpy.dstack((self.map,tmp))
#land/water/shore colors
color = [[139,69,19,255], [0,0,255,255], [100,50,10,255]]
for a in range(0,15,1):
if a < 2:
tmp = numpy.where(tmp[::,::,:1]!=a,tmp,color[a])
else:
tmp = numpy.where(tmp[::,::,:1]!=a,tmp,color[2])
tmp = tmp.astype('uint8')
self.minimap['map_image'].set_data('RGBA',self.minimap['map_image'].width*4,tmp.tobytes())
self.minimap['map'] = self.minimap['map_image'].get_texture()
#disable anti-aliasing/bilinear-filtering
glTexParameteri(self.minimap['map'].target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri(self.minimap['map'].target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
#save image
self.minimap['map'].save('tmp.png')
#update adjacent tiles
#----------------------------------------------------------------------------------|
def flux(self,x,y):
#upper left wrap check
if y+1 > len(self.map)-1 and x-1 < 0:
mx = self.mx
my = self.my
elif y+1 <= len(self.map)-1 and x-1 < 0:
mx = self.mx
my = 0
elif y+1 > len(self.map)-1 and x-1 >= 0:
mx = 0
my = self.my
else:
mx = 0
my = 0
if self.map[(y+1)-my][(x-1)+mx] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[(y+1)-my][(x-1)+mx] == 10:
self.s_count -= 1
self.g_count += 1
#upper left
self.map[(y+1)-my][(x-1)+mx] = self.table[0][self.map[(y+1)-my][(x-1)+mx]]
#up wrap check
if y+1 > len(self.map)-1:
my = self.my
else:
my = 0
#up
if self.map[(y+1)-my][x] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[(y+1)-my][x] == 8:
self.s_count -= 1
self.g_count += 1
elif self.map[(y+1)-my][x] == 10:
self.s_count -= 1
self.g_count += 1
elif self.map[(y+1)-my][x] == 11:
self.s_count -= 1
self.g_count += 1
self.map[(y+1)-my][x] = self.table[1][self.map[(y+1)-my][x]]
#upper right wrap check
if y+1 > len(self.map)-1 and x+1 > len(self.map[0])-1:
mx = self.mx
my = self.my
elif y+1 > len(self.map)-1 and x+1 <= len(self.map[0])-1:
mx = 0
my = self.my
elif y+1 <= len(self.map)-1 and x+1 > len(self.map[0])-1:
mx = self.mx
my = 0
else:
mx = 0
my = 0
if self.map[(y+1)-my][(x+1)-mx] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[(y+1)-my][(x+1)-mx] == 11:
self.s_count -= 1
self.g_count += 1
#upper right
self.map[(y+1)-my][(x+1)-mx] = self.table[2][self.map[(y+1)-my][(x+1)-mx]]
#right wrap check
if x+1 > len(self.map[0])-1:
mx = self.mx
else:
mx = 0
if self.map[y][(x+1)-mx] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[y][(x+1)-mx] == 5:
self.s_count -= 1
self.g_count += 1
elif self.map[y][(x+1)-mx] == 11:
self.s_count -= 1
self.g_count += 1
elif self.map[y][(x+1)-mx] == 13:
self.s_count -= 1
self.g_count += 1
#right
self.map[y][(x+1)-mx] = self.table[3][self.map[y][(x+1)-mx]]
#lower right wrap check
if y-1 < 0 and x+1 > len(self.map[0])-1:
mx = self.mx
my = self.my
elif y-1 >= 0 and x+1 > len(self.map[0])-1:
mx = self.mx
my = 0
elif y-1 < 0 and x+1 <= len(self.map[0])-1:
mx = 0
my = self.my
else:
mx = 0
my = 0
if self.map[(y-1)+my][(x+1)-mx] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[(y-1)+my][(x+1)-mx] == 13:
self.s_count -= 1
self.g_count += 1
elif self.map[(y-1)+my][(x+1)-mx] == 4:
self.s_count -= 1
self.g_count += 1
#lower right
self.map[(y-1)+my][(x+1)-mx] = self.table[4][self.map[(y-1)+my][(x+1)-mx]]
#bottom wrap check
if y-1 < 0:
my = self.my
else:
my = 0
if self.map[(y-1)+my][x] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[(y-1)+my][x] == 3:
self.s_count -= 1
self.g_count += 1
elif self.map[(y-1)+my][x] == 12:
self.s_count -= 1
self.g_count += 1
elif self.map[(y-1)+my][x] == 13:
self.s_count -= 1
self.g_count += 1
#bottom
self.map[(y-1)+my][x] = self.table[5][self.map[(y-1)+my][x]]
#lower left wrap check
if y-1 < 0 and x-1 < 0:
mx = self.mx
my = self.my
elif y-1 >= 0 and x-1 < 0:
mx = self.mx
my = 0
elif y-1 < 0 and x-1 >= 0:
mx = 0
my = self.my
else:
mx = 0
my = 0
if self.map[(y-1)+my][(x-1)+mx] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[(y-1)+my][(x-1)+mx] == 12:
self.s_count -= 1
self.g_count += 1
#lower left
self.map[(y-1)+my][(x-1)+mx] = self.table[6][self.map[(y-1)+my][(x-1)+mx]]
#left wrap check
if x-1 < 0:
mx = self.mx
else:
mx = 0
if self.map[y][(x-1)+mx] == 1:
self.w_count -= 1
self.s_count += 1
elif self.map[y][(x-1)+mx] == 6:
self.s_count -= 1
self.g_count += 1
elif self.map[y][(x-1)+mx] == 10:
self.s_count -= 1
self.g_count += 1
elif self.map[y][(x-1)+mx] == 12:
self.s_count -= 1
self.g_count += 1
#left
self.map[y][(x-1)+mx] = self.table[7][self.map[y][(x-1)+mx]]
if __name__ == '__main__':
window = Prototype()
pyglet.app.run()