BETA

Discord用のチーム分けbotをpythonで作る

投稿日:2020-01-27
最終更新:2020-02-19

はじめに

discordのボイスチャットにいるメンバーを、コマンド一つでチーム分けを自動で行ってくれるbotを、
discord公式APIラッパーのdiscord.pyを利用して作成してみました!
なお、以下の内容については、他のところでたくさん触れられてりることもあり、本記事では触れません。

  • discord botのアカウント作成、および登録方法
  • discord.pyのインストール方法

「実装方法とかどうでもいい!今すぐチーム分けできるbotがほしい!」なんて方は、
以下のサイトを参考にしてみてください。
Forkするリポジトリを下記のmake-teamに変えて、作成すればすぐにチーム分けbotを利用できます。
Discord Bot 最速チュートリアル【Python&Heroku&GitHub】
https://github.com/Rabbit-from-hat/make-team

動作環境

  • Python 3.8.1
  • discord.py 1.2.5

仕様

基本的な仕様は、以下のサイトを参考にさせてもらいました。
https://ojige.hatenablog.com/entry/2018/07/26/131120

チームをつくる方法は、とりあえず3つ用意しました。

  • チーム数を指定して、同じメンバー数になるようチームを作成(余剰分はチームから除外)
  • チーム数を指定して、人数差がある状態でチームを作成
  • チームのメンバー数を指定して作成

実行コマンド

チームを作成したい人が、ボイスチャンネルに入った状態のまま、
以下のコマンドをテキストチャンネルに入力することで、チーム作成が可能です。
ボイスチャンネルに入らないまま実行すると、botから実行できなかった旨のメッセージが届きます。
また、指定した数が0だったり、ボイスチャンネルのメンバー以上の数を指定すると、
botから実行できなかった旨のメッセージが届きます。

/team チーム数

  • 指定した数のチームを作成
  • メンバー数が同じになるように作成
  • チーム数を指定しなくても実行可。デフォルトで"2"を指定

/team_norem チーム数

  • 指定した数のチームを作成
  • 人数差は考慮されないまま、指定されたチーム数を作成
  • チーム数を指定しなくても実行可。デフォルトで"2"を指定

/group メンバー数

  • 指定したメンバー数でチームを作成
  • メンバー数を指定しない場合、デフォルトとして"1"を指定

実行例

どれも以下のような形で、botからメッセージが届く。

  • コマンド成功時
  • コマンド失敗時

ファイル構成

make_team/  
  ├main.py  
  └modules/  
    └grouping.py  

一つのpythonファイルにまとめてもよかったのですが、以下の理由でこの構成にしました。

  • 他のbotにチーム分けのアクションを追加したいときに、一つのファイルだと面倒。
  • この構成なら「grouping.py」を移動させて、移動先のコマンドを受け取る処理(main.py)だけを書き直すだけで済む。

実装

全体の流れとしては、以下の通り。

  1. 入力されたコマンドを取得
  2. コマンド入力者のボイスチャットのステータスを確認
  3. コマンド入力者が入っているボイスチャットのメンバー一覧を取得
  4. チーム分けを実施
  5. 結果をメッセージとしてbotから送信

一つ一つ説明していきます。

入力されたコマンドを取得

import os  
import traceback  

import discord  
from discord.ext import commands  

from modules.grouping import MakeTeam  

token = os.environ['DISCORD_BOT_TOKEN']  
bot = commands.Bot(command_prefix='/')  

"""起動処理"""  
@bot.event  
async def on_ready():  
    print('-----Logged in info-----')  
    print(bot.user.name)  
    print(bot.user.id)  
    print(discord.__version__)  
    print('------------------------')  

"""コマンド実行"""  
# メンバー数が均等になるチーム分け  
@bot.command()  
async def team(ctx, specified_num=2):  
    make_team = MakeTeam()  
    remainder_flag = 'true'  
    msg = make_team.make_party_num(ctx,specified_num,remainder_flag)  
    await ctx.channel.send(msg)  

# メンバー数が均等にはならないチーム分け  
@bot.command()  
async def team_norem(ctx, specified_num=2):  
    make_team = MakeTeam()  
    msg = make_team.make_party_num(ctx,specified_num)  
    await ctx.channel.send(msg)  

# メンバー数を指定してチーム分け  
@bot.command()  
async def group(ctx, specified_num=1):  
    make_team = MakeTeam()  
    msg = make_team.make_specified_len(ctx,specified_num)  
    await ctx.channel.send(msg)  

"""botの接続と起動"""  
bot.run(token)  

bot = commands.Bot(command_prefix='/')
ここで、botがコマンドだと認識するためのプレフィックスとなります。
今回はスラッシュ'/'にしましたが、ここを円マーク'¥'にすると、「¥team チーム数」でコマンド入力することで実行されるようになります。

@bot.event  
async def on_ready():  
    # 処理  

上記の形で書くと、botが起動したときに呼び出されます。
今回は呼び出されると、自身のbotの情報を標準出力するものとなってます。

@bot.command()  
async def XXXX(ctx, A):  
    # 処理  

「XXXX」に、ユーザに入力してもらうコマンドを指定します。
例えば「XXXX」に"command"を指定した場合、「/command」がbotを動作させるためのコマンドになります。

引数に指定されている「ctx」は、必須となります。
この「ctx」を利用することで、ユーザ名やボイスチャンネルのステータスやら、discord上の情報を取得できます。

「A」の部分に、コマンド入力時に指定した数が入ります。
(コマンド入力時に指定するチーム数やメンバー数に相当)

メンバーリストを取得

def set_mem(self, ctx):  
    state = ctx.author.voice # コマンド実行者のVCステータスを取得  
    if state is None:   
        return False  

    self.channel_mem = [i.name for i in state.channel.members] # VCメンバリスト取得  
    self.mem_len = len(self.channel_mem) # 人数取得  
    return True  

このset_memでは、コマンド入力者のボイスチャンネルのステータス(どかしらのボイスチャンネルに入っているか)を確認しています。
ボイスチャンネルに入っている場合のみ、コマンド入力者が入っているボイスチャンネルのメンバーリストを取得しています。

ctx.author.voice
コマンド入力者のボイスチャンネルのステータスを確認することができます。

ctx.author.voice.channel.members
コマンド入力者が入っているボイスチャンネルのメンバーリストを取得することができます。

チーム分けを実施

# チーム数を指定した場合のチーム分け  
def make_party_num(self, ctx, party_num, remainder_flag='false'):  
    team = []  
    remainder = []  

    # メンバーリストを取得  
    if self.set_mem(ctx) is False:  
        return self.vc_state_err  

    # 指定数の確認  
    if party_num > self.mem_len or party_num <= 0:  
        return '実行できません。チーム分けできる数を指定してください。(チーム数を指定しない場合は、デフォルトで2が指定されます)'  

    # メンバーリストをシャッフル  
    random.shuffle(self.channel_mem)  

    # チーム分けで余るメンバーを取得  
    if remainder_flag:  
        remainder_num = self.mem_len % party_num  
        if remainder_num != 0:   
            for r in range(remainder_num):  
                remainder.append(self.channel_mem.pop())  
            team.append("=====余り=====")  
            team.extend(remainder)  

    # チーム分け  
    for i in range(party_num):   
        team.append("=====チーム"+str(i+1)+"=====")  
        team.extend(self.channel_mem[i:self.mem_len:party_num])  

    return ('\n'.join(team))  

さて、ようやく今回のメインです!
今回3つのチーム分けの方法を用意しましたが、どれも処理の方法は概ね同じです。
ここでは、「チームメンバー数が均等になるチーム分け」の処理を例に、説明したいと思います。

まず、set_mem()でメンバーリストを取得したあと、リストを一度シャッフルします。

random.shuffle(self.channel_mem)  
# [Aさん,Bさん,Cさん,Dさん,Eさん] => [Aさん,Dさん,Bさん,Eさん,Cさん]   

次に、チーム数をもとにして、均等に分けたときに余る人数を算出します。

remainder_num = self.mem_len % party_num  
# 余る人数 = 全体のメンバー数 % 指定されたチーム数  

余る人がいる場合は、余る人数分だけ、シャッフルしたリストから末尾にいる人を除きます。

for r in range(remainder_num):  
    remainder.append(self.channel_mem.pop())  
team.append("=====余り=====")  
team.extend(remainder)  

# [Aさん,Dさん,Bさん,Eさん,Cさん] => [Aさん,Dさん,Bさん,Eさん]  
# 待機する人 = Cさん  

以下の形で、チーム分け完了後の状況をまとめる配列に格納します。

team = [  
    "=====余り=====",  
    Cさん,  
]  

余る人数分除いた後は、残ったメンバーリストでチーム分けを行います。
チーム分けにはスライスを使いました。チーム数分だけスライスします。

for i in range(party_num):   
    team.append("=====チーム"+str(i+1)+"=====")  
    team.extend(self.channel_mem[i:self.mem_len:party_num])  

# メンバーリスト[ 振り分けるチームナンバー(※) : 全体のメンバー数 : 指定したチーム数 ]  
# ※"0"始まり(チーム1の場合、"0"となる)  

スライスを利用した場合の例は以下の通りです。

  • 例1: 10人を2チームに分ける場合
メンバーリスト=[A,B,C,D,E,F,G,I,J,K]  

1チーム目  
スライス内容 -> メンバーリスト[ 0 : 10 : 2]  
スライス実行後 -> チーム1[A,C,E,G,J]  

2チーム目  
スライス内容 -> メンバーリスト[ 1 : 10 : 2]  
スライス実行後 -> チーム2[B,D,F,I,K]  
  • 例2: 9人を3チームに分ける場合
メンバーリスト=[A,B,C,D,E,F,G,I,J]  

1チーム目  
スライス内容 -> メンバーリスト[ 0 : 9 : 3]  
スライス実行後 -> チーム1[A,D,G]  

2チーム目  
スライス内容 -> メンバーリスト[ 1 : 9 : 3]  
スライス実行後 -> チーム2[B,E,I]  

3チーム目  
スライス内容 -> メンバーリスト[ 2 : 9 : 3]  
スライス実行後 -> チーム2[C,F,J]  

スライスした後、最終的には以下の配列になります。

team = [  
    "=====余り=====",  
    Cさん,  
    "=====チーム1=====",  
    Aさん,  
    Bさん,  
    "=====チーム1=====",  
    Eさん,  
    Dさん,  
]  

結果をメッセージとしてbotから送信

return ('\n'.join(team))  
await ctx.channel.send(msg)  

チームが分け終わると、チーム分けが完了している配列をメッセージとして送信します。
配列そのままを送信すると、改行されず一行の文面として送信されます。
見づらいメッセージとなってしまうため、配列の各要素の末尾に改行コードを付けて送信します。

ctx.channel.send
コマンドが入力されたチャンネルに、メッセージを送信します。

以上が、チーム分けの大まかな実装部分になります。
ソースはgithubに載せてあるので、よかったらみてください!
https://github.com/Rabbit-from-hat/make-team

おわりに

pythonをはじめて使ったのですが、すごい書きやすくて楽しかったです。
こんな書き方しないなど、規則的におかしいところを見つけ次第直していこうと思います。

今回は基本的な(?)チーム分けしかできなかったので、このチーム分けのbotを改修してきたいなーと思います。
機会があればまた記事にします。

【この先の展開メモ】

  • チーム分けのメッセージを埋め込みする
  • ゲーム内特有のランク等の帯域を指定してのチーム分け
  • ゲームマスタを指定して、チーム分け
  • 作ったチームが気に入らなかった時のリメイクコマンドの実装
  • 特定のプレイヤーを予めチームに固定した状態でのチーム分け

参考

Pythonで実用Discord Bot(discordpy解説)
PythonでDiscordボットを作る時のFAQ
Pythonで簡単なDiscord Botの作り方
PythonでDiscordBotを書く方法
Discord.py ドキュメントの歩き方
pythonでdiscordのボイスチャット内メンバーのリストを作成したい
discord.pyとherokuでDiscordBot
Grouping_bot
Pythonで始めるHeroku 2018
Discord.pyでチーム分けを自動化
TeamMaker
Discord-shuffler

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

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

作ったアプリや、その他技術的な備忘録を書くブログ。

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう