4
\$\begingroup\$

Here is my code about our final project in uni called 'Mars Lander'. The thing is that my code seems to be all over the place (more specifically in the 'main' function at the end of the code...) therefore I would like to get some help with putting things in place! It's my first time making a game using the 'pygame' library and this one was definitely worth the effort!

import pygame
import sys
from random import uniform, randint  # used for the random starting velocity of the lander, random clock time etc.
from time import clock  # to show the time elapsed
import math  # used to calculate the magnitude of the gravity force applied on the lander

WIDTH = 1200  # width of the game window
HEIGHT = 750  # height of the game window
FPS = 20  # frames per second
pause = False  # variable which is used to determine whether the game is paused or not

# Initialise pygame
pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT))

clock_game = pygame.time.Clock()

pygame.font.init()  # you have to call this at the start if you want to use this module.
myfont = pygame.font.SysFont('Comic Sans MS', 15)
alert_large = pygame.font.SysFont("Comic Sans MS", 18)
large_text = pygame.font.SysFont("Comic Sans MS", 50)


class Background(pygame.sprite.Sprite):  # class for the background image
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)  # call Sprite initializer
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = location  # the location of the image should be inputted as a tuple (x,y)
        # where x is the left side position of the image whereas y is the top side position


# obstacles landing pad meteors classes
class Lander(pygame.sprite.Sprite):  # class for the lander image
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = location  # the location of the image should be inputted as a tuple (x,y)
        # where x is the left side position of the image whereas y is the top side position
        self.rot_image = self.image
        self.angle = 0
        self.veloc_y = uniform(0.0, 1.0)
        self.veloc_x = uniform(-1.0, 1.0)
        self.fuel = 500
        self.altitude = 0
        self.damage = 0

    def free_fall(self):
        self.rect.y += self.veloc_y
        self.rect.x += self.veloc_x
        self.veloc_y += 0.1

    def reset_stats(self):
        self.rect.top = 0
        self.veloc_y = uniform(0.0, 1.0)
        self.veloc_x = uniform(-1.0, 1.0)
        self.fuel = 500
        self.angle = 0
        self.damage = 0
        self.rot_image = pygame.transform.rotate(self.image, self.angle)

    def check_boundaries(self):
        if self.rect.top < 0:
            self.rect.top = 0
            self.veloc_y = uniform(0.0, 1.0)
        if self.rect.right < 0:
            self.rect.left = WIDTH
        if self.rect.left > WIDTH:
            self.rect.right = 0
        if self.rect.bottom > HEIGHT:
            self.reset_stats()
            self.rect.left = randint(0, 1123)
            return True
        else:
            return False

    def get_fuel(self):
        return self.fuel

    def burn_fuel(self):  # decreases the fuel when 'space' key is pressed
        self.fuel -= 5

    def start_engine(self):
        self.burn_fuel()
        self.veloc_x = self.veloc_x + 0.33 * math.sin(math.radians(-self.angle))
        self.veloc_y = self.veloc_y - 0.33 * math.cos(math.radians(self.angle))

    def rotate_left(self):
        self.angle += 1 % 360
        self.rot_image = pygame.transform.rotate(self.image, self.angle)

    def rotate_right(self):
        self.angle -= 1 % 360
        self.rot_image = pygame.transform.rotate(self.image, self.angle)

    def to_ground(self):
        self.altitude = 1000 - self.rect.top*1.436
        return self.altitude

    def get_damage(self):
        return self.damage

    def check_landing(self, pad):
        check_velocity_y = [True if self.veloc_y < 5 else False]

        check_velocity_x = [True if -5 < self.veloc_x < 5 else False]

        check_angle = [True if -7 <= self.angle <= 7 else False]

        check_above_pad = [True if (self.rect.left > pad.rect.left and self.rect.right < pad.rect.right) else False]

        check_touch = [True if (self.rect.bottom == pad.rect.top) else False]

        if check_above_pad[0] and check_angle[0] and check_velocity_x[0] and check_velocity_y[0] and check_touch[0]:
            return True
        else:
            return False


class EngineThrust(pygame.sprite.Sprite):  # class for the thrust image
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)  # call Sprite initializer
        self.image = pygame.image.load(image_file)
        self.rot_image = self.image
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = location  # the location of the image should be inputted as a tuple (x,y)
        # where x is the left side position of the image whereas y is the top side position
        self.thrst_angle = lndr.angle

    def rotate_thrust(self):
        self.rot_image = pygame.transform.rotate(self.image, self.thrst_angle)


class LandingPad(pygame.sprite.Sprite):  # class for the landing pad image
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)  # call Sprite initializer
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = location  # the location of the image should be inputted as a tuple (x,y)
        # where x is the left side position of the image whereas y is the top side position


class GameScore:  # class for the score of the game
    def __init__(self):
        self.score = 0

    def successful_land(self):
        self.score += 50

    def get_score(self):  # Returns game score
        return self.score


class Lives:  # class for the lives of the player
    def __init__(self, lives):
        self.lives = lives   # Holds lives left

    def crashed(self):  # Decrement lives by 1
        self.lives -= 1

    def get_lives(self):  # Return lives number
        return self.lives

    def game_over(self):  # Check if there are no lives left
        return self.lives == 0


class SysFailure:  # class for the lander system errors
    def __init__(self):
        self.random_alert = 0  # Will carry alert time
        self.random_key = 0  # Will holds key value

    def get_alert(self):  # Set a new alert time and return it
        self.random_alert = randint(int(clock()+5), int(clock() + 15))
        return self.random_alert

    def get_key(self):  # Randomize and return key value
        self.random_key = randint(1, 3)
        return self.random_key


class Obstacle(pygame.sprite.Sprite):  # class for the obstacle images
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)  # call Sprite initializer
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = location  # the location of the image should be inputted as a tuple (x,y)
        # where x is the left side position of the image whereas y is the top side position
        self.destroyed = False

    def get_status(self):  # Return the status of the obstacle
        return self.destroyed

    def obstacle_collision(self, lander):  # Increment lander damage by 10 % if the meteor collides with the lander
        if lander.rect.colliderect(self.rect):
            lander.damage += 10
            return True
        else:
            return False


class Meteor(pygame.sprite.Sprite):  # Class for the meteor images
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)  # Call Sprite initializer
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.bottom = location  # The location of the image should be inputted as a tuple (x,y)
        # where x is the left side position of the image whereas y is the top side position
        self.speed_y = uniform(5, 10)
        self.speed_x = uniform(-2, 2)
        self.destroyed = False

    def storm_fall(self):  # Set y and x-axis speed of the meteor
        self.rect.x += self.speed_x
        self.rect.y += self.speed_y

    def meteor_collision(self, lander):  # Increment lander damage by 25 % if the meteor collides with the lander
        if lander.rect.colliderect(self.rect):
            lander.damage += 25
            return True
        else:
            return False

    def get_status(self):  # Return the status of the meteor
        return self.destroyed

    def reset_stats(self):  # Set the bottom of the sprite to its initial value
        self.rect.bottom = 0


class Storm:  # Class for the meteor storms
    def __init__(self):
        self.random_storm = 0  # Holds random storm time

    def storm_time(self):  # Randomize and return storm time
        self.random_storm = randint(int(clock()+3), int(clock() + 12))
        return self.random_storm


def resume():  # Resume the game
    global pause
    pause = False


def paused():   # Pause the game
    global game_status
    crash_msg = large_text.render('You Have Crashed!', False, (255, 0, 0))
    screen.blit(crash_msg, (420, 300))  # Display crash message in the middle of the screen

    while pause:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:  # Quit the game if the 'X' button is clicked
                sys.exit()
            if event.type == pygame.KEYDOWN:  # Wait for a key to be pressed and if so resumes the game
                resume()

        pygame.display.update()
        clock_game.tick(FPS)


obstacles = pygame.sprite.Group()  # Create obstacle sprite group
meteors = pygame.sprite.Group()  # Create meteor sprite group


bckgd = Background('mars_background_instr.png', [0, 0])

lndr = Lander('lander.png', [randint(0, 1123), 0])

pad_1 = LandingPad('pad.png', [randint(858, 1042), 732])

pad_2 = LandingPad('pad_tall.png', [randint(458, 700), 620])

pad_3 = LandingPad('pad.png', [randint(0, 300), 650])

"""
Create 5 obstacles each being placed on a fixed location
on the background image!
"""

obstacle_1 = Obstacle('pipe_ramp_NE.png', [90, 540])

obstacle_2 = Obstacle('building_dome.png', [420, 575])

obstacle_3 = Obstacle('satellite_SW.png', [1150, 435])

obstacle_4 = Obstacle('rocks_ore_SW.png', [1080, 620])

obstacle_5 = Obstacle('building_station_SW.png', [850, 640])

# Add to the sprite group 'obstacles'
obstacles.add(obstacle_1, obstacle_2, obstacle_3, obstacle_4, obstacle_5)


""" 
Create 10 meteors using the Meteor class which are placed
at random x-axis locations starting with the bottom of the image rectangle 
lying at 0 on the y-axis! 
"""

meteor1 = Meteor('spaceMeteors_1.png', [randint(300, 900), 0])

meteor2 = Meteor('spaceMeteors_2.png', [randint(300, 900), 0])

meteor3 = Meteor('spaceMeteors_3.png', [randint(300, 900), 0])

meteor4 = Meteor('spaceMeteors_4.png', [randint(300, 900), 0])

meteor5 = Meteor('spaceMeteors_1.png', [randint(300, 900), 0])

meteor6 = Meteor('spaceMeteors_2.png', [randint(300, 900), 0])

meteor7 = Meteor('spaceMeteors_1.png', [randint(300, 900), 0])

meteor8 = Meteor('spaceMeteors_4.png', [randint(300, 900), 0])

meteor9 = Meteor('spaceMeteors_1.png', [randint(300, 900), 0])

meteor10 = Meteor('spaceMeteors_3.png', [randint(300, 900), 0])

# Add to the sprite group assigned to the 'meteors' variable
meteors.add(meteor1, meteor2, meteor3, meteor4, meteor5, meteor6, meteor7, meteor8, meteor9, meteor10)

storm = Storm()  # Storm variable

lives_left = Lives(3)  # Each time the player starts with 3 lives

lander_score = GameScore()  # Holds the score of the game

alert_signal = SysFailure()  # Holds the lander system failure causes

game_status = True  # Holds the status of the game


def main():  # The main function which runs the game
    global game_status, pause  # change name
    random_signal = alert_signal.get_alert()  # Holds the randomized alert signal time
    random_key = alert_signal.get_key()  # Carries the randomized key used to decide which control failure will occur
    # during the alert signal
    random_storm = storm.storm_time()  # Random meteor storm time
    meteor_storm = False  # Set to True whenever a storm should occur
    meteor_shower = False  # Set to True whenever a storm should occur
    print(random_storm)
    meteor_number = randint(1, 10)  # Determines the number of meteors the storm will contain
    print(meteor_number)
    while game_status:  # main game loop

        clock_game.tick(FPS)
        screen.fill([255, 255, 255])  # Fill the empty spaces with white color
        screen.blit(bckgd.image, bckgd.rect)  # Place the background image
        screen.blit(pad_1.image, pad_1.rect)  # Put the first landing pad on the background
        screen.blit(pad_2.image, pad_2.rect)  # Put the second landing pad on the background
        screen.blit(pad_3.image, pad_3.rect)  # Put the last landing pad on the background

        for obstacle in obstacles:  # draw every one of the obstacles
            # if a collision occurs the obstacle gets destroyed and it is no longer shown
            if not obstacle.get_status():
                screen.blit(obstacle.image, obstacle.rect)
                if obstacle.obstacle_collision(lndr):
                    obstacle.destroyed = True

        # Waits for an event
        for event in pygame.event.get():  # If the user clicks the 'X' button on the window it quits the program
            if event.type == pygame.QUIT:
                sys.exit()

        pressed_key = pygame.key.get_pressed()  # Take pressed key value

        if not meteor_storm:  # As soon as the clock passes the random storm time it causes meteor rain
            if clock() > random_storm:
                meteor_storm = True
                meteor_shower = True

        if meteor_shower:
            delay = 0  # Each meteor is drawn with 1 second delay
            count = 0  # Counts the meteors number
            for meteor in meteors:
                if count < meteor_number:
                    delay += 1
                    if clock() > random_storm + delay:
                        if not meteor.get_status():  # Draw every one of the meteors
                            #  if a collision occurs the meteor gets destroyed and it is no longer shown
                            meteor.storm_fall()  # Give x-axis and y-axis velocity to the meteors
                            screen.blit(meteor.image, meteor.rect)
                            if meteor.meteor_collision(lndr):
                                meteor.destroyed = True
                    count += 1

        if pressed_key[pygame.K_ESCAPE]:  # Stop game if the 'Esc' button is pressed
            game_status = False
        elif lives_left.game_over():  # Terminate program if the player has no lives left
            game_status = False
        elif lndr.get_fuel() <= 0:  # Remove lander controls if it is out of fuel
            screen.blit(lndr.rot_image, lndr.rect)
        else:
            if not random_signal < clock() < random_signal+2:  # While the clock is not in the 2 sec alert time
                if lndr.get_damage() < 100:  # While the lander hasn't sustained 100% damage
                    if pressed_key[pygame.K_SPACE]:  # Show thrust image when 'space' is pressed
                        thrst = EngineThrust('thrust.png', [lndr.rect.left+31, lndr.rect.bottom-10])  # Create thrust
                        # sprite
                        lndr.start_engine()  # Call 'start engine' function (Lander)
                        thrst.rotate_thrust()  # Call 'rotate_engine' which rotates the thrust along with the lander
                        screen.blit(thrst.rot_image, thrst.rect)
                        pygame.display.update()

                    if pressed_key[pygame.K_LEFT]:  # Rotate lander anticlockwise when 'left' is pressed
                        lndr.rotate_left()

                    if pressed_key[pygame.K_RIGHT]:  # Rotate lander clockwise when 'left' is pressed
                        lndr.rotate_right()

                    if lndr.check_landing(pad_1) or lndr.check_landing(pad_2) or lndr.check_landing(pad_3):  # Call
                        # 'check_landing' method on each pad sprite which checks whether the lander has landed on
                        # the landing pad
                        lander_score.successful_land()  # Increment score with 50 pts
                        for meteor in meteors:
                            meteor.destroyed = False
                            meteor.reset_stats()
                        random_signal = alert_signal.get_alert()
                        random_key = alert_signal.get_key()
                        random_storm = storm.storm_time()
                        meteor_number = randint(1, 10)
                        print(random_storm)
                        print(meteor_number)
                        meteor_shower = False
                        meteor_storm = False
                        for obstacle in obstacles:
                            obstacle.destroyed = False
                        lndr.reset_stats()
                else:
                    lndr.damage = 100  # Stop lander damage at 100 %
            else:
                alert_msg = alert_large.render('*ALERT*', False, (0, 0, 255))
                screen.blit(alert_msg, (190, 80))  # Display alert message
                if random_key == 1:
                    if pressed_key[pygame.K_SPACE]:  # Show thrust image when 'space' is pressed
                        thrst = EngineThrust('thrust.png', [lndr.rect.left + 31, lndr.rect.bottom - 10])
                        lndr.start_engine()
                        thrst.rotate_thrust()
                        screen.blit(thrst.rot_image, thrst.rect)
                        pygame.display.update()

                    if pressed_key[pygame.K_LEFT]:  # Rotate lander anticlockwise when 'left' is pressed
                        lndr.rotate_left()
                elif random_key == 2:
                    if pressed_key[pygame.K_LEFT]:  # Rotate lander anticlockwise when 'left' is pressed
                        lndr.rotate_left()

                    if pressed_key[pygame.K_RIGHT]:  # Rotate lander clockwise when 'right' is pressed
                        lndr.rotate_right()
                else:
                    if pressed_key[pygame.K_SPACE]:  # Show thrust image when 'space' is pressed
                        thrst = EngineThrust('thrust.png', [lndr.rect.left + 31, lndr.rect.bottom - 10])
                        lndr.start_engine()
                        thrst.rotate_thrust()
                        screen.blit(thrst.rot_image, thrst.rect)
                        pygame.display.update()

                    if pressed_key[pygame.K_RIGHT]:  # Rotate lander clockwise when 'right' is pressed
                        lndr.rotate_right()

        screen.blit(lndr.rot_image, lndr.rect)

        time_passed = myfont.render('{:.1f} s'.format(clock()), False, (255, 0, 0))
        screen.blit(time_passed, (72, 10))  # Display clock in seconds

        velocity_y = myfont.render('{:.1f} m/s'.format(lndr.veloc_y), False, (255, 0, 0))
        screen.blit(velocity_y, (280, 56))  # Display y-axis velocity (downward, meters per second)

        velocity_x = myfont.render('{:.1f} m/s'.format(lndr.veloc_x), False, (255, 0, 0))
        screen.blit(velocity_x, (280, 33))  # Display x-axis velocity (sideways, meters per second)

        fuel_remaining = myfont.render('{:d} kg'.format(lndr.fuel), False, (255, 0, 0))
        screen.blit(fuel_remaining, (72, 33))  # Display remaining fuel in kg

        altitude = myfont.render('{:.0f} m'.format(lndr.to_ground()), False, (255, 0, 0))
        screen.blit(altitude, (280, 10))  # Display altitude in meters

        lander_damage = myfont.render('{} %'.format(lndr.get_damage()), False, (255, 0, 0))
        screen.blit(lander_damage, (95, 56))  # Display damage suffered by the mars lander

        game_score = myfont.render('{:.0f} pts'.format(lander_score.get_score()), False, (255, 0, 0))
        screen.blit(game_score, (77, 82))  # Display altitude in meters

        lndr.free_fall()  # Call 'free_fall' method in class 'Lander'

        if lndr.check_boundaries():  # Call 'check_boundaries' method located in 'Lander' class
            for meteor in meteors:
                meteor.destroyed = False
                meteor.reset_stats()
            random_signal = alert_signal.get_alert()  # Get a new random time for the alert
            random_key = alert_signal.get_key()  # Get a new random key for the lander control failure
            random_storm = storm.storm_time()  # Get a new random time for the storm
            meteor_number = randint(1, 10)  # Get a new random number for the meteors
            lives_left.crashed()  # Reduce lives with 1
            print(random_storm)
            print(meteor_number)
            meteor_shower = False
            meteor_storm = False
            for obstacle in obstacles:  # Reset all obstacles and make them visible
                obstacle.destroyed = False
            pause = True  # Set 'pause' to True so the game pauses when 'paused' method is called
            paused()  # Call 'paused'

        pygame.display.update()  # Refresh (update) display

    pygame.quit()  # quit game if game_status = False


main()
\$\endgroup\$
2
\$\begingroup\$

You try to extract behaviour into classes, which is a good idea, but you failed at recognising patterns that could be abstracted by a single class and created way too much classes for the same thing: a Sprite set at certain coordinates; heck, even your LandingPad and Background classes are exactly the same, that should have raised a red flag.

The same goes with your Lives and GameScore classes which could be a simple integer as they add nothing more.

You should also avoid globals and code at top-level. They, however, are better in an __init__ of some class or other. Thus I’d create a MarsLanding class to hold that and the main function. This would help for refactoring, also.

Lastly, you perform drawing, sprites updates and collision detection manually but pygame allows to automate it through groups. See for instance the spritecollide function or the Group.draw method.

Proposed improvements follows:

import math
from time import clock
from random import uniform, randint, choice

import pygame


def init():
    pygame.init()
    pygame.font.init()


class MarsLander:
    def __init__(self, fps=20, width=1200, height=750):
        self.screen = pygame.display.set_mode((width, height))
        self.clock = pygame.time.Clock()
        self.FPS = fps
        self.regular_font = pygame.font.SysFont('Comic Sans MS', 15)
        self.alert_font = pygame.font.SysFont('Comic Sans MS', 18)
        self.large_font = pygame.font.SysFont('Comic Sans MS', 50)

        self.score = 0
        self.lives = 3

        self.obstacles = pygame.sprite.Group()
        self.meteors = pygame.sprite.Group()
        self.landing_pads = pygame.sprite.Group()
        self.background = Sprite('mars_background_instr.png', 0, 0)

        self.lander = Lander(width)
        self.height = height

        # Create sprites for landing pads and add them to the pads group
        # TODO have coordinates dependent on actual width and height
        Sprite('pad.png', 732, randint(858, 1042)).add(self.landing_pads)
        Sprite('pad_tall.png', 620, randint(458, 700)).add(self.landing_pads)
        Sprite('pad.png', 650, randint(0, 300)).add(self.landing_pads)

        self.reset_obstacles()
        self.create_new_storm()
        self.create_new_alert()

    @property
    def game_over(self):
        return self.lives < 1

    def reset_obstacles(self):
        """Create obstacles at a fixed location and add the to the obstacles group"""
        # TODO have coordinates dependent on actual width and height

        self.obstacles.empty()
        Sprite('pipe_ramp_NE.png', 540, 90).add(self.obstacles)
        Sprite('building_dome.png', 575, 420).add(self.obstacles)
        Sprite('satellite_SW.png', 435, 1150).add(self.obstacles)
        Sprite('rocks_ore_SW.png', 620, 1080).add(self.obstacles)
        Sprite('building_station_SW.png', 640, 850).add(self.obstacles)

    def create_new_storm(self, number_of_images=4):
        """Create meteors and add the to the meteors group"""
        # TODO have coordinates dependent on actual width and height

        now = int(clock())
        self.random_storm = randint(now + 3, now + 12)

        self.meteors.empty()
        for i in range(randint(1, 10)):
            image_name = 'spaceMeteors_{}.png'.format(randint(1, number_of_images))
            Meteor(image_name, -2 * i * self.FPS, randint(300, 900)).add(self.meteors)

    def create_new_alert(self):
        self.random_alert = randint(int(clock() + 5), int(clock() + 15))
        self.alert_key = choice((pygame.K_SPACE, pygame.K_LEFT, pygame.K_RIGHT))

    def draw_text(self, message, position, color=(255, 0, 0)):
        text = self.regular_font.render(message, False, color)
        self.screen.blit(text, position)

    def run(self):
        meteor_storm = False  # Set to True whenever a storm should occur

        while not self.game_over:
            self.clock.tick(self.FPS)

            # If the user clicks the 'X' button on the window it quits the program
            if any(event.type == pygame.QUIT for event in pygame.event.get()):
                return

            self.screen.fill([255, 255, 255])  # Fill the empty spaces with white color
            self.screen.blit(self.background.image, self.background.rect)  # Place the background image
            self.landing_pads.draw(self.screen)
            self.obstacles.draw(self.screen)

            # Check for collisions with obstacles and remove hit ones
            obstacles_hit = pygame.sprite.spritecollide(self.lander, self.obstacles, True)
            self.lander.damage += 10 * len(obstacles_hit)

            pressed_key = pygame.key.get_pressed()  # Take pressed key value

            if not meteor_storm and clock() > self.random_storm:
                # As soon as the clock passes the random storm time it causes meteor rain
                meteor_storm = True

            if meteor_storm:
                self.meteors.update()
                self.meteors.draw(self.screen)

                # Check for collisions with meteors and remove hit ones
                meteors_hit = pygame.sprite.spritecollide(self.lander, self.meteors, True)
                self.lander.damage += 25 * len(meteors_hit)

            if pressed_key[pygame.K_ESCAPE]:  # Stop game if the 'Esc' button is pressed
                return

            if self.random_alert < clock() < self.random_alert + 2:
                alert_msg = self.large_font.render('*ALERT*', False, (0, 0, 255))
                self.screen.blit(alert_msg, (190, 80))
                thrust = self.lander.handle_inputs(pressed_key, self.alert_key)
            else:
                thrust = self.lander.handle_inputs(pressed_key)
            if thrust:
                self.screen.blit(thrust.rot_image, thrust.rect)
            self.screen.blit(self.lander.rot_image, self.lander.rect)

            self.draw_text('{:1.f} s'.format(clock()), (72, 10))
            self.draw_text('{:.1f} m/s'.format(self.lander.veloc_y), (280, 56))
            self.draw_text('{:.1f} m/s'.format(self.lander.veloc_x), (280, 33))
            self.draw_text('{:d} kg'.format(self.lander.fuel), (72, 33))
            self.draw_text('{:.0f} m'.format(self.lander.altitude), (280, 10))
            self.draw_text('{} %'.format(self.lander.damage), (95, 56))
            self.draw_text('{:.0f} pts'.format(self.score), (77, 82))

            self.lander.free_fall()
            pygame.display.update()

            landing_pad_reached = pygame.sprite.spritecollideany(self.lander, self.landing_pads)
            if landing_pad_reached or self.lander.rect.bottom > self.height:
                self.create_new_alert()
                self.create_new_storm()
                self.reset_obstacles()
                meteor_storm = False
                if landing_pad_reached and self.lander.has_landing_position():
                    self.score += 50
                else:
                    self.lives -= 1
                    should_exit = self.show_crash()
                    if should_exit:
                        return
                self.lander.reset_stats()

    def show_crash(self):
        """Display crash message in the middle of the screen and wait for a key press"""
        crash_msg = self.large_font.render('You Have Crashed!', False, (255, 0, 0))
        self.screen.blit(crash_msg, (420, 300))

        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    # Quit the game if the 'X' button is clicked
                    return True
                if event.type == pygame.KEYDOWN:
                    # Wait for a key to be pressed and if so resumes the game
                    return False

            pygame.display.update()
            self.clock.tick(self.FPS)


class Sprite(pygame.sprite.Sprite):
    def __init__(self, image_file, top, left):
        super().__init__()
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.top = top
        self.rect.left = left


class EngineThrust(Sprite):  # class for the thrust image
    def __init__(self, lander_rect, lander_angle):
        super().__init__('thrust.png', lander_rect.bottom - 10, lander_rect.left + 31)
        self.rot_image = pygame.transform.rotate(self.image, lander_angle)


class Meteor(Sprite):
    def __init__(self, image_file, top, left):
        super().__init__(image_file, top, left)
        self.speed_y = uniform(5, 10)
        self.speed_x = uniform(-2, 2)

    def update(self):
        self.rect.x += self.speed_x
        self.rect.y += self.speed_y


class Lander(Sprite):
    def __init__(self, width):
        super().__init__('lander.png', 0, 0)
        self.width = width
        self.reset_stats()

    def reset_stats(self):
        self.rect.top = 0
        self.rect.left = randint(0, self.width - self.rect.width)
        self.veloc_y = uniform(0.0, 1.0)
        self.veloc_x = uniform(-1.0, 1.0)
        self.fuel = 500
        self.angle = 0
        self.damage = 0
        self.rot_image = self.image

    def free_fall(self):
        self.rect.y += self.veloc_y
        self.rect.x += self.veloc_x
        self.veloc_y += 0.1

        if self.rect.top < 0:
            self.rect.top = 0
            self.veloc_y = uniform(0.0, 1.0)

        if self.rect.rigth < 0:
            self.rect.left = self.width

        if self.rect.left > self.width:
            self.rect.right = 0

    def start_engine(self):
        self.fuel -= 5
        self.veloc_x = self.veloc_x + 0.33 * math.sin(math.radians(-self.angle))
        self.veloc_y = self.veloc_y - 0.33 * math.cos(math.radians(self.angle))

    @property
    def altitude(self):
        return 1000 - self.rect.top * 1.436

    @property
    def can_land(self):
        return self.fuel > 0 and self.damage < 100

    def has_landing_position(self):
        return self.can_land and (self.veloc_y < 5) and (-5 < self.veloc_x < 5) and (-7 <= self.angle <= 7)

    def handle_inputs(self, pressed_key, alert_key=None):
        if not self.can_land:
            return

        thrust = None
        rotated = False
        if alert_key != pygame.K_SPACE and pressed_key[pygame.K_SPACE]:
            # Show thrust image when 'space' is pressed
            thrust = EngineThrust(self.rect, self.angle)
            self.start_engine()

        if alert_key != pygame.K_LEFT and pressed_key[pygame.K_LEFT]:
            # Rotate lander anticlockwise when 'left' is pressed
            self.angle += 1
            rotated = True

        if alert_key != pygame.K_RIGHT and pressed_key[pygame.K_RIGHT]:
            # Rotate lander clockwise when 'left' is pressed
            self.angle -= 1
            rotated = True

        if rotated:
            self.angle %= 360
            self.rot_image = pygame.transform.rotate(self.image, self.angle)

        return thrust


if __name__ == '__main__':
    init()
    game = MarsLander()
    game.run()
    pygame.quit()

You may have seen that I changed some constants into parameters with default values, this will allow you to improve the game customization if you need to by integrating argparse for instance.

Other changes may include restarting hazards every once in a while (spritecollideany might be of some help to detect when every meteor have run off the background)

\$\endgroup\$
  • \$\begingroup\$ Thank you for taking time out to answer my question! I am looking forward to implementing many of your suggestions. \$\endgroup\$ – Close Enough Apr 20 '18 at 16:33

Your Answer

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

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