2
\$\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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jan 9, 2018 at 23:10
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I hadn't realised I posted it already. I think I accidentally hit the wrong button. \$\endgroup\$ Commented Jan 9, 2018 at 23:27
  • \$\begingroup\$ The current version is much better. \$\endgroup\$ Commented Jan 10, 2018 at 6:05

1 Answer 1

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.)

answered Jan 10, 2018 at 1:42
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.