3
\$\begingroup\$

I'm trying to learn how to become a good programmer. I think this means making my code more readable for others. I would really appreciate any feedback whatsoever. I'm not quite sure what specifically I'm most lacking in so that's about as precise as I can get.

Currently I've split my program up into 5 parts:

  1. 'game.py': main program which runs the game

  2. 'entitie.py': describes how entities function (both enemy and player entities).*

  3. 'screen.py': describes how the screen works, takes care of blitting text and drawing all the entities

  4. 'levels.py': describes how enemies spawn in each of the levels, as well as what happens when the next level is reached

  5. 'game_mechanics.py': really just a file I plan to throw all my functions in that don't really fit anywhere else

I've heard about 'inheritance' and I suppose it might be better to split enemy and player entities up and use the overlap from a more basic entity, but I've never done that and it feels to me the code would become less readable.

game.py:

"""
Written by Nathan van 't Hof
9 January 2018

Main file used to control the game.
"""

from entitie import Entity
import screen
import pygame
import sys
import os
import time
import levels


def init():
    player = Entity('player', x=300, y=500)
    entities = []
    return player, entities


def update(player, entities):
    player.update_movement()
    for entity in entities:
        entity.update_movement()
    return player, entities

# initialize values
pygame.mixer.pre_init(44100, 16, 2, 4096)
pygame.init()
player, entities = init()
playing = True

# play music
pygame.mixer.music.load(os.path.join(os.getcwd(), "background.mp3"))
pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(-1)

# set display
display = screen.display()

# set basic values
level = 0
list_levels = [levels.level_0, levels.level_1, levels.level_2, levels.level_3, levels.level_4, levels.level_5]
previous_addition = time.time()
dt = 1

while playing:
    # update values
    player, entities = update(player, entities)
    player.update_score()
    level, entities, difficulty = levels.check_level_up(player.score, level, entities)

    # check if player wants to leave
    for event in pygame.event.get():
        if event.type == pygame.QUIT or event.type == pygame.KEYDOWN:
            playing = False

    # check if player died
    restart = player.check_die(entities, display)
    if restart > 0:
        level = 0
        entities = []

    # allow player to move
    player.interact(display.WIDTH, display.HEIGHT)

    # add enemies
    if time.time() - previous_addition > dt:
        entities, dt = list_levels[level](entities, display, player.x, player.y, player.score, difficulty)
        previous_addition = time.time()

    # draw all the levels
    entities = display.draw(entities, player)

# exit properly
pygame.quit()
sys.exit()

entitie.py

"""
Written by Nathan van 't Hof
9 January 2018

This is an entity class which can both be used for the player
as well as for enemies.

Player is a square whose velocity is dependant on an acceleration, dependant on distance of mouse in regards to
player object
Enemies are constant velocity squares

If player collides with a non-same color enemy, one life is retracted
"""

import game_mechanics
import time
import pygame
import win32api
from random import choice
import ctypes

# to ensure the mouse lines up with the pixels
user32 = ctypes.windll.user32
user32.SetProcessDPIAware()

possible_colors = [(250,0,0), (0,250,0), (0,0,250)]


class Entity:

    def __init__(self, type_entity, x=0, y=0, vx=0, vy=0, width=20):
        if type_entity == 'player':
            self.lives = 3
            self._input = True
            self.color = (0, 0, 250)
            self._width = 10
            self.high_score = 0

        if type_entity == 'enemy':
            self.lives = 1
            self._input = False
            self.color = choice(possible_colors)
            self._width = width

        self.score = 0

        self.x = x
        self._vx = vx
        self._ax = 0

        self.y = y
        self._vy = vy
        self._ay = 0

        self._last_updated = time.time()
        self._time_initiated = time.time()
        self._time_last_died = time.time()

        self._state_left_mouse = 0

    def update_movement(self):
        dt = time.time() - self._last_updated
        self._last_updated = time.time()
        self.x += self._vx * dt + 0.5 * self._ax * dt * dt
        self._vx += dt * self._ax
        self.y += self._vy * dt + 0.5 * self._ay * dt * dt
        self._vy += dt * self._ay

        # check if it is the player
        if self._input:

            # check if left mouse has been pressed, if so, it changes color
            new_state_left_mouse = win32api.GetKeyState(0x01)
            # Button state changed
            if new_state_left_mouse != self._state_left_mouse:
                self._state_left_mouse = new_state_left_mouse
                if new_state_left_mouse < 0:
                    self.color = (self.color[1], self.color[2], self.color[0])

    def update_score(self):
        self.score = int(time.time() - self._time_initiated)
        self.high_score = max(self.high_score, self.score)

    def check_die(self, entities, display):
        restart = 0

        # cannot immediately die
        if time.time() - self._time_last_died > 3:
            # check if any of the enemies cross the player
            for entity in entities:
                if entity.color != self.color:
                    if game_mechanics.collision_detect(self.x, entity.x, self.y, entity.y, self._width, entity._width, self._width, entity._width):
                        restart += self.die()
                        break
            # check if player is out of bounds
            if self.x > display.WIDTH or self.x < 0 or self.y > display.HEIGHT or self.y < 0 and restart == 0:
                restart = self.die()
        return restart

    def die(self):
        self._time_last_died = time.time()
        self.lives -= 1
        self.x = 500
        self.y = 500
        if self.lives == 0:
            self.restart()
            return 1
        return 0

    def restart(self):
        self.score = 0
        self.lives = 3
        self.x = 200
        self._vx = 0
        self._ax = 0

        self.y = 200
        self._vy = 0
        self._ay = 0

        self._last_updated = time.time()
        self._time_initiated = time.time()

    def interact(self, screen_x, screen_y):
        # only if the entity is the player (not really necessary, just an extra precaution)
        if self._input:
            # determine accelerations based on distance of mouse to player object
            x_mouse, y_mouse = win32api.GetCursorPos()
            dx = ((x_mouse - self.x)/screen_x)
            dy = ((y_mouse - self.y)/screen_y)

            self._ax = min(300000, 15000 * dx)
            self._ay = min(300000, 15000 * dy)

            # break hard when you deccelerate
            if self._ax * self._vx < 0 and self._ay * self._vy < 0:
                self._vx = self._vx * 0.8
                self._vy = self._vy * 0.8

    def draw(self, display):
        pygame.draw.rect(display, self.color, (self.x, self.y, self._width, self._width))

screen.py

"""
Written by Nathan van 't Hof
9 January 2018

The screen object all the objects are drawn on.
"""


import pygame
from win32api import GetSystemMetrics

class display:

    def __init__(self):

        self.BLACK = (0, 0, 0)
        self.WIDTH = GetSystemMetrics(0)
        self.HEIGHT = GetSystemMetrics(1)

        # full screen
        self.windowSurface = pygame.display.set_mode((self.WIDTH, self.HEIGHT), pygame.FULLSCREEN)
        self.windowSurface.fill(self.BLACK)

        self.font = pygame.font.Font(None, 32)

    def draw(self, entities, player):
        self.windowSurface.fill(self.BLACK)
        new_entities = []
        for entity in entities:
            entity.draw(self.windowSurface)
            # if entity is no longer in screen it is not added to the entity list, meaning it is no longer kept track of
            if not (entity.x > self.WIDTH or entity.x < -entity._width or
                    entity.y > self.HEIGHT or entity.y < -entity._width):
                new_entities.append(entity)
        player.draw(self.windowSurface)

        label = self.font.render('score : ' + str(player.score), 1, (250,250,250))
        self.windowSurface.blit(label, (20, 20))

        label = self.font.render('lives : ' + str(player.lives), 1, (250,250,250))
        self.windowSurface.blit(label, (20, 40))

        label = self.font.render('high-score : ' + str(player.high_score), 1, (250, 250, 250))
        self.windowSurface.blit(label, (20, 60))

        pygame.display.flip()
        return new_entities

levels.py

"""
Written by Nathan van 't Hof
9 January 2018

Used to control the behaviour of new enemies per level.
"""


from entitie import Entity
from random import randint


def level_up(level, entities):
    level += 1
    if level == 1:
        for entity in entities:
            entity._vx = 50
    elif level == 3:
        for entity in entities:
            entity._vx = -80
    return level, entities


def check_level_up(score, level, entities):
    """
    Keeps track of what level the player is currently in,
    once the last level is reached the difficulty is ramped up and it starts over.
    """
    max_level = 5
    difficulty = 1 + (score / 80) * 0.2
    correct_level = (score % 80) / 15

    if level == 0 and score > 7:
        level, entities = level_up(level, entities)
    elif correct_level > level and level != max_level:
        level, entities = level_up(level, entities)
    elif correct_level < level:
        level = correct_level
    return level, entities, difficulty


def level_0(entities, display, x_play, y_play, score, difficulty):
    """
    Adds entities with 0 speed at random locations in the field
    """
    entity = Entity('enemy',
                            x=randint(0,int(display.WIDTH)),
                            y=randint(0,int(display.HEIGHT)),
                            width=randint(10,40) * difficulty
                            )
    entities.append(entity)
    dt = 0.1 / difficulty
    return entities, dt


def level_1(entities, display, x_play, y_play, score, difficulty):
    """
    Adds entities on the left of the field, moving towards the right.
    """
    entity = Entity('enemy',
                             x = 0,
                             y = randint(0,int(display.HEIGHT)),
                             vx = 80,
                             width = randint(10,100)* difficulty
                             )
    entities.append(entity)
    dt = 0.7/ difficulty
    return entities, dt


def level_2(entities, display, x_play, y_play, score, difficulty):
    """
    Adds entities on the left of the field, moving towards the right.
    """
    for i in range(2):
        entity = Entity('enemy',
                                x = 0,
                                y = randint(0,int(display.HEIGHT)),
                                vx = randint(70, 120),
                                width = randint(10,60)* difficulty
                                )
        entities.append(entity)
    dt = 0.7/ difficulty
    return entities, dt


def level_3(entities, display, x_play, y_play, score, difficulty):
    """
    Adds entities on the right of the field, moving towards the left.
    """
    for i in range(3):
        entity = Entity('enemy',
                                x = display.WIDTH,
                                y = randint(0,int(display.HEIGHT)),
                                vx = randint(-180, -90),
                                vy = randint(-30, 30),
                                width = randint(10,70)* difficulty
                                )
        entities.append(entity)
    dt = 0.4/ difficulty
    return entities, dt

def level_4(entities, display, x_play, y_play, score, difficulty):
    """
    Adds entities around the field, moving towards the player.
    """
    positions = [[randint(0, display.WIDTH), 0],
                 [randint(0, display.WIDTH), display.HEIGHT],
                 [0, randint(0, display.HEIGHT)],
                 [display.WIDTH, randint(0, display.HEIGHT)]]
    for position in positions:
        entity = Entity('enemy',
                                x = position[0],
                                y = position[1],
                                vx = (x_play - position[0]) / 8,
                                vy = (y_play - position[1]) / 8,
                                width = randint(10,60)* difficulty
                                )
        entities.append(entity)
    dt = 70./ score / difficulty
    return entities, dt


def level_5(entities, display, x_play, y_play, score, difficulty):
    """
    Adds entities around the field, moving towards the player.
    Also adds entities going from left to right
    """
    positions = [[randint(0, display.WIDTH), 0],
                 [randint(0, display.WIDTH), display.HEIGHT],
                 [0, randint(0, display.HEIGHT)],
                 [display.WIDTH, randint(0, display.HEIGHT)]]
    for position in positions:
        entity = Entity('enemy',
                                x = position[0],
                                y = position[1],
                                vx = (x_play - position[0]) / 8,
                                vy = (y_play - position[1]) / 8,
                                width = randint(10,60)* difficulty
                                )
        entities.append(entity)

    for i in range(2):
        entity = Entity('enemy',
                                x = 0,
                                y = randint(0,int(display.HEIGHT)),
                                vx = randint(70, 120),
                                width = randint(10,60)* difficulty
                                )
        entities.append(entity)

    dt = 50./score / difficulty
    return entities, dt            

game_mechanics.py

"""
Written by Nathan van 't Hof
9 January 2018

This is used for extra mechanics that don't fit in anywhere else properly.

Currently only used to detect if two squares overlap.
"""


def collision_detect(x1, x2, y1, y2, w1, w2, h1, h2):
    """
    Check whether two rectangles (both parallel to the x-y axes) overlap

    :param x1: x value specified corner rectangle 1
    :param x2: x value specified corner rectangle 2
    :param y1: y value specified corner rectangle 1
    :param y2: y value specified corner rectangle 2
    :param w1: width rectangle 1
    :param w2: width rectangle 2
    :param h1: height rectangle 1
    :param h2: height rectangle 2
    :return: True if rectangles do overlap, else False
    """
    if x1 > x2 and x1 < x2 + w2 or x1 + w1 > x2 and x1 + w1 < x2 + w2:
        if y1 > y2 and y1 < y2 + h2 or y1 + h1 > y2 and y1 + h1 < y2 + h2:
            return True
    return False
\$\endgroup\$
  • 1
    \$\begingroup\$ I hadn't realised I posted it already. I think I accidentally hit the wrong button. \$\endgroup\$ – Nathan Jan 9 '18 at 23:27
  • \$\begingroup\$ The current version is much better. \$\endgroup\$ – Mast Jan 10 '18 at 6:05
3
\$\begingroup\$

Let's start with a few small things:

  1. Your collision detection routine is not needed, because pygame provides a collision detector for both rectangles and sprites. Just call whatever flavor of obj.collideXXX is most appropriate.

  2. You import windows-specific modules a few times. First, be aware that pygame has mouse support, so you don't need to call Windows for mouse information. Next, please make an effort to hide your windows-specific modules and code behind an interface. Ideally, make it conditional on the program actually running on windows. You can use the platform module for this.

Now, with those out of the way, here are some suggestions in no particular order:

  • Your level_0, level_1, etc. functions in levels.py should be objects. You are returning tuples because you have more than one piece of data depending on the level- that's a good indicator for an object.

    class Level:
        pass
    
    class L1(Level):
        def create_enemies(self, display):
            # ...
            return enemies
    
        def timeout(self, difficulty):
            return 0.1 / difficulty
    
  • I recommend that you create a Player subclass of Entity. There is too much player-specific code in the Entity class that doesn't apply to Enemies. You might want to also create an Enemy subclass, but that may not be necessary. Try it with just Player first and see what you get.

  • Your check_level_up is taking a lot of inputs, and returning a lot of outputs. You should probably make that a player method. You can make the level a player attribute, too, if it seems appropriate. Alternatively, just do something like:

    if player.level_up():
        player.level += 1 
        cur_level = Levels[player.level]
    
  • Your display.draw function should not be filtering your enemies that are still alive. The display object shouldn't know anything about that. Use a collision-detection with the main screen to determine what to draw, and let the enemies kill themselves when they move offscreen. (That is, let the Entity class handle it, since you're talking about Entity-specific data.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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