Google プログラミング

【JavaScript】DiscordでCoCTRPG用のダイスボットを作った

2022年2月23日

実はだいぶ前から完成していて運用しているのですが、CoCTRPG(コールオブクトゥルフ)のダイスボットを作成しました。旧版CoCですが技術自体は新版やその他TRPGでも利用できます。
今回は備忘録としてダイスボットの作り方を書いていきます。

元ネタ

https://note.com/exteoi/n/nf1c37cb26c41

いきなり丸投げですが、上記のサイトを見ればBot自体は完成します。実際私も上記のサイトを参考にしています。

JavaScriptを書く

ダイスボットの仕組みとなるプログラムをJavaScriptで書きます。上のサイトに従えばほぼほぼ理解できるとは思います。
ただ、Glitchの画面でプログラミングするのはきついのでVisual Studio Codeというエディタを使用します。このエディタなら快適にプログラミングが可能です。

ダイスボットで必要な仕組みはもちろん、ダイスを振ることです。1d10とかね。そこで「1d10」というリプライをダイスボットにしたら、そのユーザーにロールの結果をリプライするようにしましょう。
正確には「数値d数値」です。正規表現を使えばこの条件に合うリプライであることを絞り込めます。

client.on("message", (message) => {
    if (message.isMemberMentioned(client.user) && message.author != client.user) { {

上の解説サイトでもあるようにこのIF文の中で条件をさらに絞って書いていきます。このIF文はダイスボットにメッセージがきたときに対応するIF文です。ユーザーがリプライした内容がいい加減な文字列ならば無視しても良いですが、ダイスロールならばちゃんと返さなければなりません。

if (
            message.content.match(
            /[^d\\d]*(([1-9]{0,1})d(100|20|12|10|8|6|4|3|2)((([x/])([1-9])){0,1}((\\+|-)(\\d{1,2})){0,1}){0,1}).*/
            )
) {

これはメッセージが特定の条件に合う時(.content.match)アクションを行うためのIF文です。これでユーザーが正しい書式でダイスボットにロールしてきたことが分かりましたね。

ここで少し雑談ですが、オンラインでのTRPGは進行に時間がかかりますよね。少しでも楽にしたい。そうだ、成功か失敗かを表示してくれたら楽なんじゃないか。3d10とかやった時に累計値を表示してくれたらわざわざ人間が計算しなくても良いんじゃないか。

きっとその方が良いですよね。それも仕組みとして取り組みましょう。

1d100で成功率80ならユーザーは「1d100 80」とダイスボットにリプライすれば良い。




成功率未指定と成功率指定のパターンを想定すると上の画像のような物になるはず。




期待するリプライは上の画像のような感じ。

if (message.content.match(/[^d\\d]*(([1-9]{0,1})d(100|20|12|10|8|6|4|3|2)((([x/])([1-9])){0,1}((\\+|-)(\\d{1,2})){0,1}){0,1}).*/)) {
            var getDisceString = message.content;
            var tmp = getDisceString.split(" ");

            if(tmp.length > 2){
                
                var prefix = tmp[1].split("d");
                var random = null;
                var tmpVa = [];

                for (var i = 0; i < prefix[0]; i++) { random = Math.floor(Math.random() * prefix[1] + 1); tmpVa.push(random); message.reply(tmpVa[i]); } let total = tmpVa.reduce((sum, element) => sum + element, 0);

                if(tmp[2] >= total){
                    if(tmpVa.length > 1) {
                        message.reply("累計: " + total + " 成功");
                    } else {
                        message.reply("成功");
                    }
                } else {
                    if(tmpVa.length > 1) {
                        message.reply("累計: " + total + " 失敗");
                    } else {
                        message.reply("失敗");
                    }
                }
            } else {
                var prefix = tmp[1].split("d");
                var random = null;
                var tmpVa = [];

                for (var i = 0; i < prefix[0]; i++) { random = Math.floor(Math.random() * prefix[1] + 1); tmpVa.push(random); message.reply(tmpVa[i]); } if (tmpVa.length > 1) {
                    let total = tmpVa.reduce((sum, element) => sum + element, 0);
                    message.reply("累計: " + total);
                }
            }

そしてこれが実際のコードです。

var getDisceString = message.content;
var tmp = getDisceString.split(" ");

これで受け取った文字列(ダイスボットが受け取ったリプライ)を配列にしています。
@ダイスボット 1d100 80
この場合半角スペースで区切って要素数が三つの配列になります。
成功率が指定されている場合とされていない場合で配列の要素数が異なりますのでまずは.lengthで配列の要素数を取得して、取得した要素数によって処理を別けます。

「@ダイスボット」の部分は使用しません。なのでダイスの数を取得したいときはインデックス1(二つ目の要素)を指定します。

for (var i = 0; i < prefix[0]; i++) {
                    random = Math.floor(Math.random() * prefix[1] + 1);
                    tmpVa.push(random);
                    message.reply(tmpVa[i]);
}

このfor文ではダイスの数だけユーザーにランダムな値をリプライしています。
ランダムな値と言っても1d100の100を超えないように条件づけています。

let total = tmpVa.reduce((sum, element) => sum + element, 0);

if(tmp[2] >= total){
    if(tmpVa.length > 1) {
        message.reply("累計: " + total + " 成功");
    } else {
        message.reply("成功");
    }
} else {
    if(tmpVa.length > 1) {
        message.reply("累計: " + total + " 失敗");
    } else {
        message.reply("失敗");
    }
}

そして成功可否をリプライしますが、ダイスが2つ以上の場合は累計も付与します。

// Response for Uptime Robot
const http = require("http");
http
    .createServer(function (request, response) {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Discord bot is active now \n");
    })
    .listen(3000);

// Discord bot implements
const discord = require("discord.js");
const client = new discord.Client();

client.on("ready", (message) => {
    client.user.setPresence({ game: { name: "CoCTRPG" } });

    console.log("bot is ready!");
});

client.on("message", (message) => {
    if (message.isMemberMentioned(client.user) && message.author != client.user) {
        if (message.content.match(/help/)) {
            message.reply(
            "[1d10]でダイスコール。[一時的狂気]で一時的狂気の提示、不定の狂気 で不定の狂気提示。[初期値]でCoCの技能初期値表示"
            );
            return;
        }

        if (message.content.match(/test/)) {
            message.reply("test message!");
            return;
        }

        if (message.content.match(/初期値/)) {
            message.reply(
            "著作権上の問題で省略"
            );
            return;
        }

        if (message.content.match(/一時的狂気/)) {
            var temporary = [
            "著作権上の問題で省略",
            ];
            var tmpNumber = Math.floor(Math.random() * 11);
            message.reply(temporary[tmpNumber]);
            return;
        }

        if (message.content.match(/不定の狂気/)) {
            var temporary = [
            "著作権上の問題で省略"
            ];
            var tmpNumber = Math.floor(Math.random() * 11);
            message.reply(temporary[tmpNumber]);
            return;
        }

        if (
            message.content.match(
            /[^d\\d]*(([1-9]{0,1})d(100|20|12|10|8|6|4|3|2)((([x/])([1-9])){0,1}((\\+|-)(\\d{1,2})){0,1}){0,1}).*/
            )
        ) {
            var getDisceString = message.content;
            var tmp = getDisceString.split(" ");

            if(tmp.length > 2){
                var prefix = tmp[1].split("d");
                var random = null;
                var tmpVa = [];

                for (var i = 0; i < prefix[0]; i++) { random = Math.floor(Math.random() * prefix[1] + 1); tmpVa.push(random); message.reply(tmpVa[i]); } let total = tmpVa.reduce((sum, element) => sum + element, 0);

                if(tmp[2] >= total){
                    if(tmpVa.length > 1) {
                        message.reply("累計: " + total + " 成功");
                    } else {
                        message.reply("成功");
                    }
                } else {
                    if(tmpVa.length > 1) {
                        message.reply("累計: " + total + " 失敗");
                    } else {
                        message.reply("失敗");
                    }
                }
            } else {
                var prefix = tmp[1].split("d");
                var random = null;
                var tmpVa = [];

                for (var i = 0; i < prefix[0]; i++) { random = Math.floor(Math.random() * prefix[1] + 1); tmpVa.push(random); message.reply(tmpVa[i]); } if (tmpVa.length > 1) {
                    let total = tmpVa.reduce((sum, element) => sum + element, 0);
                    message.reply("累計: " + total);
                }
            }
        }
    }
    return;
});

if (process.env.DISCORD_BOT_TOKEN == undefined) {
    console.log("please set ENV: DISCORD_BOT_TOKEN");
    process.exit(0);
}

client.login(process.env.DISCORD_BOT_TOKEN);

以上が全コードです。あんまり綺麗なコードではありませんが、どうぞご参考に。

-Google, プログラミング
-