Nの外部記憶

作ったアプリや、作成時の備忘録を書くブログ。やりたいことが多すぎるッ!

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

はじめに

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

  • 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.3.4

仕様

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

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

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

実行コマンド

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

/team チーム数

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

/team_norem チーム数

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

/group メンバー数

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

実行例

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

  • コマンド成功時
    undefined.jpg
  • コマンド失敗時
    undefined.jpg

ファイル構成

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