ElixirでDiscordのbotを作る

ElixirでDiscordのbot作るの楽しい～って思ったので書きました。
Elixir自体のインストールは[こちら](<https://elixir-lang.org/install.html>)からどうぞ。
Discord botアカウントの作成等は[こちらの記事](<https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf>)が参考になります。

# 作るもの

- `!dice 2d6`と送信するとダイスを振ってくれる
- 結果は`[(,区切りの各ダイス値)] 合計:(ダイスの合計値)`のような形式で返ってくる

こんな感じのダイスロールbotを作ります。

# 作る

```
mix new discord_bot --sup
```

で新しいプロジェクトを作成します。
`discord_bot`の部分は適宜変更してください。

## ライブラリを導入する

今回は[discord_alchemy](<https://hex.pm/packages/discord_alchemy>)を使用します。
Discord APIのラッパーライブラリです。

`mix.exs`を開き、depsを以下のように変更してください。

```elixir:mix.exs
defp deps do
  [
    {:alchemy, "~> 0.6.1", hex: :discord_alchemy}
  ]
end
```

変更後、

```
mix deps.get
```

を実行することで、パッケージが取り込まれます。

## pingされたら返事する

とりあえずbot界のHello, world!(？)を作りましょう。

最初に、`config.exs`にbotのトークンを書きます。

```elixir:config/config.exs
config :discord_bot,
  discord_token: "bot token"
```

次に、botの処理を書きます。
`lib/discord_bot/application.ex`を書き換えます。

```elixir:lib/discord_bot/application.ex
defmodule DiscordBot.Application do
  use Application

  defmodule Commands do
    use Alchemy.Cogs

    Cogs.def ping do
      Cogs.say "pong!"
    end
  end

  def start(_, _) do
    token = Application.get_env(:discord_bot, :discord_token)
    run = Alchemy.Client.start(token)
    use Commands
    run
  end
end
```

これで`!ping`を送信したら`pong!`と返ってくるbotが完成しました！

```
mix run --no-halt
```

を実行することで、botが動作します。

## ダイスを振る

さて、ついにダイスを振るコマンドを実装していきます。

`Commands`モジュールで

```elixir
Cogs.def コマンド名(引数) do
  # 処理
end
```

とすることでコマンドを定義できます。

まずは`6面ダイスを2個振った結果を返すコマンド`を`Commands`内に定義してみます。

```elixir
Cogs.def dice do
  dices = 1..2 |> Enum.map(fn _ -> 1..6 |> Enum.random end)
  result = dices |> Enum.sum
  Cogs.say("[#{dices |> Enum.join(",")}] 合計:#{result}")
end
```

![2019-05-25_025646.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142757/2ccede8c-dbf1-ed8e-3708-fc2b9bc33d4c.png)

できました！
やっていることは単純で、リスト`[1, 2]`のそれぞれにリスト`[1, 2, ..., 6]`からランダムに選ばれた値を突っ込んでるだけです。

Elixirのパイプライン演算子`|>`は、左の式の結果を右の式の第一引数に渡すものです。とてもべんりです。

## m面ダイスをn個振る

最後に、`!dice ndm`の形式でダイスの個数、面数を指定できるようにします。
ついでに

- 個数は1～100
- 面数は1～1000

という制限もつけます。

`Cogs.def コマンド名(引数)`の`引数`にはコマンド後の文字列が入ってくるので、それを利用してダイスの個数と面数を取得します。

後は、固定値だった`2`と`6`を`個数`と`面数`に置き換えるだけです。

```elixir
Cogs.def dice(roll) do
  case Regex.run(~r/\A(\d|[1-9]\d+)d(\d|[1-9]\d+)\z/, roll) do
    [_, n, m] ->
      num = n |> String.to_integer
      face = m |> String.to_integer

      cond do
        num <= 0 || num > 100 ->
          Cogs.say("個数は1以上、100以下にしてください")
        face <= 0 || face > 1000 ->
          Cogs.say("面数は1以上、1000以下にしてください")
        true ->
          dices = 1..num |> Enum.map(fn _ -> 1..face |> Enum.random end)
          result = dices |> Enum.sum
          Cogs.say("[#{dices |> Enum.join(",")}] 合計:#{result}")
      end
    _ ->
      Cogs.say("ndmの形式で入力してください")
  end
end
```

![2019-05-25_065253.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142757/cba1fd24-e984-7d47-dbbe-d1e85c3c89af.png)

これでダイスロールbotのできあがりです！

引数なしの`!dice`コマンドを残しておくと、`!dice`した時は2d6を振り、`!dice ndm`した時はm面ダイスをn個振ってくれるのでべんりかもしれません。

# さいごに

Elixirたのしい～～～！！！
文章を書くのは苦手なのでとてもつかれた。

# おまけ

完成した`lib/discord_bot/application.ex`の全文です。

```elixir:lib/discord_bot/application.ex
defmodule DiscordBot.Application do
  use Application

  defmodule Commands do
    use Alchemy.Cogs

    Cogs.def dice do
      dices = 1..2 |> Enum.map(fn _ -> 1..6 |> Enum.random end)
      result = dices |> Enum.sum
      Cogs.say("[#{dices |> Enum.join(",")}] 合計:#{result}")
    end

    Cogs.def dice(roll) do
      case Regex.run(~r/\A(\d|[1-9]\d+)d(\d|[1-9]\d+)\z/, roll) do
        [_, n, m] ->
          num = n |> String.to_integer
          face = m |> String.to_integer

          cond do
            num <= 0 || num > 100 ->
              Cogs.say("個数は1以上、100以下にしてください")
            face <= 0 || face > 1000 ->
              Cogs.say("面数は1以上、1000以下にしてください")
            true ->
              dices = 1..num |> Enum.map(fn _ -> 1..face |> Enum.random end)
              result = dices |> Enum.sum
              Cogs.say("[#{dices |> Enum.join(",")}] 合計:#{result}")
          end
        _ ->
          Cogs.say("ndmの形式で入力してください")
      end
    end
  end

  def start(_, _) do
    token = Application.get_env(:discord_bot, :discord_token)
    run = Alchemy.Client.start(token)
    use Commands
    run
  end
end
```