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

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