-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
294 lines (274 loc) · 15.6 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import random, math
import pgzrun
WIDTH = 450 # Screen width
HEIGHT = 350 # Screen height
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 = 30
self.bonus = 9
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 = 5 # 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 """
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
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 """
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 lose?
# Check if the player loses. 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"
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"
# 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
"""
screen.fill("black")
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-1, size, 3), "white" )
# 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.circle( game.ship.position, size*2, "yellow" ) # Draw the player
# 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*3), game.ship.position[1] + (cos_angle*size*3)), "yellow" )
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*3), game.ship.position[1] + (cos_angle*size*3)), "yellow" )
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()