Wio Terminal に Vacuum Invaders を移植

「[Wio Terminal の CircuitPython に Stage ライブラリを追加する](https://qiita.com/inachi/items/78b3747b84671c098765)」にて Wio Terminal に Stage ライブラリを組み込みましたが、今回はそれを使って PyBadge 用のゲーム [Vacuum Invaders](https://github.com/python-ugame/vacuum-invaders) を Wio Terminal に移植してみます。

※ オリジナルの PyBadge でのプレイの様子は[ここ](https://learn.adafruit.com/circuitpython-stage-game-library/example-games)で見れます。

PyBadge の画面が 160x128 であるのに対して、Wio Terminal は 320x240 なので、そのままだと画面の左上 1/4 ぐらいのスペースで動くゲームになってしまうので、ゲーム画面を広げて、エイリアン数も増やしてみました。

PyBadge にはまがりなりにもスピーカーがついていて、Stageライブラリでは wav ファイルからの効果音を鳴らすことができるのですが、Wio Terminal にはブザーしかないので、今回は[simpleio](https://learn.adafruit.com/circuitpython-essentials/circuitpython-pwm) を使って音を出しています。

※ simpleio は [CircuitPython Libraries](https://circuitpython.org/libraries)に含まれています。

結果としてはこんなゲームになりました。

![IMG_5473.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106817/52b81435-9316-7d03-422b-7a12aed270d0.gif)

元のゲームソースにある [tiles.bmp](https://github.com/python-ugame/vacuum-invaders/blob/master/tiles.bmp)と以下の code.py、加えて simpleio ライブラリを Wio Terminal に転送しておけば遊べるはずです。


* code.py

```python
import time
import random
from micropython import const
from simpleio import tone

import board
import stage
import ugame


GAME_FPS = const(34)  # フレームレート

STAGE_WIDTH = const(15)  # ゲームステージの幅(タイル単位)
STAGE_HEIGHT = const(15)  # ゲームステージの高さ(タイル単位)

ALIENS_WIDTH = const(12)  # エイリアングリッドの幅(タイル単位)
ALIENS_HEIGHT = const(5)  # エイリアングリッドの高さ(タイル単位)

TILE_WIDTH_PIXELS = const(16)  # １タイルの幅(ピクセル単位)
TILE_HEIGHT_PIXELS = const(16)  # １タイルの硬さ(ピクセル単位)

STAGE_WIDTH_PIXELS = const(240)  # STAGE_WIDTH * TILE_WIDTH_PIXELS
STAGE_HEIGHT_PIXELS = const(240)  # STAGE_HEIGHT * TILE_HEIGHT_PIXELS
ALIENS_WIDTH_PIXELS = const(192)  # ALIENS_WIDTH * TILE_WIDTH_PIXELS
ALIENS_HEIGHT_PIXELS = const(80)  # ALIENS_HEIGHT * TILE_HIGHT_PIXELS
DEAD_LINE = const(160)  # (STAGE_HEIGHT - ALIENS_HEIGHT) * TILE_HEIGHT_PIXELS

TILE_SPACE = const(1)  # Bank内の宇宙画像のインデックス
TILE_STAR1 = const(2)  # Bank内の星１画像のインデックス
TILE_STAR2 = const(3)  # Bank内の星２画像のインデックス
TILE_SHIP = const(4)  # Bank内の宇宙船画像のインデックス
TILE_BOMB = const(6)  # Bank内の爆破画像のインデックス
TILE_ALIEN = const(8)  # Bank内のエイリアン画像のインデックス
TILE_SAUCER = const(9)  # Bank内の円盤画像のインデックス
TILE_MISSILE = const(12)  # Bank内のミサイル画像のインデックス

tiles = stage.Bank.from_bmp16('tiles.bmp')  # タイルやスプライトのための画像読み込み

text_area = stage.Text(9, 1)  # テキスト表示エリアの用意(横文字数、縦文字数)
text_area.move(80, 120)  # テキストエリアの表示位置


class Ship(stage.Sprite):
    '''宇宙船スプライト。
    '''

    def __init__(self):
        '''初期化。
        '''
        super(Ship, self).__init__(tiles, TILE_SHIP, 110, 214)
        self.dx = 0  # 移動増分
        self.x = 110  # 初期 x 位置
        self.tick = 0  # 
        self.dead = False  # やられたか

    def update(self):
        '''表示の更新。
        '''
        super(Ship, self).update()
        self.tick = not self.tick

        keys = ugame.buttons.get_pressed()
        self.set_frame(4, 0 if self.tick else 4)
        if keys & ugame.K_RIGHT:
            self.dx = min(self.dx + 1, 4)
            self.set_frame(5, 0)
        elif keys & ugame.K_LEFT:
            self.dx = max(self.dx - 1, -4)
            self.set_frame(5, 4)
        else:
            self.dx = self.dx // 2
        if keys & ugame.K_SELECT:
            if missile.y <= -TILE_HEIGHT_PIXELS:
                missile.move(self.x, self.y)
                tone(board.BUZZER, 523.25, 0.005)
                tone(board.BUZZER, 783.99, 0.005)
            elif missile1.y <= TILE_HEIGHT_PIXELS:
                missile1.move(self.x, self.y)
                tone(board.BUZZER, 523.25, 0.005)
                tone(board.BUZZER, 783.99, 0.005)
            elif missile2.y <= TILE_HEIGHT_PIXELS:
                missile2.move(self.x, self.y)
                tone(board.BUZZER, 523.25, 0.005)
                tone(board.BUZZER, 783.99, 0.005)
        if keys & ugame.K_O:
            pause(' Pause...')
        self.x = max(min(self.x + self.dx, 224), 0)
        self.move(self.x, self.y)


class Saucer(stage.Sprite):
    '''円盤スプライト。
    '''

    def __init__(self):
        '''初期化。
        '''
        super(Saucer, self).__init__(tiles, 9, 0, 0)
        self.tick = 0
        self.dx = 4

    def update(self):
        '''表示の更新。
        '''
        super(Saucer, self).update()
        self.tick = (self.tick + 1) % 6
        self.layer.frame(TILE_SAUCER, 0 if self.tick >= 3 else 4)
        if self.x >= 240 or self.x <= -TILE_WIDTH_PIXELS:
            self.dx = -self.dx
        self.move(self.x + self.dx, self.y)
        if abs(self.x - ship.x) < 4 and bomb.y >= 240:
            bomb.move(self.x, self.y)


class Bomb(stage.Sprite):
    '''爆破スプライト。
    '''

    def __init__(self):
        '''初期化。
        '''
        super(Bomb, self).__init__(tiles, TILE_BOMB, 0, 240)
        self.boom = 0

    def update(self):
        '''表示の更新。
        '''
        super(Bomb, self).update()
        if self.y >= STAGE_HEIGHT_PIXELS:
            return
        if self.boom:
            if self.boom == 1:
                tone(board.BUZZER, 493.88, 0.005)
                tone(board.BUZZER, 349.23, 0.005)
            self.set_frame(12 + self.boom, 0)
            self.boom += 1
            if self.boom > 4:
                self.boom = 0
                ship.dead = True
                self.move(self.x, 240)
            return
        self.move(self.x, self.y + 8)
        self.set_frame(6, 0 if self.y % TILE_HEIGHT_PIXELS else 4)
        if stage.collide(self.x + 4, self.y + 4, self.x + 12, self.y + 12,
                         ship.x + 4, ship.y + 4, ship.x + 12, ship.y + 12):
            self.boom = 1


class Missile(stage.Sprite):
    '''ミサイルスプライト。
    '''

    def __init__(self, power):
        '''初期化。
        '''
        super(Missile, self).__init__(tiles, TILE_MISSILE, 0, -32)
        self.boom = 0
        self.power = power

    def update(self):
        '''表示の更新。
        '''
        super(Missile, self).update()
        if self.boom:
            if self.boom == 1:
                tone(board.BUZZER, 493.88, 0.005)
                tone(board.BUZZER, 349.23, 0.005)
            self.set_frame(TILE_MISSILE + self.boom)
            self.boom += 1
            if self.boom > 4:
                self.boom = 0
                self.kill()
                aliens.tile(self.ax, self.ay, 0)
                aliens.dirty = True
            return

        if self.y <= -32:
            return
        self.move(self.x, self.y - 8)
        self.set_frame(TILE_MISSILE - self.power,
                       0 if self.y % TILE_HEIGHT_PIXELS == 6 else 4)
        self.ax = (self.x + 8 - aliens.x) // TILE_WIDTH_PIXELS
        self.ay = (self.y + 4 - aliens.y) // TILE_HEIGHT_PIXELS
        if (aliens.tile(self.ax, self.ay)
            and (self.x + 10 - aliens.x) % TILE_WIDTH_PIXELS > 4):
            aliens.tile(self.ax, self.ay, 7)
            self.move(self.x, self.y - 4)
            self.boom = 1

    def kill(self):
        '''
        '''
        self.move(self.x, -32)
        self.set_frame(12 - self.power)


class Aliens(stage.Grid):
    '''エイリアングリッド。
    '''

    def __init__(self):
        '''初期化。
        '''
        super(Aliens, self).__init__(tiles, ALIENS_WIDTH, ALIENS_HEIGHT)
        for y in range(ALIENS_HEIGHT):
            for x in range(ALIENS_WIDTH):
                self.tile(x, y, TILE_ALIEN)
        self.tick = 0
        self.left = 0  # 左側エイリアン全滅数列ピクセル数
        self.right = 0  # 右側エイリアン全滅列ピクセル数
        self.descend = 0
        self.dx = 2  # 水平方法移動単位ピクセル数
        self.dirty = False

    def update(self):
        '''表示の更新。
        '''
        self.tick = (self.tick + 1) % 4
        self.layer.frame(0, 0 if self.tick >= 2 else 4)
        if self.tick in (0, 2):
            if (self.x >= (TILE_WIDTH_PIXELS * (STAGE_WIDTH - ALIENS_WIDTH)
                           - self.dx + self.right)
                or self.x <= 2 - self.left):
                self.y += 1
                self.descend += 1
                if self.descend >= 4:
                    self.descend = 0
                    self.dx = -self.dx
                    self.x += self.dx
            else:
                self.x += self.dx
            self.move(self.x, self.y)

    def reform(self):
        '''エイリアングリッドの左右の端に関する情報を更新する。
        '''
        self.left = TILE_WIDTH_PIXELS * (ALIENS_WIDTH - 1)
        self.right = TILE_HEIGHT_PIXELS * (ALIENS_WIDTH - 1)
        for x in range(ALIENS_WIDTH):
            for y in range(ALIENS_HEIGHT):
                if self.tile(x, y):  # 指定座標のエイリアンが生きている
                    self.left = min(TILE_WIDTH_PIXELS * x, self.left)
                    self.right = min((ALIENS_WIDTH - 1) * TILE_WIDTH_PIXELS
                                     - TILE_WIDTH_PIXELS * x, self.right)
        self.dirty = False


def pause(info):
    '''ポーズ。

    :param info: ポーズ中に表示するテキスト。
    '''
    while ugame.buttons.get_pressed() & ugame.K_O:
        time.sleep(0.25)
    text_area.cursor(0, 0)
    text_area.text(info)
    game.render_block()
    while not ugame.buttons.get_pressed() & ugame.K_O:
        time.sleep(0.25)
    text_area.clear()
    game.render_block()
    while ugame.buttons.get_pressed() & ugame.K_O:
        time.sleep(0.25)


while True:
    space = stage.Grid(tiles, STAGE_WIDTH, STAGE_HEIGHT)
    game = stage.Stage(ugame.display, GAME_FPS)

    for y in range(STAGE_HEIGHT):
        for x in range(STAGE_WIDTH):
            space.tile(x, y, TILE_SPACE)
    for i in range(15):
        space.tile(random.randint(0, STAGE_WIDTH - 1),
                   random.randint(0, STAGE_HEIGHT - 1),
                   random.randint(TILE_STAR1, TILE_STAR2))

    aliens = Aliens()
    aliens.move(8, 17)

    saucer = Saucer()
    bomb = Bomb()
    ship = Ship()
    missile = Missile(0)
    missile1 = Missile(1)
    missile2 = Missile(2)
    sprites = [saucer, bomb, ship, missile, missile1, missile2]
    game.layers = [text_area] + sprites + [aliens, space]
    game.render_block()

    while (aliens.left + aliens.right < STAGE_WIDTH_PIXELS - TILE_WIDTH_PIXELS
           and aliens.y < DEAD_LINE
           and not ship.dead):
        for sprite in sprites:
            sprite.update()
        aliens.update()
        game.render_sprites(sprites)
        game.render_block(aliens.x + aliens.left - 1,
                          aliens.y - 1,
                          aliens.x + ALIENS_WIDTH_PIXELS + 1 - aliens.right,
                          aliens.y + ALIENS_HEIGHT_PIXELS)
        if aliens.dirty:
            aliens.reform()
        game.tick()

    if ship.dead or aliens.y >= DEAD_LINE:
        pause('Game Over')
    else:
        pause('You won!')

```