Top View


Author yoshino

Alexaでポ○モン風ゲームをつくった話

2019/12/06

スキル名

アレモンスキル

Alexa Monstarを略して、アレモンにしました。

完成品

途中エラーが出る箇所がありますが、なんとかジムリーダーに挑戦することができました。

使用技術

  • Alexa Developer Console
  • Node.js

各会話でやってること

LaunchRequestHandler

最初にスキルを立ち上げた際に、発火するHandlerです。

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    async handle(handlerInput) {
        // DynamoDBから永続化データを取得
        const data = await handlerInput.attributesManager.getPersistentAttributes()
        let speakOutput

     // ユーザー名が取得できた時の処理
        if (data.username) {
            let alemonText = ''
            data.alemons.forEach((alemon) => {
                alemonText += alemon.name + ','
            })
            speakOutput = `
                ${data.username}、おかえりなさい!
                ${data.username}が持っているアレモンは
                ${alemonText}だよ。
                ゲームを再開する時は、再開すると言ってね。
                ゲームを最初からやり直す時は、やり直すと言ってね。
            `
        } else {
            speakOutput = `
                アレモンスキルへようこそ。このスキルではアレクサモンスターを育てて、バトルすることができるよ。
                新しい<prosody pitch="low">ア</prosody>レモンをゲットして、ジムリーダーに挑戦しよう!
                アレモン達のHPがたくさん残っている人が勝ちだよ。
                まずは君の名前を教えてね
                `
            repromptOutput = `君の名前を教えてね`
        }
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};

ここでは、最初にDynamoDBにあるデータを取得しています。 ユーザー名が保存されている場合は、「おかえりなさい」と返答してくれ、既に所持しているモンスターを列挙してくれます。

ユーザー名が保存されておらず、新規プレイの場合は、名前を聞きます。 名前を答えると、その名前が保存されるHandlerが発火するようになっています。

SearchAlemonIntentHandler

次に、アクションとして「アレモンを探す」を選択した場合の処理になります。

const SearchAlemonIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'SearchAlemonIntent';
    },
    async handle(handlerInput) {
        const data = await handlerInput.attributesManager.getPersistentAttributes()
        const nameArray = [
            'チュウ', 'ダイル', 'キング', 'ックス', 'ゴン', 'ライク'
        ]
        const level = Math.ceil(Math.random()*10)
        const enemy = {
            level: level,
            name: 'アレ' + nameArray[Math.floor(Math.random() * nameArray.length)],
            hp: 8 * level,
            attack: Math.ceil(Math.random() * level * 2)
        }
        const speakOutput = `
            野生の${enemy.name}が現れた!レベルは<say-as interpret-as="cardinal">${enemy.level}</say-as>だ!
            どのアクションを実行する?
            たたかう<break time="1s"/>
            モンスターボールを使う<break time="1s"/>
            逃げる
        `;

        const attributes = {
            username: data.username,
            alemons: data.alemons,
            items: data.items,
            enemy: enemy
        }

        handlerInput.attributesManager.setPersistentAttributes(attributes); // セット
        await handlerInput.attributesManager.savePersistentAttributes(); // 保存

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
}

敵の名前は、'アレチュウ', 'アレダイル', 'アレキング', 'アレックス', 'アレゴン', 'アレライク'の中からランダムで決まります。

レベルも1~10までのレベルからランダムに決まり、レベルが高いほどHPと攻撃力が高くなるように、出現するモンスターを定義しています。

この際にDynamoDBに敵のモンスターを保存します。

敵が出現すると、

  • たたかう
  • モンスターボールを投げる
  • 逃げる

の三択から、次のアクションを選ぶように、促します。

BattleIntentHandler

最後に、アレモンに遭遇した際の処理です。

const BattleIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'BattleIntent';
    },
    async handle(handlerInput) {
        const action = handlerInput.requestEnvelope.request.intent.slots.Battle.resolutions.resolutionsPerAuthority[0].values[0].value.name
        const data = await handlerInput.attributesManager.getPersistentAttributes()
        let lastEnemy = null

        // ジムリーダー戦の場合
        if (data.enemy.name === 'アレリュー') {
            lastEnemy = data.enemy
        }

        let speakOutput = ''
        switch (action) {
            case 'たたかう':
                speakOutput = await tatakauAction(handlerInput, lastEnemy)
                break;
            case 'モンスターボールを使う':
                if (lastEnemy) {
                    speakOutput = 'たたかうか逃げるを選んでね'
                } else {
                    speakOutput = await ballAction(handlerInput)
                }
                break;
            case '逃げる':
                speakOutput = await runAction(handlerInput, lastEnemy)
                break;
        }
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
}
const action = handlerInput.requestEnvelope.request.intent.slots.Battle.resolutions.resolutionsPerAuthority[0].values[0].value.name

ここでは、ユーザーが発したアクションの中から、スロットに登録してあるアクションを取得します。 スロットには

  • たたかう
  • モンスターボールを使う
  • 逃げる

の三種類が登録されています。

tatakauActionの中では、所持するアレモンのレベルに応じて、相手のモンスターにダメージを与え、 相手のモンスターが生きていれば、反撃される、という処理を書いています。

処理が長くなってしまうので、ここでは割愛します。

作ってみて、つまづいたとこ・うまくいってないところ

人の名前問題

これに一番悩んでて、まだ解決してません、、、笑

最初にユーザーの名前を聞く箇所があるのですが、そこのSlotにAmazon.FirstNameを使用しています。

ただ、このFirstNameの種類が少ないのか、ほとんどの人の名前をうまく認識してくれません。。

ここの解決策、もしわかる方いれば是非コメントください!

ゲーム性をもたせることの難しさ

初めてAlexaでゲームを作ったのですが、そもそもゲームを作る上で、ちょうどいい難易度に調整することが大変でした。

ただ、少しずつ調整して、ちょうどいい難易度にしていくのは楽しいですね。

音声UI設計の難しさ

Alexaという端末の問題から、パラメーターの記憶が難しいことも一つ音声UIの問題かなと思いました。 今回は対策として、「ステータスを確認」と言えば、いつでも持っているモンスターのリストがわかるように対応しました。

作る上で助かったもの

attributesManagerが便利

Alexa SDK ver.2を初めてしっかり使ったのですが、DynamoDBの値を管理する上でattributeManagerはとても便利でした。

以前はlambdaから直接DynamoのAPIを叩いていたのですが、それよりもコードがシンプルになって、書きやすかったです。

Alexa道場

実装の上でわからなかった部分は、Alexa道場の動画を参考にさせてもらいました。

畠中さんのわかりやすい説明、とても助かりました。ありがとうございました。

まとめ

以上、Alexaでポ○モン風ゲームをつくった話でした。

Alexaの開発環境、どんどん進化していってるなと感じました。

是非興味ある方、作ってみてください!

yoshino

yoshino

Twitter X

Fusicエンジニア。Webアプリケーションやブロックチェーンプログラミングをしています。PHP, Vue.js, Solidity etc..