-
Notifications
You must be signed in to change notification settings - Fork 0
/
Level.py
445 lines (351 loc) · 16.3 KB
/
Level.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# -*- coding: utf-8 -*-
"""
Created on Sun Jan 13 13:17:57 2019
@author:
"""
import constants as const
import Wall
import Character
import Bomb
from pathlib import Path
import pygame
import sys
def openImage(imageFile, convertAlpha = False):
'''
Verifies an image file is valid
- imageFile, file location of image to be loaded, in pathlib format
- convertAlpha, if true, use convert_alpha() instead of convert(), for png files with transparency
'''
if imageFile and not imageFile.is_file():
raise RuntimeError(str(imageFile) + ' is not a valid image file.')
try:
if convertAlpha:
image = pygame.image.load(str(imageFile)).convert_alpha()
else:
image = pygame.image.load(str(imageFile)).convert()
return image
except IOError:
pygame.mixer.music.stop()
pygame.display.quit()
pygame.quit()
raise RuntimeError('Error: File ' + str(imageFile) + " does not exist!")
def checkNumeric(value):
if not isinstance(value, int) and not isinstance(value, float):
raise RuntimeError('Error: ' + str(value) + ' is not a number')
return value
def checkPositive(value):
if isinstance(value, int) and not value > 0:
raise RuntimeError('Error: ' + str(value) + ' is not a positive number')
return value
class Level(object):
'''
This class holds all required information for a level, to include layout, level
size parameters, character starting positions, the state of the level door,
and images for walls, background, and the door
'''
def __init__(self, levelNum):
checkNumeric(levelNum)
checkPositive(levelNum)
self.layout = []
self.numEnemies = 0
self.enemyStartPosit = []
# Set the current working directory, read and write:
dataDir = Path.cwd() / "data"
#Open csv level file to create level layout
levelFile = dataDir.joinpath("level" + str(levelNum) + ".csv")
self.layout = self.levelParser(levelFile)
@property
def backgroundImage(self):
''' Accessor. '''
return self.__backgroundImage
@backgroundImage.setter
def backgroundImage(self, backgroundImage):
'''Sets the background tile image. Allows only a pygame surface'''
if not isinstance(backgroundImage, pygame.SurfaceType):
raise RuntimeError(str(backgroundImage) + ' is not a valid pygame image.')
self.__backgroundImage = backgroundImage
@property
def wallImage(self):
''' Accessor. '''
return self.__wallImage
@wallImage.setter
def wallImage(self, wallImage):
'''Sets the wall tile image. Allows only a pygame surface'''
if not isinstance(wallImage, pygame.SurfaceType):
raise RuntimeError(str(wallImage) + ' is not a valid pygame image.')
self.__wallImage = wallImage
@property
def breakableImage(self):
''' Accessor. '''
return self.__breakableImage
@breakableImage.setter
def breakableImage(self, breakableImage):
'''Sets the breakable wall tile image. Allows only a pygame surface'''
if not isinstance(breakableImage, pygame.SurfaceType):
raise RuntimeError(str(breakableImage) + ' is not a valid pygame image.')
self.__breakableImage = breakableImage
@property
def doorOpenedImage(self):
''' Accessor. '''
return self.__doorOpenedImage
@doorOpenedImage.setter
def doorOpenedImage(self, doorOpenedImage):
'''Sets the open door image. Allows only a pygame surface'''
if not isinstance(doorOpenedImage, pygame.SurfaceType):
raise RuntimeError(str(doorOpenedImage) + ' is not a valid pygame image.')
self.__doorOpenedImage = doorOpenedImage
@property
def doorClosedImage(self):
''' Accessor. '''
return self.__doorClosedImage
@doorClosedImage.setter
def doorClosedImage(self, doorClosedImage):
'''Sets the closed door image. Allows only a pygame surface'''
if not isinstance(doorClosedImage, pygame.SurfaceType):
raise RuntimeError(str(doorClosedImage) + ' is not a valid pygame image.')
self.__doorClosedImage = doorClosedImage
@property
def layout(self):
''' Accessor. '''
return self.__layout
@layout.setter
def layout(self, layout):
''' Prevents level layout from being assigned None '''
if layout == None:
raise RuntimeError('Level layout matrix cannot be None')
self.__layout = layout
@property
def levelWidth(self):
''' Accessor. '''
return self.__levelWidth
#no levelWidth.setter
@property
def levelHeight(self):
''' Accessor. '''
return self.__levelHeight
#no levelHeight.setter
@property
def playerStartPosit(self):
''' Accessor. '''
return self.__playerStartPosit
@playerStartPosit.setter
def playerStartPosit(self, playerStartPosit):
'''Sets the player starting position '''
if not isinstance(playerStartPosit, tuple) or len(playerStartPosit) != 2:
raise RuntimeError(str(playerStartPosit) + ' is not a valid valid value for player starting position. Must be a tuple containing x and y.')
elif playerStartPosit[0] < 0 or playerStartPosit[1] < 0:
raise RuntimeError(str(playerStartPosit) + ' is not a valid valid value for player starting position. Both x and y must be greater than 0.')
self.__playerStartPosit = playerStartPosit
@property
def numEnemies(self):
''' Accessor. '''
return self.__numEnemies
@numEnemies.setter
def numEnemies(self, numEnemies):
'''Sets the number of enemies in a level'''
checkNumeric(numEnemies)
if numEnemies != 0:
checkPositive(numEnemies)
self.__numEnemies = numEnemies
@property
def enemyStartPosit(self):
''' Accessor. '''
return self.__enemyStartPosit
@enemyStartPosit.setter
def enemyStartPosit(self, enemyStartPosit):
''' Prevents enemy start position list from being assigned None '''
if enemyStartPosit == None:
raise RuntimeError('Enemy start position list cannot be None')
self.__enemyStartPosit = enemyStartPosit
@property
def bossLevel(self):
''' Accessor. '''
return self.__bossLevel
@bossLevel.setter
def bossLevel(self, bossLevel):
'''Sets the value of bossLevel, used for determining whether the level is a boss level. Type is boolean '''
if bossLevel and not isinstance(bossLevel, bool):
raise RuntimeError(str(bossLevel) + ' is not a valid valid value for bossLevel.')
self.__bossLevel = bossLevel
@property
def bossStartPosit(self):
''' Accessor. '''
return self.__bossStartPosit
@bossStartPosit.setter
def bossStartPosit(self, bossStartPosit):
'''Sets the player starting position '''
if bossStartPosit and (not isinstance(bossStartPosit, tuple) or len(bossStartPosit) != 2):
raise RuntimeError(str(bossStartPosit) + ' is not a valid valid value for boss starting position. Must be a tuple containing x and y.')
elif bossStartPosit and (bossStartPosit[0] < 0 or bossStartPosit[1] < 0):
raise RuntimeError(str(bossStartPosit) + ' is not a valid valid value for boss starting position. Both x and y must be greater than 0.')
self.__bossStartPosit = bossStartPosit
def levelParser(self, levelFile):
'''
Loads a level file, sets appropriate images for level, and returns the layout
- levelFile, file location of level to be loaded, in pathlib format
'''
layout = []
graphicsDir = Path.cwd() / "graphics"
if not levelFile or not levelFile.is_file():
raise RuntimeError(str(levelFile) + ' is not a valid file.')
try:
with levelFile.open() as f:
levelParams = f.readline().split(",")
self.__levelWidth = int(levelParams[const.LEVEL_WIDTH])
self.__levelHeight = int(levelParams[const.LEVEL_HEIGHT])
self.bossLevel = False
backgroundNum = int(levelParams[const.LEVEL_BG_GFX])
wallNum = int(levelParams[const.LEVEL_WALL_GFX])
breakableNum = int(levelParams[const.LEVEL_BREAK_GFX])
enemyNum = int(levelParams[const.LEVEL_ENEMY_GFX])
for i in range(self.levelHeight):
line = f.readline().split(",")
line[-1] = line[-1][0] #remove "\n" from the last element of each line imported from the csv file
layout.append(line)
for y in range(self.levelHeight):
for x in range(self.levelWidth):
if layout[y][x] == '':
layout[y][x] = None
elif layout[y][x] == const.TILE_WALL:
layout[y][x] = Wall.Wall(False, 0, x, y)
elif layout[y][x] == const.TILE_BREAKABLE:
layout[y][x] = Wall.Wall(True, 0, x, y)
elif layout[y][x] == const.TILE_DOOR_HIDDEN:
layout[y][x] = Wall.Wall(True, 0, x, y, True)
elif layout[y][x] == const.TILE_PLAYER_START:
layout[y][x] = None
self.playerStartPosit = (x, y)
elif layout[y][x] == const.TILE_ENEMY_SPAWN:
layout[y][x] = None
self.numEnemies += 1
self.enemyStartPosit.append((x, y))
elif layout[y][x] == const.TILE_BOSS_SPAWN:
layout[y][x] = None
self.bossStartPosit = (x, y)
self.bossLevel = True
f.close()
except IOError:
pygame.mixer.music.stop()
pygame.display.quit()
pygame.quit()
raise RuntimeError('Error: File ' + str(levelFile) + " does not exist!")
backgroundFile = graphicsDir.joinpath("back" + str(backgroundNum) + ".png")
self.backgroundImage = openImage(backgroundFile)
wallFile = graphicsDir.joinpath("wall" + str(wallNum) + ".png")
self.wallImage = openImage(wallFile)
breakableFile = graphicsDir.joinpath("break" + str(breakableNum) + ".png")
self.breakableImage = openImage(breakableFile)
doorOpenedFile = graphicsDir.joinpath("door_opened.png")
self.doorOpenedImage = openImage(doorOpenedFile, True)
doorClosedFile = graphicsDir.joinpath("door_closed.png")
self.doorClosedImage = openImage(doorClosedFile, True)
self.enemyFile = graphicsDir.joinpath("enemy" + str(enemyNum) + ".png")
return layout
def showDoor(self):
'''
Shows a door where a breakable wall was previously shown, used when the wall hiding the door breaks
'''
for y in range(self.levelHeight):
for x in range(self.levelWidth):
if isinstance(self.layout[y][x], Wall.Wall) and self.layout[y][x].door:
self.layout[y][x] = const.TILE_DOOR_CLOSED
def openDoor(self):
'''
Opens the door of the level
'''
for y in range(self.levelHeight):
for x in range(self.levelWidth):
if self.layout[y][x] == const.TILE_DOOR_CLOSED:
self.layout[y][x] = const.TILE_DOOR_OPENED
def destroyWalls(self, x, y, blastRange):
'''Used when a bomb explodes to put blast graphics in all four directions
returns powerups and blasts so they can be drawn onscreen by the caller
- x, x location where blast starts
- y, y location where blast starts
- blastRange, used to limit how far the blasts extend in each direction
'''
checkNumeric(x)
if x != 0:
checkPositive(x)
checkNumeric(y)
if y != 0:
checkPositive(y)
checkNumeric(blastRange)
checkPositive(blastRange)
walls = []
powerups = []
hitWallUp = False
hitWallDown = False
hitWallLeft = False
hitWallRight = False
blasts = []
blasts.append(Bomb.Blast(x, y, const.CENTER_FLAME, True))
for i in range(1, blastRange + 1):
if y - i > 0 and not hitWallUp:
if isinstance(self.layout[y - i][x], Wall.Wall):
walls.append(self.layout[y - i][x])
hitWallUp = True
if self.layout[y - i][x].breakable:
blasts.append(Bomb.Blast(x, y - i, const.UP_FLAME, True))
if not hitWallUp:
if i < blastRange:
blasts.append(Bomb.Blast(x, y - i, const.UP_FLAME, False))
else:
blasts.append(Bomb.Blast(x, y - i, const.UP_FLAME, True))
if y + i < self.levelHeight and not hitWallDown:
if isinstance(self.layout[y + i][x], Wall.Wall):
walls.append(self.layout[y + i][x])
hitWallDown = True
if self.layout[y + i][x].breakable:
blasts.append(Bomb.Blast(x, y + i, const.DOWN_FLAME, True))
if not hitWallDown:
if i < blastRange:
blasts.append(Bomb.Blast(x, y + i, const.DOWN_FLAME, False))
else:
blasts.append(Bomb.Blast(x, y + i, const.DOWN_FLAME, True))
if x - i > 0 and not hitWallLeft:
if isinstance(self.layout[y][x - i], Wall.Wall):
walls.append(self.layout[y][x - i])
hitWallLeft = True
if self.layout[y][x - i].breakable:
blasts.append(Bomb.Blast(x - i, y, const.LEFT_FLAME, True))
if not hitWallLeft:
if i < blastRange:
blasts.append(Bomb.Blast(x - i, y, const.LEFT_FLAME, False))
else:
blasts.append(Bomb.Blast(x - i, y, const.LEFT_FLAME, True))
if x + i < self.levelWidth and not hitWallRight:
if isinstance(self.layout[y][x + i], Wall.Wall):
walls.append(self.layout[y][x + i])
hitWallRight = True
if self.layout[y][x + i].breakable:
blasts.append(Bomb.Blast(x + i, y, const.RIGHT_FLAME, True))
if not hitWallRight:
if i < blastRange:
blasts.append(Bomb.Blast(x + i, y, const.RIGHT_FLAME, False))
else:
blasts.append(Bomb.Blast(x + i, y, const.RIGHT_FLAME, True))
for theWall in walls:
result = theWall.destroy(self)
if result:
powerups.append(result)
return powerups, blasts
def startNewLevel(num):
'''
Loads level file based on number passed, returns level, player and enemies
returns the new level, player, enemy list, and boss (None if no boss in level)
- num, level number which is passed to the level initialization method
'''
level = Level(num)
x, y = level.playerStartPosit
player = Character.PlayerCharacter(level, x, y)
enemies = []
if level.bossLevel:
x, y = level.bossStartPosit
boss = Character.Boss(level, x, y)
else:
boss = None
for i in range(level.numEnemies):
x, y = level.enemyStartPosit[i]
enemies.append(Character.Enemy(level, x, y))
return (level, player, enemies, boss)