600日後に筆者はこうなりました
非エンジニアながら、React
、TypeScript
、ChakraUI
でWebアプリケーション化しています
ポケモン対戦は覚えることが多い
ポケモン対戦をやったことある方は、この章については読み飛ばしていただいて問題ありません。
なぜそもそも、ポケモン対戦を題材としたのかということについてです。自分の趣味ということもありますが、最大の理由として、ポケモン対戦は覚えることが非常に多く、初心者の参入障壁が高いという問題点があると考えているためです。
現在、ポケモン対戦は毎年世界大会が実施され、国内でも新作が発売されると、10万人程度が参加するコンテンツとなりました。youtubeなどの動画サイトでも対戦の模様は動画として上がっており、もし興味が湧いたら検索してご覧いただきたいのですが、まあ覚えることが多い。
- 種族値・個体値・努力値(公式名称では無いです。)
- ポケモンの特性
- 技の効果
- 対戦でよく使われる育成方法、いわゆる型 などなど
初心者が負けて覚えるのは当然ですが、それでも少しでも勝つ喜びを味わってもらう補助ができないかと考え、ツールの作成に着手しました。
#対戦中の負担を軽減する
- 対戦始めたばかりの方が、少しでも対戦しやすくなるツール
- 自分が対戦中にど忘れした際、簡単に確認できるツール
この2点を意識して、表示する項目を選定しました。
今回は対戦の勝敗に直結すると考えている
- 特性
- 素早さ
慣れていてもど忘れしてしまう
- 威力が相手の体重に依存する技の威力
この3つを、まず簡単に調べることができるツールに決定。
さらに、対戦中は常に90秒という選択時間に追われることを考え、パッと入力してパッと答えが返ってくる上に扱いが簡単な、linebotをインターフェイスとして活用することとしました。
なぜ、素早さが対戦の勝敗に直結するかは、非常に長くなるので本記事では割愛します。
環境・利用API
node v16.10.0
Visual Studio Code 1.60.2
axios 0.22.0
ngrok 2.3.40
PokeAPI
https://pokeapi.co/
ポケモンのAPIとしては他にもありましたが、データの網羅性が高く、今後の機能拡張がしやすい点を考えて、PokeAPIを選択しました。
サンプルコード
LINE Messaging APIとPokeAPI それぞれのコードを記載します。
LINE Messaging API / Axiosのコード
'use strict';
// ########################################
// 初期設定など
// ########################################
// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
// ローカル(自分のPC)でサーバーを公開するときのポート番号です
const PORT = process.env.PORT || 3000;
// Messaging APIで利用するクレデンシャル(秘匿情報)です。
const config = {
channelSecret: '作成したBotのチャネルシークレット',
channelAccessToken: '作成したBotのチャネルアクセストークン'
};
// ########## ▼▼▼ サンプル関数 ▼▼▼ ##########
// (この行をサンプル関数丸ごと全部と置き換えてね)
// ########## ▲▲▲ サンプル関数 ▲▲▲ ##########
// ########################################
// LINEサーバーからのWebhookデータを処理する部分
// ########################################
// LINE SDKを初期化します
const client = new line.Client(config);
// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
// 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します
if (event.type !== 'message' || event.message.type !== 'text') {
return Promise.resolve(null);
}
// サンプル関数を実行します
return sampleFunction(event);
}
// ########################################
// Expressによるサーバー部分
// ########################################
// expressを初期化します
const app = express();
// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
// 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行
if (req.body.events.length === 0) {
res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します(なくてもよい)
console.log('検証イベントを受信しました!'); // ターミナルに表示します
return; // これより下は実行されません
} else {
// 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します
console.log('受信しました:', req.body.events);
}
// あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
// 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});
// 最初に決めたポート番号でサーバーをPC内だけに公開します
// (環境によってはローカルネットワーク内にも公開されます)
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);
PokeAPIによるデータ取得
const getPoke = async (userId, tag) => {
let result = '';
try {
// axiosでPokeAPIを叩きます
const res = await axios.get('https://pokeapi.co/api/v2/pokemon/' + encodeURIComponent(tag));
const item = res.data;
const speed = item.stats[5].base_stat;
const weight = item.weight;
let powerWeight = '';
if(weight < 90){
powerWeight = '20';
} else if(weight < 249){
powerWeight = '40';
} else if(weight < 499){
powerWeight = '60';
} else if(weight < 999){
powerWeight = '80';
} else if(weight < 1999){
powerWeight = '100';
} else{powerWeight ='120'
}
try{
const abi2 = item.abilities[2].ability.name;//特性が3つある
const abi0 = item.abilities[0].ability.name;
const abi1 = item.abilities[1].ability.name;
result = `特性1 ${abi0}
特性2 ${abi1}
特性3 ${abi2}
素早さ種族値 ${speed}
くさむすび、けたぐりの威力${powerWeight}`;
console.log(`「${tag}」`);
} catch(error) {
try{const abi1 = item.abilities[1].ability.name;//特性が2つある
const abi0 = item.abilities[0].ability.name;
result = `特性1 ${abi0}
特性2 ${abi1}
素早さ種族値 ${speed}
くさむすび、けたぐりの威力${powerWeight}`;
console.log(`「${tag}」`);
} catch(error) {const abi0 = item.abilities[0].ability.name;
result = `特性1 ${abi0}
素早さ種族値 ${speed}
くさむすび、けたぐりの威力${powerWeight}`;
console.log(`「${tag}」`);
}
} // 正常に取得できればここで終了
} catch (error) {
// HTTPステータスコードが404ならタグが見つからない、それ以外は別のHTTPエラーです
const { status, statusText } = error.response;
if (status == 404) {
result = 'ポケモンの名前を正確に入力してください。英語で。';
} else {
result = `エラー: ${status} ${statusText}`;
}
}
// リプライではなく「プッシュ」を送ります
// Botからユーザーへ一方的に通知を送ることができる機能です
await client.pushMessage(userId, {
type: 'text',
text: result,
});
}
const sampleFunction = async (event) => {
// ユーザーIDとメッセージ文字列を関数に渡します
// メッセージ文字列でQiitaタグを検索し、結果をQiitaから受け取ったらユーザーIDに対して「プッシュ」送信します
// 検索には少し時間がかかるので、これの結果は後でユーザーに送られます
getPoke(event.source.userId, event.message.text);
// こちらが先に返事を返します
// ユーザーからのメッセージに対する「リプライ」です
// リプライは、受信したメッセージ1つにつき1回しか使えません
return client.replyMessage(event.replyToken, {
type: 'text',
text: '検索しています……'
});
};
特性の数が変わると、ポケモンによってJSONの形式が異なるため、単純な繰り返し処理でのデータ取得ではエラーが発生するため、苦戦しました。
今回は例外処理を用いましたが、コードが非常に汚いので改善が必要かと思います。
動作
ポケモンの名前を英語で入力すると、特性、素早さ、体重依存の技を受けるときの威力が返ってきます。
必要なデータがサクサクと取れるので、対戦中確認したいときには一々ネットで調べる必要もなく便利です。
今後の追加機能
- 日本語化機能
- ダメージ計算機能
- 体重差に応じた技威力の表示
まずは、日本語化でしょうか。私のようにニックネームを英語にしたいから英語でプレイするという方ばかりではないと思いますので。
他には、入力した内容を判別して、〇〇が✖️✖️で△△を攻撃した時のダメージは□□といった具合で、ダメージ計算ツールも実装できればより使いやすいかと考えています。ダメージ計算ツール自体は巷に溢れているため、ひとまずの実装優先度は低いと考えて一旦見送り。
また、攻撃する側とされる側の体重差に応じて技威力が変わる技については、あまり簡単に計算してくれるツールが見当たらないので、できれば実装したいのですが、これも該当する技の利用頻度が低いため、ひとまず見送っています。