Excelでテトリスを実現！VBAは使わずにRPAで作ってみたらいい感じだった【UiPath Forms 23.4】

先日、Excelのセル幅を変えて方眼紙のようにすると何かに似ていることに気づきました。
そう、テトリスのゲーム画面です。

「よし、Excelで動くテトリスを作ろう！」
謎のモチベーションが湧いてきました。

Excelでテトリスが作りたい！でもVBAとか使いたくない！
そんなワガママな人にも、この記事が参考になると思います。

でも、そんなこと、できるの？大丈夫、こんな私でもできました！

## Excel上で動くテトリス

作ったものがこちらです！
![tetris.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/1133cc38-a61e-b6ff-d0a9-9e1ff50a0044.gif)

動く！動くぞ！
本来仕事をするためのExcelでテトリスをするという背徳感がたまりません、はいー。

こちら、VBAは使ってません！完全オーガニックです！
VBAの代わりにUiPathというローコードのRPAツールを使っています。（個人利用であれば、無償で使えるコミュニティ版がありますので、テトリスし放題です！）

RPAを知ってもらいたい時にインパクトのあるものが欲しい！と思ったことありませんか？
そんな時にも使えると思います。

UiPath Studio 23.4で機能拡充されたフォームやローカルトリガーを使用しています。並列アクティビティやトリガ使用時のデータの使用方法は、UiPathを使ったことがある人も参考になると思います。

::: note info
RPAって何？
RPAは業務を自動化するためのツールです。
プログラミング知識がない人でも、アプリやブラウザのデータを読み書きしたりExcelにデータを読み書きする自動化処理を作ることができます。
:::

## テトリスってどうやって動いてるの？

そもそもテトリスって何？という方はいないと思いますが、テトリミノと呼ばれるブロックが上から落ちてくるので、それを上まで積まないようにします。ブロックを横1列全て並べると消すことができます。

テトリスってどう動いているのかご存じですか？
だいたいの流れを知っていた方が今回の記事の内容が理解しやすいと思いますので、まずはテトリスの作り方をChatGPT先生に聞いてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/08012133-17ce-af7a-b6c6-6a138083c3d6.png)

```python:tetris.py
import pygame
import random

# テトリスの初期設定
pygame.init()
WIDTH, HEIGHT = 800, 600
GRID_SIZE = 30
GRID_WIDTH, GRID_HEIGHT = WIDTH // GRID_SIZE, HEIGHT // GRID_SIZE
FPS = 30

# 色の設定
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

# テトリスのブロックを表すクラス
class Block:
    shapes = [
        [[1, 1, 1, 1]],
        [[1, 1], [1, 1]],
        [[1, 1, 0], [0, 1, 1]],
        [[0, 1, 1], [1, 1, 0]],
        [[1, 1, 1], [0, 1, 0]],
        [[1, 1, 1], [1, 0, 0]],
        [[1, 1, 1], [0, 0, 1]]
    ]

    def __init__(self):
        self.shape = random.choice(self.shapes)
        self.color = random.choice([RED, GREEN, BLUE])
        self.x = GRID_WIDTH // 2 - len(self.shape[0]) // 2
        self.y = -len(self.shape)

    def rotate(self):
        self.shape = list(zip(*self.shape[::-1]))

    def move_down(self):
        self.y += 1

    def move_left(self):
        self.x -= 1

    def move_right(self):
        self.x += 1

    def draw(self):
        for row in range(len(self.shape)):
            for col in range(len(self.shape[row])):
                if self.shape[row][col]:
                    rect = pygame.Rect((self.x + col) * GRID_SIZE, (self.y + row) * GRID_SIZE, GRID_SIZE, GRID_SIZE)
                    pygame.draw.rect(screen, self.color, rect)

    def check_collision(self, grid):
        for row in range(len(self.shape)):
            for col in range(len(self.shape[row])):
                if self.shape[row][col] and (self.y + row >= GRID_HEIGHT or grid[self.y + row][self.x + col]):
                    return True
        return False

    def merge(self, grid):
        for row in range(len(self.shape)):
            for col in range(len(self.shape[row])):
                if self.shape[row][col]:
                    grid[self.y + row][self.x + col] = self.color


# ゲームの初期化
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
grid = [[BLACK] * GRID_WIDTH for _ in range(GRID_HEIGHT)]
block = Block()
game_over = False

# ゲームループ
while not game_over:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            game_over = True
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                block.move_left()
                if block.check_collision(grid):
                    block.move_right()
            elif event.key == pygame.K_RIGHT:
                block.move_right()
                if block.check_collision(grid):
                    block.move_left()
            elif event.key == pygame.K_DOWN:
                block.move_down()
                if block.check_collision(grid):
                    block.move_up()
            elif event.key == pygame.K_UP:
                block.rotate()
                if block.check_collision(grid):
                    block.rotate()

    if not block.check_collision(grid):
        block.move_down()
    else:
        block.merge(grid)
        block = Block()
        if block.check_collision(grid):
            game_over = True

    screen.fill(BLACK)
    for row in range(GRID_HEIGHT):
        for col in range(GRID_WIDTH):
            rect = pygame.Rect(col * GRID_SIZE, row * GRID_SIZE, GRID_SIZE, GRID_SIZE)
            pygame.draw.rect(screen, grid[row][col], rect, 1)
    block.draw()

    pygame.display.flip()
    clock.tick(FPS)

pygame.quit()

```

わからーん！文字が小さすぎてみえなーい！
という人のために、この処理を軽く説明していきます。
このプログラムはPythonというプログラムで書かれていますが、今回はPythonではなくてRPAで作っていくので詳細に理解する必要はありません。

大きく6つあると思ってください
・初期化処理
・ゲーム画面処理
・移動処理
・タイマー処理
・描画処理
・その他ゲーム処理

### 初期化処理

初期化処理ではフィールドやテトリミノを定義します。
今回のRPAでも同じように必要な作業です。

### ゲーム画面

サンプルコードのPythonでは、pygameというパッケージを使ってボタンやゲームが表示されるフィールドを作っています。
今回のRPAでは、ゲームを表示するメインの画面はExcelを使用します。今回使用するRPAツールのUiPathには、簡単に画面を作ることができるFormsという機能がありますので、こちらを使っていきます。

### 描画処理

テトリス、マリオ、ドラクエなど、どのゲームも画面は常に動いています。これは表示するデータを作っておいて画面に高速に描画することで実現しています。高速に描画し続けているので、人間には動いているように見えます。
今回のRPAでは、Excelに高速にデータを書き込み、動いているように見せます。

### 移動処理

キー操作があった場合に移動する処理を作る必要があります。
例えば右ボタンが押されたら、右に1つ移動する処理です。キー操作のタイミングではあくまで内部のデータを書き換えるだけで、描画処理のタイミングで一気に書き換えます。
テトリスでは画面端や他のブロックがあったら移動できないなど、移動判定処理がありますが、ここがテトリス作成の一番の難所です。
キー操作や作成した画面をクリックした際にどのように動くかは、今回使用するRPAツールのUiPath Studioには、ローカルトリガー機能がありますので、こちらで作っていきます。

### タイマー処理

テトリスでは一定時間経過するとテトリミノが下に１つ落ちる処理を作る必要があります。プログラミングでは、タイマー割り込みなどを使い処理を作成していきます。
今回のRPAでは、永久に繰り返すループ処理を作ってその中で一定間隔待ってからテトリミノを下に落とすようにします。

### その他ゲーム処理

下に移動できなくなったタイミングでミノを固定して次のミノを生成する必要があります。この間に列を削除できないか、ゲームオーバーになっていないかを判定する処理が入ります。点数やレベルを増やしたりと細かな処理を作っていく必要があります。

以上です。
勘のいい人なら、もうお気づきかましれません。けっこう作るのは大変です！笑
ワークフローと呼ばれるXamlファイルが40個もできてしまいました。
細かく分割しているので1個1個の処理は大したことが無いのですが、なかなかの大作です。

## RPAでテトリスを作る
細かい作り方をここから紹介していきます。
テトリスのゲーム処理は他のプログラミング言語と変わらないため、UiPath Studioでどのように作るかを解説していきます。
テトリスのゲーム処理を見たい方は、ソースコードはGitHubに公開していますので、そちらをご覧ください。

https://github.com/manabutech-kyushu/uipath-tetris


### 初期化処理

トリガー処理を作成する際、データを保存・参照するためにグローバル変数と定数を使用しています。
変数はXamlファイル内でアクティビティ間でデータを共有する仕組みです。
Xaml間でデータを共有する引数という仕組みもありますが、トリガーなどでは引数を渡すことができないため、どこからでも変更・参照が可能なグローバル変数を使用しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/28f74249-ee86-0d5d-f72a-16d57fc4fb69.png)

実際の作り方ですが、データマネージャーパネルから作成できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/cdec466b-c16e-9832-89ef-8eadf0f0f0d5.png)

スコープをグローバルにすると変数や定数が作れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/c2760252-b7f0-1fc8-f82b-6543fc6614be.png)

定数は変更ができませんので、フィールドやテトリミノの幅などの定義に使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/ccd9a2ad-d05f-6b55-57b4-bf8b9a37ef9e.png)

Excelで使用する描画範囲も定数として定義します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/88a14b3c-d8eb-c43b-e8c1-2312390e2962.png)

グローバル変数では、複数のXamlファイルで使用する現在座標やフィールド、カレントミノ、ネクストミノ情報を作成します。フィールドやミノデータはExcelで書き込みがしやすいようにデータテーブル型で定義しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/682e8c81-71bb-3376-577a-3a71e2715082.png)

ゲーム処理で使用するグローバル変数も用意しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/3af34340-292b-a04d-6aea-c76ff9a10a97.png)

### 描画処理

RPAで内部のデータをExcelに高速で書き込んでいきますが、描画データはフィールドとカレントミノ情報を統合して書き込みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/d29b6788-c1a6-fc29-65d7-5e153d8e1b80.png)

描画処理.xamlのワークフローは次のようになっています。
フィールドを描画データとしてコピーして、カレントミノのデータで何か文字があるものは現在座標とのオフセットから位置を判断して書き込みます。その後、描画範囲に書き込んでいます。
Excelは同一ファイルを開こうとするとエラーとなります。つまり、誰かが書き込もうとしているときは書き込まないようにしなくてはいけません。排他制御とかプログラミングでいうとミューテックスとかいう概念が必要になってきます。
ミュージカルだかミュータントだかそういう難しいことは考えたくありませんので、キー操作や一定間隔での移動ではExcelに書きに行かずに裏で書き込むデータを更新しておきます。そのデータを高速で常に書き込みにいきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/dbb8b118-35cf-9b94-0e3a-590e3f9403a9.png)

消えた感じを出すためにラインを消して描画を待ってから上から詰めるようにして消えたように見せています。
![tetris2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/af51b0e4-cf19-19b3-c684-428ff861db33.gif)

### 操作画面の作成
ボタン操作画面はフォーム機能を使用して作成します。
今回はUiPath Studio23.4を使用していますが、23.4からUiPath Forms機能がパワーアップして、前よりも作りやすくなっています。
事前にパッケージの管理より、「UiPath.Forms.Activities」をインストールしてから新規にフォームを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/e60ea8d1-5952-e955-f208-7435afacba81.png)

フォームはパーツをドラッグ＆ドロップでプログラミングすることなく作成することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/e210c9a9-a984-4782-3b92-01260fd9fc4e.png)

### 描画画面（Excel）の作成

Excelはセル幅を調整してテトリスっぽくなるようにします。
フィールドとネクストミノの描画範囲を格子状にして分かりやすくしています。
それ以外の部分は、好きなように文字を配置して大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/c5531421-5806-6e2a-4fb9-59963c94b6cb.png)

Excelで処理をするメリットはテトリミノの色管理が簡単になることです。内部のデータでは「I」「J」など文字情報として管理し、Excelの条件付き書式で色が変わるようにしています。
「W」は壁を意味します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/d62caabd-d1c1-35bb-ebaa-1a6598d9b91f.png)

「X」はゲームオーバーの際にブロックが灰色になる演出を入れたかったので、使っています。なぜかゲームオーバー画面を凝って作っています。
![tetris3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/507a5464-ca4a-2671-75b4-ff021f8fd62f.gif)


### トリガー処理
フォームで作成したボタンが押された際に処理が動くようにトリガーを作成します。
フォームのボタンにマウスを移動させると、トリガーを作成するボタンが出てきます。
ボタンをクリックするとXamlファイルが作成されます。
後は好きな処理を追加していくだけです。例では回転ボタンが押された時に回転する処理を呼び出すようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/8513d40d-2857-cbca-ec6f-1a7892813827.png)

ローカルトリガー機能を有効にするためには、「ローカルトリガーを実行」アクティビティを使う必要があります。このアクティビティによってトリガーが受け付けられるようになります。
エクセルへの描画、ブロックを定期的に移動させる、キー操作があれば、特定の処理を行うようなマルチタスクが求められます。
サラダをつぎ分け、グラスが空になってないか気遣いながら、おっさんの面白くない話を聞く。そんなホステスのような機敏な動きがRPAとExcelにできるのか？
できます。ホステスアクティビティはありませんが、並列アクティビティがありますので、こちらを使います。
並列アクティビティは、今回のように並列に処理を行いたい時に使用します。
こちらは高速に処理を切り替えて、擬似的に並列に動いているだけです。
悟空の残像拳のように高速に動いて複数いるように見せているだけであって、天津飯の四身の拳のように実体があるわけではないということです！
（先日、スラムダンクなど例えが古すぎて若い世代には伝わらなくなっていることに気づきました！
この例えも分からないんだろうなー。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/79d60cf2-eab4-fa01-7c72-dbda6703026e.png)

ちなみに、フォームを使わなくてもキーボードで操作できるようにもしています。ゲームを作る場合はこちらの方が適しているかもしれません。この場合は、ホットキートリガーを使います。
スケジュールモードはSequencial、Concurrentとありますが順次実行か同時実行で、詳しい説明が公式ドキュメントに無かったのですがConcurrentの場合はトリガー実行中にトリガーを実行するような挙動になると思われます。今回は多重にトリガーが発生してしまうとタイミングによって移動判定が狂う可能性があるためSequenceにしています。（フォームのトリガーにもプロパティパネルからスケジュールモードを切り替えられるようになっています。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/dba0ac6d-ec47-3c50-feea-d88cfc360db3.png)


### タイマー処理

繰り返し処理を作ってその中で一定間隔待ってからテトリミノを下に落とすようにします。
処理内容の多さによって間隔がまちまちになってしまうのですが、今回はまあ良しとします。
「バ、バグではありません。仕様です！」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/c996679d-30cb-0dfa-20cc-7bb85cdad236.png)

## 最後に
いかがでしたか？
Excelでテトリスを作ることができました。ファミコンくらいのゲームであれば実現できそうです。（頑張れば）
テトリスはRPAに興味を持ってもらうにはインパクトもあって良いと思います。
プログラミングよりは簡単なので、テトリス作成に挫折した人も作ってみてはどうでしょうか？
	
UiPath FriendsコミュニティやTwitterなどで、俺はUiPathでマリオ作ったよ！私はドラクエ作ったよ！などの報告が聞けることを楽しみにしています！
RPAは本来、業務を自動化するものです。
テトリスにより、自動化される業務がないとは思いますが、今回使っているフォームやトリガーは業務自動化にも有用な機能です。

面白かった方は励みになりますので、記事のいいねお願いします！