In this assignment, your task is to complete a simple 2D game using the Pygame module.
In the game, a drone moves through space and collects samples of rare minerals.
The player controls the drone using the keyboard:
The drone is represented by a grey rectangle and changes its color to white while collecting samples.
Sample collection consumes a large amount of energy and therefore cannot be used continuously. It automatically stops after a fixed period of time. After that, the system must recharge, which also takes a fixed amount of time before collection can be activated again.
Samples are represented by blue circles. They are unstable and randomly appear and disappear on the screen.
There are also asteroids moving through space (red circular objects). The player must avoid collisions with them. Any collision immediately ends the game.
The goal of the game is to collect as many samples as possible.
Example of the game can be seen on following picture:
Your task is to complete the code provided in the Starter Code section. Look for the keyword “TODO” in the code and follow the instructions. You can also use the GIF animation above as a reference. Your finished game should behave in the same way.
Do not use hardcoded values in your code to set dimensions, time delay etc. Use provided constants such as DRONE_SIZE, COLLECTING_TIME etc. instead. You can find those constants find at the beginning of the Starter Code. Constant names are written in uppercase letters.
Note that the classes Asteroid, Sample, and Drone use the self.position attribute to represent the object’s position (both x and y coordinates), stored as a NumPy array. Using NumPy arrays for positions simplifies operations such as computing distances between objects in 2D space. A similar approach is used for velocity.
It is also recommended to check Final Exam Information where you can find general information about PyGame tasks and also the Asteroids game task which shares many similarities with our homework.
There is no automatic evaluation. You can obtain up to 4 points which will be assigned manually based on proper functionality. To obtain 4 points:
Below is a basic code structure you can use. Copy-paste the following code and finish the game according to docstrings.
Look for TODO keyword to find what you are supposed to do.
Submit your final version as game.py.
import time import numpy as np import pygame import random import sys SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 FPS = 30 BLACK = (0, 0, 0) WHITE = (255, 255, 255) BLUE = (50, 50, 255) RED = (255, 0, 0) # ------------------- Drone Settings -------------------- DRONE_SIZE = 30 DRONE_COLOR = (154, 155, 156) DRONE_COLOR_COLLECTING = WHITE THRUSTER_FLAME_LENGTH = 36 THRUSTER_FLAME_WIDTH = 16 THRUSTER_FLAME_COLOR = (167, 195, 252) COLLECTING_TIME = 1000 # ms COLLECTING_COOLDOWN = 5000 ACCELERATION = 1 BOUNDARY_FORCE = 2 # ------------------- Sample Settings -------------------- SAMPLE_RADIUS = 8 SAMPLE_LIFETIME = 10000 # ms SAMPLE_SPAWN_PERIOD_MAX = 10000 SAMPLE_SPAWN_PERIOD_MIN = 1000 # -------------------- Asteroid Settings ----------------- ASTEROID_VELOCITY_MAX = 5 ASTEROID_VELOCITY_MIN = 1 ASTEROID_SPAWN_PERIOD = 10000 # ms ASTEROID_RADIUS_MAX = 120 ASTEROID_RADIUS_MIN = 10 # initialization pygame.init() clock = pygame.time.Clock() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) class Asteroid: """A moving circular obstacle that can collide with the drone.""" def __init__(self, position, velocity, radius): """Create an asteroid object with a position, velocity, and radius.""" self.position = position self.velocity = velocity self.radius = radius def move(self): """Move the asteroid according to its velocity.""" self.position = self.position + self.velocity def draw(self): """Draw the asteroid on the screen.""" pygame.draw.circle(screen, RED, self.position.astype(int), self.radius) @property def is_visible(self): """TODO: Return True if the asteroid is still visible on the screen, False otherwise. An asteroid may be partially visible (its center can be outside the screen). In such cases, this method should still return True.""" class Drone: """Player-controlled mining drone that moves using directional thrusters.""" def __init__(self): """Create the drone at the center of the screen.""" self.position = np.array([SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2]) self.velocity = np.array([0, 0]) self.collecting = False self.collecting_end = pygame.time.get_ticks() self.collecting_next = pygame.time.get_ticks() def draw(self, keys_pressed): """TODO: Draw the drone and active thruster flames. Notes: 1. Drone changes its color when collecting samples (use constants DRONE_COLOR, DRONE_COLOR_COLLECTING) 2. Pressing W or UP arrow key activates UPPER thruster 3. Pressing S or DOWN arrow key activates LOWER thruster 4. Pressing A or LEFT arrow key activates LEFT thruster 5. Pressing D or RIGHT arrow key activates RIGHT thruster 6. Drone's position is defined by geometric center of rectangle shape representing the drone """ rectangle = pygame.Rect(self.position - np.array([DRONE_SIZE, DRONE_SIZE])//2, (DRONE_SIZE, DRONE_SIZE)) color = DRONE_COLOR # TODO: change the color if the drone if currently collecting samples pygame.draw.rect(screen, color, rectangle) # TODO: add drawing "flames" from active thrusters # use THRUSTER_FLAME_LENGTH, THRUSTER_FLAME_WIDTH, THRUSTER_FLAME_COLOR constants @property def is_collecting(self): """Return True if the drone is currently collecting samples.""" return self.collecting def start_collecting(self): """TODO: Start collecting samples. Notes: 1. Collecting will only start if time longer than COLLECTING_COOLDOWN passed since the end of previous collecting. 2. When collecting starts, update the state so that: * the drone enters collecting mode * the time when collecting should stop is set using COLLECTING_TIME 3. Also update the time when collecting can be started again, taking both collecting duration and cooldown into account. """ def update_collecting(self): """TODO: Stop collecting when the collecting time has expired.""" def move(self, keys_pressed): """TODO: Update drone velocity and position according to active thrusters and drone's position. Notes: 1. The drone is "pushed" in direction which is opposite than the position of activated thruster. For example: if thruster on the left side of the drone is active, the drone is "pushed" to the right. "Pushing" means changing drone's velocity in particular direction by ACCELERATION. In case of active left thruster drone should increase its velocity in direction of X axis. 2. Update drone's position based on velocity in X and Y directions. 3. If the drone leaves the screen area (defined by SCREEN_WIDTH and SCREEN_HEIGHT), there is additional acceleration that has and effect on the drone. This acceleration has value BOUNDARY_FORCE and it always pushes the drone back into screen area. 4. Drone is considered outside of the screen if its position (geometric center of the rectangle) is outside of the screen. """ class Sample: """Collectible mineral sample that disappears after a limited time.""" def __init__(self, position, end_time): """Create a sample at the given position with an expiration time.""" self.position = position self.end_time = end_time def draw(self): """Draw the circle representing the sample""" pygame.draw.circle(screen, BLUE, self.position, SAMPLE_RADIUS) @property def is_alive(self): """Return True if the sample has not expired yet, False otherwise.""" return pygame.time.get_ticks() < self.end_time class Game: """Main game controller that manages objects, updates, drawing, and collisions.""" def __init__(self): """Create a new game state.""" self.drone = Drone() self.samples = [] self.asteroids = [] self.running = True now = pygame.time.get_ticks() self.next_sample_spawn = now + self._sample_spawn_period() self.next_asteroid_spawn = now self.collected = 0 def _sample_spawn_period(self): """helper method that returns random delay before the next sample spawn.""" return random.randrange(SAMPLE_SPAWN_PERIOD_MIN, SAMPLE_SPAWN_PERIOD_MAX) def spawn_asteroid(self): """TODO: Spawn an asteroid at a random screen edge moving toward the drone. Notes: 1. Asteroid should spawn at a random position and at a randomly chosen screen edge 2. When the asteroid spawns it starts to move towards current drone position and its velocity is constant (does not change during asteroid's lifetime) 3. Length (norm) of asteroid's velocity vector is random value between ASTEROID_VELOCITY_MIN and ASTEROID_VELOCITY_MAX (both values included) 4. Asteroid is represented by circle object with random radius between ASTEROID_RADIUS_MIN and ASTEROID_RADIUS_MAX (both values included) 5. Use class Asteroid to create new asteroid object 6. Newly created asteroid object should be appended to self.asteroids list """ def spawn_sample(self): """TODO: Spawn a sample at a random screen position. Notes: 1. Use Sample class to create new sample object 2. Append new sample object to self.samples list 3. Update time of next sample spawn accordingly (use _sample_spawn_period helper method) """ def collect_samples(self): """TODO: Collect samples that are fully inside the drone rectangle shape. Notes: 1. print "Sample collected." and update self.collected counter in case of successful sample collection 2. For successful sample collection the sample represented by circular shape has to be fully inside the drone rectangle shape (otherwise it is not collected) 3. Successfully collected samples are removed from self.samples list """ def move(self, keys_pressed): """Move the drone and all asteroids.""" self.drone.move(keys_pressed) for asteroid in self.asteroids: asteroid.move() def check_collision(self): """TODO: Return True if any asteroid collides with the drone, False otherwise. Notes: 1. Asteroid collides with the drone if there is any overlap between drone's rectangular shape and asteroid's circular shape 2. HINT: For each asteroid, find the point on the drone rectangle closest to the asteroid center. Check the distance between this closest point and the asteroid center. """ def draw(self, keys_pressed): """Draw all game objects.""" for asteroid in self.asteroids: asteroid.draw() self.drone.draw(keys_pressed) for sample in self.samples: sample.draw() def update(self, keys_pressed): """Update spawning, collecting, and remove expired or invisible objects.""" now = pygame.time.get_ticks() if now > self.next_sample_spawn: self.spawn_sample() if now > self.next_asteroid_spawn: self.spawn_asteroid() self.next_asteroid_spawn = now + ASTEROID_SPAWN_PERIOD self.drone.update_collecting() if keys_pressed[pygame.K_SPACE]: self.drone.start_collecting() if self.drone.is_collecting: self.collect_samples() self.samples = [sample for sample in self.samples if sample.is_alive] self.asteroids = [asteroid for asteroid in self.asteroids if asteroid.is_visible] def main(): """Main game loop""" game = Game() while game.running: clock.tick(FPS) # Event handling for event in pygame.event.get(): if event.type == pygame.QUIT: game.running = False keys_pressed = pygame.key.get_pressed() if keys_pressed[pygame.K_q]: # Q key game.running = False game.move(keys_pressed) game.update(keys_pressed) screen.fill(BLACK) game.draw(keys_pressed) pygame.display.flip() if game.check_collision(): print("Collision detected!") game.running = False time.sleep(1) pygame.quit() print(f"Game Over. Collected: {game.collected}") sys.exit() if __name__ == "__main__": main()