11
\$\begingroup\$

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.

\$\endgroup\$
3
  • 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
    Aug 4, 2020 at 18:51
  • \$\begingroup\$ @Mast thanks for the suggestion. Is this a better topic? \$\endgroup\$ Aug 4, 2020 at 19:03
  • \$\begingroup\$ No, I've fixed it for you. Leave the concerns to the question body. \$\endgroup\$
    – Mast
    Aug 4, 2020 at 19:04

1 Answer 1

6
+50
\$\begingroup\$

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?

\$\endgroup\$
7
  • \$\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\$ 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
    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
    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\$ 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
    Aug 11, 2020 at 18:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.