Symfony4でポケモンおすすめパーティを自動生成するアプリを作ってみた


カイノと申します。
[Symfony Advent Calendar 2020](https://qiita.com/advent-calendar/2020/symfony)の20日目を担当させていただきます。
明日は @ttskch さんです。

# 投稿の経緯
Laravelのアドベントカレンダー9日目で投稿した[「ポケモンAPIを叩いて使用率ランキングを取得してみた」](https://qiita.com/kaino5454/items/e12a0214c76aea6cdc2b)
の記事ではポケモン使用率ランキングの取得や表示をLaravelで作ってみました。

使用率ランキングと同時によく選出されるポケモン一覧をさくっと見れるものが作れて多少は便利なのですが、
自動でパーティを作ってくれる機能が欲しかったので

Symfonyも始めたばかりですが
勉強もかねてSymfonyで作ってみました。

※[Laravelの方](https://qiita.com/kaino5454/items/e12a0214c76aea6cdc2b)の記事と多少重複するところがあります。

# 完成イメージ
![スクリーンショット 2020-12-20 20.54.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574173/d349653b-2d84-659a-f0c8-8f7162c119be.png)




##環境
Symfony 4.4.18
PHP 7.3.24


##APIについて
ポケモンホームのバトルデータから取得しています。
ページ下部の記事を参考にしているのですが、こちらのAPIはスマホ向けアプリのAPIなのでUAは偽装しています。

##やっていること
- 使用率ランキングの取得コマンドの生成
- コマンドをcronで回す
- 使用率ランキングを表示
- おすすめパーティ表示機能追加
 - 最初の１匹はTOP30からランダム選出
 - 残り5匹は最初の１匹とよく一緒に選出される中からランダムに5引き選出

# 使用率ランキングの取得コマンドの生成

##makerをインストール
```
# コマンドラインからコマンドを作成するため
composer req maker

# 作成できているか確認
php bin/console
```
![スクリーンショット 2020-12-20 20.31.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574173/19d01d16-902c-3518-f495-68f40881e5ce.png)
こんな感じに表示されました。

##コマンド生成

```
## コマンド生成実行
php bin/console make:command

Choose a command name (e.g. app:tiny-kangaroo):
 >getRank(※作成したいコマンド名を入力)


 created: src/Command/GetRankCommand.php

           
  Success! 
           

 Next: open your new command class and customize it!
 Find the documentation at https://symfony.com/doc/current/console.html
```

`Success!`と出れば作成されています。

作成語の中身は下記のようになっています。

```src/Command/GetRankCommand.php
<?php

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class GetRankCommand extends Command
{
    protected static $defaultName = 'getRank';

    protected function configure()
    {
        $this
            ->setDescription('Add a short description for your command')
            ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')
            ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $arg1 = $input->getArgument('arg1');

        if ($arg1) {
            $io->note(sprintf('You passed an argument: %s', $arg1));
        }

        if ($input->getOption('option1')) {
            // ...
        }

        $io->success('You have a new command! Now make it your own! Pass --help to see your options.');

        return 0;
    }
}

```

## コマンドの中身を記述

```src/Command/GetRankCommand.php

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // 使用するフォルダを作成
        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('./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("./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("./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);
        return true;
    }
```

#コマンドをcronで回す

```
crontab -e

##　以下を追加
* 1 * * * php bin/console getRank 1>> /dev/null 2>&1

```



cronは左から、[分] [時] [日] [月] [曜日] [コマンド]のように書きます。
「* 1 * * *」とすることで毎時設定になります。

cronについては[こちら](https://qiita.com/katsukii/items/d5f90a6e4592d1414f99)が参考になると思います。


ここまでの操作で毎時最新のポケモン対戦環境の使用率ランキングを取得ができるようになりました。

# 使用率ランキング一覧表示とおすすめパーティ表示機能追加

## コントローラ生成

```
bin/console make:controller PokemonController


 created: src/Controller/TestController.php
 created: templates/test/index.html.twig

           
  Success! 
           

 Next: Open your new controller class and add some pages!
```

`Success!`と出れば作成されています。

## コントローラ記述

```src/Controller/PokemonController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class PokemonController extends AbstractController
{
    /**
     * @Route("/pokemon", name="pokemon")
     */
    public function index(): Response
    {
        $fp           = fopen("../with_poke_lists.json", 'r');
        $json         = fgets($fp);
        $with_poke_lists = json_decode($json, true);
        fclose($fp);
        $poke_ranks = array_keys($with_poke_lists);

        return $this->render('pokemon/index.html.twig', [
            'poke_ranks' => $poke_ranks,
            'with_poke_lists' => $with_poke_lists,
        ]);
    }

    /**
     * @Route("/pokemon/recommend", name="pokemon_recommend")
     */
    public function pokemonRecommend(): Response
    {
        $fp           = fopen("../with_poke_lists.json", 'r');
        $json         = fgets($fp);
        $with_poke_lists = json_decode($json, true);
        fclose($fp);

        $poke_ranks = array_keys($with_poke_lists);
        // ランキングTOP30からランダム1匹選出
        $poke1 = $poke_ranks[rand(0, 29)];

        //0から9までの数値から抽選を行う
        $nums = range(0, 9);
        shuffle($nums);
        $nums = array_slice($nums, 0, 5);

        $pokemons = [];
        foreach ($nums as $num) {
            $pokemons[] = $with_poke_lists[$poke1][$num];
        }

        return $this->render('pokemon/recommend.html.twig', [
            'poke1' => $poke1,
            'pokemons' => $pokemons,
        ]);
    }
}

```

## 選出ランキング一覧のビュー記述

```templates/pokemon/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}ポケモン採用率ランキング{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
    .poke_rank { border: solid 2px #F5F5F5; margin-top: 60px; }
    .recommend_link {
        display       : inline-block;
        position      : fixed;
        top           : 20px;
        right         : 30rem;
        padding       : 25px 25px;
        background    : #FFA500;
        color         : #ffffff;
        opacity       : 0.5;
        border-radius : 50%;
        font-size     : 20pt;
        font-weight   : bold;
        line-height   : 1.2em;
        letter-spacing: 2px;
        text-align    : center;
        cursor        : pointer;
        z-index       : 999;
        }
        .recommend_link:hover {
        opacity       : 0.9;
        }
</style>

<div class="example-wrapper">
    <h1>ポケモン採用率ランキング</h1>
    <a href="/pokemon/recommend" class="recommend_link">自動おすすめ<br>パーティ</a>
    {% set num = 1 %}
    {% for index in poke_ranks %}

        {% set img_num = index %}
        {# フォーム違い対応 #}
        {% if img_num matches '/_/' %}
            {% set img_num = img_num[0:-2] %}
            {# 三鳥 #}
            {% if img_num in ["144", "145", "146"] %}
                {% set img_num = img_num~"-galar" %}
            {% endif %}
            {# ヒヒダルマ #}
            {% if img_num in "555" %}
                {% set img_num = img_num~"-galar" %}
            {% endif %}
            {# 霊獣 #}
            {% if img_num in ["641", "642", "645"] %}
                {% set img_num = img_num~"-therian" %}
            {% endif %}
            {# ウーラオス #}
            {% if img_num in "892" %}
                {% set img_num = img_num~"_1" %}
            {% endif %}
        {% endif %}
        <div class="poke_rank poke_{{img_num}}">
            {{num}}位<br>
            <img src="sprites/sprites/pokemon/{{img_num}}.png" style="height-max: 200px;width: 200px"/>
            <br>
            <p>【よく一緒に選出されるポケモン】</p>
            {% for pokemon in with_poke_lists[index] %}
                {% set img_num = pokemon['id'] %}
                {# フォーム違い対応 #}
                {% if pokemon['form'] > 0 %}
                    {# 三鳥 #}
                    {% if img_num in ["144", "145", "146"] %}
                        {% set img_num = img_num~"-galar" %}
                    {% endif %}
                    {# ヒヒダルマ #}
                    {% if img_num in "555" %}
                        {% set img_num = img_num~"-galar" %}
                    {% endif %}
                    {# 霊獣 #}
                    {% if img_num in ["641", "642", "645"] %}
                        {% set img_num = img_num~"-therian" %}
                    {% endif %}
                    {# ウーラオス #}
                    {% if img_num in "892" %}
                        {% set img_num = img_num~"_1" %}
                    {% endif %}
                {% endif %}
                <img src="sprites/sprites/pokemon/{{img_num}}.png" style="height-max: 100px;width: 100px"/>
            {% endfor %}
        </div>
        {% set num = num + 1 %}
    {% endfor %}

</div>
{% endblock %}

```

## おすすめパーティ自動生成のビュー記述

```templates/pokemon/recommend.html.twig
{% extends 'base.html.twig' %}

{% block title %}ポケモン採用率ランキング | おすすめパーティ{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
    .poke_party { border: solid 2px #F5F5F5; width: 80%; margin-top: 80px;}
    .recommend_link {
        display       : inline-block;
        position      : fixed;
        top           : 40px;
        right         : 47rem;
        padding       : 25px 25px;
        background    : #FFA500;
        color         : #ffffff;
        opacity       : 0.5;
        border-radius : 50%;
        font-size     : 20pt;
        font-weight   : bold;
        line-height   : 1.2em;
        letter-spacing: 2px;
        text-align    : center;
        cursor        : pointer;
        z-index       : 999;
        }
    .recommend_link:hover {
        opacity       : 0.9;
    }
    .back_link {
        display       : inline-block;
        position      : fixed;
        top           : 40px;
        right         : 38rem;
        padding       : 25px 25px;
        background    : #87CEEB;
        color         : #ffffff;
        opacity       : 0.5;
        border-radius : 50%;
        font-size     : 20pt;
        font-weight   : bold;
        line-height   : 1.2em;
        letter-spacing: 2px;
        text-align    : center;
        cursor        : pointer;
        z-index       : 999;
        }
    .back_link:hover {
        opacity       : 0.9;
    }
</style>

<div class="example-wrapper">
    <h1>自動生成<br>おすすめパーティ</h1>
    <a href="/pokemon/recommend" class="recommend_link">自動生成</a>
    <a href="/pokemon" class="back_link">戻る</a>
    <div class="poke_party">
        {% set img_num = poke1 %}
        {% if poke1 matches '/_/' %}
            {% set img_num = poke1[0:-2] %}
            {# 三鳥 #}
            {% if img_num in ["144", "145", "146"] %}
                {% set img_num = img_num~"-galar" %}
            {% endif %}
            {# ヒヒダルマ #}
            {% if img_num in "555" %}
                {% set img_num = img_num~"-galar" %}
            {% endif %}
            {# 霊獣 #}
            {% if img_num in ["641", "642", "645"] %}
                {% set img_num = img_num~"-therian" %}
            {% endif %}
            {# ウーラオス #}
            {% if img_num in "892" %}
                {% set img_num = img_num~"_1" %}
            {% endif %}
        {% endif %}
        <img src="../sprites/sprites/pokemon/{{img_num}}.png" style="height-max: 100px;width: 100px"/>
        {% for pokemon in pokemons %}
            {% set img_num = pokemon['id'] %}
            {# フォーム違い対応 #}
            {% if pokemon['form'] > 0 %}
                {# 三鳥 #}
                {% if img_num in ["144", "145", "146"] %}
                    {% set img_num = img_num~"-galar" %}
                {% endif %}
                {# ヒヒダルマ #}
                {% if img_num in "555" %}
                    {% set img_num = img_num~"-galar" %}
                {% endif %}
                {# 霊獣 #}
                {% if img_num in ["641", "642", "645"] %}
                    {% set img_num = img_num~"-therian" %}
                {% endif %}
                {# ウーラオス #}
                {% if img_num in "892" %}
                    {% set img_num = img_num~"_1" %}
                {% endif %}
            {% endif %}
            <img src="../sprites/sprites/pokemon/{{img_num}}.png" style="height-max: 100px;width: 100px"/>
        {% endfor %}
    </div>
</div>
{% endblock %}
```

## 画像追加
ポケモンの画像は[こちら](https://github.com/PokeAPI/sprites)から使わせていただきました。

```
cd public
git clone https://github.com/PokeAPI/sprites
```

※ウーラオス（いちげき）、ブリザポス、レイスポスの画像はないので自分でいれる必要があります。



これで完成です！

# 完成後の見た目
## 選出ランキング一覧
![FireShot Capture 136 - ポケモン採用率ランキング - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574173/2027f377-e77d-4107-be5d-5904b6ec0cc4.png)

## おすすめパーティ自動生成ビュー
![スクリーンショット 2020-12-20 21.20.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574173/f1ba9cad-0b55-815b-17fa-bbd82fb299cd.png)

# [Laravelの方](https://qiita.com/kaino5454/items/e12a0214c76aea6cdc2b)の課題で改善したこと
・おすすめパーティを自動でレコメンドしてくれる機能を追加した。
・見た目を少しだけ盛った。
・フォーム違いを画像に反映した(ガラルファイヤーなど）
・cronで定期バッチ処理にした

# 終わりに
今回は、Laravelで書いたことをSymfonyに移し換えることと自動おすすめパーティレコメンド機能の追加をしました。
コマンド生成やtwigへの移し換え、やりたかったバッチ処理追加などができました。

これでようやく、ポケモン選出パを考える時には悩まずにサクッと決められそうです。
タイプ相性微妙じゃね？と思ったらアレンジしてもいいですからね。指針ができるのはいいなと思います。

Symfonyはまだ不慣れなところも多いので、改善点等ありましたらコメントにて教えていただけますと幸いです。

▶︎TO BE CONTINUED

## 参考
・[ポケモンホームAPIを使って今採用すべきポケモンを可視化する](https://qiita.com/Takuya-Shuto-engineer/items/89e5b43aaec90b5353ec)
・[ポケモンホームのバトルデータ（ランクバトル）のJSONを解析する。](https://retrorocket.biz/archives/1178#title-7)