├── living.jpg ├── portal.jpg ├── bricks_d.jpg ├── gate_closed.jpg ├── thetrumanshow.jpg ├── README.md └── bricks.py /living.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuba-siekierzynski/labyrinth3d/HEAD/living.jpg -------------------------------------------------------------------------------- /portal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuba-siekierzynski/labyrinth3d/HEAD/portal.jpg -------------------------------------------------------------------------------- /bricks_d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuba-siekierzynski/labyrinth3d/HEAD/bricks_d.jpg -------------------------------------------------------------------------------- /gate_closed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuba-siekierzynski/labyrinth3d/HEAD/gate_closed.jpg -------------------------------------------------------------------------------- /thetrumanshow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuba-siekierzynski/labyrinth3d/HEAD/thetrumanshow.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Labyrinth 3D - my fun with pyglet 2 | 3 | The code launches an openGL-based little 3D first-person perspective game (kind of Doom-like) and lets the user control the area (just use your mouse and arrow keys). It is just a small demo of pyglet library possibilities. 4 | 5 | ## Contents 6 | - bricks.py - main code 7 | - \*.jpg - graphic files (textures) 8 | 9 | ## Requirements 10 | - pyglet 11 | - graphic files located in the same directory as the bricks.py file 12 | 13 | A sample screenshot: 14 | 15 | ![](https://user-images.githubusercontent.com/23619663/31903344-209735e4-b828-11e7-855b-6a31830d5e8b.png) 16 | 17 | A small video sample:
18 | [![](https://j.gifs.com/kZZMGr.gif)](https://www.instagram.com/p/BQ-8ubDDTNi) 19 | -------------------------------------------------------------------------------- /bricks.py: -------------------------------------------------------------------------------- 1 | from pyglet.gl import * 2 | from pyglet.window import key 3 | import math 4 | import random 5 | import sys 6 | 7 | ADOM = {0: '.', 1: '#', 2: '<', 3: '>'} 8 | # tribute to Ancient Domains of Mystery ;) 9 | 10 | N, S, E, W = 1, 2, 4, 8 11 | # directions translated into bitnums to store information on all cleared walls in one variable per cell 12 | 13 | GO_DIR = {N: (0, -1), S: (0, 1), E: (1, 0), W: (-1, 0)} 14 | # dictionary with directions translated to digging moves 15 | 16 | REVERSE = {E: W, W: E, N: S, S: N} 17 | # when a passage is dug from a cell, the other cell obtains the reverse passage, too 18 | 19 | 20 | class Lab: 21 | 22 | def __init__(self, size, seed): 23 | self.SIZE = size 24 | self.seed = seed 25 | self.lab = list(list(0 for i in range(self.SIZE[0])) for j in range(self.SIZE[1])) 26 | self.hash_lab = list(list(0 for i in range(self.SIZE[0] * 2 + 1)) for j in range(self.SIZE[1] * 2 + 1)) 27 | random.seed(self.seed) 28 | 29 | def dig(x, y): 30 | # digs passage from a cell (x, y) in an unvisited cell 31 | dirs = [N, E, W, S] 32 | random.shuffle(dirs) 33 | # shuffles directions each time for more randomness 34 | for Dir in dirs: 35 | new_x = x + GO_DIR[Dir][0] 36 | new_y = y + GO_DIR[Dir][1] 37 | if (new_y in range(self.SIZE[1])) and \ 38 | (new_x in range(self.SIZE[0])) and \ 39 | (self.lab[new_y][new_x] == 0): 40 | # checks if the new cell is not visited 41 | self.lab[y][x] |= Dir 42 | self.lab[new_y][new_x] |= REVERSE[Dir] 43 | # if so, apply info on passages to both cells 44 | dig(new_x, new_y) 45 | # repeat recursively 46 | dig(self.SIZE[0] // 2, self.SIZE[1] // 2) 47 | 48 | # draw hash_lab border 49 | for j in range(self.SIZE[1] * 2 + 1): 50 | self.hash_lab[j][0] = 1 51 | self.hash_lab[j][self.SIZE[0] * 2] = 1 52 | for i in range(self.SIZE[0] * 2 + 1): 53 | self.hash_lab[0][i] = 1 54 | self.hash_lab[self.SIZE[1] * 2][i] = 1 55 | 56 | # put hash_lab matrix (cross-walls) 57 | for j in range(0, self.SIZE[1] * 2, 2): 58 | for i in range(0, self.SIZE[0] * 2, 2): 59 | self.hash_lab[j][i] = 1 60 | 61 | # translate into roguelike lab 62 | for j in range(self.SIZE[1]): 63 | for i in range(self.SIZE[0]): 64 | if (self.lab[j][i] & S) == 0: 65 | self.hash_lab[(j + 1) * 2][(i + 1) * 2 - 1] = 1 66 | if (self.lab[j][i] & E) == 0: 67 | self.hash_lab[(j + 1) * 2 - 1][(i + 1) * 2] = 1 68 | self.hash_lab[0][1], self.hash_lab[self.SIZE[1] * 2][self.SIZE[0] * 2 - 1] = 0, 0 69 | 70 | def draw_ascii(self): 71 | # displays the labyrinth in ASCII for reference 72 | print("Labyrinth of Kuba #" + str(self.seed) + " (" + str(self.SIZE[0]) + "x" + str(self.SIZE[1]) + ")") 73 | # prints the seed (for reference) and the lab size 74 | 75 | print("_" * (self.SIZE[0] * 2)) 76 | for j in range(self.SIZE[1]): 77 | if j != 0: 78 | print("|", end='') 79 | else: 80 | print("_", end='') 81 | for i in range(self.SIZE[0]): 82 | if self.lab[j][i] & S != 0: 83 | print(" ", end='') 84 | else: 85 | print("_", end='') 86 | if self.lab[j][i] & E != 0: 87 | if (self.lab[j][i] | self.lab[j][i + 1]) & S != 0: 88 | print(" ", end='') 89 | else: 90 | print("_", end='') 91 | elif (j == self.SIZE[1] - 1) & (i == self.SIZE[0] - 1): 92 | print("_", end='') 93 | else: 94 | print("|", end='') 95 | print("") 96 | 97 | def draw_hash(self): 98 | for j in range(0, self.SIZE[1] * 2 + 1): 99 | for i in range(0, self.SIZE[0] * 2 + 1): 100 | print(ADOM[self.hash_lab[j][i]], end='') 101 | print("") 102 | 103 | 104 | class Model: 105 | 106 | def get_tex(self, file): 107 | tex = pyglet.image.load(file).texture 108 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 109 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 110 | return pyglet.graphics.TextureGroup(tex) 111 | 112 | def add_cube(self, pos): 113 | 114 | tex_coords = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1,)) 115 | # texture coordinates 116 | 117 | x, y, z = 0, 0, -1 118 | X, Y, Z = x+1, y+1, z+1 119 | 120 | self.batch.add(4, GL_QUADS, self.side, 121 | ('v3f', (X + pos[0], y + pos[1], z + pos[2], x + pos[0], y + pos[1], z + pos[2], 122 | x + pos[0], Y + pos[1], z + pos[2], X + pos[0], Y + pos[1], z + pos[2],)), 123 | tex_coords) # back 124 | self.batch.add(4, GL_QUADS, self.side, 125 | ('v3f', (x + pos[0], y + pos[1], Z + pos[2], X + pos[0], y + pos[1], Z + pos[2], 126 | X + pos[0], Y + pos[1], Z + pos[2], x + pos[0], Y + pos[1], Z + pos[2],)), 127 | tex_coords) # front 128 | self.batch.add(4, GL_QUADS, self.side, 129 | ('v3f', (x + pos[0], y + pos[1], z + pos[2], x + pos[0], y + pos[1], Z + pos[2], 130 | x + pos[0], Y + pos[1], Z + pos[2], x + pos[0], Y + pos[1], z + pos[2],)), 131 | tex_coords) # left 132 | self.batch.add(4, GL_QUADS, self.side, 133 | ('v3f', (X + pos[0], y + pos[1], Z + pos[2], X + pos[0], y + pos[1], z + pos[2], 134 | X + pos[0], Y + pos[1], z + pos[2], X + pos[0], Y + pos[1], Z + pos[2],)), 135 | tex_coords) # right 136 | self.batch.add(4, GL_QUADS, self.bottom, 137 | ('v3f', (x + pos[0], y + pos[1], z + pos[2], X + pos[0], y + pos[1], z + pos[2], 138 | X + pos[0], y + pos[1], Z + pos[2], x + pos[0], y + pos[1], Z + pos[2],)), 139 | tex_coords) # bottom 140 | self.batch.add(4, GL_QUADS, self.top, 141 | ('v3f', (x + pos[0], Y + pos[1], Z + pos[2], X + pos[0], Y + pos[1], Z + pos[2], 142 | X + pos[0], Y + pos[1], z + pos[2], x + pos[0], Y + pos[1], z + pos[2],)), 143 | tex_coords) # top 144 | 145 | def add_floor(self, pos): 146 | tex_coords = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1,)) 147 | # texture coordinates 148 | x, y, z = 0, 0, -1 149 | X, Y, Z = x + 1, y + 1, z + 1 150 | self.batch.add(4, GL_QUADS, self.bottom, 151 | ('v3f', (x + pos[0], y + pos[1], z + pos[2], X + pos[0], y + pos[1], z + pos[2], 152 | X + pos[0], y + pos[1], Z + pos[2], x + pos[0], y + pos[1], Z + pos[2],)), 153 | tex_coords) # bottom 154 | 155 | def add_gate(self, pos): 156 | tex_coords = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1,)) 157 | # texture coordinates 158 | x, y, z = 0, 0, -1 159 | X, Y, Z = x + 1, y + 1, z + 1 160 | self.batch.add(4, GL_QUADS, self.gate, 161 | ('v3f', (X + pos[0], y + pos[1], z + pos[2], x + pos[0], y + pos[1], z + pos[2], 162 | x + pos[0], Y + pos[1], z + pos[2], X + pos[0], Y + pos[1], z + pos[2],)), 163 | tex_coords) # back 164 | 165 | def add_portal(self, pos): 166 | tex_coords = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1,)) 167 | # texture coordinates 168 | x, y, z = 0, 0, -1 169 | X, Y, Z = x + 1, y + 1, z + 1 170 | self.batch.add(4, GL_QUADS, self.portal, 171 | ('v3f', (x + pos[0], y + pos[1], Z + pos[2], X + pos[0], y + pos[1], Z + pos[2], 172 | X + pos[0], Y + pos[1], Z + pos[2], x + pos[0], Y + pos[1], Z + pos[2],)), 173 | tex_coords) # front 174 | 175 | def add_ceiling(self, pos): 176 | tex_coords = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1,)) 177 | # texture coordinates 178 | x, y, z = 0, 0, -1 179 | X, Y, Z = x + 1, y + 1, z + 1 180 | self.batch.add(4, GL_QUADS, self.ceiling, 181 | ('v3f', (x + pos[0], Y + pos[1], Z + pos[2], X + pos[0], Y + pos[1], Z + pos[2], 182 | X + pos[0], Y + pos[1], z + pos[2], x + pos[0], Y + pos[1], z + pos[2],)), 183 | tex_coords) # top 184 | 185 | def __init__(self): 186 | self.batch = pyglet.graphics.Batch() 187 | # create batch 188 | 189 | self.top = self.get_tex('bricks_d.jpg') 190 | self.side = self.get_tex('bricks.jpg') 191 | self.bottom = self.get_tex('grass.jpg') 192 | self.gate = self.get_tex('gate_closed.jpg') 193 | self.portal = self.get_tex('portal.jpg') 194 | self.ceiling = self.get_tex('bricks_d.jpg') 195 | 196 | for i in range(labyrinth.SIZE[1] * 2 + 1): 197 | for j in range(labyrinth.SIZE[0] * 2 + 1): 198 | if labyrinth.hash_lab[j][i] == 1: 199 | self.add_cube((i, 0, j)) 200 | elif labyrinth.hash_lab[j][i] == 0: 201 | self.add_floor((i, 0, j)) 202 | self.add_ceiling((i, 0, j)) 203 | self.add_gate((1, 0, 0)) 204 | self.add_portal((LAB_SIZE[1] * 2 - 1, 0, LAB_SIZE[0] * 2)) 205 | 206 | # add to batch to draw all at once 207 | 208 | def draw(self): 209 | global pos 210 | self.batch.draw() 211 | pyglet.text.Label('Hello Truman!', color=(255, 255, 255, 255), font_name='Arial', font_size=8, 212 | x=50, y=-10, anchor_x='center', anchor_y='center').draw() 213 | pyglet.image.load('thetrumanshow.jpg').blit(-400, -50, -600) 214 | 215 | """ 216 | pyglet.text.Label('X:' + str(int(window.player.pos[0])) + 217 | ' Y:' + str(int(window.player.pos[1])) + 218 | ' Z:' + str(int(window.player.pos[2])), 219 | font_name='monospace', font_size=12, x=10, y=10, anchor_x='center', anchor_y='center').draw() 220 | try: 221 | pyglet.text.Label(str(labyrinth.hash_lab[int(window.player.pos[2]+1)][int(window.player.pos[0])]), 222 | font_name='monospace', font_size=12, x=10, y=30, anchor_x='center', anchor_y='center').draw() 223 | except: 224 | pass 225 | """ 226 | 227 | 228 | class Player: 229 | def __init__(self, pos, rot): 230 | self.pos = list(pos) 231 | self.rot = list(rot) 232 | self.lock = True 233 | 234 | def mouse_motion(self, dx, dy): 235 | dx /= 6 236 | dy /= 6 237 | self.rot[0] += dy 238 | self.rot[1] -= dx 239 | 240 | def update(self, dt, keys): 241 | pN, pS, pE, pW = False, False, False, False 242 | 243 | try: 244 | if self.lock: # checks if the player is locked by the labyrinth walls 245 | pN = bool(labyrinth.hash_lab[int(self.pos[2] + 0.7)][int(self.pos[0])]) 246 | pS = bool(labyrinth.hash_lab[int(self.pos[2] + 1.3)][int(self.pos[0])]) 247 | pE = bool(labyrinth.hash_lab[int(self.pos[2] + 1.1)][int(self.pos[0] + 0.2)]) 248 | pW = bool(labyrinth.hash_lab[int(self.pos[2] + 1.1)][int(self.pos[0] - 0.2)]) 249 | except: 250 | pN, pS, pE, pW = False, False, False, False 251 | 252 | s = dt * 5 253 | rotY = -self.rot[1] / 180 * math.pi 254 | dx, dz = s * math.sin(rotY), s * math.cos(rotY) 255 | if keys[key.W]: 256 | if (dx > 0 and not pE) or (dx < 0 and not pW): 257 | self.pos[0] += dx 258 | if (dz > 0 and not pN) or (dz < 0 and not pS): 259 | self.pos[2] -= dz 260 | if keys[key.S]: 261 | if (dx < 0 and not pE) or (dx > 0 and not pW): 262 | self.pos[0] -= dx 263 | if (dz < 0 and not pN) or (dz > 0 and not pS): 264 | self.pos[2] += dz 265 | if keys[key.A]: 266 | if (dz < 0 and not pE) or (dz > 0 and not pW): 267 | self.pos[0] -= dz 268 | if (dx > 0 and not pN) or (dx < 0 and not pS): 269 | self.pos[2] -= dx 270 | if keys[key.D]: 271 | if (dz > 0 and not pE) or (dz < 0 and not pW): 272 | self.pos[0] += dz 273 | if (dx < 0 and not pN) or (dx > 0 and not pS): 274 | self.pos[2] += dx 275 | if keys[key.C]: 276 | self.pos = [1.5, 0.5, -0.5] 277 | self.rot = [0, 180] 278 | if keys[key.Q]: 279 | self.pos[1] += s 280 | if keys[key.E]: 281 | self.pos[1] -= s 282 | if keys[key.L]: 283 | self.lock = bool(1-self.lock) 284 | pN, pS, pE, pW = False, False, False, False 285 | """ 286 | try: 287 | print(self.pos, self.rot, dx, dz, "C:", str(labyrinth.hash_lab[int(self.pos[2]+1)][int(self.pos[0])]), 288 | "N:", pN, "S:", pS, "E:", pE, "W:", pW, "L:", self.lock) 289 | except: 290 | pass 291 | """ 292 | 293 | 294 | class Window(pyglet.window.Window): 295 | 296 | def push(self, pos, rot): 297 | glPushMatrix() 298 | glRotatef(-rot[0], 1, 0, 0) 299 | glRotatef(-rot[1], 0, 1, 0) 300 | glTranslatef(-pos[0], -pos[1], -pos[2], ) 301 | 302 | def Projection(self): 303 | glMatrixMode(GL_PROJECTION) 304 | glLoadIdentity() 305 | 306 | def Model(self): 307 | glMatrixMode(GL_MODELVIEW) 308 | glLoadIdentity() 309 | 310 | def set2d(self): 311 | # set up the window context as 2D 312 | self.Projection() 313 | gluOrtho2D(0, self.width, 0, self.height) 314 | # min and max render distance 315 | self.Model() 316 | 317 | def set3d(self): 318 | # set up the window context as 3D 319 | self.Projection() 320 | gluPerspective(60, self.width/self.height, 0.05, 1000) 321 | # 70 - fov, field of view; w/h - aspect ratio; 0.05/1000 - min/max render distance 322 | self.Model() 323 | 324 | def setLock(self, state): 325 | self.lock = state 326 | self.set_exclusive_mouse(state) 327 | 328 | lock = True 329 | mouse_lock = property(lambda self: self.lock, setLock) 330 | 331 | def on_mouse_motion(self, x, y, dx, dy): 332 | if self.mouse_lock: self.player.mouse_motion(dx, dy) 333 | 334 | def on_key_press(self, KEY, MOD): 335 | if KEY == key.ESCAPE: self.close() 336 | elif KEY == key.SPACE: self.mouse_lock = not self.mouse_lock 337 | 338 | def __init__(self, *args, **kwargs): 339 | super().__init__(*args, **kwargs) 340 | self.set_minimum_size(200, 200) 341 | self.keys = key.KeyStateHandler() 342 | self.push_handlers(self.keys) 343 | pyglet.clock.schedule(self.update) 344 | 345 | self.model = Model() 346 | self.player = Player(start_pos, start_rot) 347 | 348 | def update(self, dt): 349 | self.player.update(dt, self.keys) 350 | 351 | def on_draw(self): 352 | self.clear() 353 | self.set3d() 354 | self.push(self.player.pos, self.player.rot) 355 | self.model.draw() 356 | glPopMatrix() 357 | 358 | 359 | # Let's start! 360 | random.seed() 361 | seed = random.randint(0, 1000) 362 | LAB_SIZE = (20, 20) 363 | if sys.getrecursionlimit() < LAB_SIZE[0] * LAB_SIZE[1]: 364 | sys.setrecursionlimit(LAB_SIZE[0] * LAB_SIZE[1]) 365 | 366 | labyrinth = Lab(LAB_SIZE, seed) 367 | 368 | start_pos = [1.5, 0.5, -0.5] 369 | start_rot = [0, 180] 370 | 371 | if __name__ == '__main__': 372 | window = Window(caption='Labyrinth 3D v2.0 - #' + str(seed), resizable=True, fullscreen=True) 373 | window.set_mouse_visible(False) 374 | window.set_exclusive_mouse(True) 375 | glClearColor(0.1, 0.2, 0.3, 1) 376 | glEnable(GL_DEPTH_TEST) 377 | glShadeModel(GL_SMOOTH) 378 | # glEnable(GL_CULL_FACE) 379 | 380 | pyglet.app.run() 381 | 382 | """ 383 | # shuffles directions each time for more randomness 384 | for dir in dirs: 385 | new_x = x1 + GO_DIR[dir][0] 386 | new_y = y1 + GO_DIR[dir][1] 387 | if (new_y in range(SIZE[1])) and\ 388 | (new_x in range(SIZE[0])) and\ 389 | (lab[new_y][new_x] == 0): 390 | # checks if the new cell is not visited 391 | lab[y][x] |= dir 392 | lab[new_y][new_x] |= REVERSE[dir] 393 | # if so, apply info on passages to both cells 394 | solve(new_x, new_y) 395 | # repeat recursively 396 | 397 | - preparing the solve() function - some notes below for further decision-making: 398 | - recursion vs while True loop (until (x2, y2) reached on path list) 399 | - visited - number (min number of visits each time) 400 | - path - list of cells' coordinates (append good ones, pop bad ones) 401 | - backtracking status - processing new branch or withdrawing from dead-end - needed True/False to determine\ 402 | if the crossroad cell should be marked as visited more than once (not if back from a dead-end and checking\ 403 | a new alternative - maybe manually putting the minus) 404 | 405 | - backward tracking on the branch: 406 | - if min num of visits = 0 - it's a new path to check - append each visited cell to the path list, decrease by one the number of visits on the crossroad cell, invert backtracking status 407 | - if min num of visits = 1 - it's a dead-end - pop the whole branch from path list (one by one) until a crossroads with unvisited branches reached; then proceed the new path 408 | - apply path to hash_lab to display during print (,.:;) 409 | 410 | """ --------------------------------------------------------------------------------