I just completed the first release for my first game on Python, called Dungeon Ball. I'm looking for people to test out the app and give me some feedback and constructive criticism. I'd like to use this project as a way to improve my programming skills and hopefully learn some efficient programming practices.
The game is called Dungeon Ball. It is pretty basic at the moment. It is pretty similar to games on breaking bricks with a ball and a racquet/paddle but I haven't included the bricks just yet. Currently, the goal is to just keep the ball from falling using the paddle. By hitting the paddle you get points, which causes you to level up. The higher the level, the faster the paddle and ball move.
Main.py
import pygame
from pygame.locals import *
import numpy as np
import math
from sys import exit
pygame.init()
from variables import *
def gameOver():
pygame.mixer.music.stop()
sounds['gameOver'].play()
keyStatus = True
blinkerCount = 0
blinkerState = True
blinkTime = 15
while keyStatus:
pygame.draw.rect(DISPLAYSURF, colours['grey'], dimensions['arena'])
# pygame.draw.rect(DISPLAYSURF, colours['brown'], dimensions['arena'], borderWidth)
if blinkerState:
textSurfaceObj = fonts['largeFont'].render('GAME OVER!', True, colours['red'])
textRectObj = textSurfaceObj.get_rect()
textRectObj.center = (boxSize[0]/2, boxSize[1]/2)
DISPLAYSURF.blit(textSurfaceObj, textRectObj)
scoreSurface = fonts['midFont'].render('Score : {}'.format(gameStatus['points']), True, colours['white'])
scoreSurfaceRect = scoreSurface.get_rect()
scoreSurfaceRect.center = (boxSize[0]/2, boxSize[1]/2 + 50)
DISPLAYSURF.blit(scoreSurface, scoreSurfaceRect)
blinkerCount += 1
if blinkerCount % blinkTime == 0:
blinkerCount = 0
blinkerState = not blinkerState
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN:
sounds['gameOver'].stop()
keyStatus = False
elif event.key == pygame.K_ESCAPE:
pygame.quit()
exit()
pygame.display.update()
fpsClock.tick(FPS)
if keyStatus == False:
break
main()
def renderFunction():
global gameStatus
pygame.draw.rect(DISPLAYSURF, colours['black'], dimensions['arena'])
pygame.draw.rect(DISPLAYSURF, colours['brown'], dimensions['arena'], borderWidth)
pygame.draw.rect(DISPLAYSURF, colours['red'], dimensions['paddle'])
pygame.draw.circle(DISPLAYSURF, colours['blue'], (ball['position']['x'], ball['position']['y']), ball['rad'] , 0)
pointSurface = fonts['tinyFont'].render('Points : ' + str(gameStatus['points']), True, colours['white'])
pointSurfaceRect = pointSurface.get_rect()
pointSurfaceRect.center = (40, boxSize[1]-10)
DISPLAYSURF.blit(pointSurface, pointSurfaceRect)
levelSurface = fonts['tinyFont'].render('Level : ' + str(gameStatus['level']), True, colours['white'])
levelSurfaceRect = levelSurface.get_rect()
levelSurfaceRect.center = (boxSize[0]-40, boxSize[1]-10)
DISPLAYSURF.blit(levelSurface, levelSurfaceRect)
def introScreen():
keyStatus = True
blinkerCount = 0
blinkerState = True
blinkTime = 15
pygame.mixer.music.load('audio/startScreenMusic.wav')
pygame.mixer.music.play(-1, 0.0)
while keyStatus:
pygame.draw.rect(DISPLAYSURF, colours['grey'], dimensions['arena'])
# pygame.draw.rect(DISPLAYSURF, colours['brown'], dimensions['arena'], borderWidth)
textSurfaceObj = fonts['largeFont'].render(gameStatus['name'], True, colours['gold'])
textRectObj = textSurfaceObj.get_rect()
textRectObj.center = (boxSize[0]/2, boxSize[1]/2)
DISPLAYSURF.blit(textSurfaceObj, textRectObj)
if blinkerState:
spaceSurfaceObj = fonts['midFont'].render('Press Enter to Continue', True, colours['white'])
spaceRectObj = spaceSurfaceObj.get_rect()
spaceRectObj.center = (boxSize[0]/2, boxSize[1]/2+50)
DISPLAYSURF.blit(spaceSurfaceObj, spaceRectObj)
versionSurface = fonts['tinyFont'].render(gameStatus['version'], True, colours['white'])
versionSurfaceRect = versionSurface.get_rect()
versionSurfaceRect.center = (boxSize[0]-20, boxSize[1]-10)
DISPLAYSURF.blit(versionSurface, versionSurfaceRect)
blinkerCount += 1
if blinkerCount % blinkTime == 0:
blinkerCount = 0
blinkerState = not blinkerState
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN:
keyStatus = False
elif event.key == pygame.K_ESCAPE:
pygame.quit()
exit()
pygame.display.update()
fpsClock.tick(FPS)
keyStatus=True
pygame.mixer.music.stop()
def eventHandler():
global dimensions
keys=pygame.key.get_pressed()
if keys[K_LEFT] and not (dimensions['paddle'].left <= (dimensions['arena'].left+borderWidth)):
direction = -1*paddle['speed']
# print('hi left')
paddle['position']['x'] += direction
elif keys[K_RIGHT] and not (dimensions['paddle'].right >= (dimensions['arena'].right-borderWidth)):
direction = paddle['speed']
# print('hi right')
paddle['position']['x'] += direction
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
dimensions['paddle'] = pygame.Rect(paddle['position']['x'], paddle['position']['y'], paddle['length'], 10)
def ballEngine():
global gameStatus
if (ball['position']['x'] <= (dimensions['arena'].left+borderWidth+ball['rad'])):
# print('LeftSideBounce')
ball['direction'] = 180 - ball['direction'] + np.random.randint(-1*gameStatus['random'],gameStatus['random'])
sounds['wallHit'].play()
elif (ball['position']['x'] >= (dimensions['arena'].right-borderWidth-ball['rad'])):
# print('RightSideBounce')
ball['direction'] = 180 - ball['direction'] + np.random.randint(-1*gameStatus['random'],gameStatus['random'])
sounds['wallHit'].play()
elif ball['position']['y'] <= (dimensions['arena'].top+borderWidth+ball['rad']):
# print('TopBounce')
ball['direction'] = 360 - ball['direction'] + np.random.randint(-1*gameStatus['random'],gameStatus['random'])
if ball['direction'] >= 250 and ball['direction'] <= 290:
ball['direction'] += np.random.randint(-2*gameStatus['random'],2*gameStatus['random'])
sounds['wallHit'].play()
elif ball['position']['y'] >= (dimensions['arena'].bottom - borderWidth - ball['rad']):
# print('BottomBounce')
# ball['speed'] = 0
# gameStatus = True
gameOver()
# print(ball['direction'])
if (ball['position']['y'] >= (paddle['position']['y']-ball['rad']) and ball['position']['y'] <= paddle['position']['y']+dimensions['paddle'].height+ball['rad']) and ball['position']['x'] >= dimensions['paddle'].left and ball['position']['x'] <= dimensions['paddle'].right:
# print('Paddle hit')
ball['direction'] = 360 - ball['direction'] + np.random.randint(-1*gameStatus['random'],gameStatus['random'])
gameStatus['points'] = gameStatus['points'] + 1
sounds['paddleHit'].play()
print(ball['position'], paddle['position'], ball['direction'])
gameStatus['paddleHitsPerLevel'] += 1
if ball['position']['y'] >= dimensions['paddle'].top and ball['position']['y'] <= dimensions['paddle'].bottom:
ball['position']['y'] = dimensions['paddle'].top - ball['rad']
if gameStatus['paddleHitsPerLevel'] == (gameStatus['level']*5) and not gameStatus['points'] == 0:
ball['speed'] += 2
gameStatus['level'] += 1
gameStatus['random'] += 2
gameStatus['paddleHitsPerLevel'] = 0
sounds['levelUp'].play()
if gameStatus['points'] % 10 == 0 and not gameStatus['points'] == 0:
paddle['speed'] += 1
if (ball['direction']>360 or ball['direction'] < 0):
ball['direction'] %= 360
if ball['direction'] % 90 >= 85 and ball['direction'] % 90 <=89 or ball['direction'] % 90 >= 0 and ball['direction'] % 90 <= 5:
ball['direction'] += np.random.randint(-2*gameStatus['random'],2*gameStatus['random'])
if ball['position']['y'] < borderWidth+ball['rad']:
ball['position']['y'] = borderWidth+ball['rad']
elif ball['position']['x'] < borderWidth+ball['rad']:
ball['position']['x'] = borderWidth+ball['rad']
elif ball['position']['x'] > dimensions['arena'].right-borderWidth-ball['rad']:
ball['position']['x'] = dimensions['arena'].right-borderWidth-ball['rad']
ball['position']['x'] += int(ball['speed']*math.cos(ball['direction']*math.pi/180))
ball['position']['y'] += int(ball['speed']*math.sin(ball['direction']*math.pi/180))
def init():
global ball, paddle, gameStatus
ball['position']['x']=boxSize[0]/2
ball['position']['y']=int(boxSize[1]/3)
ball['direction']=np.random.randint(295, 325)
ball['speed']=5
ball['rad']=5
paddle['position']['x']=boxSize[0]/2
paddle['position']['y']=boxSize[1]-50
paddle['length']=100
paddle['speed']=5
gameStatus['points']=0
gameStatus['level']=1
gameStatus['random']=5
def main():
introScreen()
init()
pygame.mixer.music.load('audio/gamePlayMusic.wav')
pygame.mixer.music.play(-1, 0.0)
while True:
eventHandler()
ballEngine()
renderFunction()
pygame.display.update()
fpsClock.tick(FPS)
if __name__ == '__main__':
fpsClock = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode(boxSize, 0, 32)
pygame.display.set_caption(gameStatus['name'])
main()
Variables.py
import numpy as np
import pygame
pygame.init()
from pygame.locals import *
import os
FPS = 30
borderWidth = 5
boxSize = (700, 400)
colours = {'black':(0, 0, 0),
'red': (255, 0, 0),
'blue':(0, 0, 255),
'brown':(210, 105, 30),
'green':(0, 255, 0),
'white':(255, 255, 255),
'gold':(255, 215, 0),
'silver':(192, 192, 192),
'grey':(128, 128, 128)}
ball = {'position':{'x':boxSize[0]/2, 'y':boxSize[1]/3}, 'direction':np.random.randint(295, 325), 'speed':5, 'rad':5}
paddle = {'position':{'x':boxSize[0]/2, 'y':boxSize[1]-50}, 'length':100, 'speed':5}
dimensions = {
'arena': pygame.Rect(0, 0, boxSize[0], boxSize[1]+10),
'paddle': pygame.Rect(paddle['position']['x'], paddle['position']['y'], paddle['length'], 10)
}
gameStatus = {'points': 0, 'level': 1, 'random': 5, 'paddleHitsPerLevel':0, 'name': 'Dungeon Ball', 'version': 'v1.0'}
fonts = {
'largeFont':pygame.font.Font(os.path.join(os.getcwd(),'fonts','Ancient_Modern_Tales_Regular.ttf'), 64),
'midFont':pygame.font.Font(os.path.join(os.getcwd(),'fonts', 'Old_School_Adventures_Regular.ttf'), 12),
'tinyFont': pygame.font.Font(os.path.join(os.getcwd(),'fonts', 'Old_School_Adventures_Regular.ttf'), 8)
}
sounds = {
'paddleHit': pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'paddle_hit.wav')),
'wallHit': pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'wall_hit.wav')),
'gameOver':pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'game_over.wav')),
'levelUp': pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'level_up.wav'))
}
Currently, the release is only for Linux and Windows. Mac users could try and build the environment and run the program directly. First step, you need to download your OS specific release and unzip it. Enter the extracted directory. Windows users just double click to run. Linux users need to open the directory in terminal and run ./main.
-
1\$\begingroup\$ The current question title, which states your concerns about the code, applies to too many questions on this site to be useful. The site standard is for the title to simply state the task accomplished by the code. Please see How to Ask for examples, and revise the title accordingly. \$\endgroup\$Mast– Mast ♦2020年08月04日 18:51:20 +00:00Commented Aug 4, 2020 at 18:51
-
\$\begingroup\$ @Mast thanks for the suggestion. Is this a better topic? \$\endgroup\$Souvik Saha– Souvik Saha2020年08月04日 19:03:28 +00:00Commented Aug 4, 2020 at 19:03
-
\$\begingroup\$ No, I've fixed it for you. Leave the concerns to the question body. \$\endgroup\$Mast– Mast ♦2020年08月04日 19:04:58 +00:00Commented Aug 4, 2020 at 19:04
1 Answer 1
I have not tried playing the game, but had a look at the code and I have some suggestions.
Code readability suggestions
boxSize[0]
and boxSize[1]
is used all over your code and not very readable. There is only one place where you actually use the variable boxSize
without indexes, so I would do the opposite and define width = 700
and height=400
so that you can refer to them where needed, and then in the one line that you used boxSize
you change that to
ISPLAYSURF = pygame.display.set_mode((width, height), 0, 32)
Readability 2
ball['direction']=np.random.randint(295, 325)
I think the numbers 295 and 325 refer to angles but they could be named to make that clearer.
Readability 3
ball['rad']
I think rad
is short for radius
but it's not a good name. Generally avoid short versions of words. Especially rad which in mathematics commonly refers to radians used to measure angles, which confused me while thinking about the direction of the ball.
Readability 4
if keys[K_LEFT] and not (dimensions['paddle'].left <= (dimensions['arena'].left+borderWidth)):
not <=
is requivalent to just >
so it would be more readable to rather write
if keys[K_LEFT] and (dimensions['paddle'].left > (dimensions['arena'].left+borderWidth)):
Logic 1
if blinkerCount % blinkTime == 0:
blinkerCount = 0
blinkerState = not blinkerState
Since you're resetting blinkerCount
to 0 every time, you don't need the modulo operation, you can just change the if-clause to if blinkerCount == blinkTime
. The modulo operation would make sense if you didn't reset to 0.
Logic 2
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN:
sounds['gameOver'].stop()
keyStatus = False
elif event.key == pygame.K_ESCAPE:
pygame.quit()
exit()
Both elif
here are redundant and can be replaced by just if
.
Since pygame.QUIT
and pygame.KEYDOWN
are different things, an event cannot by definition be equal to both, so the "else" in the elif
is not needed.
Avoid repetition 1
You are making many calls to
pygame.draw.rect(DISPLAYSURF
which I would create a new function for, so that you don't have to repeat this over and over.
It would be something like
def rectangle(color, _dimensions):
pygame.draw.rect(DISPLAYSURF, colors[color], _dimensions)
and then in the other places of your code you can replace something like
pygame.draw.rect(DISPLAYSURF, colours['grey'], dimensions['arena'])
with just
rectangle('grey', dimensions['arena'])
Avoid repetition 2
sounds = {
'paddleHit': pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'paddle_hit.wav')),
'wallHit': pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'wall_hit.wav')),
'gameOver':pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'game_over.wav')),
'levelUp': pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', 'level_up.wav'))
}
See how 70% of each line here is identical to the other? This is where you want to create a function for this, like
def get_sound(filename):
return pygame.mixer.Sound(os.path.join(os.getcwd(), 'audio', filename))
so that you can replace the above with
sounds = {
'paddleHit': get_sound('paddle_hit.wav'),
'wallHit': get_sound('wall_hit.wav'),
'gameOver':get_sound('game_over.wav'),
'levelUp': get_sound('level_up.wav')
}
(It can be made even shorter if the keys were named same as the files)
Code quality and readability
if ball['position']['y'] < borderWidth+ball['rad']:
This kind of code is quite hard to read and surely a waste of space and time to write as well. I recommend you look up basic objects/classes, so that you can define a class ball
and set its properties, so that you can instead write
if ball.y < borderWidth+ball.radius:
See how much easier that is?
-
\$\begingroup\$ Okay these are really insightful. Thanks for your suggestions. If you can please try out the game when you're free? I'd like a critique on the aesthetic as well. Also do you suggest any features I could add? \$\endgroup\$Souvik Saha– Souvik Saha2020年08月10日 20:24:44 +00:00Commented Aug 10, 2020 at 20:24
-
\$\begingroup\$ The problem with trying out the game is that I would have to set up an environment with pygame and maybe other libraries, and run code that I don't know all the details of, on my own computer, which is time consuming and a risk. I would try it if you could make it playable online somehow, in a virtual environment or something. Otherwise, I recommend making a recording of when you play it yourself and put that on Youtube, that makes it a lot more available to others and enables you to collect feedback from other people easier. \$\endgroup\$user985366– user9853662020年08月11日 00:26:03 +00:00Commented Aug 11, 2020 at 0:26
-
\$\begingroup\$ There is also gamedev stackexchange, I don't know their policies for posting, but it might be open to feedback on your own games. If not, there are other websites and communities for that. \$\endgroup\$user985366– user9853662020年08月11日 00:27:38 +00:00Commented Aug 11, 2020 at 0:27
-
\$\begingroup\$ I've created an exe file of the app so you do not have to create the environment. But no problem. Yeah I think I could do a video on YouTube or something similar. \$\endgroup\$Souvik Saha– Souvik Saha2020年08月11日 03:09:59 +00:00Commented Aug 11, 2020 at 3:09
-
\$\begingroup\$ Running an unknown exe file from an unknown person is an even greater risk than setting up the environment and running the code that I can at least read through. Not worth it, I'm afraid. I'd be happy to provide more code review input to an updated version of the code though, perhaps via github? \$\endgroup\$user985366– user9853662020年08月11日 18:57:39 +00:00Commented Aug 11, 2020 at 18:57