BETA

DiscordのTRPG用のダイスボットを自作してみた

投稿日:2019-02-22
最終更新:2019-02-28
※この記事は外部サイト(https://qiita.com/Sashimimochi/items/3cbea...)からのクロス投稿です

始まり

始めに断っておきますが、私はTRPGもDiscordもにわか勢です。

最近、身内でTRPG(主にCoC)をぼちぼちやるのですが、毎回ダイス振って判定するのってメンドくさいよねって話になりました。
セッション環境はdiscordを使い始めたので、正直なところ既存で素晴らしいものがあるので「discord-bcdicebot使えよ」ってのが賢い選択だと思います。ですが、ポンコツの私には使い方がわからなかったので、勉強も兼ねて友人に手伝ってもらいながら作ってみました。

ソースコード全体はそのうちGitHubに晒します。
2019/2/27 GitHubに公開しました。

PythonからGoogleスプレッドシートにアクセスする

ダイスを振るだけなら不要なのですが、技能の成否判定も自動でしたいのでキャラクターシートをGoogleスプレッドシートに作って判定することにしました。
Googleスプレッドシートの操作方法はこちらを参考にしました。

こちらを参考にOAuth用のクライアントIDを作成してjsonファイルをダウンロードするところまで進めます。
セキュリティを度外視するならGoogle Apps ScriptでHTTPリクエストを受け付けるという方法もあるかとは思います。

スプレッドシートの仕様は適当でこんな感じです。

技能 合計値 純減 初期値 職業P 興味P
HP 9 0 9 0 0
MP 7 0 7 0 0
SAN 60 0 60 0 0
こぶし 50 0 50 0 0
図書館 50 0 25 25 0

初期値の決め方は下記を参照しました。

この中で使うのは技能合計値のカラムです。これを以下のような感じでpythonから取得します。

def get_gs():  
    scopes = ['https://www.googleapis.com/auth/spreadsheets']  
    json_file = './hoge.json'#OAuth用クライアントIDの作成でダウンロードしたjsonファイル  
    credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes)  
    http_auth = credentials.authorize(Http())  

    # スプレッドシート用クライアントの準備  
    doc_id = 'doc_id'#これはスプレッドシートのURLのうちhttps://docs.google.com/spreadsheets/d/以下の部分です  
    gs = gspread.authorize(credentials)  
    gfile   = gs.open_by_key(doc_id) #読み書きするgoogle spreadsheet  
    return gfile

ユーザーごとにでシートを切り替えられるようにします。シート名はなんでも良いのですが、私はdiscordから自動でシートを切り替えられるようにdiscordのIDをシート名にしています。

def get_charactor(sheet_name):  

    gfile = get_gs()  

    worksheet = gfile.worksheet(sheet_name)  

    charactor = {}  
    #技能名カラム  
    cell_keys = worksheet.col_values(1)  
    #合計値カラム  
    cell_values = worksheet.col_values(2)  
    for k,v in zip(cell_keys, cell_values):  
        charactor[k] = v  
    return charactor

ここまででスプレッドシートの設定は終了です。

Discord側のBOTを作る

次にDiscordの設定をしていきます。

Bot作成

DiscordのBOTの作り方はこちらを参考にさせていただきました

メッセージを取る

discordのメッセージ欄に特定の入力があったらダイスを振るようにします。入力形式は以下の仕様にします。ダイスを振るトリガーはdiceにします。

dice 1d100 技能名

これで受け取ってメッセージを返す待機botの処理を実装します。

client = discord.Client()  
client_id = conf['client_id']  

@client.event  
async def on_ready():  
    print('Logged in')  
    print('-----')  

@client.event  
async def on_message(message):  
    # 開始ワード  
    if message.content.startswith('dice'):  
        # 送り主がBotじゃないか  
        if client.user != message.author:  
            info = parse('dice {}d{} {}', message.content)  
            if info:  
                if info[1].isdecimal() and info[0].isdecimal():  
                    dice_num = int(info[0])  
                    dice_size = int(info[1])  
                    key = info[2]  
                    # メッセージを書きます  
                    m = message.author.name + ' '  
                    if key == '一時的狂気':  
                        m = temp_madness()  
                    elif key == '不定の狂気':  
                        m = ind_madness()  
                    elif key == 'dice':  
                        m = simple_dice(dice_size, dice_num)  
                    else:  
                        chara = get_charactor(str(message.author))  
                        msg, result = judge(chara, key, dice_size, dice_num)  
                        m += msg  
                        if result:  
                            d = damage(chara, key)  
                        else:  
                            d = None  
                        if d is not None:  
                            m += '\nダメージ: ' + str(np.sum(d)) + ' = ' + str(d)  
                    # メッセージが送られてきたチャンネルへメッセージを送ります  
                    await client.send_message(message.channel, m)  

client.run(client_id)

ダイス

ダイス振る部分を実装します。単純に入力したダイスを振るものと成否判定をするものの2種類用意します。

1〜dice_sizeまでの一様整数乱数を1つ生成します。

def dice(dice_size):  
    num = np.random.randint(1, int(dice_size))  
    return num

単純にダイスを振る場合は次のようにしています。上記のダイスをdice_num回分振ります。2d6なら1d6のダイスを2回振っています。あとでメッセージ表示の際に個別のダイス結果も見たいのでダイス結果はnumpy.arrayにしています。合計値はnp.sumで計算します。msgはdiscordに返すメッセージです。

def simple_dice(dice_size, dice_num):  
    dice_val = np.array([], dtype=np.int64)  
    for i in range(dice_num):  
        dice_val = np.append(dice_val, dice(dice_size))  
    msg = 'dice: ' + str(np.sum(dice_val)) + ' = ' + str(dice_val)  
    return msg

discord上ではこんな感じになります。

成否判定をするときもsimple_diceをベースに実装します。

成否判定

スプレッドシートから引っ張ってきた情報を参照して成否判定をさせます。
取得したスプレッドシートの情報は辞書型にして持たせておきます。

charactor = {  
    'HP': 9,  
    'MP': 7,  
    'SAN': 60,  
    'こぶし': 50,  
    '図書館': 50  
}

と言っても、インスタンス生成とかしているわけではないので、判定のたびにスプレッドシートからデータを参照しているので、たぶん非効率的です。

処理の流れは

  1. 入力メッセージからダイスのサイズと数を取得する
  2. ダイスを振ってダイス値を取得する
  3. 技能値をダイス値を比較する

といった感じです。ちなみに卓ルールで5以下でクリティカル、96以上でファンブルにしています。returnのbool値はダメージ判定時に使用します。

def judge(charactor, key, dice_size, dice_num):  
    dice_val = np.array([], dtype=np.int64)  
    for i in range(dice_num):  
        dice_val = np.append(dice_val, dice(dice_size))  
    if int(charactor[key]) >= np.sum(dice_val):  
        msg = key + ' ' + str(charactor[key]) + ' >= ' + str(np.sum(dice_val)) + ' = ' + str(dice_val)  
        if np.sum(dice_val) <= 5:  
            msg += ' 【クリティカル】'  
        msg += ' Success'  
        return msg, True  
    else:  
        msg = key + ' ' + str(charactor[key]) + ' < ' + str(np.sum(dice_val)) + ' = ' + str(dice_val)  
        if np.sum(dice_val) >= 96:  
            msg += ' 【ファンブル】'  
        msg += ' Fail'  
        return msg, False

discord上ではこのように見えます。

ダメージ判定

特定の技能名で技能判定が成功した際に自動でダメージロールを振るようにしました。トリガーとなる技能名はあらかじめダメージがわかっているこぶし,頭突き,キックだけに絞って実装します。マーシャルアーツは考慮していません。
ダメージボーナスがあればマイナスも含めて追加しています。

def damage(charactor, key):  
    d = np.array([], dtype=np.int64)  
    if key == 'こぶし':  
        d = np.append(d, dice(3))  
    elif key == '頭突き':  
        d = np.append(d, dice(4))  
    elif key == 'キック':  
        d = np.append(d, dice(6))  
    else:  
        return None  

    if 'd' in charactor['db']:  
        result = parse('{}d{}', charactor['db'])  
        dice_size = int(result[1])  
        dice_num = int(result[0])  
        for i in range(np.abs(dice_num)):  
            if dice_num < 0:  
                d = np.append(d, -dice(dice_size))  
            else:  
                d = np.append(d, dice(dice_size))  
    return d

成功

失敗

ダメージボーナスあり

ダメージボーナスあり(マイナス)

狂気表

dice 1d10 狂気の種類

狂気の種類一時的発狂不定の狂気の2種類が振れます。1d10はフォーマットの統一の為につけていますが、実質使っていません。
一瞬、狂気表もスプレッドシートに書こうかと思いましたが、処理速度をあげる為にハードコーディングしてます(正直、面倒臭かったので)

def temp_madness():  
    roll = {}  
    roll[1] = '鸚鵡返し(誰かの動作・発言を真似することしか出来なくなる)'  
    #(中略)  
    roll[20] = '過信(自分を全能と信じて、どんなことでもしてしまう)'  
    msg = roll[dice(20)]  
    msg += '\n一時的狂気(' + str(dice(10)+4) + 'ラウンドまたは' + str(dice(6)*10+30) + '分)'  
    return msg  

def ind_madness():  
    roll = {}  
    roll[1] = '失語症(言葉を使う技能が使えなくなる)'  
    #(中略)  
    roll[10] = '殺人癖(誰彼構わず殺そうとする) '  
    msg = roll[dice(10)]  
    msg += '\n不定の狂気(' + str(dice(10)*10) + '時間)'  
    return msg

一時的狂気

不定の狂気

Herokuにデプロイする

そのままではローカルでbotを起動している時しか使えないので不便です。そこでサーバーを立てて常時使えるようにします。今回はHerokuを使います。

Procfile

まずは、Heroku上で動かす実行スクリプトを作成します。今回はtrpg_bot.pyを実行するだけなので

woker: python trpg_bot.py

とします。

requirement.txt

requirement.txtにpythonファイル内で使っているライブラリを記入します。今回は以下のライブラリを使用しています。

oauth2client  
httplib2  
gspread  
discord  
parse  
numpy

デプロイ

基本的にはHerokuのアカウントを作ってCreate New AppしてHeroku Gitにしたがって進めれば良いです。

# Herokuにログインする  
$ heroku login  
# リポジトリをクローンする  
$ heroku git:clone -a trpg_dice_bot  
$ cd trpg_dice_bot

cloneしたリポジトリに作成したアプリケーションなどを格納します。

  • trpg_bot.py #メインアプリケーション
  • config.json #設定ファイル
  • requirement.txt # さっき作ったやつ
  • Procfile #さっき作ったやつ
  • oauth.json #Googleスプレッドシートを作った時にダウンロードしたoathのjsonファイルです
$ git add .  
$ git commit -am "make it better"  
$ git push heroku master

無事デプロイに成功すると

remote: Verifying deploy... done.

のようなメッセージが表示されます。
あとはResourcesからアプリを起動すればdiscord上でbotがオンラインになるはずです。
ちなみにHerokuの無料枠は550時間です。

これで快適なTRPGライフが送れるはず。

少しでも誰かの参考になれば幸いです。

その他のリファレンス

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@sashimimochiの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!