Lander: the videogame in python
I added some “vocals” (text to speech) in italian, because my pc is in Italian. Change them into your language. Remember to install the module for win32com as I suggested you in this article.
How to install the win32com module (to make the computer speak)
This code is from Daniel Pope (the author of Lander is Tim Martin).
I added this soundtrack, that is made by me (two songs that alternates as the game goes). You can find it here, you can use it for free (not for commercial use) giving credit to Giovanni Gatto.
godzilla
oxygen
To use the synthesized voice of wndows I used this lines
# search for the article with the installation of win32com from win32com.client import Dispatch s.speak("Premi spazio per cominciare") # the other code with the voice is # positioned where the win / lose part is # in the class GameClass s.speak("Bravissimo")
import pgzrun import random import math import glob from win32com.client import Dispatch s = Dispatch("SAPI.SpVoice") s.speak("Premi spazio per cominciare") """ Pi Lander * A basic Lunar Lander style game in Pygame Zero * Run with 'pgzrun pi_lander.py', control with the LEFT, RIGHT and UP arrow keys * Author Tim Martin: www.Tim-Martin.co.uk * Licence: Creative Commons Attribution-ShareAlike 4.0 International * http://creativecommons.org/licenses/by-sa/4.0/ """ WIDTH = 1400 # Screen width HEIGHT = 950 # Screen height playing = None tracks = ['godzilla', 'oxygen'] music.set_volume(0.2) music.play(tracks[0]) class LandingSpotClass: """ Each instance defines a landing spot by where it starts, how big it is and how many points it's worth """ landing_spot_sizes = ["small", "medium", "large"] def __init__(self, starting_step): self.starting = starting_step random_size = random.choice(LandingSpotClass.landing_spot_sizes) # And randomly choose size if random_size == "small": self.size = 4 self.bonus = 8 elif random_size == "medium": self.size = 10 self.bonus = 4 else: # Large self.size = 20 self.bonus = 2 def get_within_landing_spot(self, step): if (step >= self.starting) and (step < self.starting + self.size): return True return False class LandscapeClass: """ Stores and generates the landscape, landing spots and star field """ step_size = 3 # Landscape is broken down into steps. Define number of pixels on the x axis per step. world_steps = int(WIDTH / step_size) # How many steps can we fit horizontally on the screen small_height_change = 3 # Controls how bumpy the landscape is large_height_change = 10 # Controls how steep the landscape is features = ["mountain", "valley", "field"] # What features to generate n_stars = 30 # How many stars to put in the background n_spots = 4 # Max number of landing spots to generate def __init__(self): self.world_height = [] # Holds the height of the landscape at each step self.star_locations = [] # Holds the x and y location of the stars self.landing_spots = [] # Holds the landing spots def get_within_landing_spot(self, step): """ Calculate if a given step is within any of the landing spots """ for spot in self.landing_spots: if spot.get_within_landing_spot(step) == True: return True return False def get_landing_spot_bonus(self, step): for spot in self.landing_spots: if spot.get_within_landing_spot(step) == True: return spot.bonus return 0 def reset(self): """ Generates a new landscape """ # First: Choose which steps of the landscape will be landing spots del self.landing_spots[:] # Delete any previous LandingSpotClass objects next_spot_start = 0 # Move from left to right adding new landing spots until either # n_spots spots have been placed or we run out of space in the world while len(self.landing_spots) < LandscapeClass.n_spots and next_spot_start < LandscapeClass.world_steps: next_spot_start += random.randint(10, 50) # Randomly choose location to start landing spot new_landing_spot = LandingSpotClass(next_spot_start) # Make a new landing object at this spot self.landing_spots.append(new_landing_spot) # And store it in our list next_spot_start += new_landing_spot.size # Then take into account its size before choosing the next # Second: Randomise the world map del self.world_height[:] # Clear any previous world height data feature_steps = 0 # Keep track of how many steps we are into a feature self.world_height.append(random.randint(300, 500)) # Start the landscape between 300 and 500 pixels down for step in range(1, LandscapeClass.world_steps): # If feature_step is zero, we need to choose a new feature and how long it goes on for if feature_steps == 0: feature_steps = random.randint(25, 75) current_feature = random.choice(LandscapeClass.features) # Generate the world by setting the range of random numbers, must be flat if in a landing spot if self.get_within_landing_spot(step) == True: max_up = 0 # Flat max_down = 0 # Flat elif current_feature == "mountain": max_up = LandscapeClass.small_height_change max_down = -LandscapeClass.large_height_change elif current_feature == "valley": max_up = LandscapeClass.large_height_change max_down = -LandscapeClass.small_height_change elif current_feature == "field": max_up = LandscapeClass.small_height_change max_down = -LandscapeClass.small_height_change # Generate the next piece of the landscape current_height = self.world_height[step - 1] next_height = current_height + random.randint(max_down, max_up) self.world_height.append(next_height) feature_steps -= 1 # Stop mountains getting too high, or valleys too low if next_height > 570: current_feature = "mountain" # Too low! Force a mountain elif next_height < 200: current_feature = "valley" # Too high! Force a valley # Third: Randomise the star field del self.star_locations[:] for star in range(0, LandscapeClass.n_stars): star_step = random.randint(0, LandscapeClass.world_steps - 1) star_x = star_step * LandscapeClass.step_size star_y = random.randint(0, self.world_height[star_step]) # Keep the stars above the landscape self.star_locations.append((star_x, star_y)) class ShipClass: """ Holds the state of the player's ship and handles movement """ max_fuel = 1000 # How much fuel the player starts with booster_power = 0.05 # Power of the ship's thrusters rotate_speed = 10 # How fast the ship rotates in degrees per frame gravity = [0., 0.01] # Strength of gravity in the x and y directions def __init__(self): """ Create the variables which will describe the players ship """ self.angle = 0 # The angle the ship is facing 0 - 360 degrees self.altitude = 0 # The number of pixels the ship is above the ground self.booster = False # True if the player is firing their booster self.fuel = 0 # Amount of fuel remaining self.position = [0, 0] # The x and y coordinates of the players ship self.velocity = [0, 0] # The x and y velocity of the players ship self.acceleration = [0, 0] # The x and y acceleration of the players ship def reset(self): """ Set the ships position, velocity and angle to their new-game values """ self.position = [750., 100.] # Always start at the same spot self.velocity = [-random.random(), random.random()] # But with some initial speed self.acceleration = [0., 0.] # No initial acceleration (except gravity of course) self.angle = random.randint(0, 360) # And pointing in a random direction self.fuel = ShipClass.max_fuel # Fill up fuel tanks def rotate(self, direction): """ Rotate the players ship and keep the angle within the range 0 - 360 degrees """ if direction == "left": self.angle -= ShipClass.rotate_speed elif direction == "right": self.angle += ShipClass.rotate_speed if self.angle > 360: # Remember than adding or subtracting 360 degrees does not change the angle self.angle -= 360 elif self.angle < 0: self.angle += 360 def booster_on(self): """ When booster is firing we accelerate in the opposite direction, 180 degrees, from the way the ship is facing """ sounds.engine.play() self.booster = True self.acceleration[0] = ShipClass.booster_power * math.sin(math.radians(self.angle + 180)) self.acceleration[1] = ShipClass.booster_power * math.cos(math.radians(self.angle + 180)) self.fuel -= 2 def booster_off(self): """ When the booster is not firing we do not accelerate """ self.booster = False self.acceleration[0] = 0. self.acceleration[1] = 0. def update_physics(self): """ Update ship physics in X and Y, apply acceleration (and gravity) to the velocity and velocity to the position """ for axis in range(0, 2): self.velocity[axis] += ShipClass.gravity[axis] self.velocity[axis] += self.acceleration[axis] self.position[axis] += self.velocity[axis] # Update player altitude. Note that (LanscapeClass.step_size * 3) is the length of the ship's legs ship_step = int(self.position[0] / LandscapeClass.step_size) if ship_step < LandscapeClass.world_steps: self.altitude = game.landscape.world_height[ship_step] - self.position[1] - (LandscapeClass.step_size * 3) def get_out_of_bounds(self): """ Check if the player has hit the ground or gone off the sides """ if self.altitude <= 0 or self.position[0] <= 0 or self.position[0] >= WIDTH: return True return False tracknum = 1 # number to change track when you win or not class GameClass: """ Holds main game data, including the ship and landscape objects. Checks for game-over """ def __init__(self): self.time = 0. # Time spent playing in seconds self.score = 0 # Player's score self.game_speed = 30 # How fast the game should run in frames per second self.time_elapsed = 0. # Time since the last frame was changed self.blink = True # True if blinking text is to be shown self.n_frames = 0 # Number of frames processed self.game_on = False # True if the game is being played self.game_message = "PI LANDER\nPRESS SPACE TO START" # Start of game message self.ship = ShipClass() # Make a object of the ShipClass type self.landscape = LandscapeClass() self.reset() # Start the game with a fresh landscape and ship def reset(self): self.time = 0. self.ship.reset() self.landscape.reset() def check_game_over(self): """ Check if the game is over and update the game state if so """ global tracknum if self.ship.get_out_of_bounds() == False: return # Game is not over self.game_on = False # Game has finished. But did we win or loose? # Check if the player looses. This is if the ship's angle is > 20 degrees # the ship is not over a landing site, is moving too fast or is off the side of the screen ship_step = int(self.ship.position[0] / LandscapeClass.step_size) if self.ship.position[0] <= 0 \ or self.ship.position[0] >= WIDTH \ or self.landscape.get_within_landing_spot(ship_step) == False \ or abs(self.ship.velocity[0]) > .5 \ or abs(self.ship.velocity[1]) > .5 \ or (self.ship.angle > 20 and self.ship.angle < 340): self.game_message = "YOU JUST DESTROYED A 100 MEGABUCK LANDER\n\nLOOSE 250 POINTS\n\nPRESS SPACE TO RESTART" s.speak("Hai distrutto la nave") if tracknum == 1: music.play(tracks[1]) tracknum = 0 self.score -= 250 else: # If the player has won! Update their score based on the amount of remaining fuel and the landing bonus points = self.ship.fuel / 10 points *= self.landscape.get_landing_spot_bonus(ship_step) self.score += points self.game_message = "CONGRATULATIONS\nTHAT WAS A GREAT LANDING!\n\n" + str(round(points)) + " POINTS\n\nPRESS SPACE TO RESTART" s.speak("Bravissimo") if tracknum == 0: music.play(tracks[0]) tracknum = 1 # Create the game object game = GameClass() def draw(): """ Draw the game window on the screen in the following order: start message, mountain range, bonus points, stars, statistics, player's ship """ # music global playing screen.fill("darkblue") size = LandscapeClass.step_size if game.game_on == False: screen.draw.text(game.game_message, center=(WIDTH / 2, HEIGHT / 5), align="center") # Get the x and y coordinates of each step of the landscape and draw it as a straight line for step in range(0, game.landscape.world_steps - 1): x_start = size * step x_end = size * (step + 1) y_start = game.landscape.world_height[step] y_end = game.landscape.world_height[step + 1] screen.draw.line((x_start, y_start), (x_end, y_end), "white") # Every second we flash the landing spots with a thicker line by drawing a narrow rectangle if (game.blink == True or game.game_on == False) and game.landscape.get_within_landing_spot(step) == True: screen.draw.filled_rect(Rect(x_start - size, y_start - 3, size, 4), "gold") # Draw the bonus point notifier if game.blink == True or game.game_on == False: for spot in game.landscape.landing_spots: x_text = spot.starting * size y_text = game.landscape.world_height[spot.starting] + 10 # The extra 10 pixels puts the text below the landscape screen.draw.text(str(spot.bonus) + "x", (x_text, y_text), color="white") # Draw the stars for star in game.landscape.star_locations: screen.draw.line(star, star, "white") # Draw the stats screen.draw.text("SCORE: " + str(round(game.score)), (10, 10), color="white", background="black") screen.draw.text("TIME: " + str(round(game.time)), (10, 25), color="white", background="black") screen.draw.text("FUEL: " + str(game.ship.fuel), (10, 40), color="white", background="black") screen.draw.text("ALTITUDE: " + str(round(game.ship.altitude)), (WIDTH - 230, 10), color="white", background="black") screen.draw.text("HORIZONTAL SPEED: {0:.2f}".format(game.ship.velocity[0]), (WIDTH - 230, 25), color="white", background="black") screen.draw.text("VERTICAL SPEED: {0:.2f}".format(-game.ship.velocity[1]), (WIDTH - 230, 40), color="white", background="black") screen.draw.filled_circle(game.ship.position, size * 2, "yellow") # Draw the player screen.draw.circle(game.ship.position, size * 2, "orange") # Draw the player screen.draw.filled_circle(game.ship.position, size * 1, "orange") # Use sin and cosine functions to draw the ship legs and booster at the correct angle # Requires the values in radians (0 to 2*pi) rather than in degrees (0 to 360) sin_angle = math.sin(math.radians(game.ship.angle - 45)) # Legs are drawn 45 degrees either side of the ship's angle cos_angle = math.cos(math.radians(game.ship.angle - 45)) screen.draw.line(game.ship.position, (game.ship.position[0] + (sin_angle * size * 4), game.ship.position[1] + (cos_angle * size * 4)), "red") sin_angle = math.sin(math.radians(game.ship.angle + 45)) cos_angle = math.cos(math.radians(game.ship.angle + 45)) screen.draw.line(game.ship.position, (game.ship.position[0] + (sin_angle * size * 4), game.ship.position[1] + (cos_angle * size * 4)), "red") if game.ship.booster == True: sin_angle = math.sin(math.radians(game.ship.angle)) # Booster is drawn at the same angle as the ship, just under it cos_angle = math.cos(math.radians(game.ship.angle)) screen.draw.filled_circle((game.ship.position[0] + (sin_angle * size * 3), game.ship.position[1] + (cos_angle * size * 3)), size, "orange") def update(detlatime): """ Updates the game physics 30 times every second """ game.time_elapsed += detlatime if game.time_elapsed < 1. / game.game_speed: return # A 30th of a second has not passed yet game.time_elapsed -= 1. / game.game_speed # New frame - do all the simulations game.n_frames += 1 if game.n_frames % game.game_speed == 0: # If n_frames is an exact multiple of the game FPS: so once per second game.blink = not game.blink # Invert blink so True becomes False or False becomes True # Start the game if the player presses space when the game is not on if keyboard.space and game.game_on == False: game.game_on = True game.reset() elif game.game_on == False: return # If the game is on, update the movement and the physics if keyboard.left: # Change space ship rotation game.ship.rotate("left") elif keyboard.right: game.ship.rotate("right") if keyboard.up and game.ship.fuel > 0: # Fire boosters if the player has enough fuel game.ship.booster_on() else: game.ship.booster_off() game.time += detlatime game.ship.update_physics() game.check_game_over() pgzrun.go()
Video of Lander with music and voice