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
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...
#2: Post edited
- 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
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