Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Code Reviews

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Post History

81%
+7 −0
Code Reviews A simple game with pygame

I've just started playing around with pygame and have written a small game in it, of which I'd like a review. Note that I'm not only a complete beginner in pygame, but I also have very little exper...

1 answer  ·  posted 3y ago by celtschk‭  ·  last activity 3y ago by Peter Taylor‭

#3: Nominated for promotion by user avatar Alexei‭ · 2022-02-20T08:24:10Z (over 2 years ago)
#2: Post edited by user avatar celtschk‭ · 2021-09-20T19:45:25Z (about 3 years ago)
Added missing part of settings.py
  • I've just started playing around with pygame and have written a small game in it, of which I'd like a review. Note that I'm not only a complete beginner in pygame, but I also have very little experience in Python in general, therefore I also would welcome comments on my Python code that are not related to pygame specifically.
  • It is a simple game where a ship moves on the surface of water while submarines move in the opposite direction below it. Your task is to destroy the submarines with water bombs released from the ship. While you can have several bombs (up to 15) moving at the same time, each but the first one costs you score to drop (and you cannot throw another one if your score is too low). Indeed, your only game control is dropping bombs, which you do with the down arrow key.
  • Destroying submarines give you score, the more the deeper down the ship is. Submarines are spawned randomly, with varying speeds.
  • Other than the down key, the program also recognises the keys `F`, to toggle fullscreen mode, and `Q` to quit the game.
  • The main file, called `uboot.py` (U-Boot is the German term for submarine), is as follows:
  • ```
  • import pygame
  • import random
  • # python files from this game
  • import settings
  • from colours import get_colour
  • from objects import MovingObject
  • class Game:
  • "The game"
  • # maximal number of simultaneous submarines
  • max_subs = settings.limits["submarine"]
  • #maximal number of simultaneous bombs
  • max_bombs = settings.limits["bomb"]
  • # background (sky) colour
  • c_background = get_colour("sky")
  • # water colour
  • c_water = get_colour("water")
  • # colour of the score display
  • c_text = get_colour("text")
  • # the game's frames per second
  • fps = settings.fps
  • def __init__(self, width, height):
  • """
  • Initialize the game with screen dimensions width x height
  • """
  • self.width = width
  • self.height = height
  • self.waterline = int(settings.sky_fraction * height)
  • self.running = False
  • pygame.init()
  • self.screen = pygame.display.set_mode((width,height))
  • pygame.display.set_caption(settings.game_name)
  • pygame.mouse.set_visible(False)
  • pygame.key.set_repeat(0)
  • self.clock = pygame.time.Clock()
  • # create the ship
  • self.ship = MovingObject(settings.image_files["ship"],
  • start = (0, self.waterline),
  • adjust_start = (-0.5,0),
  • end = (self.width, self.waterline),
  • adjust_end = (0.5,0),
  • speed = settings.speeds["ship"],
  • origin = (0.5,1),
  • repeat = True)
  • # initially there are no submarines nor bombs
  • self.submarines = []
  • self.bombs = []
  • # spawn a submarine on average every 3 seconds
  • self.p_spawn = settings.spawn_rates["submarine"]/self.fps
  • self.score = 0
  • self.font = pygame.font.SysFont(settings.font["name"],
  • settings.font["size"])
  • def write_string(self, string, position):
  • """
  • Write a string at a given position on screen
  • """
  • text = self.font.render(string, True, self.c_text)
  • self.screen.blit(text, position)
  • def moving_objects(self):
  • yield self.ship
  • for sub in self.submarines:
  • yield sub
  • for bomb in self.bombs:
  • yield bomb
  • def draw(self):
  • """
  • Draw the game graphics
  • """
  • self.screen.fill(Game.c_background)
  • pygame.draw.rect(self.screen, Game.c_water,
  • (0,
  • self.waterline,
  • self.width,
  • self.height - self.waterline))
  • self.ship.draw_on(self.screen)
  • for obj in self.moving_objects():
  • obj.draw_on(self.screen)
  • self.write_string("Bombs available: "
  • + str(self.get_available_bombs()),
  • (20, 20))
  • self.write_string("Bomb cost: " + str(self.get_bomb_cost()),
  • (20,50))
  • self.write_string("Score: " + str(self.score),
  • (20+self.width//2, 20))
  • pygame.display.flip()
  • def get_bomb_cost(self, count=1):
  • "Returns the score cost of dropping another count bombs"
  • l = len(self.bombs)
  • return sum((l+k)**2 for k in range(count))
  • def get_available_bombs(self):
  • "Returns the maximum number of extra bombs that can be thrown"
  • available_bombs = self.max_bombs - len(self.bombs)
  • while self.get_bomb_cost(available_bombs) > self.score:
  • available_bombs -= 1
  • return available_bombs
  • def drop_bomb(self):
  • "Drop a bomb, if possible"
  • # don't drop a new bomb if there already exist a naximal
  • # number of them, or the score would go negative
  • if self.get_available_bombs() > 0:
  • ship_pos = self.ship.get_position();
  • # don't drop a bomb off-screen
  • if ship_pos[0] > 0 and ship_pos[0] < self.width:
  • # the score must be updated before adding the new bomb
  • # because adding the bomb changes the cost
  • self.score -= self.get_bomb_cost();
  • newbomb = MovingObject(settings.image_files["bomb"],
  • start = ship_pos,
  • end = (ship_pos[0], self.height),
  • speed = settings.speeds["bomb"],
  • origin = (0.5, 0))
  • self.bombs.append(newbomb)
  • def spawn_submarine(self):
  • "Possibly spawn a new submarine"
  • if random.uniform(0,1) < self.p_spawn:
  • ship_speed = self.ship.speed
  • total_depth = self.height - self.waterline
  • min_depth = 0.1*total_depth + self.waterline
  • max_depth = self.height - 20
  • sub_depth = random.uniform(min_depth, max_depth)
  • sub_speed = random.uniform(settings.speeds["submarine_min"],
  • settings.speeds["submarine_max"])
  • newsub = MovingObject(settings.image_files["submarine"],
  • start = (self.width, sub_depth),
  • end = (0, sub_depth),
  • adjust_end = (-1,0),
  • speed = sub_speed)
  • self.submarines.append(newsub)
  • def handle_hits(self):
  • """
  • Check if any bomb hit any submarine, and if so, remove both
  • and update score
  • """
  • for sub in self.submarines:
  • for bomb in self.bombs:
  • bb_sub = sub.get_bounding_box()
  • bb_bomb = bomb.get_bounding_box()
  • if bb_sub.colliderect(bb_bomb):
  • subpos = sub.get_position()
  • self.score += int((subpos[1] - self.waterline) /
  • self.height * 20 + 0.5)
  • sub.deactivate()
  • bomb.deactivate()
  • def handle_events(self):
  • """
  • Handle all events
  • """
  • for event in pygame.event.get():
  • if event.type == pygame.QUIT:
  • self.running = False
  • if event.type == pygame.KEYDOWN:
  • # Down arrow drops a bomb
  • if event.key == pygame.K_DOWN:
  • self.drop_bomb()
  • # F toggles fullscreen display
  • elif (event.key == pygame.K_f):
  • size = (self.width, self.height)
  • if self.screen.get_flags() & pygame.FULLSCREEN:
  • pygame.display.set_mode(size)
  • else:
  • pygame.display.set_mode(size, pygame.FULLSCREEN)
  • # Q quits the game
  • elif event.key == pygame.K_q:
  • pygame.event.post(pygame.event.Event(pygame.QUIT))
  • def update_state(self):
  • """
  • Update the state of the game
  • """
  • # move all objects
  • for sub in self.moving_objects():
  • sub.move(1/self.fps)
  • # handle bombs hitting submarines
  • self.handle_hits()
  • # remove inactive objects
  • self.submarines = [sub for sub in self.submarines if sub.is_active()]
  • self.bombs = [bomb for bomb in self.bombs if bomb.is_active()]
  • # spawn new submarines at random
  • if len(self.submarines) < Game.max_subs:
  • self.spawn_submarine()
  • def run(self):
  • """
  • Run the game
  • """
  • self.running = True
  • while self.running:
  • self.draw()
  • self.clock.tick(60)
  • self.handle_events()
  • self.update_state()
  • if __name__=='__main__':
  • Game(settings.width, settings.height).run()
  • ```
  • There are three further code files.
  • The first one, `objects.py`, contains a class representing any moving object in the game. Objects are moving along a straight line, either repeatedly (this is used for the ship) or just once (after which they get deactivated, which basically means they are no longer drawn and can get discarded). It looks like this:
  • ```
  • import pygame
  • class MovingObject:
  • "This class represents any moving object in the game."
  • # A storage for images, so that they aren't loaded each time
  • # another object uses the same image is
  • imagestore = {}
  • def __init__(self, path, start, end, speed,
  • origin = (0,0), repeat=False,
  • adjust_start = (0,0), adjust_end = (0,0)):
  • """
  • Create a new moving object.
  • Mandatory Arguments:
  • path: the file path to the image to display
  • start: the pixel at which the movement starts
  • end: the pixel at which the movement ends
  • speed: the fraction of the distance to move per second
  • Optional arguments:
  • origin: which point of the image to use for placement
  • repeat: whether to repeat the movement
  • adjust_start: adjustment of the start position
  • adjust_end: adjustment of the end position
  • All coordinates in the optional arguments are in units of
  • image width or image height, as opposed to pixel coordinates
  • as used in the mandatory arguments. For example, if the image
  • has width of 5 pixels and height of 4 pixels, the arguments
  • start = (5,5), adjust_start = (-1,0.5)
  • will result in an actual starting point of (0,7).
  • """
  • if not path in MovingObject.imagestore:
  • MovingObject.imagestore[path] =\
  • pygame.image.load(path).convert_alpha()
  • self.image = MovingObject.imagestore[path]
  • width = self.image.get_width()
  • self.start = tuple(s + width * a for s,a in zip(start,adjust_start))
  • self.end = tuple(e + width * a for e,a in zip(end,adjust_end))
  • self.repeat = repeat
  • self.pos = self.start
  • self.speed = speed
  • self.dist = 0
  • self.disp = (-self.image.get_width()*origin[0],
  • -self.image.get_height()*origin[1])
  • self.active = True
  • def move(self, seconds):
  • """
  • Move the object.
  • Arguments:
  • seconds: The number of seconds passed.
  • """
  • if self.active:
  • self.dist += self.speed * seconds
  • if self.dist > 1:
  • if self.repeat:
  • self.dist = 0
  • else:
  • self.active = False
  • def get_position(self, displace = False):
  • """
  • Return the current position of the object.
  • Arguments:
  • displace: If True, give the position of the upper left corner.
  • If False (default), give the position of the origin.
  • Return value: The pixel coordinates of the object.
  • """
  • return tuple(int(s + self.dist*(e-s) + (d if displace else 0))
  • for e, s, d in zip(self.end, self.start, self.disp))
  • def draw_on(self, surface):
  • """
  • Draw the object on a pygame surface, if active
  • """
  • if self.is_active():
  • surface.blit(self.image, self.get_position(True))
  • def is_active(self):
  • """
  • Returns true if the object is active, False otherwise
  • """
  • return self.active
  • def deactivate(self):
  • """
  • Set the object's state to not active.
  • """
  • self.active = False
  • def get_bounding_box(self):
  • """
  • Get the bounding box of the objects representation on screen
  • """
  • pos = self.get_position()
  • return pygame.Rect(pos,
  • (self.image.get_width(),
  • self.image.get_height()))
  • ```
  • Next, there is the file `colours.py` which provide a single function to translate colour names into RGB values. For this it uses both names defined in the settings file (posted below) and names from X11's rgb.txt, which I also copied to the game directory. Strictly speaking it currently only needs three lines of that file, which I'll reproduce below, but if you'd like to use the complete file, you can find it [here][rgb.txt] (or locally in `/etc/X11/rgb.txt` if you are on a Linux system with X11 installed). The three lines actually needed are
  • ```
  • 0 0 0 black
  • 0 0 255 blue
  • 135 206 235 sky blue
  • ```
  • so if you save these three lines in a text file named `rgb.txt` (be careful to keep the tab characters intact!) it should work, too.
  • Here's the file `colours.py`:
  • ```
  • import settings
  • # get the colour names from X11's rgb.txt
  • rgbvalues = {}
  • with open("rgb.txt", "r") as rgbfile:
  • for line in rgbfile:
  • if line == "" or line[0] == '!':
  • continue
  • rgb, name = line.split('\t\t')
  • rgbvalues[name.strip()] = tuple(int(value) for value in rgb.split())
  • def get_colour(name):
  • """
  • Get the rgb values of a named colour.
  • The name is looked up in the settings, or failing that, in the rgb
  • database. If the settings give a string as colour, that string is
  • again looked up, otherwise the result is assumed to be a valid
  • colour representation and returned.
  • """
  • while name in settings.colours:
  • colour = settings.colours[name]
  • if type(colour) is str:
  • name = colour
  • else:
  • return colour
  • return rgbvalues[name]
  • ```
  • Finally, there is a file `settings.py` which, as the name says, contains various game settings, such as the screen resolution. Here it is:
  • ```
  • # frame rate
  • fps = 60
  • # speeds of objects
  • speeds = {
  • "ship": 0.1,
  • "submarine_min": 0.05,
  • "submarine_max": 0.2,
  • "bomb": 0.1
  • }
  • # maximum number of objects
  • limits = {
  • "submarine": 10,
  • "bomb": 15
  • }
  • # spawn rate (in average spawns per second) of randomly spawned obcets
  • # (currently only submarines)
  • spawn_rates = {
  • "submarine": 1/3
  • }
  • # colours used in the game
  • colours = {
  • "sky": "sky blue",
  • "water": "blue",
  • "text": "black"
  • }
  • # font used in the game
  • font = {
  • "name": "Courier New",
  • "size": 30
  • }
  • ```
  • Also, the game loads three graphics files with images of a ship silhouette, a submarine silhouette, and a bomb. Here they are:
  • The ship, `schiff.png`: ![ship silhuette](https://software.codidact.com/uploads/ZZxcZ77Z9xAeRW4uPQrjCEUg)
  • The submarine, `Uboot.png`: ![submarine silhuette](https://software.codidact.com/uploads/X7ofspG2pPp6XYWVMfXB5Jz5)
  • The bomb, `bomb.png`: ![bomb image](https://software.codidact.com/uploads/JXUngWeDFMZbBjwo5jswh8BR)
  • Im running the game using the command
  • ```
  • python3 ./uboot.py
  • ```
  • In case it matters, the installed version of Python is 3.8.10, and the version of pygame is 1.9.6.
  • [rgb.txt]: https://github.com/gco/xee/blob/master/rgb.txt
  • I've just started playing around with pygame and have written a small game in it, of which I'd like a review. Note that I'm not only a complete beginner in pygame, but I also have very little experience in Python in general, therefore I also would welcome comments on my Python code that are not related to pygame specifically.
  • It is a simple game where a ship moves on the surface of water while submarines move in the opposite direction below it. Your task is to destroy the submarines with water bombs released from the ship. While you can have several bombs (up to 15) moving at the same time, each but the first one costs you score to drop (and you cannot throw another one if your score is too low). Indeed, your only game control is dropping bombs, which you do with the down arrow key.
  • Destroying submarines give you score, the more the deeper down the ship is. Submarines are spawned randomly, with varying speeds.
  • Other than the down key, the program also recognises the keys `F`, to toggle fullscreen mode, and `Q` to quit the game.
  • The main file, called `uboot.py` (U-Boot is the German term for submarine), is as follows:
  • ```
  • import pygame
  • import random
  • # python files from this game
  • import settings
  • from colours import get_colour
  • from objects import MovingObject
  • class Game:
  • "The game"
  • # maximal number of simultaneous submarines
  • max_subs = settings.limits["submarine"]
  • #maximal number of simultaneous bombs
  • max_bombs = settings.limits["bomb"]
  • # background (sky) colour
  • c_background = get_colour("sky")
  • # water colour
  • c_water = get_colour("water")
  • # colour of the score display
  • c_text = get_colour("text")
  • # the game's frames per second
  • fps = settings.fps
  • def __init__(self, width, height):
  • """
  • Initialize the game with screen dimensions width x height
  • """
  • self.width = width
  • self.height = height
  • self.waterline = int(settings.sky_fraction * height)
  • self.running = False
  • pygame.init()
  • self.screen = pygame.display.set_mode((width,height))
  • pygame.display.set_caption(settings.game_name)
  • pygame.mouse.set_visible(False)
  • pygame.key.set_repeat(0)
  • self.clock = pygame.time.Clock()
  • # create the ship
  • self.ship = MovingObject(settings.image_files["ship"],
  • start = (0, self.waterline),
  • adjust_start = (-0.5,0),
  • end = (self.width, self.waterline),
  • adjust_end = (0.5,0),
  • speed = settings.speeds["ship"],
  • origin = (0.5,1),
  • repeat = True)
  • # initially there are no submarines nor bombs
  • self.submarines = []
  • self.bombs = []
  • # spawn a submarine on average every 3 seconds
  • self.p_spawn = settings.spawn_rates["submarine"]/self.fps
  • self.score = 0
  • self.font = pygame.font.SysFont(settings.font["name"],
  • settings.font["size"])
  • def write_string(self, string, position):
  • """
  • Write a string at a given position on screen
  • """
  • text = self.font.render(string, True, self.c_text)
  • self.screen.blit(text, position)
  • def moving_objects(self):
  • yield self.ship
  • for sub in self.submarines:
  • yield sub
  • for bomb in self.bombs:
  • yield bomb
  • def draw(self):
  • """
  • Draw the game graphics
  • """
  • self.screen.fill(Game.c_background)
  • pygame.draw.rect(self.screen, Game.c_water,
  • (0,
  • self.waterline,
  • self.width,
  • self.height - self.waterline))
  • self.ship.draw_on(self.screen)
  • for obj in self.moving_objects():
  • obj.draw_on(self.screen)
  • self.write_string("Bombs available: "
  • + str(self.get_available_bombs()),
  • (20, 20))
  • self.write_string("Bomb cost: " + str(self.get_bomb_cost()),
  • (20,50))
  • self.write_string("Score: " + str(self.score),
  • (20+self.width//2, 20))
  • pygame.display.flip()
  • def get_bomb_cost(self, count=1):
  • "Returns the score cost of dropping another count bombs"
  • l = len(self.bombs)
  • return sum((l+k)**2 for k in range(count))
  • def get_available_bombs(self):
  • "Returns the maximum number of extra bombs that can be thrown"
  • available_bombs = self.max_bombs - len(self.bombs)
  • while self.get_bomb_cost(available_bombs) > self.score:
  • available_bombs -= 1
  • return available_bombs
  • def drop_bomb(self):
  • "Drop a bomb, if possible"
  • # don't drop a new bomb if there already exist a naximal
  • # number of them, or the score would go negative
  • if self.get_available_bombs() > 0:
  • ship_pos = self.ship.get_position();
  • # don't drop a bomb off-screen
  • if ship_pos[0] > 0 and ship_pos[0] < self.width:
  • # the score must be updated before adding the new bomb
  • # because adding the bomb changes the cost
  • self.score -= self.get_bomb_cost();
  • newbomb = MovingObject(settings.image_files["bomb"],
  • start = ship_pos,
  • end = (ship_pos[0], self.height),
  • speed = settings.speeds["bomb"],
  • origin = (0.5, 0))
  • self.bombs.append(newbomb)
  • def spawn_submarine(self):
  • "Possibly spawn a new submarine"
  • if random.uniform(0,1) < self.p_spawn:
  • ship_speed = self.ship.speed
  • total_depth = self.height - self.waterline
  • min_depth = 0.1*total_depth + self.waterline
  • max_depth = self.height - 20
  • sub_depth = random.uniform(min_depth, max_depth)
  • sub_speed = random.uniform(settings.speeds["submarine_min"],
  • settings.speeds["submarine_max"])
  • newsub = MovingObject(settings.image_files["submarine"],
  • start = (self.width, sub_depth),
  • end = (0, sub_depth),
  • adjust_end = (-1,0),
  • speed = sub_speed)
  • self.submarines.append(newsub)
  • def handle_hits(self):
  • """
  • Check if any bomb hit any submarine, and if so, remove both
  • and update score
  • """
  • for sub in self.submarines:
  • for bomb in self.bombs:
  • bb_sub = sub.get_bounding_box()
  • bb_bomb = bomb.get_bounding_box()
  • if bb_sub.colliderect(bb_bomb):
  • subpos = sub.get_position()
  • self.score += int((subpos[1] - self.waterline) /
  • self.height * 20 + 0.5)
  • sub.deactivate()
  • bomb.deactivate()
  • def handle_events(self):
  • """
  • Handle all events
  • """
  • for event in pygame.event.get():
  • if event.type == pygame.QUIT:
  • self.running = False
  • if event.type == pygame.KEYDOWN:
  • # Down arrow drops a bomb
  • if event.key == pygame.K_DOWN:
  • self.drop_bomb()
  • # F toggles fullscreen display
  • elif (event.key == pygame.K_f):
  • size = (self.width, self.height)
  • if self.screen.get_flags() & pygame.FULLSCREEN:
  • pygame.display.set_mode(size)
  • else:
  • pygame.display.set_mode(size, pygame.FULLSCREEN)
  • # Q quits the game
  • elif event.key == pygame.K_q:
  • pygame.event.post(pygame.event.Event(pygame.QUIT))
  • def update_state(self):
  • """
  • Update the state of the game
  • """
  • # move all objects
  • for sub in self.moving_objects():
  • sub.move(1/self.fps)
  • # handle bombs hitting submarines
  • self.handle_hits()
  • # remove inactive objects
  • self.submarines = [sub for sub in self.submarines if sub.is_active()]
  • self.bombs = [bomb for bomb in self.bombs if bomb.is_active()]
  • # spawn new submarines at random
  • if len(self.submarines) < Game.max_subs:
  • self.spawn_submarine()
  • def run(self):
  • """
  • Run the game
  • """
  • self.running = True
  • while self.running:
  • self.draw()
  • self.clock.tick(60)
  • self.handle_events()
  • self.update_state()
  • if __name__=='__main__':
  • Game(settings.width, settings.height).run()
  • ```
  • There are three further code files.
  • The first one, `objects.py`, contains a class representing any moving object in the game. Objects are moving along a straight line, either repeatedly (this is used for the ship) or just once (after which they get deactivated, which basically means they are no longer drawn and can get discarded). It looks like this:
  • ```
  • import pygame
  • class MovingObject:
  • "This class represents any moving object in the game."
  • # A storage for images, so that they aren't loaded each time
  • # another object uses the same image is
  • imagestore = {}
  • def __init__(self, path, start, end, speed,
  • origin = (0,0), repeat=False,
  • adjust_start = (0,0), adjust_end = (0,0)):
  • """
  • Create a new moving object.
  • Mandatory Arguments:
  • path: the file path to the image to display
  • start: the pixel at which the movement starts
  • end: the pixel at which the movement ends
  • speed: the fraction of the distance to move per second
  • Optional arguments:
  • origin: which point of the image to use for placement
  • repeat: whether to repeat the movement
  • adjust_start: adjustment of the start position
  • adjust_end: adjustment of the end position
  • All coordinates in the optional arguments are in units of
  • image width or image height, as opposed to pixel coordinates
  • as used in the mandatory arguments. For example, if the image
  • has width of 5 pixels and height of 4 pixels, the arguments
  • start = (5,5), adjust_start = (-1,0.5)
  • will result in an actual starting point of (0,7).
  • """
  • if not path in MovingObject.imagestore:
  • MovingObject.imagestore[path] =\
  • pygame.image.load(path).convert_alpha()
  • self.image = MovingObject.imagestore[path]
  • width = self.image.get_width()
  • self.start = tuple(s + width * a for s,a in zip(start,adjust_start))
  • self.end = tuple(e + width * a for e,a in zip(end,adjust_end))
  • self.repeat = repeat
  • self.pos = self.start
  • self.speed = speed
  • self.dist = 0
  • self.disp = (-self.image.get_width()*origin[0],
  • -self.image.get_height()*origin[1])
  • self.active = True
  • def move(self, seconds):
  • """
  • Move the object.
  • Arguments:
  • seconds: The number of seconds passed.
  • """
  • if self.active:
  • self.dist += self.speed * seconds
  • if self.dist > 1:
  • if self.repeat:
  • self.dist = 0
  • else:
  • self.active = False
  • def get_position(self, displace = False):
  • """
  • Return the current position of the object.
  • Arguments:
  • displace: If True, give the position of the upper left corner.
  • If False (default), give the position of the origin.
  • Return value: The pixel coordinates of the object.
  • """
  • return tuple(int(s + self.dist*(e-s) + (d if displace else 0))
  • for e, s, d in zip(self.end, self.start, self.disp))
  • def draw_on(self, surface):
  • """
  • Draw the object on a pygame surface, if active
  • """
  • if self.is_active():
  • surface.blit(self.image, self.get_position(True))
  • def is_active(self):
  • """
  • Returns true if the object is active, False otherwise
  • """
  • return self.active
  • def deactivate(self):
  • """
  • Set the object's state to not active.
  • """
  • self.active = False
  • def get_bounding_box(self):
  • """
  • Get the bounding box of the objects representation on screen
  • """
  • pos = self.get_position()
  • return pygame.Rect(pos,
  • (self.image.get_width(),
  • self.image.get_height()))
  • ```
  • Next, there is the file `colours.py` which provide a single function to translate colour names into RGB values. For this it uses both names defined in the settings file (posted below) and names from X11's rgb.txt, which I also copied to the game directory. Strictly speaking it currently only needs three lines of that file, which I'll reproduce below, but if you'd like to use the complete file, you can find it [here][rgb.txt] (or locally in `/etc/X11/rgb.txt` if you are on a Linux system with X11 installed). The three lines actually needed are
  • ```
  • 0 0 0 black
  • 0 0 255 blue
  • 135 206 235 sky blue
  • ```
  • so if you save these three lines in a text file named `rgb.txt` (be careful to keep the tab characters intact!) it should work, too.
  • Here's the file `colours.py`:
  • ```
  • import settings
  • # get the colour names from X11's rgb.txt
  • rgbvalues = {}
  • with open("rgb.txt", "r") as rgbfile:
  • for line in rgbfile:
  • if line == "" or line[0] == '!':
  • continue
  • rgb, name = line.split('\t\t')
  • rgbvalues[name.strip()] = tuple(int(value) for value in rgb.split())
  • def get_colour(name):
  • """
  • Get the rgb values of a named colour.
  • The name is looked up in the settings, or failing that, in the rgb
  • database. If the settings give a string as colour, that string is
  • again looked up, otherwise the result is assumed to be a valid
  • colour representation and returned.
  • """
  • while name in settings.colours:
  • colour = settings.colours[name]
  • if type(colour) is str:
  • name = colour
  • else:
  • return colour
  • return rgbvalues[name]
  • ```
  • Finally, there is a file `settings.py` which, as the name says, contains various game settings, such as the screen resolution. Here it is:
  • ```
  • # Settings for the uboot game
  • # Name of the game
  • game_name = "U-Boot"
  • # screen resolution/window size
  • width = 1024
  • height = 768
  • # fraction of the screen that's sky
  • sky_fraction = 0.2
  • # file names of the game graphics
  • image_files = {
  • "ship": "schiff.png",
  • "submarine": "Uboot.png",
  • "bomb": "bomb.png"
  • }
  • # frame rate
  • fps = 60
  • # speeds of objects
  • speeds = {
  • "ship": 0.1,
  • "submarine_min": 0.05,
  • "submarine_max": 0.2,
  • "bomb": 0.1
  • }
  • # maximum number of objects
  • limits = {
  • "submarine": 10,
  • "bomb": 15
  • }
  • # spawn rate (in average spawns per second) of randomly spawned obcets
  • # (currently only submarines)
  • spawn_rates = {
  • "submarine": 1/3
  • }
  • # colours used in the game
  • colours = {
  • "sky": "sky blue",
  • "water": "blue",
  • "text": "black"
  • }
  • # font used in the game
  • font = {
  • "name": "Courier New",
  • "size": 30
  • }
  • ```
  • Also, the game loads three graphics files with images of a ship silhouette, a submarine silhouette, and a bomb. Here they are:
  • The ship, `schiff.png`: ![ship silhuette](https://software.codidact.com/uploads/ZZxcZ77Z9xAeRW4uPQrjCEUg)
  • The submarine, `Uboot.png`: ![submarine silhuette](https://software.codidact.com/uploads/X7ofspG2pPp6XYWVMfXB5Jz5)
  • The bomb, `bomb.png`: ![bomb image](https://software.codidact.com/uploads/JXUngWeDFMZbBjwo5jswh8BR)
  • Im running the game using the command
  • ```
  • python3 ./uboot.py
  • ```
  • In case it matters, the installed version of Python is 3.8.10, and the version of pygame is 1.9.6.
  • [rgb.txt]: https://github.com/gco/xee/blob/master/rgb.txt
#1: Initial revision by user avatar celtschk‭ · 2021-09-14T14:34:10Z (about 3 years ago)
A simple game with pygame
I've just started playing around with pygame and have written a small game in it, of which I'd like a review. Note that I'm not only a complete beginner in pygame, but I also have very little experience in Python in general, therefore I also would welcome comments on my Python code that are not related to pygame specifically.

It is a simple game where a ship moves on the surface of water while submarines move in the opposite direction below it. Your task is to destroy the submarines with water bombs released from the ship. While you can have several bombs (up to 15) moving at the same time, each but the first one costs you score to drop (and you cannot throw another one if your score is too low). Indeed, your only game control is dropping bombs, which you do with the down arrow key.

Destroying submarines give you score, the more the deeper down the ship is. Submarines are spawned randomly, with varying speeds.

Other than the down key, the program also recognises the keys `F`, to toggle fullscreen mode, and `Q` to quit the game.

The main file, called `uboot.py` (U-Boot is the German term for submarine), is as follows:
```
import pygame
import random

# python files from this game
import settings
from colours import get_colour
from objects import MovingObject

class Game:
    "The game"

    # maximal number of simultaneous submarines
    max_subs = settings.limits["submarine"]

    #maximal number of simultaneous bombs
    max_bombs = settings.limits["bomb"]

    # background (sky) colour
    c_background = get_colour("sky")

    # water colour
    c_water = get_colour("water")

    # colour of the score display
    c_text = get_colour("text")

    # the game's frames per second
    fps = settings.fps

    def __init__(self, width, height):
        """
        Initialize the game with screen dimensions width x height
        """
        self.width = width
        self.height = height
        self.waterline = int(settings.sky_fraction * height)

        self.running = False

        pygame.init()
        self.screen = pygame.display.set_mode((width,height))
        pygame.display.set_caption(settings.game_name)
        pygame.mouse.set_visible(False)
        pygame.key.set_repeat(0)

        self.clock = pygame.time.Clock()

        # create the ship
        self.ship = MovingObject(settings.image_files["ship"],
                                 start = (0, self.waterline),
                                 adjust_start = (-0.5,0),
                                 end = (self.width, self.waterline),
                                 adjust_end = (0.5,0),
                                 speed = settings.speeds["ship"],
                                 origin = (0.5,1),
                                 repeat = True)

        # initially there are no submarines nor bombs
        self.submarines = []
        self.bombs = []

        # spawn a submarine on average every 3 seconds
        self.p_spawn = settings.spawn_rates["submarine"]/self.fps

        self.score = 0

        self.font = pygame.font.SysFont(settings.font["name"],
                                        settings.font["size"])

    def write_string(self, string, position):
        """
        Write a string at a given position on screen
        """
        text = self.font.render(string, True, self.c_text)
        self.screen.blit(text, position)

    def moving_objects(self):
        yield self.ship

        for sub in self.submarines:
            yield sub

        for bomb in self.bombs:
            yield bomb

    def draw(self):
        """
        Draw the game graphics
        """
        self.screen.fill(Game.c_background)
        pygame.draw.rect(self.screen, Game.c_water,
                         (0,
                          self.waterline,
                          self.width,
                          self.height - self.waterline))

        self.ship.draw_on(self.screen)

        for obj in self.moving_objects():
            obj.draw_on(self.screen)

        self.write_string("Bombs available: "
                          + str(self.get_available_bombs()),
                          (20, 20))

        self.write_string("Bomb cost: " + str(self.get_bomb_cost()),
                          (20,50))

        self.write_string("Score: " + str(self.score),
                          (20+self.width//2, 20))
        
        pygame.display.flip()

    def get_bomb_cost(self, count=1):
        "Returns the score cost of dropping another count bombs"
        l = len(self.bombs)
        return sum((l+k)**2 for k in range(count))

    def get_available_bombs(self):
        "Returns the maximum number of extra bombs that can be thrown"
        available_bombs = self.max_bombs - len(self.bombs)
        while self.get_bomb_cost(available_bombs) > self.score:
               available_bombs -= 1
        return available_bombs

    def drop_bomb(self):
        "Drop a bomb, if possible"

        # don't drop a new bomb if there already exist a naximal
        # number of them, or the score would go negative
        if self.get_available_bombs() > 0:
            ship_pos = self.ship.get_position();

            # don't drop a bomb off-screen
            if ship_pos[0] > 0 and ship_pos[0] < self.width:
                # the score must be updated before adding the new bomb
                # because adding the bomb changes the cost
                self.score -= self.get_bomb_cost();

                newbomb = MovingObject(settings.image_files["bomb"],
                                       start = ship_pos,
                                       end = (ship_pos[0], self.height),
                                       speed = settings.speeds["bomb"],
                                       origin = (0.5, 0))
                self.bombs.append(newbomb)

    def spawn_submarine(self):
        "Possibly spawn a new submarine"
        if random.uniform(0,1) < self.p_spawn:
            ship_speed = self.ship.speed

            total_depth = self.height - self.waterline
            min_depth = 0.1*total_depth + self.waterline
            max_depth = self.height - 20
            sub_depth = random.uniform(min_depth, max_depth)
            sub_speed = random.uniform(settings.speeds["submarine_min"],
                                       settings.speeds["submarine_max"])

            newsub = MovingObject(settings.image_files["submarine"],
                                  start = (self.width, sub_depth),
                                  end = (0, sub_depth),
                                  adjust_end = (-1,0),
                                  speed = sub_speed)
            
            self.submarines.append(newsub)                  

    def handle_hits(self):
        """
        Check if any bomb hit any submarine, and if so, remove both
        and update score
        """
        for sub in self.submarines:
            for bomb in self.bombs:
                bb_sub = sub.get_bounding_box()
                bb_bomb = bomb.get_bounding_box()
                if bb_sub.colliderect(bb_bomb):
                    subpos = sub.get_position()
                    self.score += int((subpos[1] - self.waterline) /
                                      self.height * 20 + 0.5)
                    sub.deactivate()
                    bomb.deactivate()

    def handle_events(self):
        """
        Handle all events
        """
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.KEYDOWN:
                # Down arrow drops a bomb
                if event.key == pygame.K_DOWN:
                    self.drop_bomb()

                # F toggles fullscreen display
                elif (event.key == pygame.K_f):
                    size = (self.width, self.height)
                    if self.screen.get_flags() & pygame.FULLSCREEN:
                        pygame.display.set_mode(size)
                    else:
                        pygame.display.set_mode(size, pygame.FULLSCREEN)

                # Q quits the game
                elif event.key == pygame.K_q:
                    pygame.event.post(pygame.event.Event(pygame.QUIT))
        
    def update_state(self):
        """
        Update the state of the game
        """
        # move all objects
        for sub in self.moving_objects():
            sub.move(1/self.fps)

        # handle bombs hitting submarines
        self.handle_hits()

        # remove inactive objects
        self.submarines = [sub for sub in self.submarines if sub.is_active()]
        self.bombs = [bomb for bomb in self.bombs if bomb.is_active()]

        # spawn new submarines at random
        if len(self.submarines) < Game.max_subs:
            self.spawn_submarine()

    def run(self):
        """
        Run the game
        """
        self.running = True
        while self.running:
            self.draw()
            self.clock.tick(60)
            self.handle_events()
            self.update_state()

if __name__=='__main__':
    Game(settings.width, settings.height).run()
```
There are three further code files.

The first one, `objects.py`, contains a class representing any moving object in the game. Objects are moving along a straight line, either repeatedly (this is used for the ship) or just once (after which they get deactivated, which basically means they are no longer drawn and can get discarded). It looks like this:
```
import pygame

class MovingObject:
    "This class represents any moving object in the game."

    # A storage for images, so that they aren't loaded each time
    # another object uses the same image is
    imagestore = {}

    def __init__(self, path, start, end, speed,
                 origin = (0,0), repeat=False,
                 adjust_start = (0,0), adjust_end = (0,0)):
        """
        Create a new moving object.

        Mandatory Arguments:
          path:          the file path to the image to display
          start:         the pixel at which the movement starts
          end:           the pixel at which the movement ends
          speed:         the fraction of the distance to move per second

        Optional arguments:
          origin:        which point of the image to use for placement
          repeat:        whether to repeat the movement
          adjust_start:  adjustment of the start position
          adjust_end:    adjustment of the end position

        All coordinates in the optional arguments are in units of
        image width or image height, as opposed to pixel coordinates
        as used in the mandatory arguments. For example, if the image
        has width of 5 pixels and height of 4 pixels, the arguments

           start = (5,5), adjust_start = (-1,0.5)

        will result in an actual starting point of (0,7).
        """
        if not path in MovingObject.imagestore:
            MovingObject.imagestore[path] =\
                 pygame.image.load(path).convert_alpha()
        self.image = MovingObject.imagestore[path]

        width = self.image.get_width()

        self.start = tuple(s + width * a for s,a in zip(start,adjust_start))
        self.end = tuple(e + width * a for e,a in zip(end,adjust_end))

        self.repeat = repeat

        self.pos = self.start

        self.speed = speed
        self.dist = 0

        self.disp = (-self.image.get_width()*origin[0],
                     -self.image.get_height()*origin[1])

        self.active = True

    def move(self, seconds):
        """
        Move the object.

        Arguments:
          seconds: The number of seconds passed.
        """
        if self.active:
            self.dist += self.speed * seconds
            if self.dist > 1:
                if self.repeat:
                    self.dist = 0
                else:
                    self.active = False

    def get_position(self, displace = False):
        """
        Return the current position of the object.

        Arguments:
          displace: If True, give the position of the upper left corner.
                    If False (default), give the position of the origin.

        Return value: The pixel coordinates of the object.
        """
        return tuple(int(s + self.dist*(e-s) + (d if displace else 0))
                     for e, s, d in zip(self.end, self.start, self.disp))

    def draw_on(self, surface):
        """
        Draw the object on a pygame surface, if active
        """
        if self.is_active():
            surface.blit(self.image, self.get_position(True))

    def is_active(self):
        """
        Returns true if the object is active, False otherwise
        """
        return self.active

    def deactivate(self):
        """
        Set the object's state to not active.
        """
        self.active = False

    def get_bounding_box(self):
        """
        Get the bounding box of the objects representation on screen
        """
        pos = self.get_position()
        return pygame.Rect(pos,
                           (self.image.get_width(),
                            self.image.get_height()))
```
Next, there is the file `colours.py` which provide a single function to translate colour names into RGB values. For this it uses both names defined in the settings file (posted below) and names from X11's rgb.txt, which I also copied to the game directory. Strictly speaking it currently only needs three lines of that file, which I'll reproduce below, but if you'd like to use the complete file, you can find it [here][rgb.txt] (or locally in `/etc/X11/rgb.txt` if you are on a Linux system with X11 installed). The three lines actually needed are
```
  0   0   0		black
  0   0 255		blue
135 206 235		sky blue
```
so if you save these three lines in a text file named `rgb.txt` (be careful to keep the tab characters intact!) it should work, too.

Here's the file `colours.py`:
```
import settings

# get the colour names from X11's rgb.txt
rgbvalues = {}
with open("rgb.txt", "r") as rgbfile:
    for line in rgbfile:
        if line == "" or line[0] == '!':
            continue
        rgb, name = line.split('\t\t')
        rgbvalues[name.strip()] = tuple(int(value) for value in rgb.split())

def get_colour(name):
    """
    Get the rgb values of a named colour.

    The name is looked up in the settings, or failing that, in the rgb
    database. If the settings give a string as colour, that string is
    again looked up, otherwise the result is assumed to be a valid
    colour representation and returned.
    """
    while name in settings.colours:
        colour = settings.colours[name]
        if type(colour) is str:
            name = colour
        else:
            return colour
    return rgbvalues[name]
```
Finally, there is a file `settings.py` which, as the name says, contains various game settings, such as the screen resolution. Here it is:
```
# frame rate
fps = 60

# speeds of objects
speeds = {
    "ship": 0.1,
    "submarine_min": 0.05,
    "submarine_max": 0.2,
    "bomb": 0.1
    }

# maximum number of objects
limits = {
    "submarine": 10,
    "bomb": 15
    }

# spawn rate (in average spawns per second) of randomly spawned obcets
# (currently only submarines)
spawn_rates = {
    "submarine": 1/3
    }

# colours used in the game
colours = {
    "sky": "sky blue",
    "water": "blue",
    "text": "black"
    }

# font used in the game
font = {
    "name": "Courier New",
    "size": 30
    }
```
Also, the game loads three graphics files with images of a ship silhouette, a submarine silhouette, and a bomb. Here they are:

The ship, `schiff.png`: ![ship silhuette](https://software.codidact.com/uploads/ZZxcZ77Z9xAeRW4uPQrjCEUg)

The submarine, `Uboot.png`: ![submarine silhuette](https://software.codidact.com/uploads/X7ofspG2pPp6XYWVMfXB5Jz5)

The bomb, `bomb.png`: ![bomb image](https://software.codidact.com/uploads/JXUngWeDFMZbBjwo5jswh8BR)

Im running the game using the command
```
python3 ./uboot.py
```

In case it matters, the installed version of Python is 3.8.10, and the version of pygame is 1.9.6.


 [rgb.txt]: https://github.com/gco/xee/blob/master/rgb.txt