1"""
2roids.py
3========
4
5.. figure:: /_static/roids.png
6 :align: center
7
8 Asteroids style game demo using polygons.
9"""
10
11import math
12import random
13import utime
14import micropython
15import gc9a01
16import tft_config
17import tft_buttons as Buttons
18
19
20tft = tft_config.config(tft_config.TALL, buffer_size=64*64*2)
21buttons = Buttons.Buttons()
22
23
24def main():
25 '''
26 Game on!
27 '''
28
29 class Poly():
30 '''
31 Poly class to keep track of a polygon based sprite
32 '''
33
34 def __init__(
35 self,
36 # list (x,y) tuples of convex polygon, must be closed
37 polygon,
38 x=None, # x location of polygon
39 y=None, # y location of polygon
40 v_x=None, # velocity in x axis
41 v_y=None, # velocity in y axis
42 angle=None, # angle in radians polygon is facing
43 spin=None, # spin in radians per frame_time
44 scale=None, # scale factor for polygon
45 radius=None, # radius of polygon for collision detection
46 max_velocity=10, # max velocity of polygon
47 counter=0):
48
49 # scale the polygon if scale was given
50 self.polygon = (
51 polygon if scale is None else [(int(scale*x[0]), int(scale*x[1])) for x in polygon])
52
53 # if no location given assign a random location
54 self.x = random.randint(0, width) if x is None else x
55 self.y = random.randint(0, width) if y is None else y
56
57 # set angle if given
58 self.angle = float(0) if angle is None else angle
59
60 # set random spin unless one was given
61 self.spin = random.randint(-3, 3) / 16 if spin is None else spin
62
63 # set random velocity unless one was given
64 self.velocity_x = random.uniform(
65 0.50, 0.99)*6-3 + 0.75 if v_x is None else v_x
66 self.velocity_y = random.uniform(
67 0.50, 0.99)*6-3 + 0.75 if v_y is None else v_y
68
69 # set radius, max_velocity and radius counter
70 self.radius = radius
71 self.max_velocity = max_velocity
72 self.counter = counter
73
74 def rotate(self, rad):
75 '''
76 Rotate polygon in radians
77 '''
78 self.angle += rad
79 if self.angle > rad_max:
80 self.angle = 0
81 elif self.angle < 0:
82 self.angle = rad_max
83
84 def move(self):
85 '''
86 Rotate and move polygon velocity distance.
87 '''
88 if self.spin != 0:
89 self.rotate(self.spin)
90
91 self.x += int(self.velocity_x)
92 self.y += int(self.velocity_y)
93 self.x %= width
94 self.y %= height
95
96 def draw(self, color):
97 '''
98 Draw the polygon
99 '''
100 tft.polygon(self.polygon, self.x, self.y, color, self.angle, 0, 0)
101
102 def collision(self, poly):
103 '''
104 Detect collisions using overlapping radiuses.
105 Returns True on collision.
106 '''
107 return abs(
108 ((self.x - poly.x) * (self.x - poly.x) +
109 (self.y - poly.y) * (self.y - poly.y))
110 < (self.radius + poly.radius) * (self.radius + poly.radius))
111
112 def create_roid(size, x=None, y=None, v_x=None, v_y=None):
113 '''
114 Create a new roid with the given parameters.
115 '''
116 return Poly(
117 roid_poly,
118 x=x,
119 y=y,
120 v_x=v_x,
121 v_y=v_y,
122 scale=roid_scale[size],
123 radius=roid_radius[size],
124 counter=size)
125
126 def update_missiles():
127 '''
128 Update active missiles and handle asteroid hits.
129 '''
130 for missile in list(missiles): # for each missile
131 missile.draw(gc9a01.BLACK) # erase old missile
132 if missile.counter > 0: # if counter > 0 missile is active
133 missile.move() # update missile position
134
135 for roid in list(roids): # for each roid
136 if missile.collision(roid): # check if missile collides with roid
137 roid.draw(gc9a01.BLACK) # erase the roid
138 if roid.counter > 0: # if roid is not the smallest size
139 roids.append( # add first smaller roid
140 create_roid(
141 roid.counter-1,
142 x=roid.x,
143 y=roid.y,
144 v_x=roid.velocity_x,
145 v_y=roid.velocity_y))
146 roids.append( # add second smaller roid
147 create_roid(
148 roid.counter-1,
149 x=roid.x,
150 y=roid.y,
151 v_x=-roid.velocity_x,
152 v_y=-roid.velocity_y))
153
154 roids.remove(roid) # remove the roid that was hit
155 missile.counter = 0
156
157 if missile.counter > 0: # if the missile has life left
158 missile.draw(gc9a01.WHITE) # draw missile
159 missile.counter -= 1 # reduce missile life
160 else:
161
162 missiles.remove(missile) # remove exploded missile
163 else:
164 missiles.remove(missile) # remove expired missile
165
166 def update_ship():
167 '''
168 Update ship velocity and limit to max_velocity
169 '''
170 # apply drag to velocity of ship so it will eventually slow to a stop
171 ship.velocity_x -= ship.velocity_x * ship_drag_frame
172 ship.velocity_y -= ship.velocity_y * ship_drag_frame
173
174 if ship.velocity_x > ship.max_velocity: # Limit ship velocity to +/- max_velocity
175 ship.velocity_x = ship.max_velocity
176 elif ship.velocity_x < -ship.max_velocity:
177 ship.velocity_x = -ship.max_velocity
178
179 if ship.velocity_y > ship.max_velocity:
180 ship.velocity_y = ship.max_velocity
181 elif ship.velocity_y < -ship.max_velocity:
182 ship.velocity_y = -ship.max_velocity
183
184 if abs(ship.velocity_x) < 0.1: # if ship is moving very slowly, stop it
185 ship.velocity_x = 0.0
186
187 if abs(ship.velocity_y) < 0.1:
188 ship.velocity_y = 0.0
189
190 ship.move() # move the ship and draw it
191 ship.draw(gc9a01.WHITE)
192
193 def update_roids():
194 '''
195 Update roid positions handle ship collisions
196 Returns True if not hit, False if hit
197 '''
198 not_hit = True
199
200 for roid in roids: # for each roid, erase, move then draw
201 roid.draw(gc9a01.BLACK)
202 roid.move()
203 roid.draw(gc9a01.WHITE)
204
205 if roid.collision(ship): # check for roid/ship collision
206 ship.draw(gc9a01.BLACK) # erase ship
207 ship.velocity_x = 0.0 # stop movement
208 ship.velocity_y = 0.0
209
210 not_hit = False
211
212 return not_hit
213
214 def explode_ship():
215 '''
216 Increment explosion step and alternate between drawing
217 explosion poly and explosion poly rotated 45 degrees
218
219 Returns True when explosion is finished
220 '''
221 ship.counter += 1
222 if ship.counter % 2 == 0:
223 tft.polygon(explosion_poly, ship.x, ship.y, gc9a01.BLACK, 0.785398)
224 tft.polygon(explosion_poly, ship.x, ship.y, gc9a01.WHITE)
225 else:
226 tft.polygon(explosion_poly, ship.x, ship.y, gc9a01.WHITE, 0.785398)
227 tft.polygon(explosion_poly, ship.x, ship.y, gc9a01.BLACK)
228
229 if ship.counter > 25:
230 # erase explosion, move ship to center and stop explosion
231 tft.polygon(explosion_poly, ship.x, ship.y, gc9a01.BLACK)
232 # move ship to center
233 ship.x = width >> 1
234 ship.y = height >> 1
235 ship.counter = 0
236 return True
237
238 return False
239
240 # enable display and clear screen
241 tft.init()
242 tft.fill(gc9a01.BLACK)
243 width = tft.width()
244 height = tft.height()
245
246 rad_max = 2 * math.pi # 360' in radians
247
248 # ship variables
249 ship_alive = True
250 ship_radius = 7
251 ship_rad_frame = rad_max / 16 # turning rate per frame
252 ship_accel_frame = 0.6 # acceleration per frame
253 ship_drag_frame = 0.015 # drag factor per frame
254
255 ship_poly = [(-7, -7), (7, 0), (-7, 7), (-3, 0), (-7, -7)]
256
257 ship = Poly(
258 ship_poly,
259 x=width >> 1,
260 y=height >> 1,
261 v_x=0,
262 v_y=0,
263 radius=ship_radius,
264 spin=0.0)
265
266 explosion_poly = [(-4, -4), (-4, 4), (4, 4), (4, -4), (-4, -4)]
267
268 # asteroid variables
269 roid_radius = [5, 10, 16]
270 roid_scale = [0.33, 0.66, 1.0]
271
272 roid_poly = [
273 (-5, -15), (-2, -13), (11, -14), (15, -7), (14, 0),
274 (16, 5), (11, 16), (7, 16), (-7, 14), (-14, 7),
275 (-13, 1), (-14, -8), (-11, -15), (-5, -15)]
276 roids = []
277
278 # missile variables
279 missile_velocity = 8
280 missile_max = 8
281 missile_life = 20
282 missile_rate = 200
283 missile_last = utime.ticks_ms()
284 missile_poly = [(-1, -1), (1, -1), (1, 1), (-1, 1), (-1, -1)]
285 missiles = []
286
287 frame_time = 60 # target frame rate delay
288
289 # game loop
290 while True:
291 last_frame = utime.ticks_ms()
292
293 # add roids if there are none
294 if len(roids) == 0:
295 roids = [create_roid(2), create_roid(2)]
296
297 update_missiles()
298
299 # Erase the ship
300 ship.draw(gc9a01.BLACK)
301
302 if ship_alive:
303 # if left button pressed
304 if buttons.left and buttons.left.value() == 0:
305 # rotate ship counter clockwise
306 ship.rotate(-ship_rad_frame)
307
308 # if right button pressed
309 if buttons.right and buttons.right.value() == 0:
310 # rotate ship clockwise
311 ship.rotate(ship_rad_frame)
312
313 # if hyperspace button pressed move ship to random location
314 if buttons.hyper and buttons.hyper.value() == 0:
315 diameter = ship.radius * 2
316 ship.x = random.randint(diameter, width - diameter)
317 ship.y = random.randint(diameter, height - diameter)
318
319 # stop movement
320 ship.velocity_x = 0.0
321 ship.velocity_y = 0.0
322
323 # if thrust button pressed
324 if buttons.thrust and buttons.thrust.value() == 0:
325 # accelerate ship in the direction the ship is facing
326 d_y = math.sin(ship.angle) * ship_accel_frame
327 d_x = math.cos(ship.angle) * ship_accel_frame
328 ship.velocity_x += d_x
329 ship.velocity_y += d_y
330
331 # if the fire button is pressed and less than missile_max active missles
332 if buttons.thrust and buttons.fire.value() == 0 and len(missiles) < missile_max:
333
334 # limit missiles firing to once every missile_rate ms
335 if last_frame - missile_last > missile_rate:
336
337 # fire missile in direction ship in facing
338 v_y = math.sin(ship.angle) * missile_velocity
339 v_x = math.cos(ship.angle) * missile_velocity
340
341 # create new missile
342 missile = Poly(
343 missile_poly,
344 x=ship.x,
345 y=ship.y,
346 v_x=v_x,
347 v_y=v_y,
348 angle=ship.angle,
349 radius=1,
350 spin=0.0,
351 counter=missile_life)
352
353 # add to to missile list and save last fire time
354 missiles.append(missile)
355 missile_last = last_frame
356
357 update_ship()
358
359 else:
360 # explosion animation until returns True
361 ship_alive = explode_ship()
362
363 # update roids and return if ship was not hit
364 not_hit = update_roids()
365 if ship_alive:
366 ship_alive = not_hit
367
368 # wait until frame time expires
369 while utime.ticks_ms() - last_frame < frame_time:
370 pass
371
372
373main()