16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

discord.pyでソートなぞなぞBotを作った話(複数サーバ対応)

Last updated at Posted at 2019-09-01

はじめに

この記事は discord.py を用いた Discord bot 開発についての記事です。雑にCogを使うことを目的としています。あまり詳しい説明はしません。また、1つのBotインスタンスで複数サーバに対応させることをしています。参考になれば幸いです

更新箇所(2020/10/6)

  • 記事の内容を大幅に更新

更新箇所(2019/12/18)

  • Wikiコマンドのバグ修正
  • 回答時にWikipediaのページを表示する

ソートなぞなぞとは

初めに、ある単語をソートしたものが問題として与えられます。元になった単語を推測するゲームです。非自明な面白さがあります。

問題 答え
えつぴん えんぴつ
ペボルンー ボールペン
aelpp apple

こちらからBotを招待することで遊ぶことができます
https://discord.com/api/oauth2/authorize?client_id=761582339557556245&permissions=2048&scope=bot

細かい話はどうでもいいから遊んでみたいという方はこちらからクローンして遊んでみてください。(ライブラリのインポートや、環境に合わせてファイルを多少いじる必要があります。)

動作環境

  • Python 3.8.2
  • discord.py 1.5.0

Botアカウントの作成

こちらでBotアカウントを作成してください。その際にトークン(CLIENT SECRET)もコピーしておいてください。直後に使います。

Botの作成と起動

GitHubのREADMEの通りです。config.iniにBotのトークンを置いておきます

起動手順
$ git clone https://github.com/t4t5u0/sort-riddle-v2.git
$ cd sort-riddle-v2
$ echo -e '[TOKEN]\ntoken=Botのトークン'>config.ini
$ pip install -r requirements.txt
$ nohup python main.py &

以下実装の話

まずライブラリをインストールします

pip install discord

次にコードの解説です

sortriddle.py
import configparser

import discord
from discord.ext.commands import Bot


config = configparser.ConfigParser()
config.read('./config.ini')
TOKEN = config['TOKEN']['token']

bot = Bot(command_prefix='!')
bot.load_extension('cog.sort_riddle')


@bot.event
async def on_ready():
    # Bot起動時にターミナルに通知を出す
    print('-'*20)
    print('ログインしました')
    print('-'*20)
    await bot.change_presence(activity=discord.Game(name='!help'))


if __name__ == "__main__":
    bot.run(TOKEN)

bot.load_extensionで読み込んだCogを追加していきます

最小構成は以下になります

cog/sort_riddle.py
from discord.ext import commands


class SortRiddleCog(commands.Cog):

    def __init__(self, bot):
        self.bot = bot
        
    @commands.command()
    async def neko(self, ctx):
        await ctx.send(f'{ctx.author.mention} にゃーん')

def setup(bot):
    return bot.add_cog(SortRiddleCog(bot))
  1. Cogのクラスを建てる
  2. クラスを初期化する。Botを引数に取ること
  3. コマンドを追加する。commands.command()デコレータを使用します
  4. Cogを登録する。setup関数がないと動きません

img

かわいいですね


コマンド追加

このままではゲームとしては成り立たないのでコマンドを追加していきます。ソースはこちらにあります。

コマンド 内容
!start 出題
!answer 解答
!hint 答えの1文字目を表示
!giveup ギブアップし答えを表示

の4つを追加します

複数サーバへの対応

サーバ情報を保存するためのファイルを作ります。JSON配列に対しての探索は極力したくないので、探索用にサーバのIDのリストを持ちました。このリストに対して2分探索を行い、計算量の削減を行っています。現在は、小規模ということでJSONをメモリに持っていますが、将来的に規模が大きくなる場合はDBへの移行を考えています。csvモジュールはpandasに置き換えると高速化できそうです

sort_riddle.py

import bisect
import csv
import json
import re
from datetime import datetime

import discord
import requests
from discord.ext import commands


class SortRiddleCog(commands.Cog):

    def __init__(self, bot):
        self.bot = bot
        self.sort_riddle_data = []
        self.guild_id_list = []

        with open('./data/guild_id_list.csv') as f:
            self.guild_id_list = [int(y) for x in csv.reader(f) for y in x]
        with open('./data/sort_riddle_data.json') as f:
            self.sort_riddle_data = json.load(f)
  
    @commands.Cog.listener()
    async def on_guild_join(self, guild):
        to_insert_index = bisect.bisect_left(self.guild_id_list, guild.id)
        info = {
            "guild_id": guild.id,
            "guild_name": guild.name,
            "channel_id": None,
            "answer": None,
            "question": None,
            "start_time": None
        }
        self.sort_riddle_data.insert(to_insert_index, info)

        with open('./data/sort_riddle_data.json', 'w') as f:
            json.dump(self.sort_riddle_data, f, indent=4)

        bisect.insort(self.guild_id_list, guild.id)
        with open('./data/guild_id_list.csv', 'w') as f:
            writer = csv.writer(f)
            writer.writerow(self.guild_id_list)


    @commands.Cog.listener()
    async def on_guild_remove(self, guild):
        to_delete_index = bisect.bisect_left(self.guild_id_list, guild.id)

        self.sort_riddle_data.pop(to_delete_index)
        with open('./data/sort_riddle_data.json', 'w') as f:
            json.dump(self.sort_riddle_data, f, indent=4)

        self.guild_id_list.pop(to_delete_index)
        with open('./data/guild_id_list.csv', 'w') as f:
            writer = csv.writer(f)
            writer.writerow(self.guild_id_list)

commands.Cog.listner()デコレータでイベントを拾っていきます。Botがサーバに入った時に情報を追加する処理と、退出した時に削除する処理を書いています。

startコマンドの実装
sort_riddle.py>start
    @commands.command(aliases=['s'])
    async def start(self, ctx):

        if 'guild' not in dir(ctx.author):
            await ctx.send('**!start** はDM限定だにゃ')
            return

        # guild_id が登録されていなかったときの処理

        index = bisect.bisect_left(self.guild_id_list, ctx.author.guild.id)
        if ctx.author.guild.id not in self.guild_id_list:
            info = {
                "guild_id": ctx.author.guild.id,
                "guild_name": ctx.author.guild.name,
                "channel_id": None,
                "answer": None,
                "question": None,
                "start_time": None
            }
            self.sort_riddle_data.insert(index, info)

            with open('./data/sort_riddle_data.json', 'w') as f:
                json.dump(self.sort_riddle_data, f, indent=4)

            bisect.insort(self.guild_id_list, ctx.author.guild.id)
            with open('./data/guild_id_list.csv', 'w') as f:
                writer = csv.writer(f)
                writer.writerow(self.guild_id_list)

        if (q := self.sort_riddle_data[index]['question']) is not None:
            await ctx.send(f'問題は **{q}** だにゃ')
            return

    
        link = 'https://ja.wikipedia.org/w/api.php?action=query&list=random&format=json&rnnamespace=0&rnlimit=1'
        response = requests.get(link)
        json_data = response.json()

        """
        json_data
        {'batchcomplete': '',
            'continue': {'continue': '-||',
                        'rncontinue': '0.269787212577|0.269787813864|1126887|0'},
            'query': {'random': [{'id': 2226876,
                                'ns': 10,
                                'title': 'Template:Country alias 岐阜県'}]}}
        """

        a = json_data['query']['random'][0]['title'].replace(' ', '_')
        self.sort_riddle_data[index]['answer'] = a
        q = ''.join(sorted(list(a)))
        self.sort_riddle_data[index]['question'] = q
        await ctx.send(f'問題は **{q}** だにゃ')

        self.sort_riddle_data[index]['start_time'] = re.split('[-|:|.|\s]',str(datetime.now()))
        # 諸々を書き込み
        with open('./data/sort_riddle_data.json', 'w') as f:
            json.dump(self.sort_riddle_data, f, indent=4)

時刻を取るところは、実装し終わったあとにライブラリに関数があることに気づきました。(アホ) 早期リターンを心がけました

おわりに

初投稿です。また、プログラミング歴半年足らずの初心者です。ガバがありましたら容赦なくマサカリを投げつけてください。
初投稿から1年経ったのをキッカケにリファクタリングをしてみました。過去の自分が書いたプログラムって見たくないものですね…

ソースはこちらに置いてあります。ぜひ。

参考にした記事・サイト

discord.py へようこそ。 — discord.py 1.3.0a ドキュメント
Pythonで実用Discord Bot(discordpy解説) - Qiita
discord.pyのBot Commands Frameworkを用いたBot開発 - Qiita

16
12
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?