TeamMakerボット作成日記 第二弾
概要
第一回はこちら
ryo-udon.hatenadiary.jp
2019/12/05
ご指摘がありましたので追記します。
現在、Discord.pyはver 1.0 ~ になっていますが、現状このTeamMakerボットはver 0.7より以前のもののみ対応しています。
理由は下のリンクにありますが、変数名やクラス構成が大きく変わったことが原因となります。
現在、Discord.pyのversion確認をしつつ、1.0以降も対応できるように変更中です。
変更が完了次第、こちらの記事も追記したいと思います。
discordpy.readthedocs.io
2019/12/27 追記
python 3.8.0, discord.py version 1.2.5に対応したことを確認しました。
ご指摘いただきありがとうございました。
最近は便利になりました。
私の子供の頃では、チーム分けをするにもじゃんけんで延々とアイコを繰り返したり、時間をかけてくじを作ったりととりあえず時間がかかりました。
それこそ人数が10人を超えてくるとそれはもう大変です。
じゃんけんなんか早々決まるわけがないので(私はグッパージャス派でした)、結局チームわけで時間がかかって肝心の遊びの時間がなくなってしまう。そんな悲しい経験を何度もしました。
それに比べて今はどうでしょう?
1人一台スマホやパソコンを持つ時代になり、スマホやパソコンにはチーム分けのアプリがあり、劇的にその時間は短縮されました。
だけど、ちょっとまってください。
皆さんは本当にそれで満足していますか?
チームを作るたびにアプリにプレイヤーの名前を打ち込んで、あるいは参加する人の名前を膨大なリストから選んで。
(リストの人数が少ないって? それならそのままでいいと思いますよ)
意外と時間かかっていないですか?
特に大人数で協力プレイをするゲームをやる方ならなおさらこれを感じているのではないでしょうか?
かくいう私もその1人です。
そんな私が作ったのがこちら。
自動チーム分けBot その名もTeamMaker!!
今回はそんなTeamMakerに新機能を追加したので第二弾としてこのサイトでも公開したいと思います。
github.com
追加機能の概要
今回追加した機能は、トーナメント表の自動作成機能になります。
メンバーを登録すると、その人数に合わせいい感じにシードなども作ってくれるトーナメント表作成機能です。
今回、この機能を実行するに当たり、新たなライブラリが必要となりますので、そのインストールを行います。
sudo pip install pillow
これは作成したトーナメント表を画像として作成するために使用しています。
今回追加した機能の使い方自体は簡単。以下の2つのコマンドを打つだけ。
それぞれのコマンドが何をするのかを説明します。
!cup
友人たちと熱いトーナメント戦をしたい!! だけどいちいちトーナメント表を作るのは面倒くさい!
そんなあなたに魔法のコマンド!
以下の画像のように !cupと打ってみてください。
[f:id:ryo_udon:20190803000649p:plain]
するとGeneralに参加したメンバを参加者としたトーナメント表を自動で作成して貼り付けてくれます。
!cupw
ちなみにこのトーナメント表、勝ち負けの判定もできます。
コマンドで !cupw [トーナメント番号] [ラウンド番号] [買った人のID] [4-2などのスコア]を打ってもらうと、
下のようになります。
[f:id:ryo_udon:20190803000654p:plain]
これで面倒な記録も必要なし! いつでもどこでも手軽にトーナメント戦ができるようになります!
今回追加したものの構成について
今回は以前作成したTeamMakerに付け加える形で作成しましたが、TeamMaker bot専用の機能にする気はなく、
他のものにも使い回せるようにというのを意識して作成しました。
そこで作成したのが以下になります。
ここにはいずれクラス図を勉強して追加します。
(正しい表記の仕方がわからない)
主に追加したものは
* トーナメント表を作る
* トーナメント表を描く
* 何個のトーナメント表を作るかを判断する
* トーナメントを管理する
の4つです。
これらの機能を合計で4つのクラスで表現しています(つもりです)
また、これに合わせてTeamMakerクラスのスリム化を行いました。
構成としてはBotの根幹をなすBOT_BASEクラスの中にチーム分けをするTeamMakerクラスとトーナメント表の作成などを行う今回新たに作成したCUP_MAKERクラスが内包されているという構成になっています。
BOT_BASEクラスはただそれぞれのクラスを管理しているだけですので、このページではCUP_MAKERのみ説明していきます。
CUP_MAKER
CUP_MAKERクラスを構成する上で重要なクラスが2つあります。
ROUND():
このクラスがトーナメント作成での参加者及び、参加者が誰と対戦するのかを管理するこのCUP_MAKERに格納される最小のクラスになります。
トーナメント表での一番左端の参加者名しか書かれていないところも参加者が1人しかいないラウンドとして管理していきます。
このクラスはGraph構造をイメージしながら、色々と手抜きをしたものになります。
class ROUND():
def __init__(self,stageNum, id, members):
self.roundID = id
self.stageNum = stageNum
self.parentID = 0
self.childID = []
self.x = 0
self.y = 0
self.hasChildFlag = 0
self.PositionSetFlag = 0
self.SetMember(members)
self.winner = -1
self.player_id = -1
self.score = ""
def SetMember(self, members):
self.memberList = members
if len(self.memberList) == 1:
self.hasChildFlag = 1
def CreateChildRound(self,roundID):
if self.hasChildFlag == 0:
self.hasChildFlag = 1
halfLine = int(len(self.memberList) / 2)
child1List = self.memberList[:halfLine]
child2List = self.memberList[halfLine:]
newStage = self.stageNum + 1
child1Round = ROUND(newStage, roundID+1,child1List)
child2Round = ROUND(newStage, roundID+2,child2List)
self.childID = [roundID+1, roundID+2]
child1Round.SetParentID(self.parentID)
child2Round.SetParentID(self.parentID)
return [child1Round, child2Round], roundID+2
return [0], roundID
def SetParentID(self, parentID):
self.parentID = parentID
def SetPosition(self, x, y):
self.x = x
self.y = y
self.PositionSetFlag = 1
def SetWinnerID(self, id, score):
self.winner = id
self.score = score
def SetPlayerID(self, id):
self.player_id = id
def GetWinnerID(self):
return(self.winner)
各ラウンドには区別するためのIDが割り振られており、ラウンド同士の繋がりを保持するために、親と子のIDをもたせています。
aaaa ---┐ ①
├---┐
bbbb ---┘ |②
├---
|
この図を例に説明すると、aaaa, bbbb, ①, ②はどれも同じROUNDクラスで表現されています。
①の親は②であり、子はaaaa,bbbbの2つになります。
なので、各ラウンドのメンバの数を確認することでそれが参加者を示すラウンドかどうかを確認することができます。
基本的に勝敗、スコア、参加者名などの情報は全てこのクラスの中に格納されます。
本当はprivate変数などにして外部から直接編集できないように管理すべきところではありますが、ここではかなり手抜きをしています。
TOURNAMENT_MAKER()
次にこのクラスで先程のROUNDを取りまとめてトーナメントの形に整えて上げるクラスになります。
class TOURNAMENT_MAKER():
def __init__(self, members):
self.members = members
self.tournament = []
self.start_pos = [ 120, 30]
self.y_space = 40
self.x_space = 50
def CreateNormalTournament(self):
roundID = 0
FinalRound = ROUND(0,roundID,self.members)
self.tournament.append(FinalRound)
self.largestStageNum = 0
num = 0
while len(self.tournament) > num :
newRounds = [0]
newRounds, roundID = self.tournament[num].CreateChildRound(roundID)
if len(newRounds) == 2:
self.tournament.extend(newRounds)
if self.largestStageNum < self.tournament[num].roundID:
self.largestStageNum = self.tournament[num].roundID
num += 1
self.SetPosition()
def GetLargestStageNum(self):
return(self.largestStageNum)
def SetFirstRoundPosition(self):
dst_firstRound = []
for round in self.tournament:
if len(round.memberList) == 1:
dst_firstRound.append(round.roundID)
self.firstRound = []
for name in self.members:
for round_id in dst_firstRound:
if name == self.tournament[round_id].memberList[0]:
self.firstRound.append(round_id)
break
for i in range(len(self.firstRound)):
self.tournament[self.firstRound[i]].SetPosition(self.start_pos[0], self.start_pos[1] + i * self.y_space)
self.tournament[self.firstRound[i]].SetPlayerID(i)
def CalcNewCenterPosition(self, roundID):
childID = self.tournament[roundID].childID
x = max([self.tournament[childID[0]].x, self.tournament[childID[1]].x]) + self.x_space
y = 0
for id in childID:
y += self.tournament[id].y
self.tournament[roundID].SetPosition(x, int(y / 2))
def SetPosition(self):
self.SetFirstRoundPosition()
stageNum = self.largestStageNum
roundList = range(len(self.tournament))
while stageNum >= 0:
for round in roundList:
if self.tournament[round].stageNum == stageNum:
if self.tournament[round].PositionSetFlag == 0:
self.CalcNewCenterPosition(round)
stageNum -= 1
def SetMatchScore(self, roundID, winnerID, score):
self.tournament[roundID].SetWinnerID(self.firstRound[winnerID], score)
このクラスがやっていることは、
参加者のリストから必要な数のROUNDクラスを呼び出して、トーナメントの形になるように親、子の関係を作ってあげること
全てのラウンドの設置位置を計算してあげること
ROUNDクラスに勝ち負けをつける
の3つになります。
そのため、関数は複数個ありますが、実質外部から呼び出して上げる必要があるのは、
* CreateNormalTournament()
* SetMatchScore()
の2つ。
CreateNormalTournament()が1,2番の役割を、SetMatchScore()が3番の役割を果たしています。
については、トーナメントに参加するメンバをどんどん2で割ってゆき、割れなくなったら、トーナメントの追加を終えるという方法にしています。
については、参加者それぞれを格納するラウンドに最初に座標を割り当ててあげ、各ラウンドの中心をそのラウンドの子2つのちょうど真ん中になるようにy座標だけ計算してあげています。(CalcNewCenterPosition())
DRAW_SHAPE()
このクラスでは今回はpillowを使って絵を描きましたが、今後、他のライブラリを使いたいと思うことがあるかと思い、CUP_MAKER()の中でpillowの描画する関数を直接呼び出すのではなく、DRAW_SHAPE()を間に挟んであげています。
これによっていざ変えるときは、DRAW_SHAPE()の中のみを変更すればCUP_MAKER()やこのクラスを使っている他のクラスを全く変更する必要がありません。
import os, sys
from PIL import Image, ImageDraw, ImageFont
class DRAW_SHAPE():
def __init__(self, image_name):
self.size_x = 512
self.size_y = 512
self.image_name = image_name
self.SetConfigure()
def SetConfigure(self):
self.SetRectangleSize()
self.SetRectangleFillColor()
self.SetEllipseFillColor()
self.SetLineFillColor()
self.SetEdgeColor()
self.SetTextSize()
def GetCenterPoint(self):
return([self.size_x/2, self.size_y/2])
def SetCenterPoint(self, x, y):
self.size_x = x * 2
self.size_y = y * 2
def GetNewImage(self):
self.im = Image.new("RGB",(int(self.size_x), int(self.size_y )),(255,255,255))
self.draw = ImageDraw.Draw(self.im)
def SetRectangleSize(self, dx=100, dy=20):
self.rectangleXSize = dx
self.rectangleYSize = dy
def SetRectangleFillColor(self, color="white"):
self.recColor = color
def SetLineFillColor(self, color="black"):
self.lineColor = color
def SetEllipseFillColor(self, color="white"):
self.ellipseColor = color
def SetEdgeColor(self, color="Black"):
self.edgeColor = color
def DrawRectangle(self, x, y):
self.draw.rectangle([(x,y),( x+self.rectangleXSize, y+self.rectangleYSize)], fill=self.recColor, outline=self.edgeColor, width=2)
def DrawLine(self, x1,y1, x2, y2, width):
self.draw.line((x1,y1,x2,y2), fill=self.lineColor, width=width)
def DrawZigZagXtoY(self, x1, y1, x2, y2, width=10):
self.DrawLine(x1,y1,x2,y1, width)
self.DrawLine(x2,y1,x2,y2, width)
def DrawZigZagYtoX(self, x1, y1, x2, y2, width=10):
self.DrawLine(x1,y1,x1,y2, width)
self.DrawLine(x1,y2,x2,y2, width)
def DrawEllipse(self, x1,y1, x2,y2):
self.draw.ellipse(( x1, y1, x2, y2), fill=self.ellipseColor, outline=self.edgeColor)
def SetTextSize(self, textSize=10):
self.textSize = textSize
def SetModestTextSize(self, char, targetWidth):
width = self.textSize * len(char)
if width < targetWidth:
self.textSize = targetWidth / len(char)
def SetFontSize(self):
self.font = ImageFont.truetype(size=self.textSize)
def SetImageSize(self, x, y):
self.size_x = x
self.size_y = y
def DrawText(self, x1, y1, text):
self.draw.multiline_text((x1,y1), text, font=self.font, fill=(0,0,0,255))
def SetFontFile(self, fontFileName, size=10):
fileDir = os.path.dirname(os.path.abspath(__file__)) + "/../font/" + fontFileName
self.font = ImageFont.truetype(fileDir, size)
def ShawImage(self):
self.im.show()
def SaveImage(self):
img_dir = os.path.dirname(os.path.abspath(__file__)) + "/../../image/"
if os.path.exists(img_dir):
ret = 1
img = img_dir + self.image_name + ".jpg"
self.im.save(img)
return img
CUP_MAKER()
ここではこれまで説明したクラスを統括し、何個のトーナメントを作ればいいかを判断するクラスであるCUP_MAKER()を説明します。
class CUP_MAKER():
def __init__(self, member, tournament_name):
self.tournament = []
self.start_pos = [-100,0]
self.line_width = 3
self.tournament_name = tournament_name
self.member = member
self.member_size_array = self.CalcTournamentNum(member)
def CreateCupTournament(self):
for member_size in self.member_size_array:
self.tournament.append(TOURNAMENT_MAKER(self.member[:member_size]))
if len(self.member) > member_size:
self.member = self.member[member_size:]
for i in range(len(self.tournament)):
self.tournament[i].CreateNormalTournament()
def CalcTournamentNum(self, member):
list_size_array = []
divide_num = len(member)/12
min = 100
divide_size = len(member)
if divide_num >= 1 and len(member)%12 != 0:
for i in range(6, 13):
remain = len(member) % i
if min > remain:
min = remain
divide_size = i
divide_num=len(member)//divide_size
list_size_array = [divide_size for i in range(divide_num)]
for i in range(min):
list_size_array[i]+=1
else:
list_size_array.append(len(member))
return(list_size_array)
def SetRoundWinner(self, tournament_num, round_id, winner_id, score):
self.tournament[tournament_num].SetMatchScore(int(round_id), int(winner_id), score)
def DrawTournamentImage(self):
tournament_num = 0
image_list = []
for tournament in self.tournament:
image_name = self.tournament_name + str(tournament_num)
ds = DRAW_SHAPE(image_name)
ds.SetFontFile("font_1_honokamarugo_1.1.ttf", size=12)
ds.GetNewImage()
ds.DrawText( 0, 0,self.tournament_name+str(tournament_num))
for t in tournament.tournament:
width = self.line_width
if t.stageNum == 0:
if t.winner != -1:
width *= 2
ds.DrawLine(t.x, t.y, 50 + t.x, t.y, width=width)
if len(t.childID) > 1:
for child in t.childID:
width = self.line_width
if t.winner != -1:
for member in tournament.tournament[child].memberList:
if member == tournament.tournament[t.winner].memberList[0]:
width *= 2
ds.DrawText(t.x + 20, t.y - 20, t.score)
ds.DrawZigZagYtoX(t.x, t.y, 5+tournament.tournament[child].x, tournament.tournament[child].y, width=width)
ds.DrawText(t.x + 5, t.y - 20, "[" + str(t.roundID) + "]")
if len(t.memberList)==1:
ds.DrawRectangle(self.start_pos[0]+t.x,self.start_pos[1]+t.y - 10)
member_name_text = " " + str(t.player_id) + "." + t.memberList[0]
ds.DrawText(self.start_pos[0]+t.x,self.start_pos[1]+t.y-5,member_name_text)
image_dir = ds.SaveImage()
image_list.append(image_dir)
tournament_num += 1
return(image_list)
このクラスがやっているのは、TOURNAMENT_MAKER()を何個呼び出せばいいのかという判断と、それぞれのトーナメント表をDRAW_SHAPE()クラスを呼び出して上げて画像に変換してあげるところです。
そのため実はこのクラスではあまり説明することがありません。
もしトーナメント表のデザインなどが好みでないなどがあれば変更するのがこのクラスになると思います。
画像の送信
今回、トーナメント表をjpgファイルで作成し、作成した画像のパスを渡してDiscordの各サーバーに送信しています。
その部分について簡単に説明します。
if image_list != "null":
for img in image_list:
await client.send_file(message.channel, img)
私が作成したBOT_BASE()クラスは送信するメッセージ(String型)のみか、
メッセージと画像のパスを格納したリスト型のimage_listを返してきます。
そこで、そのリストに格納されたパスの先にある画像を送ってくれるのが、
send_file()関数です。この関数では送信する先のChannelと送信するファイルを指定するだけで簡単に送信することができます。
まとめ
今回は以前作成したチーム分けのbotにトーナメント表を作成する機能を追加しみました。
かなり手を抜いてしまいましたが、構成としてはそこそこのものができたと思っています。
ただ、今後、これを拡張していく上で、Discord側から受け取るコマンドに対し、どの関数を呼び出すのかというif文が増えていってしまうという問題を抱えているので、今後はそれをまずは対処しつつ、ROUND()クラスを純粋なGraph構造に近づけるように改造したいと思います。
次に加える新機能としては、まずリーグ戦を作れるようにすること。
次にリーグ戦の後にトーナメントを行うような場合に対応させることです。
以上、TeamMaker Botの話でした!!