# roids.py

Test for polygons.

Asteroids style game demo using polygons.

Note

This example requires the following modules:

- st7789py

- tft_config

- tft_buttons

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