BCDiceのAPIをDiscordのBotに組み込む

## 1\.はじめに
「Discordはテキストチャットもボイスチャットもできるのにダイスロールができない!!」
「DiscordだけでもTRPGができるようにしたい!!」
そうおもったこと、一度はありますよね?

今回はその問題をDiscord.jsというAPIとココフォリアなどに使われるBCDiceというAPIを使用して、DiscordのBotでダイスを振れるように初心者の解説をつけながら話したいと思います。


※前提知識として「Discord.js でBotを作れる」ということが含まれます。(いつか記事にします)
さらにココフォリアなどのオンセツールで[ダイスの振り方](https://docs.bcdice.org/)を知っていると良いですね。


- ### 今回作りたい機能(目標)
1\.チャットに1D4などを打つだけでダイスを振れるようにする
2\.システムに対応したダイスも振れるようにする
- ### 環境
| 名前 | ver |
| ---- | ---- |
| MacOS | 14.0 Beta |
| Node.js | 18.16.0 |
| Discord.js | 14.11.0 |
| BCDice | 4.3.0 |

## 2\.BCDiceAPIの軽い説明
まず、[bcdicce-js](https://github.com/bcdice/bcdice-js)をインストールします
<details><summary>インストールコマンド</summary>



```sh
$ npm install --save bcdice
```
</details>
nodeで使える形にしてください。

ためしに一つ動かしてみましょう
```js:bcdice_test.js
const { DynamicLoader } = require('bcdice');

async function diceroll(roll) {
  const loader = new DynamicLoader();
  const GameSystem = await loader.dynamicLoad('DiceBot');
  const result = GameSystem.eval(roll);
  console.log(result);
}

// 1D100 を振る。(ダイスロールを入れる)
diceroll("1D100")
```
結果↓
```
$ node bcdice_test.js 
{
  '$$id': 178,
  text: '(1D100) ＞ 86',
  rands: [ [ 86, 100 ] ],
  secret: false,
  success: false,
  failure: false,
  critical: false,
  fumble: false,
  detailedRands: [ { kind: 'normal', sides: 100, value: 86 } ]
}
```
このようなものが返ってくるかと思います。

わかりやすく判定だけを切り出すには7行目を
```js
console.log(result.text);
```
にすれば結果が
```
(1D100) ＞ 89
```
のようになるかと思います。

また、5行目の
```js 
const GameSystem = await loader.dynamicLoad('DiceBot')
```
は`'DiceBot'`の部分を好きなシステムに指定することができます。
DiceBotはその名の通り、ダイスを振ることができるだけです。
~~そもそもダイスを振るためのものなんですけど~~

ちなみに関数の中に
```js
console.log(GameSystem.HELP_MESSAGE)
```
でシステムに応じたダイスボットの説明、

```js
console.log(GameSystem.COMMAND_PATTERN)
```
でダイスロールの正規表現、
```js
console.log(GameSystem.ID)
```
や
```js
console.log(GameSystem.NAME)
```
は言うまでもなく、
```js
console.log(GameSystem.SORT_KEY)
```
はゲームシステムの読みが分かります。

## 3\.チャットのダイスロールを返す
まずチャットにダイスロールと思われるものが送信されているかを検知するために正規表現で指定します。
BCDiceには先ほど書いた、ダイスロールの正規表現を書き出す方法があります。
なのでまず
```js
const { DynamicLoader } = require('bcdice');

async function diceroll(roll) {
  const loader = new DynamicLoader();
  const GameSystem = await loader.dynamicLoad('DiceBot');
  console.log(GameSystem.COMMAND_PATTERN);
}

diceroll()
```
このコードを実行することで次のような結果になります
```
/^S?([+\-(]*(\d+|D\d+)|\d+B\d+|\d+T[YZ]\d+|C[+\-(]*\d+|choice|D66|(repeat|rep|x)\d+|\d+R\d+|\d+U\d+|BCDiceVersion)/i
```
これが`DiceBot`の検知するための正規表現です。
チャットにこの正規表現に引っかかるものをダイスロールすればいいわけですから、コードは以下のようなものになります。(discordAPI等略)
```js
const { DynamicLoader } = require('bcdice');

async function diceroll(system, roll) {
  const loader = new DynamicLoader();
  const GameSystem = await loader.dynamicLoad(system);
  const result = GameSystem.eval(roll);
  return result
}

client.on('messageCreate', async message => {
  if (message.author.bot) return
  if (message.content.match(/^S?([+\-(]*(\d+|D\d+)|\d+B\d+|\d+T[YZ]\d+|C[+\-(]*\d+|choice|D66|(repeat|rep|x)\d+|\d+R\d+|\d+U\d+|BCDiceVersion)/i)) {
    var rollResult = await diceroll('DiceBot', message.content);
    try {
      message.channel.send(rollResult.text)
    } catch {
      return //この際、正規表現がしっかりしているためエラーはスルー
    }
  }
});
```
<details><summary>しくみをわかりやすくすると</summary>

(3~8行目)`diceroll`という関数を作る
↓
(11行目)先ほど出した正規表現に合うメッセージが送られたかを検知する
↓
(12行目)`diceroll`にメッセージの内容を入れて、結果を`rollResult`に入れる
↓
(14行目)`rollResult`のダイスロールの結果部分だけをメッセージが送られたチャンネルに返す

</details>

このコードを動かしてみます...
<br>
![うごいたねがぞうどっとぴーえぬじー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520866/bf713820-dee1-54b9-ff92-b73a0cab01b0.png)
動きましたね。
![うごいたねそのにどっとぴーえぬじー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520866/5106f458-64d0-0807-e3b0-e86196fe548c.png)
しっかり判定も動作しているようです。

これで作りたい機能の「1\.チャットに1D4などを打つだけでダイスを振れるようにする」はできましたね!

## 4\.いろんなシステムに対応させる
今回は「クトゥルフ神話TRPG」「新クトゥルフ神話TRPG」を対応させたいと思います。
完成系の感じとしては
「coc ccb<=50」
のように、最初に宣言することでシステムを切り替えれるようにします。

まずダイスのAPIのIDを調べましょう。
コードを書いて調べるという方法もあるのですが、[BCDice様のドキュメント](https://bcdice.org/systems/)があるのでそちらを見ることを推奨します。
みたところIDは
クトゥルフ神話 : Cthulhu
新クトゥルフ神話 : Cthulhu7th
となっているらしいので、指定する言葉は「coc」と「coc7」にしましょう。

```js
const { DynamicLoader } = require('bcdice');

async function diceroll(system, roll) {
  const loader = new DynamicLoader();
  const GameSystem = await loader.dynamicLoad(system);
  const result = GameSystem.eval(roll);
  return result
}

client.on('messageCreate', async message => {
  if (message.author.bot) return;
  if (message.content.match(/^(coc|coc7) /i)) {
    if (message.content.match(/^coc /i)) {
      var rollResult = await diceroll("Cthulhu", message.content.slice(4));
      try {
        message.channel.send(rollResult.text)
      }catch {
        return
      }
    }
    if (message.content.match(/^coc7 /i)) {
      var rollResult = await diceroll("Cthulhu7th", message.content.slice(5));
      try {
        message.channel.send(rollResult.text)
      }catch {
        return
      }
    }
  }
});

```
仕組みは先ほどのコードとほとんど同じです。
変わっているのはif分(条件分岐)が増えたのと、ダイスのAPIがそれぞれ指定されているところですね。

動かしてみます
<br>
![うごいたぁ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520866/b613289e-5b00-dc08-4ab0-3ba7b51409c0.png)
しっかり判定も動いているようです。

これで作りたい機能の「2\.システムに対応したダイスも振れるようにする」もできましたね!

## 5\.両方くっつける。(最終的なコード)
どうせなら二つの機能をまとめましょうか。
特に説明することはないのでコードだけ貼ります。
(DiscordAPI関係も書いているので環境とBotがあれば動くはずです)
```js:main.js
const { DynamicLoader, Version } = require('bcdice');
const { Client, GatewayIntentBits, Guild} = require('discord.js');
const client = new Client(
  { intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent
    ]
  }
);


async function diceroll(system, roll) {
  const loader = new DynamicLoader();
  const GameSystem = await loader.dynamicLoad(system);
  const result = GameSystem.eval(roll);
  return result
}

client.on('messageCreate', async message => {
  if (message.author.bot) return;
  if (message.content.match(/^(coc|coc7) /i)) {
    if (message.content.match(/^coc /i)) {
      var rollResult = await diceroll("Cthulhu", message.content.slice(4));
      try {
        message.channel.send(rollResult.text)
      }catch {
        return
      }
    }
    if (message.content.match(/^coc7 /i)) {
      var rollResult = await diceroll("Cthulhu7th", message.content.slice(5));
      try {
        message.channel.send(rollResult.text)
      }catch {
        return
      }
    }
    return
  }
  if (message.content.match(/^S?([+\-(]*(\d+|D\d+)|\d+B\d+|\d+T[YZ]\d+|C[+\-(]*\d+|choice|D66|(repeat|rep|x)\d+|\d+R\d+|\d+U\d+|BCDiceVersion)/i)) {
    var rollResult = await diceroll("DiceBot", message.content);
    try {
      message.channel.send(rollResult.text)
    }catch {
      return
    }
  }
});

// BOTのTOKENを入れてください
client.login("<YOUR BOT TOKEN>");
```

<br>

実行結果・・・

![最終進化](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520866/df1e119a-f4b3-cd12-8c37-630f73094db4.png)
しっかり動きましたね!

## 6\.おわりに
初心者ながらに頑張って書いてみたコードですので遠回りな処理をしていたりするところが多々あるかもしれませんが、温かい目で見てくれると嬉しいです。

おそらくこれからも似たようなことをやっていくのでアドバイスなどがあればぜひ教えてください...ッ!

初めてのQiitaの記事なので慣れない箇所しかないのですが最後まで見てくださりありがとうございます。