Help us understand the problem. What is going on with this article?

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

始まり

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

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

Python + Googleスプレッドシート (+ Heroku)で実現しています。
ソースコード全体は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
db 1d4 0 1d4 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上ではこんな感じになります。
daa0ec9db009ee3a418450bc5e781ead.jpg

成否判定をするときも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上ではこのように見えます。
0c4638e5b209ab36f55615788c200e2c.jpg

ダメージ判定

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

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
  • 成功
    0f66094ef3ff2cfbb2c6899fa6dbc468.jpg
  • 失敗
    f3519de16b5129f665654e4e0334d1b7.jpg
  • ダメージボーナスあり
    5c9890fa428b2d87386954e0c80a6fa5.jpg
  • ダメージボーナスあり(マイナス)
    720e61ae3edcb3ae1cb08a82a5eec24a.jpg

狂気表

discord上で次のように入力した場合に狂気票を振るようにします。

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
  • 一時的狂気
    1c237209fa36504fbeda0ac9d5a32475.jpg
  • 不定の狂気
    eebbceb517ec43a602ee1a6e9de05c8c.jpg

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スプレッドシートを作った時にダウンロードしたoauthのjsonファイルです
$ git add .
$ git commit -am "make it better"
$ git push heroku master

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

remote: Verifying deploy... done.

のようなメッセージが表示されます。
デプロイした時点ではまだdiscord上ではbotはオフラインのはずです。
コメント 2019-02-27 220947.jpg
有効にするには、Resourcesからアプリを起動すればdiscord上でbotがオンラインになるはずです。
ちなみにHerokuの無料枠は550時間です。
Heroku
無事、オンラインになりました。
コメント 2019-02-27 220928.jpg

このあたりを参考にさせていただいています。

これで快適なTRPGライフが送れるはず。
少しでも誰かの参考になれば幸いです。

その他のリファレンス

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away