カイノと申します。
Laravel Advent Calendar 2020の9日目を担当させていただきます。
明日は @SanQさんです。
投稿の経緯
最近ポケモン剣盾でレート対戦をプレイしていて、時間に追われる日々の空き時間ではいかんせんパーティを考えたり組む時間があんまりなく、使用率ランキングとパーティ候補をパパッと見たいと思ってました。
使用率ランキングはあっても同時に出てくるパーティ一覧でさくっと見れるものは無かったので、自分で用意でできたら便利だなと思い、Laravelのartisanコマンドで作成しました。
※自動でおすすめパーティを作ろうと思っていたのですが、時間的投入コストの関係でできませんでした。
##APIについて
ポケモンホームのバトルデータから取得しています。
ページ下部の記事を参考にしているのですが、こちらのAPIはスマホ向けアプリのAPIなのでUAは偽装しています。
##やっていること
・シーズン情報API取得
・最新環境のランキングAPI取得
・トップ30匹を抽出
・よく一緒に選出されるポケモンリスト作成
使用率ランキング取得コマンド
少し長いのでコマンド内容を分割して見ていきます。
##対戦環境を取得
まずはシーズンごとの情報を取得します。
今回はphp artisan command:getrank
で実行できるように設定しました。
// 使用するフォルダを作成
if (!is_dir('./JSON')){
mkdir('JSON');
mkdir('JSON/POKEMON_INFO');
mkdir('JSON/RANKING');
mkdir('JSON/SEASON');
}
// 対戦環境を取得
$cmd = "curl https://api.battle.pokemon-home.com/cbd/competition/rankmatch/list \
-H 'accept: application/json, text/javascript, */*; q=0.01' \
-H 'countrycode: 304' \
-H 'authorization: Bearer' \
-H 'langcode: 1' \
-H 'user-agent: Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36' \
-H 'content-type: application/json' \
-d '{\"soft\":\"Sw\"}' \
-o JSON/SEASON/season.json";
echo exec($cmd);
// シーズン・タームのID情報
$fp = fopen('../backend/JSON/SEASON/season.json', 'r');
$json = fgets($fp);
// trueにして連想配列にする
$battle_env = json_decode($json, true);
fclose($fp);
# タームごとに必要な情報だけを配列にまとめる
$terms = [];
foreach ($battle_env['list'] as $data) {
$id_num = array_keys($data)[0];
foreach ($data as $id) {
$season = $id['season'];
$rule = $id['rule'];
$rst = $id['rst'];
$ts1 = $id['ts1'];
$ts2 = $id['ts2'];
$terms[] = array('id' => $id_num, 'season' => $season, 'rule' => $rule, 'rst' => $rst, 'ts1' => $ts1, 'ts2' => $ts2);
}
}
##最新環境のランキング情報を取得
// シーズンごとにループを回す
foreach ($terms as $term) {
if ($term['rule'] == 0) {
$id = $term['id'];
$rst = $term['rst'];
$ts2 = $term['ts2'];
$pokemons_file = $term['id']."-pokemons.json";
// ポケモン情報保存用のフォルダ作成
if (!is_dir("./JSON/POKEMON_INFO/season-$id")){
mkdir("./JSON/POKEMON_INFO/season-$id");
}
// 使用率ランキングを取得
$cmd = "curl -XGET https://resource.pokemon-home.com/battledata/ranking/$id/$rst/$ts2/pokemon -H 'user-agent: Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36' -H 'accept: application/json' -o JSON/RANKING/$pokemons_file";
echo exec($cmd);
// 図鑑Noが200匹ごとに分割されているため5回に分ける
for ($i=0;$i<5;$i++) {
$j = $i + 1;
$pokeinfo_file = $id."-pokeinfo-".$j.".json";
// ポケモンの同時選出、採用技構成や持ち物などを取得
$cmd = "curl -XGET https://resource.pokemon-home.com/battledata/ranking/$id/$rst/$ts2/pdetail-$j -H 'user-agent: Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36' -H 'accept: application/json' -o JSON/POKEMON_INFO/season-$id/$pokeinfo_file";
echo exec($cmd);
}
}
// 他のtermも取得する場合はbreakをコメントアウトしてください
break;
}
##最新環境のポケモン情報を用意
$recent_id = $terms[0]['id'];
$fp = fopen("../backend/JSON/RANKING/$recent_id-pokemons.json", 'r');
$json = fgets($fp);
$pokemon_ranking = json_decode($json, true);
fclose($fp);
// 各ポケモンの情報を用意
$pokemons_info = [];
for ($i=0;$i<5;$i++) {
$j = $i + 1;
$fp = fopen("../backend/JSON/POKEMON_INFO/season-$recent_id/$recent_id-pokeinfo-$j.json", 'r');
$json = fgets($fp);
$pokemon_data = json_decode($json, true);
fclose($fp);
foreach (array_keys($pokemon_data) as $index) {
$pokemons_info[$index] = $pokemon_data[$index];
}
}
トップ30匹を抽出
$top_pokemons = array_slice($pokemon_ranking, 0, 30);
$with_poke_lists = [];
foreach ($top_pokemons as $pokemon) {
##よく一緒に選出されるポケモンリスト作成
// よく一緒に選出されるポケモンリスト
$with_poke_list = $pokemons_info[$pokemon['id']][$pokemon['form']]["temoti"]["pokemon"];
if ($pokemon['form'] != 0) {
$pokemon['id'] = $pokemon['id']."_".$pokemon['form'];
}
$with_poke_lists[$pokemon['id']] = $with_poke_list;
}
// jsonファイルを生成
$json = json_encode($with_poke_lists);
file_put_contents("with_poke_lists.json", $json);
以上になります。
このコマンドを実行することで、トップ30匹の使用率ランキングおよびよく選出されるリストのjsonを作成できました。
コマンド全体
まとめたものをこちらに念のため記載します。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class getRank extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:getrank';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// 使用するフォルダを作成
if (!is_dir('./JSON')){
mkdir('JSON');
mkdir('JSON/POKEMON_INFO');
mkdir('JSON/RANKING');
mkdir('JSON/SEASON');
}
// 対戦環境を取得
$cmd = "curl https://api.battle.pokemon-home.com/cbd/competition/rankmatch/list \
-H 'accept: application/json, text/javascript, */*; q=0.01' \
-H 'countrycode: 304' \
-H 'authorization: Bearer' \
-H 'langcode: 1' \
-H 'user-agent: Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36' \
-H 'content-type: application/json' \
-d '{\"soft\":\"Sw\"}' \
-o JSON/SEASON/season.json";
echo exec($cmd);
// シーズン・タームのID情報
$fp = fopen('../backend/JSON/SEASON/season.json', 'r');
$json = fgets($fp);
// trueにして連想配列にする
$battle_env = json_decode($json, true);
fclose($fp);
# タームごとに必要な情報だけを配列にまとめる
$terms = [];
foreach ($battle_env['list'] as $data) {
$id_num = array_keys($data)[0];
foreach ($data as $id) {
$season = $id['season'];
$rule = $id['rule'];
$rst = $id['rst'];
$ts1 = $id['ts1'];
$ts2 = $id['ts2'];
$terms[] = array('id' => $id_num, 'season' => $season, 'rule' => $rule, 'rst' => $rst, 'ts1' => $ts1, 'ts2' => $ts2);
}
}
// 対戦環境ポケモン情報を取得
foreach ($terms as $term) {
if ($term['rule'] == 0) {
$id = $term['id'];
$rst = $term['rst'];
$ts2 = $term['ts2'];
$pokemons_file = $term['id']."-pokemons.json";
// ポケモン情報保存用のフォルダ作成
if (!is_dir("./JSON/POKEMON_INFO/season-$id")){
mkdir("./JSON/POKEMON_INFO/season-$id");
}
// 使用率ランキングを取得
$cmd = "curl -XGET https://resource.pokemon-home.com/battledata/ranking/$id/$rst/$ts2/pokemon -H 'user-agent: Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36' -H 'accept: application/json' -o JSON/RANKING/$pokemons_file";
echo exec($cmd);
for ($i=0;$i<5;$i++) {
$j = $i + 1;
$pokeinfo_file = $id."-pokeinfo-".$j.".json";
// ポケモンの同時選出、採用技構成や持ち物などを取得
$cmd = "curl -XGET https://resource.pokemon-home.com/battledata/ranking/$id/$rst/$ts2/pdetail-$j -H 'user-agent: Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36' -H 'accept: application/json' -o JSON/POKEMON_INFO/season-$id/$pokeinfo_file";
echo exec($cmd);
}
}
// 他のtermも取得する場合はbreakをコメントしてください
break;
}
// 最新環境ランキング取得
$recent_id = $terms[0]['id'];
$fp = fopen("../backend/JSON/RANKING/$recent_id-pokemons.json", 'r');
$json = fgets($fp);
$pokemon_ranking = json_decode($json, true);
fclose($fp);
// 各ポケモンの情報を用意
$pokemons_info = [];
for ($i=0;$i<5;$i++) {
$j = $i + 1;
$fp = fopen("../backend/JSON/POKEMON_INFO/season-$recent_id/$recent_id-pokeinfo-$j.json", 'r');
$json = fgets($fp);
$pokemon_data = json_decode($json, true);
fclose($fp);
foreach (array_keys($pokemon_data) as $index) {
$pokemons_info[$index] = $pokemon_data[$index];
}
}
// トップ30匹を抽出
$top_pokemons = array_slice($pokemon_ranking, 0, 30);
$with_poke_lists = [];
foreach ($top_pokemons as $pokemon) {
// よく一緒に選出されるポケモンリスト
$with_poke_list = $pokemons_info[$pokemon['id']][$pokemon['form']]["temoti"]["pokemon"];
if ($pokemon['form'] != 0) {
$pokemon['id'] = $pokemon['id']."_".$pokemon['form'];
}
$with_poke_lists[$pokemon['id']] = $with_poke_list;
}
$json = json_encode($with_poke_lists);
file_put_contents("with_poke_lists.json", $json);
}
}
php artisan command:getrank
で取得できます。
ビューに表示
作成したjsonを使って簡単に表示してみました。
<body>
<div class="content">
<div class="title m-b-md">
ポケモンバトル<br>
採用率ランキング
</div>
<?php
$fp = fopen("../with_poke_lists.json", 'r');
$json = fgets($fp);
$with_poke_lists = json_decode($json, true);
fclose($fp);
foreach (array_keys($with_poke_lists) as $index) {
echo "<br>";
if ($index <= 895){
$img_num = $index;
if (preg_match("/_/", $img_num)) {
$img_num = substr($index, 0, -2);
if (in_array($img_num, array("641", "642", "645"), true)) {
$img_num = $img_num."-therian";
}
}
$fp = fopen("../sprites/sprites/pokemon/$img_num.png", "rb");
$img = fread($fp, 4000);
fclose($fp);
$enc_img = base64_encode($img);
$imginfo = getimagesize('data:application/octet-stream;base64,' . $enc_img);
echo '<img src="data:' . $imginfo['mime'] . ';base64,'.$enc_img.'" style="width: 200px">';
}
foreach ($with_poke_lists[$index] as $pokemon) {
if ($pokemon['id'] <= 895){
$img_num = $pokemon['id'];
// フォーム違い対応
if ($pokemon['form'] != 0) {
if (in_array($img_num, array(641, 642, 645), true)) {
$img_num = $img_num."-therian";
}
}
$fp = fopen("../sprites/sprites/pokemon/".$img_num.".png", "rb");
$img = fread($fp, 4000);
fclose($fp);
$enc_img = base64_encode($img);
$imginfo = getimagesize('data:application/octet-stream;base64,' . $enc_img);
echo '<img src="data:' . $imginfo['mime'] . ';base64,'.$enc_img.'">';
}
}
echo "<br>";
}
?>
</div>
</body>
↓見た目はこんな感じになります。(2020/12/09 1時ごろのランキング)
サンダー環境ですね。本当にサンダーが多いみたいでレジエレキ以外の29匹はサンダーがパーティ候補として並んでいました。
一覧をさらっと見れるの結構便利かもです。
画像
ポケモンの画像はこちらから使わせていただきました。
超絶感謝!!
※ブリザポス、レイスポスは2020/12/9では入ってませんでした。
#課題
・おすすめパーティを自動でレコメンドしてくれる機能をほんとは入れたかった。
・見た目が簡素過ぎるのはすみません。
・curlコマンドをphp仕様にしていないため、正確なエラーなどを取れない。
・フォーム違いを画像に反映してない(ウーラオスなど)
・cronで定期バッチ処理にする
終わりに
ポケモンのフィルターがかかると開発がめちゃめちゃ楽しくて、仕事終わりにやっていたのですが楽しすぎて気付いたらすごい時間になってたりしました。
artisanコマンドくらいしか触ってませんが、MVC的な部分scheduleを実装してより使いやすいものに今後していき、Laravelの知見を深めていきたいと思います。
せっかくLaravelのアドベントカレンダーを担当させていただきましたが、少し中途半端感があるのはすみません。
ですがなんとか投稿できて良かったです!
▶︎TO BE CONTINUED
※追記
↓symfonyで少しグレードアップさせて書いてみました。
Symfony4でポケモンおすすめパーティを自動生成するアプリを作ってみた
参考
・ポケモンホームAPIを使って今採用すべきポケモンを可視化する
・ポケモンホームのバトルデータ(ランクバトル)のJSONを解析する。