Contact me to be added to this repository. Push the creations you make for D&D, and they will be displayed in a nice website. See the website for how to contribute.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

342 lines
10 KiB

from random import randint, choice
import numpy as np
import cv2
import base64
import sys
#algorithm: http://blankhead.fi/blog/index.php/2019/06/01/cell-flow-dungeon-layout-generation/
CELL_SIZE = 5
RETRY_CELL_PLACEMENT = True
WALL_SCALE = 6
MAP_SCALE = 12
SCALE_FACTOR = WALL_SCALE*MAP_SCALE
GRID_SIZE = 10
FLOW_COLOR = np.array([0,255,0]) #green
WALL_COLOR = np.array([255,255,255]) #white
FLOOR_COLOR = np.array([40,100,100]) #brown
GRID_COLOR = np.array([0,0,50]) #red
def randomColor():
b = randint(0,255)
g = randint(0,255)
r = randint(0,255)
return np.array([b,g,r])
class Cell(object):
def __init__(self,ID):
self.spaces = []
self.color = randomColor()
self.ID = ID
self.flow = []
def grow(self,grid):
if len(self.spaces) == 0:
x,y = grid.randomPoint()
grid[x,y] = self.ID #claim space
self.spaces.append((x,y))
return #no flow adjustment required, as this is the cell origin
if RETRY_CELL_PLACEMENT:
valid = False
tries = 0
while not valid:
tries += 1
if tries > grid.w*grid.h:
return #guarantees halting. find a better way
x0,y0 = choice(self.spaces)
conn = grid.connectedPoints(x0,y0)
if len(conn) == 0:
continue
x,y = choice(conn)
valid = grid[x,y] == 0
grid[x,y] = self.ID
self.spaces.append((x,y))
self.flow.append((x,y,x0,y0))
else:
pass #write this when interested
def getColor(self):
return self.color
def getFlow(self):
return self.flow
def getSpaces(self):
return self.spaces
class Grid(object):
def __init__(self,w,h):
self.w = w
self.h = h
self.spaces = np.zeros((w,h),dtype=np.int)
self.cells = []
def isOpen(self,x,y):
if x < 0 or x >= self.w or y < 0 or y >= self.h:
return False
return self.spaces[x,y] == 0
def __getitem__(self,key):
return self.spaces[key]
def __setitem__(self,key,value):
self.spaces[key] = value
def randomPoint(self):
x = self.w//2
y = self.h//2
while not self.isOpen(x,y):
x = randint(0,self.w-1)
y = randint(0,self.h-1)
return x,y
def connectedPoints(self,x0,y0):
pointlist = []
for (x,y) in [(0,1),(1,0),(-1,0),(0,-1)]:
ptx = x0 + x
pty = y0 + y
if ptx < 0 or ptx >= self.w or pty < 0 or pty >= self.h:
pass
else:
pointlist.append((x0 + x,y0 + y))
return pointlist
def addCell(self):
if len(self.cells) == 0:
sx,sy = GRID_SIZE//2,GRID_SIZE//2
c = Cell(len(self.cells)+1)
c.spaces = [(sx,sy)]
c.flow = []
self.spaces[sx,sy] = c.ID
for i in range(CELL_SIZE-1):
c.grow(self)
self.cells.append(c)
else:
cOld = choice(self.cells)
while True:
x0,y0 = choice(cOld.spaces)
conn = self.connectedPoints(x0,y0)
if len(conn) == 0:
continue
free = [(x,y) for (x,y) in conn if self.spaces[x,y] == 0]
if len(free) == 0:
continue
sx,sy = choice(free)
c = Cell(len(self.cells)+1)
c.spaces = [(sx,sy)]
c.flow = [(sx,sy,x0,y0)]
self.spaces[sx,sy] = c.ID
for i in range(CELL_SIZE-1):
c.grow(self)
self.cells.append(c)
break
def __str__(self):
return str(self.spaces)
def exportImage(self):
imBase = np.zeros((self.w,self.h,3))
for x in range(self.w):
for y in range(self.h):
if not self.spaces[x,y] == 0:
#print(self.cells[self.spaces[x,y]-1].getColor())
imBase[x,y,:] = self.cells[self.spaces[x,y]-1].getColor()
if sys.version_info.major >= 3:
imBase /= 255
imBase = np.kron(imBase,np.ones((WALL_SCALE,WALL_SCALE,1)))
return imBase
def getCells(self):
return self.cells
class DungeonSpace(object):
EMPTY = 0
FLOOR = 1
WALL = 2
MONSTER = 3
#add more things
class Dungeon(object):
#to those who read this code, I sincerely apologize for the
# _ _ _ _ _ _
# /_\ __| |_ ____ _ _ __ ___ ___ __| | /_\ _ _| |_(_)___ _ __ ___
# //_\\ / _` \ \ / / _` | '_ \ / __/ _ \/ _` | //_\\| | | | __| / __| '_ ` _ \
#/ _ \ (_| |\ V / (_| | | | | (_| __/ (_| | / _ \ |_| | |_| \__ \ | | | | |
#\_/ \_/\__,_| \_/ \__,_|_| |_|\___\___|\__,_| \_/ \_/\__,_|\__|_|___/_| |_| |_|
#
#that you are about to see
#Dungeon generation begins with a reasonable data structure, and then degrades into an opencv image
#In this __init__, we read it out of the image and back into a different data structure
def __init__(self,im,name="Generic Dungeon"):
self.name = name
self.spaces = []
w,h,_ = im.shape
for rx in range(w//MAP_SCALE):
col = []
for ry in range(h//MAP_SCALE):
x = rx*MAP_SCALE
y = ry*MAP_SCALE
if np.array_equal(im[x,y,:],np.array([0,0,0])):
col.append(DungeonSpace.WALL)
else:
col.append(DungeonSpace.FLOOR)
self.spaces.append(col)
def __str__(self):
return str(self.spaces)
def exportImage(self):
w = len(self.spaces)
h = len(self.spaces[0])
im = np.zeros((w*MAP_SCALE,h*MAP_SCALE,3))
for x in range(len(self.spaces)):
for y in range(len(self.spaces[0])):
im[x*MAP_SCALE:(x+1)*MAP_SCALE,y*MAP_SCALE:(y+1)*MAP_SCALE,:] = np.array([0,0,0]) if self.spaces[x][y] == DungeonSpace.WALL else np.array([.5,0,0])
return im
def scaleMap(im):
return np.kron(im,np.ones((MAP_SCALE,MAP_SCALE,1)))
def drawFlow(im,grid):
for cell in grid.getCells():
for (y1,x1,y0,x0) in cell.getFlow():
cv2.arrowedLine(im,(x1*SCALE_FACTOR + SCALE_FACTOR//2,y1*SCALE_FACTOR + SCALE_FACTOR//2),(x0*SCALE_FACTOR + SCALE_FACTOR//2,y0*SCALE_FACTOR + SCALE_FACTOR//2),100)
def drawWalls(im,grid):
newim = im.copy()
w,h,_ = im.shape
for cell in grid.getCells():
for (cx,cy) in cell.getSpaces():
for dx in range(WALL_SCALE): #I know how this looks, but this does not vectorize
for dy in range(WALL_SCALE): #due to the neighbor checking
x = cx*WALL_SCALE+dx
y = cy*WALL_SCALE+dy
#print(x,y)
if np.array_equal(im[x,y,:],np.array([0,0,0])):
pass
elif x == 0 or y == 0 or x == w-1 or y == h-1:
newim[x,y,:] = WALL_COLOR
else:
color = im[x,y,:]
floor = True
for i in [-1,0,1]:
for j in [-1,0,1]:
if not np.array_equal(im[x+i,y+j,:],color): #if a neighbor has a different color
if not np.array_equal(newim[x+i,y+j,:],WALL_COLOR): #and that color is not a wall
floor = False #become a wall
if not floor:
newim[x,y,:] = WALL_COLOR
return newim
def carveDoors(im,grid):
newim = im.copy()
for cell in grid.getCells():
color = cell.getColor()
if sys.version_info.major >= 3:
print(color)
color = color / 255
for (cy1,cx1,cy0,cx0) in cell.getFlow():
p1 = (cx1*WALL_SCALE + WALL_SCALE//2,cy1*WALL_SCALE + WALL_SCALE//2)
p0 = (cx0*WALL_SCALE + WALL_SCALE//2,cy0*WALL_SCALE + WALL_SCALE//2)
cv2.line(newim,p1,p0,color,1)
return newim
def canonicalize(im):
w,h,_ = im.shape
gray = cv2.cvtColor(im.astype(np.float32),cv2.COLOR_BGR2GRAY)
canonBW = cv2.inRange(gray,1/255,254/255) if sys.version_info.major >= 3 else cv2.inRange(gray,1,254)
canon = cv2.cvtColor(canonBW,cv2.COLOR_GRAY2BGR)
return canon,canonBW
def drawGrid(im,mask): #draws grid on canonicalized image
w,h,_ = im.shape
newim = im.copy()
c = GRID_COLOR
if sys.version_info.major >= 3:
c = c / 255
for x in range(w//MAP_SCALE):
cv2.line(newim,
(x*MAP_SCALE,0),
(x*MAP_SCALE,h-1),
c,
1) #thicc
for y in range(h//MAP_SCALE):
cv2.line(newim,
(0,y*MAP_SCALE),
(w-1,y*MAP_SCALE),
c,
1)
gridded = cv2.bitwise_and(newim,newim,mask=mask)
return gridded
def genGridDungeonB64(gSizeX,gSizeY,mScale,imScale,numCells=None):
global MAP_SCALE
global WALL_SCALE
MAP_SCALE = mScale
WALL_SCALE = imScale//MAP_SCALE
try:
g = Grid(gSizeX,gSizeY)
if numCells == None:
numCells = (gSizeX+gSizeY)//10
for _ in range(numCells):
g.addCell()
im = g.exportImage()
im = drawWalls(im,g)
im = carveDoors(im,g)
im = scaleMap(im)
im,mask = canonicalize(im)
im = drawGrid(im,mask)
except:
print("Rip?")
return "__ERROR_"
#cv2.imshow("here",im)
#cv2.waitKey(0)
#base64 encode as jpg
ret,buf = cv2.imencode('.png',im)
enc = base64.b64encode(buf)
return enc
#s = genGridDungeonB64(10,10,8,24)
#print(s)
#"""
#driver code
g = Grid(GRID_SIZE,GRID_SIZE)
print(g)
g.addCell()
g.addCell()
print(g)
im = g.exportImage()
#print(im)
cv2.imshow("grid export",im)
im = drawWalls(im,g)
cv2.imshow("draw walls",im)
im = carveDoors(im,g)
im = scaleMap(im)
#drawFlow(im,g)
im,mask = canonicalize(im)
drawn = drawGrid(im,mask)
#cv2.imshow("Here",drawn)
#cv2.waitKey(0)
cv2.imwrite("generated_dungeon.png",drawn)
d = Dungeon(im)
print(d)
res = d.exportImage()
cv2.imshow("res",res)
cv2.waitKey(0)
#"""