ポケモンバトルのAIを作ってみたよ
Pokemon Onlineをポケモン対戦ライブラリとして使う
こんにちは
ポケモン対戦って複雑なゲームですよね
一言で言うなら「相手のHPを先に0にした方が勝ち」ですが、相手のHPを0にするための攻撃技に様々な効果があったり、相手のHPは減らさないけど動きを止めて戦いを有利にする変化技があったり、攻撃技でHPを減らさなくても勝つ場合があったりします
そういうこともあって、ポケモン対戦に似たプログラムを自作しようとすると想像以上に大変です。
今回はPokemon Online という有名なポケモン対戦シミュレータのソースコードを使って、これを製作することを考えました。
Pokemon Online について
知ってる人と知らない人が半々ぐらいだと思うので説明します。
Pokemon Online
実機でのポケモン対戦をほぼ完璧にコピーした、非公式のオンラインゲームです。クライアントアプリケーションをダウンロードして、有志の方々が立てている各所のサーバーに接続して遊びます。
ポケモンとそのステータス・持ち物を指定するだけで簡単にPTが作れ、そのまま対戦が行えます。
実機と比べるとポケモンを育成する手間が省けるので、PTの試運転などに使う人はよくいるようです。
似たようなものに、Pokemon Showdown があります。こちらはwebベースですが、対戦シミュレータとしての機能はほぼ同等です。
Pokemon Showdown
Pokemon Online はオープンソースソフトウェアであり、そのソースコードはGithubで公開されています(GNUライセンス)。
https://github.com/po-devs/pokemon-online
C++がある程度理解できるという条件はつくと思いますが、ポケモン対戦という複雑なゲームをバグなしに実装している例を見ることができるのは非常に役立つと思います。
ポケモン対戦の実装部分を抜き出す
プロジェクトをローカルでビルドする方法についてはこちら
http://pokemon-online.eu/threads/how-to-run-from-the-source-develop.31020/
Pokemon Onlineのプロジェクトはいくつかのサブディレクトリに分かれていますが、この中で今回必要な、ポケモン対戦を司るクラスは主にBattleServerの中に収められています。
このディレクトリと、それと依存関係のあるlibraries以外のディレクトリはプロジェクトから除外した方がいいでしょう。ビルドにかかる時間が大幅に短くなります。
BattleServerプロジェクトをいじっていきましょう。main.cppからソースを辿っていくと、ネットワーク関係のクラスを色々と経由していき、最終的にBattleSituationというクラスのインスタンスで対戦を実行していることが分かります。
このネットワーク関係のクラスとの依存関係を経って、完全にローカルで、最小のコードで対戦を実行するのが今回の狙いです。
ここで主役の紹介です。
BattleSituation クラス (battle.h)
いわゆる局面インスタンスです。対戦はこのクラスのフィールドが書き換えられることで進んでいきます。
new(BattlePlayer, BattlePlayer, ChallengeInfo, int, TeamBattle, TeamBattle, BattleServerPluginManager) のように初期化してから
start(ContextSwitcher)と呼び出して対戦開始です。
以降局面の情報が
SIGNAL(battleInfo(int,int,QByteArray))を通じて送信されるので、コマンド選択待ちのタイミングで
battleChoiceReceived(int, BattleChoice)関数を外部から呼び出すことで対戦が進行していきます。
日本語とは思えないような雑な説明になってしまいましたが、要はこのクラスと、その中で名前が出てきた周辺クラスの動作を理解すればいいわけです。
簡単な対戦プログラムを書いてみる
ということで、個人的にはかなり苦戦はしたのですが、BattleSituationクラスを用いてポケモン対戦を自動で行うプログラムを書いてみました。
なおC++は初心者なのでコードはクソです。
//localbattletest.h
#ifndef LOCALBATTLETEST_H
#define LOCALBATTLETEST_H
#include <QObject>
#include "battle.h"
class LocalBattleTest : public QObject
{
Q_OBJECT
public:
explicit LocalBattleTest(QObject *parent = 0);
~LocalBattleTest();
QHash<int, BattleSituation *> battles;
ContextSwitcher battleThread;
BattleServerPluginManager *pluginManager;
void testBattleExecute();
void commandChoice(int battleID, BattleChoice choice);
void ShowCurrentHPs();
void databaseInit();
int newBattle(BattlePlayer player1, BattlePlayer player2, ChallengeInfo challengeInfo, TeamBattle team1, TeamBattle team2);
void ShowCurrentHPs(int battleID);
signals:
public slots:
void notifyInfo(int battleID, int playerID, const QByteArray &info);
void notifyFinished(int battleid, int result, int winner, int loser);
};
#endif // LOCALBATTLETEST_H
//localbattletest.cpp
#include "localbattletest.h"
#include "battle.h"
#include "pluginmanager.h"
#include "moves.h"
#include "abilities.h"
#include "rbymoves.h"
#include <PokemonInfo/movesetchecker.h>
#include "../Shared/battlecommands.h"
LocalBattleTest::LocalBattleTest(QObject *parent) : QObject(parent)
{
databaseInit();
battleThread.start();
pluginManager = new BattleServerPluginManager();
}
LocalBattleTest::~LocalBattleTest()
{
}
//データベースの初期化。実行ディレクトリに置いてあるdbフォルダの中身を用いる。
void LocalBattleTest::databaseInit()
{
//BattleServer::changeDbMod()の中身をほぼそのままコピペ。不要なものもあるかも
PokemonInfoConfig::setFillMode(FillMode::Server);
PokemonInfoConfig::changeMod("");
/* Really useful for headless servers */
GenInfo::init("db/gens/");
PokemonInfo::init("db/pokes/");
MoveSetChecker::init("db/pokes/");
ItemInfo::init("db/items/");
MoveInfo::init("db/moves/");
TypeInfo::init("db/types/");
NatureInfo::init("db/natures/");
CategoryInfo::init("db/categories/");
AbilityInfo::init("db/abilities/");
HiddenPowerInfo::init("db/types/");
StatInfo::init("db/status/");
GenderInfo::init("db/genders/"); //needed by battlelogs plugin
PokemonInfo::loadStadiumTradebacks();
MoveEffect::init();
RBYMoveEffect::init();
ItemEffect::init();
AbilityEffect::init();
}
//メイン
void LocalBattleTest::testBattleExecute()
{
//名前とIDを自由に設定
BattlePlayer player1("プレイヤー0", 0);
BattlePlayer player2("プレイヤー1", 1);
Team team;
//TeamBuilder(Pokemon Onlineのクライアントアプリの一部)で保存したPTを読み込める
team.loadFromFile("(***ファイルパス****)");
auto team1 = TeamBattle(team);
auto team2 = TeamBattle();
team2.generateRandom(Pokemon::gen(Gen::ORAS)); //もう片方はランダム生成
newBattle(player1, player2, ChallengeInfo(), team1, team2); //ChallengeInfo()は既定値として一番最新のルールを呼び出す
}
//新たな対戦インスタンスを開始する
int LocalBattleTest::newBattle(BattlePlayer player1, BattlePlayer player2, ChallengeInfo challengeInfo, TeamBattle team1, TeamBattle team2)
{
//今回は対戦インスタンスはプログラム中で1つしか生成しないが、元々の設計に合わせて複数のインスタンスを同時並行できるようにした方が吉。
//そのためにそれらを格納するQHashを用意している
int initialID = 10; //適当
int maxID = 0;
foreach (auto item, battles.keys()) {
if(item > maxID){
maxID = item;
}
}
auto id = battles.count() == 0 ? initialID : maxID + 1;
auto newBattle = new BattleSituation(player1, player2, challengeInfo, id, team1, team2, pluginManager);
battles.insert(id, newBattle);
//対戦インスタンスからのSignalをSlotに接続する。重要。
connect(newBattle, SIGNAL(battleInfo(int,int,QByteArray)), this, SLOT(notifyInfo(int,int,QByteArray)));
connect(newBattle, SIGNAL(battleFinished(int,int,int,int)), this, SLOT(notifyFinished(int,int,int,int)));
newBattle->start(battleThread);
return id;
}
//対戦インスタンスからのシグナルを受け取ったときの対応
void LocalBattleTest::notifyInfo(int battleID, int playerID, const QByteArray &info)
{
//対戦インスタンスから送信される情報には様々な型のあらゆるオブジェクトが含まれているが、
//それらは全てバイト配列に変換されることで同じシグナルからemitされる。
//QbyteArray型の変数infoをDataStreamを介してデコードすることで、コマンドタイプに応じた様々な型のオブジェクトが復元される。
DataStream dataStream(info);
uchar commandNumber;
qint8 who;
dataStream >> commandNumber >> who;
using namespace BattleCommands;
switch((int)commandNumber) //本当は列挙型BattleCommandの値全てのcaseに対応した処理を書くべき
{
case BattleCommand::OfferChoice: //プレイヤーにコマンド選択を求める場面で送信される
{
if(who == 0) //適当(どちらかだけにしないと2回連続で呼び出されるので)
{
ShowCurrentHPs(battleID);
}
//コマンド選択として可能な行動を全て列挙し、その中からランダムで選んだものを実際に実行する
BattleChoices choiceRegulation;
dataStream >> choiceRegulation;
QList<BattleChoice> validChoices;
bool isMegaEvo = choiceRegulation.mega;
for(int i = 0; i < 4; i++)
{
if(choiceRegulation.attackAllowed[i])
{
AttackChoice attack;
attack.attackTarget = (who == 0) ? 1 : 0;
attack.attackSlot = i;
BattleChoice battleChoice(who, attack);
battleChoice.setMegaEvo(isMegaEvo);
validChoices << battleChoice;
}
}
if(choiceRegulation.switchAllowed)
{
for(int i = 0; i < 6; i++)
{
if (!battles[battleID]->isOut(who, i) && !battles[battleID]->poke(who, i).ko())
{
SwitchChoice switchChoice;
switchChoice.pokeSlot = i;
validChoices << BattleChoice(who, switchChoice);
}
}
}
commandChoice(battleID, validChoices[rand() % validChoices.length()]);
break;
}
}
}
//対戦終了のシグナルを受け取ったときの対応
void LocalBattleTest::notifyFinished(int battleid, int result, int winner, int loser)
{
qDebug() << "ID" << battleid << "の対戦が終了しました。(result:" << result << ")";
qDebug() << "勝ったプレイヤー" << winner << "負けたプレイヤー" << loser;
}
//コマンド選択を実行
void LocalBattleTest::commandChoice(int battleID, BattleChoice choice)
{
qDebug() << "対戦ID" << battleID << "プレイヤー" << choice.slot() << "のコマンド選択…";
//デバッグ用
switch(choice.type)
{
case ChoiceType::AttackType:
{
auto moveNumber = battles[battleID]->poke(choice.slot()).move(choice.attackSlot()).num();
auto moveName = MoveInfo::Name(moveNumber);
qDebug() << moveName << "を選択";
break;
}
case ChoiceType::SwitchType:
{
auto pokeName = battles[battleID]->poke(choice.slot(), choice.pokeSlot()).nick();
qDebug() << pokeName << "への交代を選択";
break;
}
}
qDebug();
battles[battleID]->battleChoiceReceived(battles[battleID]->id(choice.slot()), choice);
}
//現在のHPを表示する。デバッグ用
void LocalBattleTest::ShowCurrentHPs(int battleID)
{
auto b = battles[battleID];
qDebug() << "プレイヤー0のポケモンの現在HP:" << b->poke(0, 0).lifePoints() << b->poke(0, 1).lifePoints() << b->poke(0, 2).lifePoints() << b->poke(0, 3).lifePoints() << b->poke(0, 4).lifePoints() << b->poke(0, 5).lifePoints();
qDebug() << "プレイヤー1のポケモンの現在HP:" << b->poke(1, 0).lifePoints() << b->poke(1, 1).lifePoints() << b->poke(1, 2).lifePoints() << b->poke(1, 3).lifePoints() << b->poke(1, 4).lifePoints() << b->poke(1, 5).lifePoints();
qDebug();
}
あらかじめTeambuilderで作成しておいたPTと、その場でランダムに生成したPTを戦わせます。
ルールは6:6のレベル100シングルバトルで、コマンド選択は完全ランダムです。
ちなみにあらかじめ作成したPTは、以下のようなPGL使用率ランキング上位のドリームチームです。
左に写っているのはTeamの保存ダイアログです。ここで保存したファイルをプログラム中で読み込むというわけです。
あとは既存のmain.cppの中身をコメントアウトして、
auto lbt = new LocalBattleTest();
lbt->testBattleExecute();
とでも書くだけで実行できます。やってみます。
コンソール画面が現れてからはほんの数秒間で全ての対戦処理が終わります。さすが21世紀のパソコンです。
プレイヤー0の圧勝で無事対戦が終了したようです。さすがドリームチームです。
もう一度実行してみます。
おそらくファイアローが繰り出したであろうブレイブバードで、対戦ありがとうございましたとなったようです。やはりドリームチームです。
プログラムのテストとしてはもう十分なのですが、プレイヤー0が勝つだけでは面白くないので、もう少し試してみます。
さすがに完全ランダムなPTではドリームチームには勝てないようなので、少し強い人の構築で対戦をさせてみましょう。
【第5回つくオフ使用構築】バレバレバレット(ICKW研Edition)
http://pattulaaaa.blog.fc2.com/blog-entry-23.html
ちょうどいいのがありました。
PTをコピーします。
プレイヤー1のPTもファイルから読み込むように、ソースコードの該当箇所をこう書き換えてみます
Team team2;
team2.loadFromFile("(***ファイルパス***)");
auto team2 = TeamBattle(team2);
さあ、どうでしょうか
参りました。でも接戦だったようです。
まとめ
ということで、Pokemon Onlineのソースコードを対戦ライブラリとして使うことで、簡単なコードで自由にポケモン対戦を実行することができました。
まあ、ライブラリというからにはdll化した方がいいと思いますが、何しろC++もQt Creatorも今回初めて使い始めたのでまだ難しい。
それと、今回のプログラムだと対戦途中に処理が止まったりすることも低確率であるので、まだ間違いもあるかもしれません。
とはいえ記事には書いていませんがこの後、複数の対戦インスタンスの同時実行や対戦中局面のコピーもできるようになったので、今はこれらを使ってもっと面白いことをやろうと考えています。
最後に、ぼくの後輩に似ている二次元画像を紹介します。
ありがとうございました。