I'm KUITARIDER.

がりゅうさんのサイキックミラクルブログ

Pokemon Onlineをポケモン対戦ライブラリとして使う

こんにちは

 

ポケモン対戦って複雑なゲームですよね

一言で言うなら「相手のHPを先に0にした方が勝ち」ですが、相手のHPを0にするための攻撃技に様々な効果があったり、相手のHPは減らさないけど動きを止めて戦いを有利にする変化技があったり、攻撃技でHPを減らさなくても勝つ場合があったりします

そういうこともあって、ポケモン対戦に似たプログラムを自作しようとすると想像以上に大変です。

 

今回はPokemon Online という有名なポケモン対戦シミュレータのソースコードを使って、これを製作することを考えました。

 

Pokemon Online について

知ってる人と知らない人が半々ぐらいだと思うので説明します。

Pokemon Online

http://pokemon-online.eu/

f:id:shingaryu:20151227021649p:plain

 

実機でのポケモン対戦をほぼ完璧にコピーした、非公式のオンラインゲームです。クライアントアプリケーションをダウンロードして、有志の方々が立てている各所のサーバーに接続して遊びます。

ポケモンとそのステータス・持ち物を指定するだけで簡単にPTが作れ、そのまま対戦が行えます。

実機と比べるとポケモンを育成する手間が省けるので、PTの試運転などに使う人はよくいるようです。

 

似たようなものに、Pokemon Showdown があります。こちらはwebベースですが、対戦シミュレータとしての機能はほぼ同等です。

Pokemon Showdown

http://pokemonshowdown.com/

 

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以外のディレクトリはプロジェクトから除外した方がいいでしょう。ビルドにかかる時間が大幅に短くなります。

f:id:shingaryu:20151227021932p:plainf:id:shingaryu:20151227021939p:plain

 

 

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使用率ランキング上位のドリームチームです。

f:id:shingaryu:20151227022352p:plain

左に写っているのはTeamの保存ダイアログです。ここで保存したファイルをプログラム中で読み込むというわけです。

 

あとは既存のmain.cppの中身をコメントアウトして、

    auto lbt = new LocalBattleTest();
    lbt->testBattleExecute();

とでも書くだけで実行できます。やってみます。

 

コンソール画面が現れてからはほんの数秒間で全ての対戦処理が終わります。さすが21世紀のパソコンです。

f:id:shingaryu:20151227030436p:plain

プレイヤー0の圧勝で無事対戦が終了したようです。さすがドリームチームです。

 

もう一度実行してみます。

f:id:shingaryu:20151227030459p:plain

おそらくファイアローが繰り出したであろうブレイブバードで、対戦ありがとうございましたとなったようです。やはりドリームチームです。

 

 

プログラムのテストとしてはもう十分なのですが、プレイヤー0が勝つだけでは面白くないので、もう少し試してみます。

さすがに完全ランダムなPTではドリームチームには勝てないようなので、少し強い人の構築で対戦をさせてみましょう。

 

 

【第5回つくオフ使用構築】バレバレバレット(ICKW研Edition)

http://pattulaaaa.blog.fc2.com/blog-entry-23.html

ちょうどいいのがありました。

 

PTをコピーします。

f:id:shingaryu:20151227030524p:plain

 

プレイヤー1のPTもファイルから読み込むように、ソースコードの該当箇所をこう書き換えてみます

    Team team2;
    team2.loadFromFile("(***ファイルパス***)");
    auto team2 = TeamBattle(team2);

 

さあ、どうでしょうか

 

044148_00

参りました。でも接戦だったようです。

 

 

まとめ

ということで、Pokemon Onlineのソースコードを対戦ライブラリとして使うことで、簡単なコードで自由にポケモン対戦を実行することができました。

まあ、ライブラリというからにはdll化した方がいいと思いますが、何しろC++もQt Creatorも今回初めて使い始めたのでまだ難しい。

それと、今回のプログラムだと対戦途中に処理が止まったりすることも低確率であるので、まだ間違いもあるかもしれません。

とはいえ記事には書いていませんがこの後、複数の対戦インスタンスの同時実行や対戦中局面のコピーもできるようになったので、今はこれらを使ってもっと面白いことをやろうと考えています。

 

 

最後に、ぼくの後輩に似ている二次元画像を紹介します。

f:id:shingaryu:20151227030544j:plain

 

ありがとうございました。

つくポケを振り返って

俺がつくばポケモン大好きクラブ、通称つくポケの存在を初めて知ったのは大学2年の6月、TLにこのツイートが流れてきたとき。

最初に思ったのは「なんでくら寿司?」ということだったが、とにかく、こんなものいつの間にできていたんだろうという感想だった。気にならなかったわけではないが、文面に「顔合わせ」と入っているのもあって、メンバーはもう確定していて、新たに募集しているわけではないのかと思ったのであまり深追いはしなかった。

 

ツイートの日付を見てもらうと分かるが、実はこの時期はBW2の発売と重なっていて、俺のポケモン熱は5歳からのポケモン人生の中で一番の盛り上がりを見せていた。

それでいて、大学2年の6月。適度に暇である。【つくばポケモン大好きクラブ】とやらの存在をそのまま見過ごせるわけはなかった。ということで、とりあえずそのツイートの主であるハイタカはフォローしておいた。

 

それなら早く連絡をとって加入すればよかったわけだが、いかんせん実態が分からない。T-ACTで活動していることは分かったが、T-ACTがよく分からないので少し気が引ける(当時は意識高い系のわけのわからないセミナーの後援組織かなんかかと思ってた)

 

そうこうしているうちに夏休みが始まり、終わる。大学生の長い夏休み中はもちろん、ポケモンに明け暮れていた(と思ったが、Twilogによると実際はPSPの「那由多の軌跡」に明け暮れていたらしい。思い出は美化されるもの)。思い出したようにT-ACTのつくばポケモン大好きクラブのページを見ると、98日に初()ミーティングをやるようである。

 

さすがにもう覚悟を決めようと思った。初ミーティングというキリの良いタイミングは逃したくなかったからだ。

結局当日の朝まで悩んだのだが、朝の8時にわざわざ起きだしてTwitterを開いているところからして答えは決まっているわけで、

最高に分かりづらいが、これがつくポケ入りを決心した瞬間のツイートである。

「つくポケに入る」とは一言も言ってないあたりが俺の人間性を伺えるところであるが、とにかくこの後正式にT-ACTに登録し、その日のMTに参加して、晴れてつくポケ入りを果たすこととなった。

 

普通に考えて悩みすぎなのだが、俺は1100円の栗どら焼きとチョコどら焼きのどちらを買うかで5分程度思考するレベルの優柔不断なので仕方ない。ちなみにチョコどら焼きはマズかった。

 

そこからつくポケ公認までは、ひたすらGoogleの素晴らしさを宣伝するおじさんと化していた。つまりひとたびGoogleアカウントを取得すれば、メーリングリスト・ブログ・ファイル共有・Wikiが全て1つのアカウントで、無料で実現できること、その素晴らしさを説いた。結局つくポケは創立から今までこの形態で問題なく運営を続けているわけで、これはがりゅうさんの素晴らしき功績として誇りたいところだが、たぶん元々ハイタカも同じことを考えていたと思うので、あまり関係ない。あとは、クイタラン人形を並べてブログのヘッダー画像を撮った。

 

2013322日に、つくばポケモン大好きクラブが次年度から一般学生団体として公認されるとの連絡が来た(ハイタカ)。つくばポケモン大好きクラブ Epsode: 0 終了である。

----------------------------------------------------------------------------------------------------------------

 

2013年度。新歓で1年生が入ってきてようやく普通のサークルらしくなった。KKK(関東国公立交流会)などもあって他大学のポケサーを意識するようになり、その界隈のフォロワーも増えた。ツイート数も増えた。そしてこの頃から変なbotを作り始める。

個人的には、12月に行われたポサミ(全国ポケサーサミット)が印象に残っている。批判的な勢力も含めて、全国のポケモンサークルの注意を惹きつけたイベントだったからだ。素直に、ポケモンサークルってこんなにあるのかと感心した記憶がある。あとマスダが見れた。

ポケモンへの興味はというと、ポケサー員として対戦イベントに参加する機会も増えたので、前半は対戦に時間を注いだ。XY発売前後からは個体値の生成過程やセーブデータ解析の可能性など、内部のゲームメカニクスの方に関心を向けるようになり、海外フォーラムも覗き始めた。

----------------------------------------------------------------------------------------------------------------

 

2014年度は役職も引退し、一応だが研究室というしがらみもできたので意図的に活動に参加する回数を減らした。というか元々兼サーしていたもう1つの方の活動にちゃんと参加するようになった。すると不思議なもので、ポケモンモチベもどんどん落ちてくる。残念ながら社会的にはそれで全く問題ないのだが、対戦以外のイベントにも参加しなかったり、段々と出不精になっていったのは良くなかったかもしれない。忘年会にはきちんと参加してデカイ枕をプレゼント交換できたのでよかった。

ポケモンモチベが落ちたと言っても、アプローチの方法を変えただけでそれなりにポケモンのことは考えていた。自分の頭だけで考えて対戦をすると良い成績が残せないことは大体分かってきたので、多少は上がったプログラミング能力を活かして、ポケモン対戦に計算機の力を取り入れる方法を模索していた。とはいえそれはそれで難しいわけで、今のところはタイプ相性の良いPTの自動生成くらいしか実現できていないが…

----------------------------------------------------------------------------------------------------------------

 

 

何の技術も身につかない、何の経歴にもならないサークルだが、だからこそ面白さは世界一だった。ポケモンサークルと名乗っておきながら、ポケモン以外でも多彩な活動範囲を持つサークルだった。男11人でスイーツを食べに行ったり、何かを決めるたびにすぐビンゴを始めたりするサークルに入るつもりはなかったのだが、どういうわけか楽しんでしまった。

汗も涙も特になかった気はするが、結局大学生活で一番の思い出になった。

大学2年の夏から2年半、つくポケに居られて本当に楽しかった。

 

……………………。

 

 

 

 

 

 

あと2年あるやないかーいwwww (大学院進学)

数値計算でポケモンのパーティを作る

※2021/07/07 追記 ブログの移行にミスって画像が消えてます。リクエストあれば書き直します。

ポケモン(シングルバトルを想定)のパーティを数値計算で自動生成してしまおうというお話

 

PTの強さを数値化できるモデルを用意し、その値が最大になるようなポケモンの組み合わせを探す。

計算は単純なダメージレースだけで、特性や補助技は一切考慮していないので実用性はないが、これから発展させていくとしてまずは現段階での結果報告。

 

Pokemonクラス

最終的に「全てのポケモンの組み合わせについてPTの強さを計算」という形になるので、ポケモン1匹を表すオブジェクトを用意する。

つまりこのようなクラスを作成して、そのインスタンスで特定のステータスを持つポケモン一匹を表す。

 

さて、問題になるのがこの中身。700匹以上いるポケモンのタイプ・種族値などの情報をどこから取得するかということ。さすがに手動はアレなのだが

 

いいものがあった。一応説明すると画像はPokemon Showdown (http://pokemonshowdown.com/)というポケモンバトルシミュレーターのソースコード(GitHubで公開されている)。

Pokemon Showdownは非公式とは思えないほどゲームシステムの再現性が極めて高く、本家の新バージョンが出たときなどの更新も速い。ということでこれのポケモンデータベースを拝借すれば信頼できるポケモン情報が手に入る。

 

pokedex.jsをダウンロードすれば後はテキストファイルとして煮るなり焼くなり好きに処理できるはず。個人的な趣味だが今回は内容を自前のデータベースに保存し直した。

 

このデータベースから情報を取得し、前述のポケモンオブジェクトを作成していく。

 

性格・努力値振り推定

ポケモンクラスの各メンバーは基本的にデータベースから取得すればいいのだが、性格・努力値はいわゆる"育て方"によって異なるパラメータであり、データベースに載っているようなあらかじめ決まりきった設定値ではない。そのため、自分で設定してやる必要がある。

今回は以下のような基準で決定した。

 

性格

上昇箇所:種族値の最大箇所

下降箇所:種族値のA・Cを比べてどちらか低い方

 

努力値の配分は基本的に種族値の上位2箇所

ただし、

・S100以上の場合無条件でS特化(性格もS上昇のものにする)

・B、DよりはHPを優先させる(例えばABはHAに、CDはHCに変更する)

 

標準ダメージ比

ポケモン同士の1:1の相性(どちらがどれくらい有利か)を定量的に評価する方法。これを発展させてPTの強さ計算へと結びつける。今回は標準ダメージ比という指標を設定した。

まず標準ダメージの説明

標準ダメージの定義:自分の攻撃タイプのうち相手に対してより有効な方を用いて、威力90の技で攻撃したときのダメージ割合

例えばドリュウズ対ファイヤーだとすると、

ドリュウズのファイヤーに対する標準ダメージD1は、ドリュウズが威力90の鋼タイプの技でファイヤーを攻撃したときのダメージ割合

ファイヤーのドリュウズに対する標準ダメージD2は、ファイヤーが威力90の炎タイプの技でドリュウズを攻撃したときのダメージ割合

 

これの比をとったものを相性値とする。つまり

ドリュウズのファイヤーに対する標準ダメージ比:D1/D2

ファイヤーのドリュウズに対する標準ダメージ比:D2/D1

であり、おそらく後者の方が大きな値となる。

 

定義から分かるように、標準ダメージ比は正の値をとり、1より大きければそのポケモンに対して有利で、1より小さければそのポケモンに対して不利である。

 

素早さの考慮

上の定義だけだと6つの能力値のうち素早さだけが全く考慮されない計算となってしまうので、補正をかけ、先手をとって攻撃することの優位性を組み込む。

A,BのうちAの素早さの方が高かったとする。A、Bがそれぞれ相手の攻撃によってNa, Nb回で倒れるとして、

Na >= NbならばAの標準ダメージ比にNb / (Nb - 1) を、Bの標準ダメージ比にその逆数をかける。

 

どういうことかというと、先攻で(そのターンの相手の返しの攻撃を食らわずに)相手を倒したときには攻撃回数が実質的に1回増えているという考え。

ちなみに、一発で倒した場合にはこの補正項は無限大になってしまうので、最終的な標準ダメージ比の値の方で適当に上限を設けている。

 

相性ベクトルの生成

PTの強さ計算の下準備。高度な線形代数を使うわけではないが、情報を二次元的に格納するためにベクトルを使う。

1つのポケモンについて以下のような相性ベクトルを用意する。これはあらゆるポケモンに対する標準ダメージ比の値を各成分にもつベクトルである。

(例)ハッサムの相性ベクトル

 

実際には、このベクトルの次元は考慮するポケモンの総数になるので、数百を超えるのが普通である。

PTの強さ

一番重要な部分なのでどう計算するか試行錯誤を繰り返したが、計算時間の制約もあり今のところは以下のような単純な方法に落ち着いている。おそらく最適ではない。

 

ある組み合わせのPTについて、各ポケモンの相性ベクトルを用意する。

 

各成分の最大値を集めたベクトルを作る。

 

ノルムを求めてPTの強さとする。

 

というわけで、PT(ポケモン1~ポケモン6)の組み合わせを総当りして、最大値ベクトルのノルムが一番大きい組み合わせを一番強いPTとして出力すれば目的が達成される。

感覚的には、どのポケモンにも対抗できる手段をもつPTの探索。ただし、ポケモン一種類についての対策度合いを最大値でのみ判断するため、対策の"厚み"とでもいう部分の評価に欠けており、実戦に応用しようとするとPTの一匹が過労死するようなことがあるかもしれない。

 

計算結果

何はともあれ結果です。

 

まず、考慮したポケモン集団はデジポケ(http://degipoke.pro.tok2.com/)の「XY環境KP集計」から取得した上位100匹(からミュウツーを除いたもの)

ガブリアス

ファイアロー

ギルガルド

ガルーラ

バシャーモ

ポリゴン2

クレセリア

ナットレイ

ゲッコウガ

マリルリ

ボーマンダ

キノガッサ

スイクン

バンギラス

ギャラドス

ランドロス

ニンフィア

クチート

カイリュー

ヒードラン

サンダー

ハッサム

サーナイト

ボルトロス

サザンドラ

ルカリオ

ローブシン

マンムー

ゲノセクト

ウルガモス

カバルドン

クレッフィ

ドリュウズ

ラグラージ

エルフーン

ライコウ

ラティオス

ヤミラミ

ヘラクロス

ジャローダ

キリキザン

トゲキッス

ミミロップ

メタグロス

ラティアス

グライオン

シャンデラ

ブラッキー

ライボルト

エーフィ

マニューラ

ジバコイル

エアームド

チルタリス

ブリガロン

ミロカロス

ヌメルゴン

エルレイド

ランターン

ソーナンス

ラッキー

ドサイドン

サンダース

エンテイ

ラプラス

トリトドン

モロバレル

バルジーナ

メタモン

ゴウカザル

ブルンゲル

オンバーン

クロバット

カビゴン

エンペルト

ボスゴドラ

ホルード

デンリュウ

ジュカイン

ニョロトノ

ヌオー

ドラミドロ

ビビヨン

シャワーズ

デスカーン

テラキオン

ニャオニクス

カイロス

ドンファン

ハピナス

ズルズキン

オーロット

ワルビアル

ムクホーク

バクーダ

サマヨール

オニゴーリ

ユキノオー

ヒヒダルマ

 

 

組み合わせるポケモンは全てこの中から選び出し、PTの強さはこの集団に対して計算するということ。

 

結果(生成されたPT)の評価方法について

1つの指標として、ポケモン徹底攻略(http://yakkun.com/)のタイプバランスチェッカーでの評価結果を載せる。

もちろんタイプ相性だけでPTの強さを計算しているわけではないのだが、それでも相性計算に支配的な要素ではあるため。あとポケモンのアイコンをちょうどいい感じに並べてくれる。

 

さて、組み合わせを総当りして~と書いたが、計算時間の都合上6匹の組み合わせ全て(= 100C6 = 1,192,052,400通り)は厳しかったので、最初にPTの一部のポケモンを指定し、残りのポケモン枠に対して組み合わせ計算を行った。

前3匹を固定(その選び方自体はランダム)して、最適な後ろ3匹の組み合わせを探索した結果を羅列。

ポケモン力が低くて気の利いたコメントが書けないのでただ羅列するだけ。

 

 

 

 

 

というわけで、どの場合でも結局のところ似たようなポケモンが出現していることがわかる。

強いポケモンを3匹組み合わせればそれだけで見た目上のバランスはとれるということか。

 

最後に、前2匹を固定して後ろの4匹を出力した結果。

タイトルを「タイプバランスチェッカーで高得点をとれるPTの自動生成手法の開発」に変えた方がいいかもしれない