Discord上で動くBBTAG辞書Bot作成話(前編)(Python[discord.py, beautifulsoup4, PyDrive] + GitHub + Heroku)
今回の記事はBBTAGアドベントカレンダー2020の企画によるものです(まだまだ参加お待ちしております)。かなり技術寄りの話になるためBBTAGに深く関わる内容ではありませんが、軽い気持ちで見ていただければと思います。前編ではこんな感じでBotは動いているよ、という説明にする予定です。後編では今回様々なサービスやライブラリを用いる中で躓いた点やより複雑な話を行います。後編に関しては興味のある方だけお読みくださいませ。
できること
Bot名は「BBTAG Dictionary」です。そのまんまですね。データベースとしての機能を持たせるため、主要なコマンドとして次のものがあります。
intro
各キャラクターのBBTAG Character Overviewの文章を表示します。日本語訳頑張ったのはここのためだったりする。
data
キャラクターの体力などの基本情報を表示します。個人的にざっと確認したい情報が載っています。
word
格闘ゲームやBBTAG特有の単語を解説します。イースターエッグ的な単語も入れていたり…。
frame
フレームデータを表示します。DustLoopWikiの情報を基にデータベースを構築しています。Discordの埋め込み表示にテーブルがあれば良かったのですが…(見逃している可能性はある)
analyze
この2つの記事をベースに「キャラの環境における立ち位置」を表示します。
もし自サーバーへの導入に興味がありましたらTwitterで言って頂ければ招待URLを送信しますのでお気軽にどうぞ。
作成のきっかけ
最初にDiscordのBotを作ろうと思ったきっかけがリアフレと行うTRPGのオンラインセッションでした。折角だしサイコロ振るBotがあれば楽だよね、という話が出ていたわけです。サイコロBot自体はネットの海を探せば見つかるとは思うのですがこの機会に自分で作ってみるか、となったのが最初のきっかけです。
色々なサイトの力を借りながら無事サイコロBotは完成しました。ですがBotの作成経験がこれっきりで終わってしまうのはもったいないな、ということでサイコロBotの発展形として「何かコマンドを打って、それに返答する」タイプのデータベース的な辞書Botを作ってみようと思い立ちました。BBTAGという今ハマっているゲームがあり、フレームなどデータとなる情報が転がっているわけですから、じゃあBBTAGの辞書Botでも作成してみるか、となったわけです。
というわけで実際どのようにBotを作成していったのかを説明していきます。…あれ、ここから先の話ってBBTAGとほぼ関係なくね?と思ったあなた、正解です。それでも読んでいただけるという方は次へお進みください。
まずはBotの外側を作ろう
Botの作成をする上で、次の3つの記事に非常に助けられました。Botを作ってみたい方は一読をお勧めします。
Botの外側を準備するだけなら意外と簡単です。Discordのアカウントを持っている方なら誰でも作ることができます。
まずはここにアクセス。ログインすると開発者向けのサイトが開きます。
次に「New Application」をクリックして、Botの名前を入力。「Create」を押せばOKです。
左のメニューの「Bot」をクリックして、そのページにある「Add Bot」をクリック。注意書きが出てきますが、気にせず「Yes」をクリックすると、Botが作成されます。これでBotの外側は完成です。簡単でしたね。余裕のある方はアイコンなどを設定してあげましょう。
ではBotをサーバーに入れましょう。左メニューの「OAuth2」をクリックします。「OAuth2 URL Generator」の所の「SCOPE」から「Bot」をクリック。そうすると招待用のURLが発行されますので、そのURLを用いてサーバーに招待します。これで準備はOKです!中身(プログラム)を入れる作業に移ります。
…とその前に。左の「Bot」メニューから「TOKEN」を取得(『COPY』でクリップボードにコピーされます)してください。トークンと聞くと聞きなれないですが、このBotにアクセスするための認証コードだと思ってください。つまり外に公開する=家の鍵をそこら中にばらまいていることと同義です。大変なことになりますので注意しましょう。
Botに中身を入れよう
ではBotに命を吹き込むためにプログラミングを行います。設計自体は慣れが要りますが、決まり事に従いながらパズルのように組み上げていくのがプログラミングです(少なくとも個人規模であれば)。今回はPythonを利用しました。
ここから先は先ほど紹介した記事「Discord Bot 最速チュートリアル【Python&Heroku&GitHub】」に沿うことになるため(実際にこの記事に従えばBotのひな型はできます)、その先で必要になるプログラミングを紹介したいと思います。
コマンドの追加
ボットを動かすためにはコマンドが必要です。Discord.pyはこのコマンド追加が非常に簡単にできるように設計されています。ありがたく使いましょう。
@bot.command()
async def wiki(ctx):
await ctx.send('日本語版Wiki:https://seesaawiki.jp/bbtag/')
例えば「!wiki」(ここの ! はプレフィックスといい、Botに対して以下に続く文字列はコマンドですよ、ということを伝えるための接頭辞です)というコマンドを作成する場合はこのように書きます。@bot.commandで宣言を行い、async def [コマンド名](ctx):で関数(実行する処理をひとまとまりにしたものと考えて大丈夫です)を宣言、テキストで返答したい場合はawait ctx.send(文字列)を入れてあげればOKです。これだけで「!wiki」と打って送信すると日本語版BBTAGWikiのリンクが返ってくるコマンドが作れました。「何かコマンドを打って、それに返答する」形になりましたね。では次にコマンドに引数(パラメータ)を与えてみましょう。…とその前に。
データを準備しよう
今回はCSVファイル(カンマ区切りのファイル)を利用してプログラム上でデータベースを作成することにしました。SQLとかは(私が分からないので)無し!Pythonには(他一般的な言語にもありますが)「辞書」というデータの形があります。連想配列ともいわれる形式なのですが、簡単に言うと「事前に〈キー,中身〉のセットで保存しておき、キーを入力すると中身が出力される」データ形式のことです。今回はこれを利用します。〈ラグナ、体力・最速通常技〉〈悠、イントロダクション〉という形でデータを準備すれば、キャラの名前を指定してあげることでデータを引き出せるわけです。というわけで今回は次のような形でデータを準備しました。
まず〈ラグナ,RG〉〈ルビー,RR〉のように名前をキーとなるコードネームに変換します(これも辞書の形ですね)。こうすることで最終的にキーを合わせればよいので、特殊な呼び方・あだ名にも後々対応できるという算段です。次にコードネームから目的のデータを引っ張り出します。〈RG,体力17000〉みたいな形です。フレームデータのみ技名の指定が必要ですが、もう一段階この操作が必要(〈ジン,JI〉〈JI,4A〉〈4A,フレームデータ〉)なだけで基本的な構造は同じです。CSVデータに関しては、例えばdataコマンドで使用するCSVの一部は次のようになっています。
code,name,health,fastest_ground,ra_ground,clash_assault,overhead,overhead_frame,commandgrab,commandgrab_frame,antiair,antiair_frame,antiair_inv,role
RG,ラグナ,17000,4A-5F,10,22,214B,18(前方への判定発生は22),ー,ー,5B,11,4~11,Point
JI,ジン,17000,4A-6F,13,22,J214C,23,ー,ー,2B,14,8~16,Support
NO,ノエル,17000,2A-7F,10,22,ー,ー,214A,29(立ち状態のみ),4A,8,6~8,Point
RA,レイチェル,17000,5A-8F,16,22,JA,13,ー,ー,4A,13,5~12(ガードポイント),Flex
TG,テイガー,20000,214A.214C-6F,16,22,ー,ー,214A.214C,6,2B,13,9~12,Support
HK,ハクメン,18000,2A-8F,1,22,J214C,20,ー,ー,2B,13,9~15,Point
NU,ニュー,16000,5A-9F,20,22,JC,21,ー,ー,4A,11,5~11,Point
HZ,ハザマ,17000,236BC-5F,10,22,236B,30,214C,15,2B,13,8~15,Point
MK,マコト,17000,4A-5F,9,26,ー,ー,ー,ー,5B,11,5~14,Point
PL,プラチナ,17000,4A-6F,11,22,JC,19,ー,ー,5B,12,6~15,Point
IZ,イザヨイ,17000,4A-6F,14,22,66>JA,19,ー,ー,214B,10,4~12,Point
AZ,アズラエル,18000,4A-6F,18,26,236C,24,ー,ー,2B,13,7~16,Point
横列にキャラごとのデータが並んでいるのでそれをひとまとめにしてキーと結びつけています。
引数を与えて辞書らしく
ではコマンド作成へ戻りましょう。関数に引数を与えるためには、次のように書きます。
@bot.command()
async def intro(ctx, *args):
if len(args) == 0:
await ctx.send('引数にキャラクターの名前を入力してください。')
return
namepr = namelist.get(args[0])
if namepr == None:
await ctx.send('存在しないキャラクター、またはキャラクターの名前が間違っています。')
return
msg = introlist.get(namepr)
embed=discord.Embed(color=0xff2e2e)
embed.add_field(name=args[0], value=msg, inline=False)
embed.set_footer(text="引用元:BBTAG Character Overview")
await ctx.send(embed=embed)
一気に記述が増えましたが、ゆっくり追っていきましょう。これはIntroコマンドの関数になります。引数はctxの後に追加することで記述可能になります。*argsと書くと(厳密は*を付けると)複数の引数を与えることができます。最初の2つのif文による条件分岐は例外処理(引数を与えなかった場合だったり、キャラクターの名前を間違って入力しているなどに対して処理を行っています)なのでそれを省きます。すると、
@bot.command()
async def intro(ctx, *args):
namepr = namelist.get(args[0])
msg = introlist.get(namepr)
embed=discord.Embed(color=0xff2e2e)
embed.add_field(name=args[0], value=msg, inline=False)
embed.set_footer(text="引用元:BBTAG Character Overview")
await ctx.send(embed=embed)
こうなります。namepr = namelist.get(args[0])でnamelistという辞書形式のデータから情報を得ようとしていることが分かるでしょうか。これでキーとなるコードネームを引き出します。次のmsg = introlist.get(namepr)で内容を引き出しているわけですね。じゃあ最後のctx.sendじゃない文字列はなんだ!となりますが、これはDiscordの埋め込み機能を用いて、表示をきれいにするコマンド群です。基本的にやっていることはctx.sendと同じく内容を送信しているだけです。
これでより辞書らしいことができるようになりました。「!intro ラグナ」とすれば内部でどう動いているか、何となくわかってきたのではないでしょうか。「そもそもnamelistとかの辞書はどうやって作るのか」に関しては今回は割愛します。なんとなく中の動きが分かってもらえれば良いので…。興味がある方は「Python 辞書」あたりで調べてみてくださいね。
===========================
以上で前半は終わりです。予想以上に濃い話になってしまった…。そしてBBTAGとの関係の無さがすごい。後編ではフレームデータを手に入れるためのスクレイピング、Googleスプレッドシートを用いたデータ更新方法などに触れる予定です。