forked from jogordo/DnD_Archive
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.
421 lines
13 KiB
421 lines
13 KiB
from random import randint, choice
|
|
|
|
import numpy as np
|
|
import cv2
|
|
|
|
import base64
|
|
import sys
|
|
import subprocess
|
|
|
|
#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
|
|
|
|
NUM_ITR = 8
|
|
|
|
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
|
|
|
|
#this function should really just be always in python 3
|
|
#maybe have it run this file in python 3 and capture output?
|
|
def genGridDungeonB64(gSizeX,gSizeY,mScale,imScale,numCells=None):
|
|
|
|
if sys.version_info.major < 3: #this here block of code is a sizeable ow
|
|
cmd = "from dungeon import genGridDungeonB64 as b; print(b({},{},{},{},{}))".format(gSizeX,gSizeY,mScale,imScale,numCells)
|
|
res = subprocess.check_output(["python3","-c",cmd])
|
|
return "bb"+res.split("\'")[-2]+"b" #padding required
|
|
|
|
|
|
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('.jpg',im)
|
|
enc = base64.b64encode(buf)
|
|
return enc
|
|
|
|
#params are pixel size
|
|
def genCaveDungeonB64(pSizeX,pSizeY):
|
|
im=np.random.randint(2,size=(pSizeX,pSizeY)).astype(np.float32)*255
|
|
#im = 1-np.random.randint(10,size=(pSizeX,pSizeY))//8
|
|
#im = im.astype(np.float32)*255
|
|
#kernel = np.ones((2,2))
|
|
#im = cv2.erode(im,kernel,iterations=1)
|
|
|
|
def getNeighborCount(x,y):
|
|
print(x,y)
|
|
count = 0
|
|
for dx in [-1,0,1]:
|
|
for dy in [-1,0,1]:
|
|
rx = x+dx
|
|
ry = y+dy
|
|
if (dx == 0 and dy == 0) or rx < 0 or ry < 0 or rx >= pSizeX or ry >= pSizeY:
|
|
continue
|
|
count += im[rx,ry]/255
|
|
print(count)
|
|
return count
|
|
|
|
for i in range(NUM_ITR):
|
|
newim = im.copy()
|
|
for x in range(pSizeX):
|
|
for y in range(pSizeY):
|
|
c = getNeighborCount(x,y)
|
|
if c >= 5 and newim[x,y] == 0:
|
|
newim[x,y] = 1
|
|
if c <= 2 and newim[x,y] == 1:
|
|
newim[x,y] = 0
|
|
im = newim
|
|
cv2.imshow("Itr " + str(i),im)
|
|
cv2.waitKey(0)
|
|
|
|
|
|
cv2.imwrite("generated_dungeon.png",im)
|
|
|
|
|
|
|
|
#size x, size y, cell count
|
|
def voronoiForest(sx,sy,cc):
|
|
cellPoints = []
|
|
for i in range(cc):
|
|
rx = randint(0,sx-1)
|
|
ry = randint(0,sy-1)
|
|
cellPoints.append((rx,ry,randomColor()))
|
|
im = np.zeros((sx,sy,3))
|
|
for x in range(sx):
|
|
for y in range(sy):
|
|
dists = [(np.cbrt((x-cx)**3 + (y-cy)**3),clr/255) for (cx,cy,clr) in cellPoints]
|
|
_,clr = min(dists,key=lambda t: t[0])
|
|
im[x,y,:] = clr
|
|
return np.kron(im,np.ones((8,8,1)))
|
|
|
|
|
|
#s = genGridDungeonB64(10,10,8,24)
|
|
#print(s)
|
|
|
|
if len(sys.argv) > 1:
|
|
|
|
if "cave" in sys.argv:
|
|
#genCaveDungeonB64(64,64)
|
|
im = voronoiForest(128,128,64)
|
|
cv2.imshow("voronoi",im)
|
|
cv2.waitKey(0)
|
|
cv2.imwrite("voronoi.png",im*255)
|
|
sys.exit(0)
|
|
|
|
g = Grid(GRID_SIZE,GRID_SIZE)
|
|
g.addCell()
|
|
g.addCell()
|
|
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)
|
|
if "color" in sys.argv:
|
|
cv2.imwrite("colored_dungeon.png",im)
|
|
#drawFlow(im,g)
|
|
cv2.imshow("flow",im)
|
|
im,mask = canonicalize(im)
|
|
drawn = drawGrid(im,mask)
|
|
|
|
#cv2.imshow("Here",drawn)
|
|
#cv2.waitKey(0)
|
|
|
|
if "gen" in sys.argv:
|
|
cv2.imwrite("generated_dungeon.png",drawn)
|
|
|
|
d = Dungeon(im)
|
|
print(d)
|
|
res = d.exportImage()
|
|
cv2.imshow("res",res)
|
|
cv2.waitKey(0)
|