チンパンジーでも作れるDiscordBot その2 〜パワーアップ編〜

第二回目です。第一回の環境構築編はこちらから。

前回は、discordrbを使ったDiscordBot開発に必要な環境の構築と、動作確認用の簡単なBotを作りました。
コマンドの作り方や、メッセージの送信方法、eventの中身についてはある程度理解してもらえたかなと思います。

今回は、前回書いた”Hello, (ユーザー名)!”と返す;;helloコマンドをさらに強化してみたいと思います。

じゃんけん機能とサーバー入退出監視はどこに行ったかって?

知らんな

;;helloコマンドをおさらい

まずは前回の;;helloコマンドの中身を軽くおさらい。

require "discordrb"

bot = Discordrb::Commands::CommandBot.new(token: "トークン", client_id: ID, prefix: ";;")

bot.command(:hello) do |event|
  username = event.user.name
  event.respond("Hello, #{username}!")
end

bot.run

細かいところは微妙に変わっていますが、とりあえず上記のソースコードがこれから行う;;helloコマンドの強化のベースになります。

まず1行目。requireと書いて使用するライブラリ(discordrb)を取り込んでいます

require "discordrb"

3行目、BotのトークンとIDからBotをinitializeし、bot変数に入れています。

bot = Discordrb::Commands::CommandBot.new(token: "トークン", client_id: ID, prefix: ";;")

5行目から8行目には、helloコマンドが実行された時の処理があります。
実行されたコマンドに関する各種情報(投稿者や投稿先チャンネルなど)を含む「event」から、「user」そしてその「name」を取り出し、「username」変数に格納。
その後に「username」変数を「”Hello, #{username}!”」という形式で文字列(String)に埋め込み、「event.respond」(event.message.channel.send_messageの省略版)でその文字列をコマンドが実行されたテキストチャンネルに送信しています。

bot.command(:hello) do |event|
  username = event.user.name
  event.respond("Hello, #{username}!")
end

最後に「bot.run」で、ここまでで設定してきたbotを実行する、といった流れです。
前回の記事ではちょっとわかりづらかったかもしれない箇所があったので、ここで改めて説明しました。
それではこれを踏まえた上で、;;helloコマンドに最後の改変を加えてみます。

いろんな言語でHello, ユーザー名!

今のhelloコマンドではコマンドを実行したユーザーの名前を返答メッセージに埋め込んでいますが、今度はコマンドを実行するたびに挨拶が変わるようにしてみましょう。
具体的には、”Hello, (ユーザー名)!”の「Hello」の部分をコマンドが実行されるたびに異なる様々な国の挨拶に変えてみたいと思います。

まずは「Hello」の部分にどんな言葉を入れるか、というのを定義する必要があります。
そのためには配列(Array)を使うのがスマートです。

配列(Array)

helloArray = ["Hello", "こんにちは", "你好", "안녕하세요", "Bonjour", "Hola", "Hallo", "Ciao"]

配列とは、大雑把に説明するとデータ(変数)の塊のようなもので、同一種類・同一グループの物をひとまとめにしてしまう事ができます。
単語帳で例えるとわかりやすいかもしれません。単語帳のように、情報を一本にまとめる事ができる配列。
実際の単語帳と違うのは、各ページに番号が振られているという点ですね。

そしてその振られている番号を「[]」で囲んで指定する事で、配列の中のどの情報が欲しいのかを指定して取り出す事ができます。
ここで注意しなくてはいけないのが、配列の要素に振られている番号は1ではなく0から始まっているという事。
なので例えば配列の1番目の要素を取ろうとすると、実際には1番目ではなくて2番目の要素を取り出していることになります。
1番目の要素を取り出すときは、1ではなく0番目と指定しなくてはいけません。

helloArray[1] #結果: "こんにちは"(2番目)
helloArray[0] #結果: "Hello"(1番目)

それでは今のhelloArrayを定義する文を「event.respond」よりも手前の所に追加しましょう。

bot.command(:hello) do |event|
  helloArray = ["Hello", "こんにちは", "你好", "안녕하세요", "Bonjour", "Hola", "Hallo", "Ciao"]
  username = event.user.name
  event.respond("Hello, #{username}!")
end

このままでは配列が定義されるだけされて一回も使われずにメッセージが送信されてしまいます。
送信する文字列に「username」を埋め込んだ時と同じ要領で「helloArray」の指定した番号の要素を埋め込んでみましょう。

event.respond("#{helloArray[0]}, #{username}!")

こうすることで「#{helloArray[0]}」の部分にhelloArrayの0番目の要素「”Hello”」が埋め込まれ、「Hello, ユーザー名!」という文字列が出来上がります。
ただ、これだと変更前と何も変わりがないのでhelloArrayからランダムな要素をbotに選ばせるようにします。
方法はいたってシンプルで、「helloArray」に.sampleと付けるだけ。これで「helloArray」からランダムなサンプルが選ばれます。

helloArray.sample

これを今のコードに応用すると、このような形になります。これで、helloArrayからランダムに選ばれた要素を送信内容に埋め込む事ができます。

event.respond("#{helloArray.sample}, #{username}!")

早速この状態でbotを動かしてみましょう。既にbotを起動している場合はCtrl+Cで終了させ、bot.rbのある場所まで「cd」コマンドで移動した上で、「ruby bot.rb」を実行してください。

いい感じですね。何回かコマンドを打つと若干偏りも出てきますが、まあこんなもんでしょう。

〇〇語で話してよBot君

これで;;helloコマンドは毎回違う言語で挨拶してくれるようになりましたが、せっかくなのでコマンドの実行時に挨拶する言語を指定させてみましょう。
この辺から少しだけややこしくなってくるので、まずは;;helloコマンドにどういった挙動をさせるのか、あらかじめ整理しておきます。

  • 「;;hello」コマンドのみを実行した場合: これまで通りランダムな言語の挨拶をする
  • 「;;hello ja」のようにパラメーターをつけてコマンドを実行した場合: ランダムではなく指定された言語で挨拶をする

と思いましたが改めてまとめるほどでもありませんでしたね・・まあいいでしょう。
要はhelloの後に半角スペースを空けてパラメーター(引数)を付けるとその言語で挨拶をさせたいわけです。
「;;hello ja」なら「こんにちは, ユーザー名!」、「;;hello en」なら「Hello, ユーザー名!」といった具合で。

まずはユーザー側が言語名を指定する事でBotが適切な挨拶を取得できるようにします。
先ほどの配列はあくまで中の要素を番号でしか識別する事ができないので、何か別の方法を使う必要がありますね。
そこで登場するのが連想配列(Hash)です。他の言語では辞書(Dictionary)と呼ばれる事もあります。

連想配列(Hash)

これも大雑把に言うと、Arrayの要素に振られている番号の部分が任意のオブジェクトに変わったようなものです。
この、Arrayでは番号だった部分は「キー」と呼ばれるもので、任意の「キー」に対応する要素をこれまでは取り出していました。
Hashでもそれは同じなのですが、番号の代わりにキーとして文字列(String)を使う事も出来ますし、シンボル(Symbol)と言うオブジェクトも扱う事ができます。

helloHash = { :en => "Hello", :ja => "こんにちは", :zh => "你好", :ko => "안녕하세요", :fr => "Bonjour", :es => "Hola", :de => "Hallo", :it => "Ciao" }
helloHash[:en] #結果: "Hello"
helloHash[:ja] #結果: "こんにちは"

オブジェクトとはその名の通り物体。コード上におけるデータの種類のようなものだと思っておけば大丈夫です。
これまで言ってきたString、Array、Hash、Symbolなどはすべてオブジェクトと呼ばれるものになります。

上記のコードの「:en」や「:ja」の部分がシンボル(Symbol)と呼ばれるオブジェクトです。
このSymbolは、簡単に言ってしまえばハッシュのキーなど、データの名前としての使用に向いた文字列です。

symbol1 = :Oreo

通常の文字列(String)とは何が違うのでしょうか。私自身も上手い説明ができなかったのでここから説明を少しだけ借りてくることにします。

まず、Symbolは文字列の皮を被った数値。コード上では文字列に見えますが、内部では数値として扱われます。
数値の方が文字列よりも処理が高速な為、ハッシュのキーなど「データの名前」として使い、データを取り出す様な使い方に向いています。

逆に、その文字列自体がデータである時にSymbolは向いていません。
その場合は代わりにStringを使います。Stringはこれまでも使ってきた様な変数の埋め込みや、正規表現を使った検索・置き換えにも対応している汎用性のある文字列です。

さらにStringは、たとえ中に入っている文字列が同じでも、厳密には微妙に違う、あくまで異なるオブジェクトとして扱われます。
Symbolは、名前が同じである限りそれは100%同じオブジェクトになります。
唯一無二でなければいけない「データの名前」などを参照する時にはSymbolが向いているというわけです。

Hashの話に戻りますが、以下のHashでは様々な言語の挨拶を言語名のSymbolで取り出せる様にしています。
これをhelloコマンドの処理の1行目に加えましょう。先ほど使った「helloArray」はもう要らなくなるので消しておいてください。

helloHash = { :en => "Hello", :ja => "こんにちは", :zh => "你好", :ko => "안녕하세요", :fr => "Bonjour", :es => "Hola", :de => "Hallo", :it => "Ciao" }
###############################
helloHash[:en] #結果: "Hello"
helloHash[:ja] #結果: "こんにちは"

言語名をコマンドから取得できる様にする

次に「;;hello ja」みたいにhelloの後についてくるパラメーター(引数)を取得できる様にします。
やり方は簡単で、「|」で囲まれていた「event」の後にカンマ区切りで新しく「language」と追記するだけです。

bot.command(:hello) do |event, language|
  # ;;hello jaと入力された場合
  language # 結果: "ja"
end

「language」の名前はなんでも構わないのですが、こうすることで「language」にはコマンドで指定された引数が一つ入ります。
なので;;hello jaと入れると、「ja」の部分が「language」に格納されるというわけです。

何も指定せずに;;helloだけが入力されると、「language」は「nil」と呼ばれるいわゆる「無」の状態になります。
「”」もついていないので文字列でもありません。正真正銘の「無」のオブジェクト、それがnilです。

# 「;;hello」と引数をつけなかった場合
language # 結果: nil

そして例えば「;;hello ja en」の様に引数(パラメーター)が2つ以上ある場合「language」には一つ目の引数の「ja」のみが格納されます。
二つ目を追加したい場合は「|event, language, language2|」の様にさらに引数を加える必要があります。
もしくは「|event, languages|」と先頭に「」をつけることで指定された全てのパラメーターが「languages」にArrayとして格納される様になります。

bot.command(:hello) do |event, *languages|
  # ;;hello ja enと入力された場合
  languages # 結果: ["ja", "en"]
end

引数が取得できる様になったところで、その引数に合わせて挨拶の部分が変わる様にします。

greeting = helloHash[language.to_sym]
event.respond("#{greeting}, #{username}!")

順番に見ていきましょう。
まず、コマンドで指定された言語が「language」にStringとして格納されているこの状態で、その言語の挨拶を「helloHash」から取得しないといけません。
しかし「helloHash」というHashのキーに使われているのはSymbolで、Stringではありません。
そこで「.to_sym」を使ってそのStringの文字列に対応したSymbolを新しく作ってしまいます。

# ;;hello jaと入力された場合
language.to_sym # 結果: :ja
language # 結果: "ja"

「to_sym」で作ったSymbolをキーとして使い、「helloHash」からその言語に対応する挨拶を持ってきて、「greeting」変数に代入します。
これで「greeting」変数には指定した言語の挨拶が入る様になります。

helloHash = { :en => "Hello", :ja => "こんにちは", :zh => "你好", :ko => "안녕하세요", :fr => "Bonjour", :es => "Hola", :de => "Hallo", :it => "Ciao" }
# ;;hello jaと入力された場合
greeting = helloHash[language.to_sym]
greeting # 結果: こんにちは

最後に「greeting」変数を送信する文字列に埋め込み「event.respond」で送信します。

event.respond("#{greeting}, #{username}!")

全体像はこんな感じになっているはずです。

bot.command(:hello) do |event, language|
  helloHash = { :en => "Hello", :ja => "こんにちは", :zh => "你好", :ko => "안녕하세요", :fr => "Bonjour", :es => "Hola", :de => "Hallo", :it => "Ciao" }
  username = event.user.name

  greeting = helloHash[language.to_sym]

  event.respond("#{greeting}, #{username}!")
end

それではbotを動かしてみましょう。もう動かし方の説明は要りませんよね。

いい感じです。しかし、これでは最初に整理した仕様を満たしていません。

  • 「;;hello」コマンドのみを実行した場合: これまで通りランダムな言語の挨拶をする
  • 「;;hello ja」のようにパラメーターをつけてコマンドを実行した場合: ランダムではなく指定された言語で挨拶をする

このリストの一つ目「コマンドのみを実行した場合」の処理が何も書かれていないため、今のコードを修正していきます。
この部分を…

greeting = helloHash[language.to_sym]

こうします。

symbolArray = [:en, :ja, :zh, :ko, :fr, :es, :de, :it]
greeting = if language == nil
             helloHash[symbolArray.sample]
           else
             helloHash[language.to_sym]
           end

出ましたif分岐。
「if 条件文」と書き、その条件文の結果がtrue(真)である場合はその中にある処理を行います。
また、「elsif 条件文」と書くことで、最初の「if 条件文」以降も条件文を使って分岐させられる様になります。
最後は必ず「else」を入れてどのパターンにも該当しなかった場合の処理が必要です。
そして「end」で閉じます。

if oreo == cookie
  # オレオはクッキー
elsif oreo == noir
  # オレオはノアール
else
  # オレオは何者でもない
end

条件文のところにはいろんな条件を書くことができます。
基本的には「比較演算子」を使って2つの値を比較することが多いです。

比較演算子 説明
== a == b aとbが等しい
!= a != b aとbが等しくない
< a < b aがbよりも小さい
> a > b aがbよりも大きい
<= a <= b aがbよりも小さいか等しい
>= a >= b aがbよりも大きいか等しい

比較演算子には実はもう少しあるのですが、主要なものはこんなもんでしょう。
今回は割愛しますが他にも色々と条件文に入るものはあります。

今回のケースではコマンドで指定された言語名「language」がnil(無)であった場合に「greeting」変数をランダムな挨拶にしたいので、こういう風に書いています。

symbolArray = [:en, :ja, :zh, :ko, :fr, :es, :de, :it]
greeting = if language == nil
             helloHash[symbolArray.sample]
           else
             helloHash[language.to_sym]
           end

まず「helloHash」のキーとして使える様に「symbolArray」と言う各言語名のSymbolの配列(Array)を作ります。
次にもし「language」がnil(無)の場合、「symbolArray」からランダムに選ばれた要素を「helloHash」のキーとして使って挨拶を取得、「greeting」変数に代入。
もし「language」がnilじゃない場合は、指定されている「language」のSymbolをキーにして挨拶を取得、「greeting」変数に代入しています。

ちなみにですが「greeting =」に続いてif分岐をつける書き方はあまり多くの言語ではされません。
Rubyだからできる様な変化球の書き方です。

これでbotを実行すると、言語名を指定していない時はランダムな言語で挨拶をし、言語が指定されているときだけその言語で挨拶する様になります。

ここでまた問題が。
この状態だと、もし「helloHash」に存在しない言語名が指定された時に、メッセージから挨拶だけが欠けてしまいます。

指定された言語名の挨拶が存在しない場合にもランダムな挨拶をさせてしまいましょう。
「if language == nil」の後に、「||」で区切って二つ目の条件文を入れます。

greeting = if language == nil || helloHash[language.to_sym] == nil
             helloHash[symbolArray.sample]
           else
             helloHash[language.to_sym]
           end

ここでの「||」はORの意味を持ちます。つまり、この場合は
language が nil もしくは helloHash[language.to_sym] が nil
と言う条件文になります。

指定された言語名の挨拶が存在しない場合、という条件が加わることになりますね。
「||」の部分は「論理演算子」と言い、「||」はどちらか1つの条件が「真」であればtrueを返しますが、
他にも「&&」など両方の条件が「真」の場合のみtrueを返すものがあります。

それではこの状態でもう一度だけbotを動かします。

完璧ですね。これが新しいhelloコマンドの全体像になります。

bot.command(:hello) do |event, language|
  symbolArray = [:en, :ja, :zh, :ko, :fr, :es, :de, :it]
  helloHash = { :en => "Hello", :ja => "こんにちは", :zh => "你好", :ko => "안녕하세요", :fr => "Bonjour", :es => "Hola", :de => "Hallo", :it => "Ciao" }
  username = event.user.name

  greeting = if language == nil || helloHash[language.to_sym] == nil
               helloHash[symbolArray.sample]
             else
               helloHash[language.to_sym]
             end

  event.respond("#{greeting}, #{username}!")
end

続く

今回はここまでです。
これで、;;helloがただ単に決められたメッセージを返すコマンドではなく、配列や連想配列、条件分岐を使って多様なメッセージが返せるコマンドにパワーアップしました。
流石にこれ以上;;helloコマンドを引っ張っていくのもアレなので、いい加減;;helloコマンドは終わりにして、次回はコマンド以外のbotの機能を使ってみようと思います。

おわり。