はじめてのVUIアプリ開発をやってみた
ポケモン剣盾アドベントカレンダーの17日目です。
ゆるWeb勉強会@札幌 Advent Calendar 2019の17日目です。はやいですね。アドベントカレンダーもラストスパートですね。
「tacckさん(主催)と愉快な仲間たち」のひとり(?)、かんちゃんです。VRの記事を書いたり、VUIアプリ開発したり、来週までにはIoTの記事も書く予定なんだけど、たぶんWeb界隈の人間です。
tacckさんのオススメでFire HD 10を買いましたが、しばらく寝かせていました(反省)
ゆるWebなのにAlexaのスキル開発を書くのは少し変かもしれませんが、スクレイピングもあるので許そうね(Alexaスキル開発のアドベントカレンダーがあったような)
AlexaってiOSでもAndroidでも動作するんだって! すごいですね!(知らなかったんかい)
何を作ったか
ガラル地方のポケモンのタイプ相性を教えてくれるAlexaのスキル(VUIアプリ)です。
ポケモンのタイプ相性を教えてくれるAlexaアプリ
— かんたくろーす@えぃぃr (@sapporo_east_k) December 14, 2019
朝まで改良した。良くなってきた。
AlexaはiPhoneでもAndroidでも使えるのね!#ポケットモンスター剣盾#ソードシールド pic.twitter.com/k6bGIiRDiw
下記画像はYouTubeのリンクです。押下すると動画が再生されます
動画② (イワークには、かくとうタイプの攻撃が2倍になることがわかりますね)
ポケモンを知らない人のための簡単な解説
※「ほのお」タイプ、「みず」タイプ、「くさ」タイプのポケモンという生き物がいて
- 「ほのお」は「くさ」に強くて「みず」に弱い
- 「みず」は「ほのお」に強くて「くさ」に弱い
- 「くさ」は「みず」に強くて「ほのお」に弱い
つまりジャンケンみたいな仕組みがあります(WindowsのPaintで作図w)
常に有利にバトルを進めるために、有利なタイプで勝負したいわけです。
しかし問題があります。
- 現在はタイプが「18種類」もあって、覚えられない
- 「2つのタイプ」を持つポケモンが多くて、何タイプが有利に働くか分かりづらい
- 今回発売した「剣盾」は新しいポケモンも多くて、何タイプが有利なのか分からないことが多い
- ググっても良いが、毎回ポケモンの名前でググると先頭に出てくるサイトが異なったり、タイプについて載ってないサイトが出てきたりする
- ポケモンの世界で「旅してる」感を味わっているのに、スマホ画面を見ることでぶち壊してしまう(個人的な意見)
……そこで!このアプリが役立つわけです。
機能
①ポケモンの名前だけを言うと、そのポケモンに対して「有利なタイプ」「不利なタイプ」を教えてくれる。
②ポケモンの名前+弱点を教えて等を言うと、そのポケモンに対して「有利なタイプ」を教えてくれる。
③ポケモンの名前+耐性を教えて等を言うと、そのポケモンに対して「不利なタイプ」を教えてくれる。
④ポケモンの名前+タイプを言うと、そのポケモンとそのタイプの相性を教えてくれる。
⑤異なるフォルムが存在する場合は警告を出してくれる(ロトム、ニャース等)
実装
データを集める
スクレイピングで簡単に集めました。
このサイトでコンソールを開いて下記JSを実行してURLを取得します。
document.querySelectorAll("#hide-1101_1 > table > tbody > tr > td > div > a").forEach(a => console.log(a.href));
URLをURL.txt
として保存し、そのファイルを下記のソースで使います。
下記はスクレイピングのソースですが、あまり良い書き方ではないです。
JSONをきれいに出力するためのライブラリとか使うべきでした(反省)
from bs4 import BeautifulSoup
from time import sleep
import urllib.request as req
# タイプ名のリスト
TYPE_LIST = ['ノーマル','ほのお','みず','でんき','くさ','こおり','かくとう','どく','じめん','ひこう','エスパー','むし','いわ','ゴースト','ドラゴン','あく','はがね','フェアリー']
# JSONを出力する変数
text = '{'
# URL.txtを開く
with open('URL.txt') as textFile:
# 1行ずつURLを取得
for url in textFile:
# スクレイピングはサーバ負荷をかけてはいけないので
# 必ず1秒間は待機する
sleep(1)
# 目的の要素を取得する
res = req.urlopen(url)
soup = BeautifulSoup(res , 'html.parser')
# タイプ表
typeInfo = soup.select('tr > td > span')
# 種族名
name = soup.find('span', class_='text-l text-bold')
if name != None:
# "種族名" : [{
text += '"{name}":[{{'.format(name=name.string)
for index,key in enumerate(TYPE_LIST):
value = typeInfo[index].string
if value != "1.0": # 等倍のデータは不要
# "タイプ名":"倍率",
text += '"{key}":"{value}",'.format(key=key,value=value)
# }],
text = text.rstrip(',') + '}],'
# ファイルに書き込む
with open('TYPE.json', mode='a') as f:
f.write(text)
print(text)
# 変数初期化
text = ""
# 全ポケの処理が終わったら
# 波括弧を閉じてファイルに書き込む
text += "}"
with open('TYPE.json', mode='a') as f:
f.write(text)
これで必要なJSONができましたね。(※実際には多少手入力で編集しています。機能⑤を実装するためです)
環境構築
Alexa developer consoleに行きます(Amazonのアカウントでログインする必要があります)
「スキルの作成」からハローワールドのスキルを作ります。
僕はPythonのほうが慣れているので、Pythonで書きます。AWS Lambdaのホストを自動でやってくれてイケイケですね。
↓ スキル作成ボタンを押下
選択ボタンを押下で作成されます。最初に出てくる「ビルド画面」でHelloWorldIntent
があることを確認してください。
画像で黄色く塗っているHelloWorldIntent
を押下すると下記の画面が出ると思います。
これらのワードをAlexaが受け取ると、処理が走ることを覚えておいてください。
次に「コードエディタ画面」を開きます。170行近いコードが既に書かれていますね。
speak_output
変数はAlexaが喋るセリフが入るところです。
LaunchRequestHandler
クラスでは起動時にAlexaが行う処理が入っています。
HelloWorldIntentHandler
クラスを見てください。
speak_output
変数にはHello World!
が入っていますよね。
つまりHelloWorldIntentのワードを受け取ると、Hello World!とアレクサが言ってくれるわけです。
設定したインテント(ワード)とハンドラーを紐づけるためには
-
can_handle
の戻り値のis_intent_name
の引数をインテントの名前と合わせる - 160行目以降にあるsb.add_request_handlerでHandlerを引数として渡す
ことが必要になります。どっちかを怠ると、Alexaはちゃんと動作しません。
試しに「HelloWorldIntent」を動作させてみましょう。
「コードエディタ画面」から「デプロイ」ボタンをタッチし、テスト画面を開きます。
「スキルテストが有効になっているステージ」を「開発中」にして、下のテキストボックスにスキル名を入力すると、スキルが起動します。helloと打てば返事してくれますね。
無事動作すれば環境構築は完了です。
タイプの相性を教えてもらうためには?
モデルの作成
自分は下記の4つのインテントを作成していますが、実装はほぼほぼ同じです。メイン機能であるCharactorIntentだけ紹介します。
このインテントはポケモンの名前を受け取っています。{pokemon}スロットを用意する必要があります。
インテントの一覧の下の方にスロットタイプがあり、ここから作成できます。
スロット値はポケモンの名前が入ってます。最初に作成したJSONのKeyをCSV形式としてインポートしています。
CharactorIntentの画面下部のインテントスロットから先ほど作成したスロットをスロットタイプに設定します。
これでポケモンの名前を言われたら、受け取れるAlexaになりました。
ビルド画面で編集をした後は「モデルを保存し、モデルのビルドをする」べきです。
コードの作成
コードエディタ画面左側の階層に最初に取得して(少し編集した)JSONを入れています。右クリックでファイル作成が出来ます。
コードの追加です。
import logging
import ask_sdk_core.utils as ask_utils
import json
import ast
from collections import OrderedDict
import pprint
from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_core.utils import is_intent_name, get_slot_value, is_request_type
from ask_sdk_model import Response
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_model.ui import SimpleCard
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class CharactorTypeIntentHandler(AbstractRequestHandler):
def can_handle(self, handler_input):
return ask_utils.is_intent_name("CharactorTypeIntent")(handler_input)
def handle(self, handler_input):
pokemon = get_slot_value(handler_input=handler_input, slot_name="pokemon")
model = get_slot_value(handler_input=handler_input, slot_name="type").replace('タイプ', '')
garal = ["バリヤード","ポニータ","ギャロップ","デスマス","マタドガス","カモネギ","マッギョ","ニャース","ジグザグマ","マッスグマ","サニーゴ","ダルマッカ"]
warningText = ""
if pokemon == "ヒヒダルマ":
warningText = "警告。ヒヒダルマのデータをお伝えしますが、フォルム等が違うとタイプも異なります。ダルマヒヒダルマ、ガラルヒヒダルマ、ガラルダルマヒヒダルマの項目も確認してみてください。"
elif pokemon == "ロトム":
warningText = "警告。ロトムのデータをお伝えしますが、フォルム等が違うとタイプも異なります。ヒートロトム、ウォッシュロトム、フロストロトム、スピンロトム、カットロトムの項目も確認してみてください。"
elif pokemon == "ザシアン":
warningText = "警告。ザシアンのデータをお伝えしますが、フォルム等が違うとタイプも異なります。剣の王ザシアンの項目も確認してみてください。"
elif pokemon == "ザマゼンタ":
warningText = "警告。ザマゼンタのデータをお伝えしますが、フォルム等が違うとタイプも異なります。盾の王ザマゼンタの項目も確認してみてください。"
elif pokemon in garal:
warningText = "警告。{0}はガラルフォルムがあります。ガラル{0}の項目も確認してみてください。".format(pokemon)
jsonData = None
with open('type.json') as f:
jsonData = json.load(f)
text = ""
for key in jsonData[pokemon]:
text += '{0}'.format(key)
dic = ast.literal_eval(text)
text = ""
for key, value in dic.items():
if str(key) == model :
text += '{0}タイプは{1}倍です。'.format(model, str(value))
if text == "":
text += '{0}タイプは1.0倍です。'.format(model)
speak_output = pokemon +"ですね。" + text
handler_input.response_builder.speak(warningText + speak_output).ask("").set_card(SimpleCard(pokemon, speak_output)).set_should_end_session(False)
return handler_input.response_builder.response
一番下の二行はとても重要です。speak
の引数はAlexaがしゃべる内容、set_card
の引数は画面付きの端末の場合に文字等を表示させることができるものです。
他の設定もいろいろありますが、公式ドキュメントを漁ってみてください。
handler_input.response_builder.speak(warningText + speak_output).ask("").set_card(SimpleCard(pokemon, speak_output)).set_should_end_session(False)
return handler_input.response_builder.response
再度、デプロイすればテストができると思います。
他にも「弱点だけ返す」「耐性だけ返す」「特定のタイプとの相性だけを返す」機能を実装したり、いろんな言い回しに対応できるように実装しています。これでバトルを常に有利に進めることができますね!
ワイ「ピカ〇ュウの弱点は?」
アレクサ「ピカチュ〇の弱点ですね。じめんタイプは2.0倍です」
ワイ「ネンドール、だいちのちから」
ピ〇チュウ「キェェェェェェェェェェェェェェェェェェェェェェェェェェェェェェェ!!!!!!」
※こんな鳴き声ではありません。アニメ版と同じ声優さんがやってくれてます。
今後の課題
ワイ「アレクサ、ロトムの弱点は?」
アレクサ「ロトムの弱点ですね。じめんタイプは2.0倍です」
ワイ「ネンドール、だいちのちから!」
ロトム「とくせい ふゆうでロトムには効かない」
ワイ「キェェェェェェェェェェェェェェェェェェェェェェェェェェェェェェェ!!!!!!」
**※こんな鳴き声です。**タイプだけで特性が考慮されていないのが課題です。
あとは全国版をつくったり、OK, Google版もつくったりするかもしれません(でも明日からはIoTのことを考えなきゃ…)
最後に
この「(非公式)ガラル図鑑」スキルは現在リリースに向けて審査中です。
ベータテストはできますが、ボクにメアドバレします。捨てアドでやってみたい方はどうぞ!
(12/16追記)審査ダメでしたw
商標的に通りませんね。どうしてこうなったんでしょうねぇ (分かってました)
フィードバックは**「商標の時点で明らかにアウトなのに」**ちゃんとして頂きました。
しかも、何パターンもテストされていたようです。ダグトリオとかラフレシアで…中の人、ありがとうございます。クソみたいな仕事をお願いしてしまってすみません(泣)
でも、この開発方法は汎用性が高くて他の方法にも使えると思います!無駄ではなかった!(と思いたい)
意外とAlexaのスキル開発に関する記事などは多くないので、この記事が1ミリでも役立つと嬉しいです。今回は静的なJSONファイルでしたが、変動するデータなら検索してデータを取ってくることになりそうですね。2020年はチキンにならずにNode.jsに挑戦しよう……。