マイクラプログラミング(統合版)いろいろやってみた[Scripting API/Functions/WebSocket/MakeCode]

# 概要
この記事は
[KLab Engineer Advent Calendar 2021](https://qiita.com/advent-calendar/2021/klab)の10日目の記事です。

こんにちはhamasan05です。
昨年に引き続きマイクラ(統合版)の記事です。
今年はマイクラでなんでもいいからゲームを一つ作ってみるということで作ってみました。

マイクラ(統合版)には複数のプログラミング可能な環境が用意されており
同じゲームを複数のやり方で実装することにチャレンジしてみました。

# どんなゲームを作ったか

マイクラのウィザーという裏ボスを出現させないようにするゲームです。
ウィザーには出現条件があり、その出現条件を満たさないように立ち回り
出現させてしまった人が負け、責任をもってウィザーを倒すという内容になります。

動画
<iframe width="560" height="315" src="https://www.youtube.com/embed/75q3m_zAqk8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

実際に遊ぶとこのような感じになります。
※ゲームの元ネタです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/d0dzZ8F3DZ4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 機能の紹介
必要な機能は下記の二つだけにしました
1. ステージのリセット
2. 必要なアイテムの付与

## ステージのリセット
- 一定の範囲に特定のブロックを配置する
- 出現条件をランダムにするために、特定のブロックの配置をランダムにする

## 必要なアイテムの付与
- 装備や消耗品の付与
- ゲームの進行に必要な配置可能なブロックの付与


# 1. Scripting APIと呼ばれているものを使った実装
公式を見てもわからなかったのでいろいろネットをさまよって
たどり着いたのが下記でした
https://wiki.bedrock.dev/scripting/scripting-intro.html

Windows10で動くスクリプトということで試してみました。

``` js:server.js
const systemServer = server.registerSystem(0, 0)
systemServer.initialize = function () {

    // (中略)

    // ボタンを押したときのイベント登録
    this.listenForEvent('minecraft:block_interacted_with', (e) => this.onInteracted(e));
};

// ボタンを押したときの挙動
systemServer.onInteracted = function (e) {
    x = e.data.block_position.x,
    y = e.data.block_position.y,
    z = e.data.block_position.z

    // アイテムを付与するボタン
    if (x === 0 && y === 5 && z === 0)
    {
        this.initUser();
    }
    
    // ステージをリセットするボタン
    if (x === 0 && y === 5 && z === 1)
    {
        this.initGame();
    }
};

// 必要なアイテムの付与
const playerInitCommands = [
    // ゲームの進行に必要な配置可能なブロックの付与
    '/give @p skull 128 1 {"minecraft:can_place_on":{"blocks":["soul_sand"]}}',
    // 装備や消耗品の付与
    "/give @p netherite_sword 1",
    "/give @p bow 1",
    "/give @p enchanted_golden_apple 64",
    "/give @p netherite_helmet 1",
    "/give @p netherite_chestplate 1",
    "/give @p netherite_leggings 1",
    "/give @p netherite_boots 1",
    "/give @p arrow 64",
]

systemServer.initUser = function () {
    playerInitCommands.forEach(function(x) {
        systemServer.executeCommand(x, (e) => {});
    });
};

// ステージのリセット
const gameInitCommands = [
    "/fill 20 6 0 30 6 10 soul_sand",
    "/fill 20 7 0 30 7 10 air",
    "/fill 20 5 0 30 5 10 air",
]

systemServer.initGame = function () {
    // 出現条件をランダムにするために、特定のブロックの配置をランダムにする
    var x = Math.floor(Math.random() * 11) + 20;
    var z = Math.floor(Math.random() * 11);
    command = `/fill ${x} 5 ${z} ${x} 5 ${z} soul_sand`;

    gameInitCommands.forEach(function(x) {
        systemServer.executeCommand(x, (e) => {});
    });
    systemServer.executeCommand(command, (e) => {});
};

```
[Github](https://github.com/yamada-masa/PopupWither/blob/master/scripts/server/server.js)

普段JavaScriptを書かない私ですが、これならいろいろ実現できそうだという感触が得られました。
一番のデメリットはWindows10でしか動かないというところと感じました。
※サーバ側でスクリプトを動かせば、クライアントはモバイル端末でもよい模様です。

# 2. Functionsで実装
Functionsはゲーム内で使えるコマンドをひとまとめにしてまとめて実行する機能です。
これを使っても実装が可能ということが分かったので試してみました

## ステージのリセット
```
fill 4 5 13 -5 7 22 air
fill 4 6 13 -5 6 22 soul_sand
scoreboard objectives add x dummy
scoreboard players random @e[name=random] x 1 100

execute @e[name=random,scores={x=1}]  ~ ~ ~ fill -5 5 13 -5 5 13 soul_sand
execute @e[name=random,scores={x=2}]  ~ ~ ~ fill -5 5 14 -5 5 14 soul_sand
execute @e[name=random,scores={x=3}]  ~ ~ ~ fill -5 5 15 -5 5 15 soul_sand
execute @e[name=random,scores={x=4}]  ~ ~ ~ fill -5 5 16 -5 5 16 soul_sand
execute @e[name=random,scores={x=5}]  ~ ~ ~ fill -5 5 17 -5 5 17 soul_sand
execute @e[name=random,scores={x=6}]  ~ ~ ~ fill -5 5 18 -5 5 18 soul_sand
execute @e[name=random,scores={x=7}]  ~ ~ ~ fill -5 5 19 -5 5 19 soul_sand
execute @e[name=random,scores={x=8}]  ~ ~ ~ fill -5 5 20 -5 5 20 soul_sand
execute @e[name=random,scores={x=9}]  ~ ~ ~ fill -5 5 21 -5 5 21 soul_sand
execute @e[name=random,scores={x=10}] ~ ~ ~ fill -5 5 22 -5 5 22 soul_sand
execute @e[name=random,scores={x=11}] ~ ~ ~ fill -4 5 13 -4 5 13 soul_sand
execute @e[name=random,scores={x=12}] ~ ~ ~ fill -4 5 14 -4 5 14 soul_sand
execute @e[name=random,scores={x=13}] ~ ~ ~ fill -4 5 15 -4 5 15 soul_sand
execute @e[name=random,scores={x=14}] ~ ~ ~ fill -4 5 16 -4 5 16 soul_sand
execute @e[name=random,scores={x=15}] ~ ~ ~ fill -4 5 17 -4 5 17 soul_sand
execute @e[name=random,scores={x=16}] ~ ~ ~ fill -4 5 18 -4 5 18 soul_sand
execute @e[name=random,scores={x=17}] ~ ~ ~ fill -4 5 19 -4 5 19 soul_sand
execute @e[name=random,scores={x=18}] ~ ~ ~ fill -4 5 20 -4 5 20 soul_sand
execute @e[name=random,scores={x=19}] ~ ~ ~ fill -4 5 21 -4 5 21 soul_sand
execute @e[name=random,scores={x=20}] ~ ~ ~ fill -4 5 22 -4 5 22 soul_sand
execute @e[name=random,scores={x=21}] ~ ~ ~ fill -3 5 13 -3 5 13 soul_sand
execute @e[name=random,scores={x=22}] ~ ~ ~ fill -3 5 14 -3 5 14 soul_sand
execute @e[name=random,scores={x=23}] ~ ~ ~ fill -3 5 15 -3 5 15 soul_sand
execute @e[name=random,scores={x=24}] ~ ~ ~ fill -3 5 16 -3 5 16 soul_sand
execute @e[name=random,scores={x=25}] ~ ~ ~ fill -3 5 17 -3 5 17 soul_sand
execute @e[name=random,scores={x=26}] ~ ~ ~ fill -3 5 18 -3 5 18 soul_sand
execute @e[name=random,scores={x=27}] ~ ~ ~ fill -3 5 19 -3 5 19 soul_sand
execute @e[name=random,scores={x=28}] ~ ~ ~ fill -3 5 20 -3 5 20 soul_sand
execute @e[name=random,scores={x=29}] ~ ~ ~ fill -3 5 21 -3 5 21 soul_sand
execute @e[name=random,scores={x=30}] ~ ~ ~ fill -3 5 22 -3 5 22 soul_sand
execute @e[name=random,scores={x=31}] ~ ~ ~ fill -1 5 13 -1 5 13 soul_sand
execute @e[name=random,scores={x=32}] ~ ~ ~ fill -1 5 14 -1 5 14 soul_sand
execute @e[name=random,scores={x=33}] ~ ~ ~ fill -1 5 15 -1 5 15 soul_sand
execute @e[name=random,scores={x=34}] ~ ~ ~ fill -1 5 16 -1 5 16 soul_sand
execute @e[name=random,scores={x=35}] ~ ~ ~ fill -1 5 17 -1 5 17 soul_sand
execute @e[name=random,scores={x=36}] ~ ~ ~ fill -1 5 18 -1 5 18 soul_sand
execute @e[name=random,scores={x=37}] ~ ~ ~ fill -1 5 19 -1 5 19 soul_sand
execute @e[name=random,scores={x=38}] ~ ~ ~ fill -1 5 20 -1 5 20 soul_sand
execute @e[name=random,scores={x=39}] ~ ~ ~ fill -1 5 21 -1 5 21 soul_sand
execute @e[name=random,scores={x=40}] ~ ~ ~ fill -1 5 22 -1 5 22 soul_sand
execute @e[name=random,scores={x=41}] ~ ~ ~ fill 0 5 13 0 5 13 soul_sand
execute @e[name=random,scores={x=42}] ~ ~ ~ fill 0 5 14 0 5 14 soul_sand
execute @e[name=random,scores={x=43}] ~ ~ ~ fill 0 5 15 0 5 15 soul_sand
execute @e[name=random,scores={x=44}] ~ ~ ~ fill 0 5 16 0 5 16 soul_sand
execute @e[name=random,scores={x=45}] ~ ~ ~ fill 0 5 17 0 5 17 soul_sand
execute @e[name=random,scores={x=46}] ~ ~ ~ fill 0 5 18 0 5 18 soul_sand
execute @e[name=random,scores={x=47}] ~ ~ ~ fill 0 5 19 0 5 19 soul_sand
execute @e[name=random,scores={x=48}] ~ ~ ~ fill 0 5 20 0 5 20 soul_sand
execute @e[name=random,scores={x=49}] ~ ~ ~ fill 0 5 21 0 5 21 soul_sand
execute @e[name=random,scores={x=40}] ~ ~ ~ fill 0 5 22 0 5 22 soul_sand
execute @e[name=random,scores={x=51}] ~ ~ ~ fill 1 5 13 1 5 13 soul_sand
execute @e[name=random,scores={x=52}] ~ ~ ~ fill 1 5 14 1 5 14 soul_sand
execute @e[name=random,scores={x=53}] ~ ~ ~ fill 1 5 15 1 5 15 soul_sand
execute @e[name=random,scores={x=54}] ~ ~ ~ fill 1 5 16 1 5 16 soul_sand
execute @e[name=random,scores={x=55}] ~ ~ ~ fill 1 5 17 1 5 17 soul_sand
execute @e[name=random,scores={x=56}] ~ ~ ~ fill 1 5 18 1 5 18 soul_sand
execute @e[name=random,scores={x=57}] ~ ~ ~ fill 1 5 19 1 5 19 soul_sand
execute @e[name=random,scores={x=58}] ~ ~ ~ fill 1 5 20 1 5 20 soul_sand
execute @e[name=random,scores={x=59}] ~ ~ ~ fill 1 5 21 1 5 21 soul_sand
execute @e[name=random,scores={x=60}] ~ ~ ~ fill 1 5 22 1 5 22 soul_sand
execute @e[name=random,scores={x=61}] ~ ~ ~ fill 2 5 13 2 5 13 soul_sand
execute @e[name=random,scores={x=62}] ~ ~ ~ fill 2 5 14 2 5 14 soul_sand
execute @e[name=random,scores={x=63}] ~ ~ ~ fill 2 5 15 2 5 15 soul_sand
execute @e[name=random,scores={x=64}] ~ ~ ~ fill 2 5 16 2 5 16 soul_sand
execute @e[name=random,scores={x=65}] ~ ~ ~ fill 2 5 17 2 5 17 soul_sand
execute @e[name=random,scores={x=66}] ~ ~ ~ fill 2 5 18 2 5 18 soul_sand
execute @e[name=random,scores={x=67}] ~ ~ ~ fill 2 5 19 2 5 19 soul_sand
execute @e[name=random,scores={x=68}] ~ ~ ~ fill 2 5 20 2 5 20 soul_sand
execute @e[name=random,scores={x=69}] ~ ~ ~ fill 2 5 21 2 5 21 soul_sand
execute @e[name=random,scores={x=70}] ~ ~ ~ fill 2 5 22 2 5 22 soul_sand
execute @e[name=random,scores={x=71}] ~ ~ ~ fill 3 5 13 3 5 13 soul_sand
execute @e[name=random,scores={x=72}] ~ ~ ~ fill 3 5 14 3 5 14 soul_sand
execute @e[name=random,scores={x=73}] ~ ~ ~ fill 3 5 15 3 5 15 soul_sand
execute @e[name=random,scores={x=74}] ~ ~ ~ fill 3 5 16 3 5 16 soul_sand
execute @e[name=random,scores={x=75}] ~ ~ ~ fill 3 5 17 3 5 17 soul_sand
execute @e[name=random,scores={x=76}] ~ ~ ~ fill 3 5 18 3 5 18 soul_sand
execute @e[name=random,scores={x=77}] ~ ~ ~ fill 3 5 19 3 5 19 soul_sand
execute @e[name=random,scores={x=78}] ~ ~ ~ fill 3 5 20 3 5 20 soul_sand
execute @e[name=random,scores={x=79}] ~ ~ ~ fill 3 5 21 3 5 21 soul_sand
execute @e[name=random,scores={x=80}] ~ ~ ~ fill 3 5 22 3 5 22 soul_sand
execute @e[name=random,scores={x=81}] ~ ~ ~ fill 4 5 13 4 5 13 soul_sand
execute @e[name=random,scores={x=82}] ~ ~ ~ fill 4 5 14 4 5 14 soul_sand
execute @e[name=random,scores={x=83}] ~ ~ ~ fill 4 5 15 4 5 15 soul_sand
execute @e[name=random,scores={x=84}] ~ ~ ~ fill 4 5 16 4 5 16 soul_sand
execute @e[name=random,scores={x=85}] ~ ~ ~ fill 4 5 17 4 5 17 soul_sand
execute @e[name=random,scores={x=86}] ~ ~ ~ fill 4 5 18 4 5 18 soul_sand
execute @e[name=random,scores={x=87}] ~ ~ ~ fill 4 5 19 4 5 19 soul_sand
execute @e[name=random,scores={x=88}] ~ ~ ~ fill 4 5 20 4 5 20 soul_sand
execute @e[name=random,scores={x=89}] ~ ~ ~ fill 4 5 21 4 5 21 soul_sand
execute @e[name=random,scores={x=90}] ~ ~ ~ fill 4 5 22 4 5 22 soul_sand
execute @e[name=random,scores={x=91}]  ~ ~ ~ fill -2 5 13 -2 5 13 soul_sand
execute @e[name=random,scores={x=92}]  ~ ~ ~ fill -2 5 14 -2 5 14 soul_sand
execute @e[name=random,scores={x=93}]  ~ ~ ~ fill -2 5 15 -2 5 15 soul_sand
execute @e[name=random,scores={x=94}]  ~ ~ ~ fill -2 5 16 -2 5 16 soul_sand
execute @e[name=random,scores={x=95}]  ~ ~ ~ fill -2 5 17 -2 5 17 soul_sand
execute @e[name=random,scores={x=96}]  ~ ~ ~ fill -2 5 18 -2 5 18 soul_sand
execute @e[name=random,scores={x=97}]  ~ ~ ~ fill -2 5 19 -2 5 19 soul_sand
execute @e[name=random,scores={x=98}]  ~ ~ ~ fill -2 5 20 -2 5 20 soul_sand
execute @e[name=random,scores={x=99}]  ~ ~ ~ fill -2 5 21 -2 5 21 soul_sand
execute @e[name=random,scores={x=100}] ~ ~ ~ fill -2 5 22 -2 5 22 soul_sand
```
[Github](https://github.com/yamada-masa/PopupWither2/blob/master/functions/stage_reset.mcfunction)

「出現条件をランダムにするために、特定のブロックの配置をランダムにする」
を実現するために記述がかなり増えてしまいました。
変数の扱いに制限があって気楽にプログラミングするという感じではなかったです。

## 必要なアイテムの付与
```
give @a[c=1] skull 128 1 {"minecraft:can_place_on":{"blocks":["soul_sand"]}}
give @a[c=1] netherite_sword 1
give @a[c=1] bow 1
give @a[c=1] enchanted_golden_apple 64
give @a[c=1] netherite_helmet 1
give @a[c=1] netherite_chestplate 1
give @a[c=1] netherite_leggings 1
give @a[c=1] netherite_boots 1
give @a[c=1] arrow 64
```
[Github](https://github.com/yamada-masa/PopupWither2/blob/master/functions/user_init.mcfunction)

こちらはほぼほぼJavaScriptと変わらない記述量でした。
ターゲットをどうするかということろが少しほかの方法で実装したときと異なる感じとなりました。

# 3. WebSocket Serverで実装(言語はPython)
普段Pythonを使って業務しているので何とかPythonで自由に書けないかといろいろ調べてみました。
WebSocketサーバを作ってクライアントからつなぐことによってプログラミングできることが分かったので
WebSocketサーバをPythonで書いてみることにしました。

下記の記事を参考にしてみました
http://www.s-anand.net/blog/programming-minecraft-with-websockets/

``` Python:main.py
import asyncio
import json
import random
from uuid import uuid4

import websockets

EVENTS = [
    "PlayerMessage",
]

async def mineproxy(websocket, path):
    print("Connected")

    msg = {
        "header": {
            "version": 1,
            "requestId": "",
            "messageType": "commandRequest",
            "messagePurpose": "subscribe",
        },
        "body": {"eventName": "PlayerMessage"},
    }
    await websocket.send(json.dumps(msg))

    async def send(cmd):
        await websocket.send(
            json.dumps(
                {
                    "header": {
                        "version": 1,
                        "requestId": str(uuid4()),
                        "messagePurpose": "commandRequest",
                        "messageType": "commandRequest",
                    },
                    "body": {
                        "version": 1,
                        "commandLine": cmd,
                        "origin": {"type": "player"},
                    },
                }
            )
        )

    async def stage_reset():

        # 一定の範囲に特定のブロックを配置する
        await send("/fill -5 5 13 4 7 22 air")
        await send("/fill -5 6 13 4 6 22 soul_sand")

        # 出現条件をランダムにするために、特定のブロックの配置をランダムにする
        x = random.randint(1, 10)
        z = random.randint(1, 10)        
        await send(f"/setblock {-5 + x} 5 {13 + z} soul_sand")

    async def user_init(name: str):
        cmds = [
            # ゲームの進行に必要な配置可能なブロックの付与
            '/give {} skull 128 1 {"minecraft:can_place_on":{"blocks":["soul_sand"]}}',
            # 装備や消耗品の付与
            "/give {} netherite_sword 1",
            "/give {} bow 1",
            "/give {} enchanted_golden_apple 64",
            "/give {} netherite_helmet 1",
            "/give {} netherite_chestplate 1",
            "/give {} netherite_leggings 1",
            "/give {} netherite_boots 1",
            "/give {} arrow 64",
        ]

        for cmd in cmds:
            await send(cmd.format(name))

    try:
        async for msg in websocket:
            print(msg)
            msg = json.loads(msg)
            if msg["body"].get("eventName", "") == "PlayerMessage":
                text = msg["body"]["properties"]["Message"]
                if text == "stage-reset":
                    await stage_reset()
                elif text == "user-init":
                    player_name = msg["body"]["properties"].get("Sender")
                    await user_init(player_name)

    except websockets.ConnectionClosedError:
        print("Disconnected")


start_server = websockets.serve(mineproxy, port=19131)
print("/connect localhost:19131")
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
```

実装してみたところ直接的にゲーム内のボタンに割り当てることが難しかったので
コマンドとして実装する形に落ち着きました。
WebSocketを制御する部分を作り込む必要があるのでコード量はその分増えましたが
かなり自由度が高いと感じました。

# 4. MakeCodeで実装(Python)
最後に試したのが[MakeCode](https://minecraft.makecode.com/)でした。
こちらは教育目的と思って後回しにしていたのですが、
教育目的だけあってハードルの低さが際立ちました。

バックエンド仕組みとしては3. WebSocketを使っています。
(むしろこちらのためにある機能のようです)

プログラミングの方法としてブロックプログラミング、JavaScript、Pythonを選択可能です。
私はもちろんPythonを選択しました。

``` Python:main.py
def stage_reset():
    # 一定の範囲に特定のブロックを配置する
    blocks.fill(AIR, world(-5, 5, 13), world(4, 7, 22))
    blocks.fill(SOUL_SAND, world(-5, 6, 13), world(4, 6, 22))
    
    # 出現条件をランダムにするために、特定のブロックの配置をランダムにする
    x = randint(1, 10) -5
    z = randint(1, 10) + 13
    blocks.place(SOUL_SAND, world(x, 5, z))

def user_init():
    # ゲームの進行に必要な配置可能なブロックの付与
    player.execute('give @s skull 128 1 {"minecraft:can_place_on":{"blocks":["soul_sand"]}}')
    # 装備や消耗品の付与
    player.execute("give @s netherite_sword 1")
    player.execute("give @s bow 1")
    player.execute("give @s netherite_helmet 1")
    player.execute("give @s netherite_chestplate 1")
    player.execute("give @s netherite_leggings 1")
    player.execute("give @s netherite_boots 1")
    player.execute("give @s arrow 64")

player.on_chat("stage-reset", stage_reset)
player.on_chat("user-init", user_init)
```
[Github](https://github.com/yamada-masa/PopupWither4/blob/master/main.py)

実装してみて感じたのはWebSocketの制御部分がすべて隠蔽されていて
マイクラを操作するための関数＋標準的なPythonの関数が準備されているため
やりたいことに対して直感的に実装できるということでした。

注意点としてMakeCodeを使ったプログラムを動かすためには
Minecraft Windows10版またはMinecraft Education Edition(iPadOS/macOS/ChromeOS/Windows10)が必要ということです。

Minecraft Windows10版ではCode Connection for Minecraftが必要になります。
これがWebSocketサーバとして動作するためほかのOSでは動かないということになります。

あくまで「プログラミングを学ぶ」用途なので「ゲーム作りをする」のには向かないといえます。

# 最後に
全部で4種類の方法でゲームを実装してみることを試してみましたが、
まだまだ簡単にできることしかできなかったので、来年は調べたことをもとに
もう少し凝ったものが作れるといいなと考えています。