サマリ
- 5対5の対戦ゲームのチーム分けを自動化した
- python と 数理最適化モジュールのpulp を使って実装した
- Discord Bot 化してデータの入力も自動化した
Final Fantasy 14 と クリスタルコンフリクト
突然ですが皆様、普段どんなゲームを遊びますか?
私は普段 FINAL FANTASY 14(以下FF14)というMMORPGを遊んでいます。
Final Fantasy 14の概要
Final Fantasy 14(以下FF14)は、スクウェア・エニックス社が開発したMMORPG(大規模多人数参加型オンラインロールプレイングゲーム)です。
このゲームでは、プレイヤーは自分自身のキャラクターを作成し、仲間と共にファンタジーの世界「エオルゼア」を冒険します。また、クエストやストーリーだけではなくハウジングや釣り、麻雀など、遊びの幅が広いのが大きな特徴のゲームとなっています。
PvPコンテンツ: クリスタルコンフリクト の概要
FF14はPvE(Player vs Enemy)のコンテンツだけではなく、PvP(Player vs Player)のコンテンツもあります。その内の一つが「クリスタルコンフリクト」です。
このコンテンツは、クリスタルをより敵陣の方に運んだほうが勝つ 5人のプレイヤー同士の対戦ゲームです。スプラトゥーンをやってる方に説明するならばガチヤグラというと通りがよいかもしれません。公式大会やスポンサーのついた大会等も開催されています。
本コンテンツとスプラトゥーンとの差異は『エイムがいらない』ところで、ノーコンな私でも楽しく遊べるコンテンツとなっています。
Twitchパートナー・なしあさん と HOP☆CON について
そんなマイブームが講じて、Twitchという配信アプリでほぼ毎日クリスタルコンフリクトの配信をしているなしあさん1主催のスクリムチーム・HOP☆CONに参加することになりました。
HOP☆CON
HOP☆CON2(ほぷこん)は週2,3回のペースで集まれる人で集まってスクリム(チーム練習)を行うチーム(チームというよりコミュニティが近い?)です。
さて、このスクリムチームですがメンバーが固定されたチームではないため、チーム練習の参加者を毎回事前に把握して、開催日程までにチームを組む必要があります。
また、チームを組む際にはスキルレートスコアというものがあり、拮抗した試合をするためにはこのスコアが対戦チーム同士でほぼ同値になる必要があります。
『これを週2,3回の練習日の度に、それもほぼ毎日配信をしているなしあさんがやるのは大変じゃない……?』
『ほなら自動化しますか!』
思い立つと勝手に突っ走るタイプの人間なので、誰も頼んでないのに早速実装を始めました。
実装
当初Excel VBAで実装することを考えていました。が、
- ランダムにチームを組む ⇒ できる
- 対戦チーム同士でスコアレートをほぼ同値にする ⇒ どうやって?
ということで、『Excel VBA チョット ワカル』程度の私には実装ができませんでした。
Python での実装
Excel VBAは諦め、比較的使い慣れているPythonを使うことにしました。
『Python チーム分け』 で調べてみるとこちらのQiitaの記事がヒットしました。
やりたいことはほぼ同じだったのですが、コード自体は記載がなかったので『数理最適化』の考え方のために参考にさせていただきました。
目的関数 と 制約条件 と 必要なデータ
というわけで、求めたい解である目的関数 と 制約条件をまずは決めます。
目的関数
対戦する2つのチームのスキルレートスコアの合計値の差を最小化する。
制約条件
- 参加者は2チームのいずれかに必ず所属すること
- 2チームに所属するメンバーはそれぞれ必ず5人になること
- 配信者である『なしあさん』は必ず配信側のチームに所属すること
- 対戦する2つのチームのスキルレートスコアの合計値の差が±2以内になること
- 比較的身体を張ることができるジョブ(タンク、メレー)が扱える人は1チームに2人以上は入るようにすること
必要なデータ
上記の目的関数と制約条件を計算するために必要なデータは以下の通りとなりました。
- プレイヤー名
- スキルレートスコア
- ジョブカテゴリ(身体を張ることができるジョブが扱えるか否か)
これらをまとめたExcelファイルを作成し、pandasを使ってデータフレームとして取り扱うこととします。
コードの流れ
- 前処理①(参加者リストから観戦者と対戦参加者をランダムに抽出)
- 前処理②(対戦参加者のスキルレートスコアDFを抽出)
- pulpによる数理最適化処理
- 結果表示
作成したコード(pulp計算部分)
# 問題を作成
problem = pulp.LpProblem('TeamAssignment', pulp.LpMinimize)
# 変数を作成(各参加者の所属チーム, 参加する戦場)
participants = list(score_participants['PlayerName'])
team_dict = pulp.LpVariable.dicts('team', (participants, team_name, bf_list), cat='Binary')
# C) 数理モデルの作成(AstraチームとUmbraチームのSkillScore合計の差を最小化)
# C-1) 数理モデルのインスタンス作成
problem += (pulp.lpSum(score_participants[score_participants['PlayerName'] == p]['SkillScore'] * team_dict[p]['Astra'][b] for p in participants for b in bf_list)
- pulp.lpSum(score_participants[score_participants['PlayerName'] == p]['SkillScore'] * team_dict[p]['Umbra'][b] for p in participants for b in bf_list))
# C-3) 制約条件:
# C-3-1) どの参加者もどこかのbattlefield, Teamに所属する
for participant in participants:
problem += (pulp.lpSum(team_dict[participant][t][b] for t in team_name for b in bf_list) == 1)
# C-3-2) どの戦場も人数は10人になる
for bf in bf_list:
problem += (pulp.lpSum(team_dict[p][t][bf] for p in participants for t in team_name) == BF_MEM_NUM)
# C-3-3) どのチームも人数は5人になる
for team in team_name:
problem += (pulp.lpSum(team_dict[p][team][b] for p in participants for b in bf_list) == TEAM_MEM_NUM)
# C-3-4) なしあさんは必ず '戦場1'の'Astra' 側に所属する
problem += (team_dict['なしあ']['Astra'][1] == 1)
# C-3-5) どの戦場も AstraとUmbraチームのスコア合計の差が2以内になる
for bf in bf_list:
problem += (pulp.lpSum(score_participants[score_participants['PlayerName'] == p]['SkillScore'] * team_dict[p]['Astra'][bf] for p in participants)
- pulp.lpSum(score_participants[score_participants['PlayerName'] == p]['SkillScore'] * team_dict[p]['Umbra'][bf] for p in participants)) <= 2
problem += (pulp.lpSum(score_participants[score_participants['PlayerName'] == p]['SkillScore'] * team_dict[p]['Umbra'][bf] for p in participants)
- pulp.lpSum(score_participants[score_participants['PlayerName'] == p]['SkillScore'] * team_dict[p]['Astra'][bf] for p in participants)) <= 2
# C-3-6) どのチームもジョブカテゴリの合計値が共に2以上である
for team in team_name:
problem += pulp.lpSum(score_participants[score_participants['PlayerName'] == p]['JobCategory'] * team_dict[p][team][b] for p in participants for b in bf_list) >= 2
# 問題を解く
problem.solve()
試運転
実際に使ってみたところ、問題が発生しました。1時間あたりチームを変えて2セット行うのですが、出ずっぱりになるプレイヤーやずっと観戦に回ってしまうプレイヤーが発生するなど、偏りが見られました。
改善
前処理①を改善しました。
具体的には、1セット目に観戦者になったプレイヤーは、2セット目は必ず対戦参加者になるように処理を追加しました。
Discord Bot化
試運転をした際に新たに課題が見つかりました。
それは 『参加者希望者を把握してプログラムに入力する』部分が手動 であるところです。
それまでは主催者のなしあさんがMicrosoft Formsでアンケートを取ったり、直近ではDiscordのリアクションで参加できる日を回答したりしていました。
試運転の際にはDiscordのリアクションを誰がつけたのか確認し、手動でリスト化してプログラムに入力したのですが、このリスト化が結構な手間でした。
ということで次は DiscordのBot を作ることにしました。
DiscordのAPIラッパーであるdiscord.pyを使って実装を行うことにしました。
Discord Bot に関しては色々な方が記事にまとめていたため、比較的楽に実装できました。
実際のBot
オペレーターBotのくみです。
組み合わせを考えてくれるBotであることと、私の中でオペレーターと言ったら”くみ”3なんでこの名前にしました。
このBotには以下の2種類のコマンドを実装しました。
- eventdate: 開催日の出席者を確認する
- teamorder: eventdate の投稿にリアクションをつけた人物の取得 と 対戦チームのオーダーの投稿
実際の動作
実際に動かしてみるとこんな感じです。
今後の予定
今後ですが、
- エラーの修正(たまに変な出力をする: チームが5人にならない時がある)
- プレイヤーの参加試合数の平均化
- メンバーの固定(大会出場メンバーの練習等のためメンバーを固定したい時がある)
- BOTの24時間稼働化
などの機能を追加し、徐々に改善していこうと思っています。
まとめ と 感想
というわけで、チーム分けを自動化しました。
数理最適化もDiscord Botも一切触ったことがなかったのですが、Qiitaの記事のおかげで作成することができました。ノウハウを残してくれた技術者の皆様に感謝です。。
まだまだ調整すべき部分が多々残ってはいるのですが、運用しながらちょっとずつ改善していければと思っています。
また、技術者の皆さんや光の戦士の皆さんにこの記事が届いて、クリコンのプレイヤー数が少しでも増えたらうれしいです。
何かありましたらコメントか X(旧Twitter) にご連絡いただけますと幸いです。
-
Nasia_FF14PvP/ Twitch: https://www.twitch.tv/nasia_ff14pvp ↩
-
お気軽スクリムチーム【HOP☆CON】 メンバー募集開始!:https://note.com/nasia/n/n9cb9e6636e20 ↩
-
ガンスリンガーストラトス: https://www.jp.square-enix.com/gunslinger-stratos/ スクエニさん、バイキングさん、家庭版いつまでも待ってます…… ↩