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.

331 lines
9.8 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. from random import randint, choice
  2. import numpy as np
  3. import cv2
  4. import base64
  5. #algorithm: http://blankhead.fi/blog/index.php/2019/06/01/cell-flow-dungeon-layout-generation/
  6. CELL_SIZE = 5
  7. RETRY_CELL_PLACEMENT = True
  8. WALL_SCALE = 6
  9. MAP_SCALE = 12
  10. SCALE_FACTOR = WALL_SCALE*MAP_SCALE
  11. GRID_SIZE = 10
  12. FLOW_COLOR = np.array([0,255,0]) #green
  13. WALL_COLOR = np.array([255,255,255]) #white
  14. FLOOR_COLOR = np.array([40,100,100]) #brown
  15. GRID_COLOR = np.array([0,0,50]) #red
  16. def randomColor():
  17. b = randint(0,255)
  18. g = randint(0,255)
  19. r = randint(0,255)
  20. return np.array([b,g,r])
  21. class Cell(object):
  22. def __init__(self,ID):
  23. self.spaces = []
  24. self.color = randomColor()
  25. self.ID = ID
  26. self.flow = []
  27. def grow(self,grid):
  28. if len(self.spaces) == 0:
  29. x,y = grid.randomPoint()
  30. grid[x,y] = self.ID #claim space
  31. self.spaces.append((x,y))
  32. return #no flow adjustment required, as this is the cell origin
  33. if RETRY_CELL_PLACEMENT:
  34. valid = False
  35. tries = 0
  36. while not valid:
  37. tries += 1
  38. if tries > grid.w*grid.h:
  39. return #guarantees halting. find a better way
  40. x0,y0 = choice(self.spaces)
  41. conn = grid.connectedPoints(x0,y0)
  42. if len(conn) == 0:
  43. continue
  44. x,y = choice(conn)
  45. valid = grid[x,y] == 0
  46. grid[x,y] = self.ID
  47. self.spaces.append((x,y))
  48. self.flow.append((x,y,x0,y0))
  49. else:
  50. pass #write this when interested
  51. def getColor(self):
  52. return self.color
  53. def getFlow(self):
  54. return self.flow
  55. def getSpaces(self):
  56. return self.spaces
  57. class Grid(object):
  58. def __init__(self,w,h):
  59. self.w = w
  60. self.h = h
  61. self.spaces = np.zeros((w,h),dtype=np.int)
  62. self.cells = []
  63. def isOpen(self,x,y):
  64. if x < 0 or x >= self.w or y < 0 or y >= self.h:
  65. return False
  66. return self.spaces[x,y] == 0
  67. def __getitem__(self,key):
  68. return self.spaces[key]
  69. def __setitem__(self,key,value):
  70. self.spaces[key] = value
  71. def randomPoint(self):
  72. x = self.w//2
  73. y = self.h//2
  74. while not self.isOpen(x,y):
  75. x = randint(0,self.w-1)
  76. y = randint(0,self.h-1)
  77. return x,y
  78. def connectedPoints(self,x0,y0):
  79. pointlist = []
  80. for (x,y) in [(0,1),(1,0),(-1,0),(0,-1)]:
  81. ptx = x0 + x
  82. pty = y0 + y
  83. if ptx < 0 or ptx >= self.w or pty < 0 or pty >= self.h:
  84. pass
  85. else:
  86. pointlist.append((x0 + x,y0 + y))
  87. return pointlist
  88. def addCell(self):
  89. if len(self.cells) == 0:
  90. sx,sy = GRID_SIZE//2,GRID_SIZE//2
  91. c = Cell(len(self.cells)+1)
  92. c.spaces = [(sx,sy)]
  93. c.flow = []
  94. self.spaces[sx,sy] = c.ID
  95. for i in range(CELL_SIZE-1):
  96. c.grow(self)
  97. self.cells.append(c)
  98. else:
  99. cOld = choice(self.cells)
  100. while True:
  101. x0,y0 = choice(cOld.spaces)
  102. conn = self.connectedPoints(x0,y0)
  103. if len(conn) == 0:
  104. continue
  105. free = [(x,y) for (x,y) in conn if self.spaces[x,y] == 0]
  106. if len(free) == 0:
  107. continue
  108. sx,sy = choice(free)
  109. c = Cell(len(self.cells)+1)
  110. c.spaces = [(sx,sy)]
  111. c.flow = [(sx,sy,x0,y0)]
  112. self.spaces[sx,sy] = c.ID
  113. for i in range(CELL_SIZE-1):
  114. c.grow(self)
  115. self.cells.append(c)
  116. break
  117. def __str__(self):
  118. return str(self.spaces)
  119. def exportImage(self):
  120. imBase = np.zeros((self.w,self.h,3))
  121. for x in range(self.w):
  122. for y in range(self.h):
  123. if not self.spaces[x,y] == 0:
  124. #print(self.cells[self.spaces[x,y]-1].getColor())
  125. imBase[x,y,:] = self.cells[self.spaces[x,y]-1].getColor()/255
  126. imBase = np.kron(imBase,np.ones((WALL_SCALE,WALL_SCALE,1)))
  127. return imBase
  128. def getCells(self):
  129. return self.cells
  130. class DungeonSpace(object):
  131. EMPTY = 0
  132. FLOOR = 1
  133. WALL = 2
  134. MONSTER = 3
  135. #add more things
  136. class Dungeon(object):
  137. #to those who read this code, I sincerely apologize for the
  138. # _ _ _ _ _ _
  139. # /_\ __| |_ ____ _ _ __ ___ ___ __| | /_\ _ _| |_(_)___ _ __ ___
  140. # //_\\ / _` \ \ / / _` | '_ \ / __/ _ \/ _` | //_\\| | | | __| / __| '_ ` _ \
  141. #/ _ \ (_| |\ V / (_| | | | | (_| __/ (_| | / _ \ |_| | |_| \__ \ | | | | |
  142. #\_/ \_/\__,_| \_/ \__,_|_| |_|\___\___|\__,_| \_/ \_/\__,_|\__|_|___/_| |_| |_|
  143. #
  144. #that you are about to see
  145. #Dungeon generation begins with a reasonable data structure, and then degrades into an opencv image
  146. #In this __init__, we read it out of the image and back into a different data structure
  147. def __init__(self,im,name="Generic Dungeon"):
  148. self.name = name
  149. self.spaces = []
  150. w,h,_ = im.shape
  151. for rx in range(w//MAP_SCALE):
  152. col = []
  153. for ry in range(h//MAP_SCALE):
  154. x = rx*MAP_SCALE
  155. y = ry*MAP_SCALE
  156. if np.array_equal(im[x,y,:],np.array([0,0,0])):
  157. col.append(DungeonSpace.WALL)
  158. else:
  159. col.append(DungeonSpace.FLOOR)
  160. self.spaces.append(col)
  161. def __str__(self):
  162. return str(self.spaces)
  163. def exportImage(self):
  164. w = len(self.spaces)
  165. h = len(self.spaces[0])
  166. im = np.zeros((w*MAP_SCALE,h*MAP_SCALE,3))
  167. for x in range(len(self.spaces)):
  168. for y in range(len(self.spaces[0])):
  169. 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])
  170. return im
  171. def scaleMap(im):
  172. return np.kron(im,np.ones((MAP_SCALE,MAP_SCALE,1)))
  173. def drawFlow(im,grid):
  174. for cell in grid.getCells():
  175. for (y1,x1,y0,x0) in cell.getFlow():
  176. 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)
  177. def drawWalls(im,grid):
  178. newim = im.copy()
  179. w,h,_ = im.shape
  180. for cell in grid.getCells():
  181. for (cx,cy) in cell.getSpaces():
  182. for dx in range(WALL_SCALE): #I know how this looks, but this does not vectorize
  183. for dy in range(WALL_SCALE): #due to the neighbor checking
  184. x = cx*WALL_SCALE+dx
  185. y = cy*WALL_SCALE+dy
  186. #print(x,y)
  187. if np.array_equal(im[x,y,:],np.array([0,0,0])):
  188. pass
  189. elif x == 0 or y == 0 or x == w-1 or y == h-1:
  190. newim[x,y,:] = WALL_COLOR
  191. else:
  192. color = im[x,y,:]
  193. floor = True
  194. for i in [-1,0,1]:
  195. for j in [-1,0,1]:
  196. if not np.array_equal(im[x+i,y+j,:],color): #if a neighbor has a different color
  197. if not np.array_equal(newim[x+i,y+j,:],WALL_COLOR): #and that color is not a wall
  198. floor = False #become a wall
  199. if not floor:
  200. newim[x,y,:] = WALL_COLOR
  201. return newim
  202. def carveDoors(im,grid):
  203. newim = im.copy()
  204. for cell in grid.getCells():
  205. for (cy1,cx1,cy0,cx0) in cell.getFlow():
  206. p1 = (cx1*WALL_SCALE + WALL_SCALE//2,cy1*WALL_SCALE + WALL_SCALE//2)
  207. p0 = (cx0*WALL_SCALE + WALL_SCALE//2,cy0*WALL_SCALE + WALL_SCALE//2)
  208. cv2.line(newim,p1,p0,cell.getColor()/255,1)
  209. return newim
  210. def canonicalize(im):
  211. w,h,_ = im.shape
  212. gray = cv2.cvtColor(im.astype(np.float32),cv2.COLOR_BGR2GRAY)
  213. canonBW = cv2.inRange(gray,1/255,254/255)
  214. canon = cv2.cvtColor(canonBW,cv2.COLOR_GRAY2BGR)
  215. return canon,canonBW
  216. def drawGrid(im,mask): #draws grid on canonicalized image
  217. w,h,_ = im.shape
  218. newim = im.copy()
  219. for x in range(w//MAP_SCALE):
  220. cv2.line(newim,
  221. (x*MAP_SCALE,0),
  222. (x*MAP_SCALE,h-1),
  223. GRID_COLOR/255,
  224. 1) #thicc
  225. for y in range(h//MAP_SCALE):
  226. cv2.line(newim,
  227. (0,y*MAP_SCALE),
  228. (w-1,y*MAP_SCALE),
  229. GRID_COLOR/255,
  230. 1)
  231. gridded = cv2.bitwise_and(newim,newim,mask=mask)
  232. return gridded
  233. def genGridDungeonB64(gSizeX,gSizeY,mScale,imScale,numCells=None):
  234. global MAP_SCALE
  235. global WALL_SCALE
  236. MAP_SCALE = mScale
  237. WALL_SCALE = imScale//MAP_SCALE
  238. g = Grid(gSizeX,gSizeY)
  239. if numCells == None:
  240. numCells = (gSizeX+gSizeY)//10
  241. for _ in range(numCells):
  242. g.addCell()
  243. im = g.exportImage()
  244. im = drawWalls(im,g)
  245. im = carveDoors(im,g)
  246. im = scaleMap(im)
  247. im,mask = canonicalize(im)
  248. im = drawGrid(im,mask)
  249. #cv2.imshow("here",im)
  250. #cv2.waitKey(0)
  251. #base64 encode as jpg
  252. ret,buf = cv2.imencode('.png',im)
  253. enc = base64.b64encode(buf)
  254. return enc
  255. #s = genGridDungeonB64(10,10,8,24)
  256. #print(s)
  257. """
  258. #driver code
  259. g = Grid(GRID_SIZE,GRID_SIZE)
  260. g.addCell()
  261. g.addCell()
  262. g.addCell()
  263. g.addCell()
  264. g.addCell()
  265. g.addCell()
  266. g.addCell()
  267. print(g)
  268. im = g.exportImage()
  269. #print(im)
  270. im = drawWalls(im,g)
  271. im = carveDoors(im,g)
  272. im = scaleMap(im)
  273. #drawFlow(im,g)
  274. im,mask = canonicalize(im)
  275. drawn = drawGrid(im,mask)
  276. #cv2.imshow("Here",drawn)
  277. #cv2.waitKey(0)
  278. cv2.imwrite("generated_dungeon.png",drawn)
  279. d = Dungeon(im)
  280. print(d)
  281. res = d.exportImage()
  282. cv2.imshow("res",res)
  283. cv2.waitKey(0)
  284. """