バトルシステムの実装
今回は「急所」と「乱数」と「タイプ一致」の判定と補正を実装していきます。
ちなみにですが、ポケモンwikiを熟読したところ、補正値の計算にも順番があり、計算後に小数点の切り捨てや五捨五超入をするなど、そこそこ複雑な計算順序がありましたが、今回はそこまで精密に再現せず、補正値(M)は一気にもとめて最終的に乗算するという方式を取ります。もしこのせいであまりにもおかしな計算結果が算出されるようであれば、そのタイミングで見直しをします。
急所の判定
ポケモンでは技を使用すると一定確率で急所(クリティカル)が発生します。初代は「すばやさ」のステータスに依存しており、すばやさが高いポケモンを使えばほぼ100%の確率でヒットしていました。そして、世代を経ることに、急所率や補正値は大きく変更を繰り返しています。なので、PHPポケモンでは最新世代の判定を参考に実装していきます。
ポケモンwiki 急所
まず、急所は「ランク」によって発生率が異なります。通常、急所ランクは0となりますが、急所に当たりやすい技(はっぱカッター、きりさく等)を使用すれば、ランクが+1されます。他にもアイテムによる上昇や、きあいだめなどの変化技でもランクが上昇していきます。
今回参考にするランク補正は、第7世代以降の判定を用います。
ランク+0 → 1/24(4.17%)
ランク+1 → 1/8(13.5%)
ランク+2 → 1/2(50%)
ランク+3以上 → 1/1(100%)
上記の確率で、急所に命中するものとします。そして、急所に当たった際の補正値には1.5倍を用います。
それでは、攻撃用トレイトに急所の判定を加えていきましょう。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
<?php
trait AttackTrait
{
/**
* 省略記号
*
* @var L レベル
* @var A 攻撃値
* @var D 防御値
* @var P 威力
* @var M 補正値
*/
/**
* 補正値
* @var float 小数点第2位までの数値
*/
private $m = 1;
/**
* 攻撃する
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param string $move_class
* @return array
*/
protected function attack($atk_pokemon, $def_pokemon, $move_class)
{
// 技のインスタンスを取得
$move = $this->getInstance($move_class);
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getName().'は'.$move->getName().'を使った!');
// タイプ相性チェック
$type_comp_msg = $this->checkTypeCompatibility($move->getType(), $def_pokemon->getTypes());
// 「こうかがない」の判定(命中率と威力がnullではなく、タイプ相性補正が0の場合)
if(!is_null($move->getAccuracy()) && !is_null($move->getPower()) && ($this->m === 0)){
// こうかがない
$this->setMessage($def_pokemon->getName().'には効果が無いみたいだ');
return;
}
// 命中判定
$hit = $this->checkHit($move->getAccuracy());
if(!$hit){
// 攻撃失敗
$this->setMessage('しかし'.$atk_pokemon->getName().'の攻撃は外れた!');
return;
}
// 必要ステータスの取得
$stats = $this->getStats($move->getSpecies(), $atk_pokemon, $def_pokemon);
// ダメージ計算
if($move->getSpecies() !== 'status'){
/**
* 物理,特殊技
*/
if(!is_null($move->getPower())){
// 急所判定(固定ダメージ技は判定不要)
$critical = $this->checkCritical($move->getCritical());
if($critical){
$this->setMessage('急所に当たった!');
}
}
// ダメージ計算
$damage = $this->calDamage(
$atk_pokemon->getLevel(), # 攻撃ポケモンのレベル
$stats['a'], # 攻撃ポケモンの攻撃値
$stats['d'], # 防御ポケモンの防御値
$move->getPower(), # 技の威力
$this->m, # 補正値
);
// タイプ相性のメッセージを返却
$this->setMessage($type_comp_msg);
}else{
/**
* 変化技
*/
$damage = 0;
}
// 返り値
return $damage;
}
※前回からの変更点として、補正値の格納用にプロパティ($this-m)を用意しました。また、必要ステータス(AとD)の取得用としてgetStatsというメソッドを作成しました。最後に第21回時点での最終コードを載せておくので、そちらを参考にしてください。
それでは、まずは急所判定について、ダメージ計算の部分を確認していきましょう。
// ダメージ計算
if($move->getSpecies() !== 'status'){
/**
* 物理,特殊技
*/
if(!is_null($move->getPower())){
// 急所判定(固定ダメージ技は判定不要)
$critical = $this->checkCritical($move->getCritical());
if($critical){
$this->setMessage('急所に当たった!');
}
}
変化技では急所判定は不要のため、物理または特殊技の場合のみ判定をしています。また、技威力がnull(固定ダメージ技等)でも急所判定は不要のため、is_nullを使用して分岐させました。
技の命中と同じく、判定用メソッド(checkCritical)でtrueかfalseを判定させ、trueが返ってきた場合のみ急所にあった旨のメッセージを返す仕様です。そして、判定に必要なものは、現在の急所ランクなので、はっぱカッターなど急所ランクが設定されているものに対してはプロパティを追加して、取得用のメソッド(getCritical)を用意しておきましょう。
それでは、判定用メソッドの処理をみてみましょう。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* 急所判定
*
* @param object $move
* @return void
*/
private function checkCritical(...$rank)
{
switch (array_sum($rank)) {
// 急所ランク+0
case 0:
$chance = 4.17; #(%)
break;
// 急所ランク+1
case 1:
$chance = 12.5; #(%)
break;
// 急所ランク+2
case 2:
$chance = 50; #(%)
break;
// 急所ランク+3以上
default:
$chance = 100; #(%)
break;
}
/**
* 0〜10000からランダムで数値を取得して、それより小さければ急所
* 確率($chance)は*100して整数で比較する
*/
if(($chance * 100) >= (mt_rand(0, 10000))){
// 急所に当たった
$this->m *= 1.5;
return true;
}
// 急所に当たらなかった
return false;
}
引数では急所ランクを任意数受け取れるように、可変長引数(…)を使用しています。現段階では技の急所ランクしかありませんが、ステータス補正を導入すれば複数を想定する必要があるためです。
まずはランクによる急所率を取得するため、switchを使用します。
switch (array_sum($rank)) {
// 急所ランク+0
case 0:
$chance = 4.17; #(%)
break;
// 急所ランク+1
case 0:
$chance = 12.5; #(%)
break;
// 急所ランク+2
case 0:
$chance = 50; #(%)
break;
// 急所ランク+3以上
default:
$chance = 100; #(%)
break;
}
可変長引数で受け取った急所ランクを、array_sumで足した値に対して分岐をしています。もし急所ランクが0であれば、それに該当する4.17%という確率を$chanceの変数にセットしています。急所ランクが3以上であれば、100%固定になるのでdefaultでの分岐を使用しました。
※もし、急所ランクの数値から急所率を算出できる式が思いついた人は、ぜひそちらを実装してみてください
確率計算については命中率と同じようにmt_randを使った判定を使用します。
/**
* 0〜10000からランダムで数値を取得して、それより小さければ急所
* 確率($chance)は*100して整数で比較する
*/
if(($chance * 100) >= (mt_rand(0, 10000))){
// 急所に当たった
$this->m *= 1.5;
return true;
}
命中率と違い、急所確率は小数点第2位までの数値が使用されているため、計算時には100を乗算して整数に直し、mt_randの幅も0〜10000の幅で用意しています。
急所ランクが0の場合は4.17%のため417で計算して、ランダムで生成した値が417以下であれば急所に命中、1.5倍の補正が入るという仕様になっています。
乱数の計算
ポケモンではステータスや補正値が決まれば必ず同じ値が算出されるということはありません。それが、乱数によるダメージ幅です。例えば、でんきショックという技を使用して、1ターン目は30のダメージを与えられたとしても、次のターンは29しか与えられないということがあります。これが乱数補正です。
乱数補正は、第2世代までが217〜255/255、第3世代以降が85〜100/100の確率です。PHPポケモンではわかりやすさからも第3世代以降の85〜100%を用いて算出します。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* 物理,特殊技
*/
if(!is_null($move->getPower())){
// 急所判定(固定ダメージ技は判定不要)
$critical = $this->checkCritical($move->getCritical());
if($critical){
$this->setMessage('急所に当たった!');
}
}
// 乱数補正値の計算
$this->calRandNum();
乱数の補正はattackメソッドに先程追加した急所判定の後に行います。こちらも変化技では不要な計算だからです。では、乱数補正値の計算用メソッド(calRandNum)を見てみましょう。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* 乱数補正値の計算
*
* @return void
*/
private function calRandNum()
{
// 85〜100の乱数をかけ、その後100で割る
$this->m *= (mt_rand(85, 100) / 100);
}
処理自体はそこまで複雑ではありません。おなじみmt_randを使って85〜100までの値を求めて100で割り、乱数のプロパティに乗算しています。もしランダムで90が算出された場合は、0.9が乱数補正値となります。
タイプ一致補正
次にタイプ一致補正の計算を行います。これは、攻撃をしたポケモンのタイプと、攻撃技のタイプが一致していればプラス補正が入るという仕様です。タイプ一致補正は過去作から現在まですべて1.5倍となっているので、PHPポケモンでも同じ数値を使用して計算します。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
// ダメージ計算
if($move->getSpecies() !== 'status'){
/**
* 物理,特殊技
*/
if(!is_null($move->getPower())){
// 急所判定(固定ダメージ技は判定不要)
$critical = $this->checkCritical($move->getCritical());
if($critical){
$this->setMessage('急所に当たった!');
}
}
// 乱数補正値の計算
$this->calRandNum();
// タイプ一致補正の計算
$this->calMatchType($move->getType(), $atk_pokemon->getTypes());
タイプ一致補正も変化技では不要になるので、attackメソッド内の乱数補正の後ろに追加します。判定するには、技タイプ、攻撃ポケモンのタイプが必要になるので、それぞれを判定用メソッド(calMachType)に引数として渡しています。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* タイプ一致補正値の計算(一致→1.5倍)
*
* @param string $move_type 技タイプ
* @param array $pokemon_types 攻撃ポケモンのタイプ
* @return void
*/
private function calMatchType($move_type, $pokemon_types)
{
if(in_array($move_type, $pokemon_types, true)){
// 攻撃ポケモンのタイプと技タイプが一致
$this->m *= 1.5;
}
}
判定の方法は至って簡単です。技タイプがポケモンのタイプ(配列)の中に含まれているかをin_arrayを使って判定して、もしマッチしたら補正値のプロパティ($this->m)に1.5を乗算しています。
これで今回実装する補正値の計算が終わりました。出力結果を見てみましょう。
急所ランクに補正がかかるとダメージ量が大きくなり、通常時でもダメージ量が異なることが確認できました。レベルが低いとその振れ幅も小さくなりわかりにくいかも知れませんが、レベルをあげてダメージ量を多くしたり、こうかばつぐんを狙えばその振れ幅はより顕著に確認できます。
最小ダメージ数の調整
では最後に、最小ダメージ数の調整をダメージ計算に含めましょう。
初代ではこの計算がされていなかったために、相性やレベルの問題でダメージ量が0になると、攻撃が命中しなかったという判定がなされます。命中率が100%で補正がかかっていなかったとしても、外れることがあるのです。
そこから世代を重ねていくことで、最小値の計算は見直されることになりました。もし算出結果が0になれば、+1をすることで最小ダメージが1になるという仕様です。この計算タイミングについても細かく指定があるようですが、PHPポケモンでは最終的に0であれば1ダメージが与えられるようにしておきます。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* ダメージ計算(カッコ毎に小数点の切り捨てをする)
* floor(floor(floor(レベル×2/5+2)×威力×A/D)/50+2)*M
*
* @param integer $l レベル
* @param integer $a 攻撃値
* @param integer $d 防御値
* @param integer $p 威力
* @param integer $m 補正値
* @return integer
*/
private function calDamage($l, $a, $d, $p, $m)
{
// 計算式を当てはめる
$result = floor(floor(floor($l * 2 / 5 + 2) * $p * $a / $d) / 50 + 2) * $m;
if($result === 0){
// 計算結果が0になった場合は+1
$result++;
}
// 整数で返却
return (int)$result;
}
ダメージ計算をするcalDamageのメソッドで、計算結果が0であれば1をプラス(++)しています。これで、もし算出結果が0になってもダメージを与えられないという現象は回避することができます。
以下、攻撃用トレイトの第21回終了時点での最終コードです。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
<?php
trait AttackTrait
{
/**
* 省略記号
*
* @var L レベル
* @var A 攻撃値
* @var D 防御値
* @var P 威力
* @var M 補正値
*/
/**
* 補正値
* @var float 小数点第2位までの数値
*/
private $m = 1;
/**
* 攻撃する
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param string $move_class
* @return array
*/
protected function attack($atk_pokemon, $def_pokemon, $move_class)
{
// 技のインスタンスを取得
$move = $this->getInstance($move_class);
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getName().'は'.$move->getName().'を使った!');
// タイプ相性チェック
$type_comp_msg = $this->checkTypeCompatibility($move->getType(), $def_pokemon->getTypes());
// 「こうかがない」の判定(命中率と威力がnullではなく、タイプ相性補正が0の場合)
if(!is_null($move->getAccuracy()) && !is_null($move->getPower()) && ($this->m === 0)){
// こうかがない
$this->setMessage($def_pokemon->getName().'には効果が無いみたいだ');
return;
}
// 命中判定
$hit = $this->checkHit($move->getAccuracy());
if(!$hit){
// 攻撃失敗
$this->setMessage('しかし'.$atk_pokemon->getName().'の攻撃は外れた!');
return;
}
// 必要ステータスの取得
$stats = $this->getStats($move->getSpecies(), $atk_pokemon, $def_pokemon);
// ダメージ計算
if($move->getSpecies() !== 'status'){
/**
* 物理,特殊技
*/
if(!is_null($move->getPower())){
// 急所判定(固定ダメージ技は判定不要)
$critical = $this->checkCritical($move->getCritical());
if($critical){
$this->setMessage('急所に当たった!');
}
}
// 乱数補正値の計算
$this->calRandNum();
// タイプ一致補正の計算
$this->calMatchType($move->getType(), $atk_pokemon->getTypes());
// ダメージ計算
$damage = $this->calDamage(
$atk_pokemon->getLevel(), # 攻撃ポケモンのレベル
$stats['a'], # 攻撃ポケモンの攻撃値
$stats['d'], # 防御ポケモンの防御値
$move->getPower(), # 技の威力
$this->m, # 補正値
);
// タイプ相性のメッセージを返却
$this->setMessage($type_comp_msg);
}else{
/**
* 変化技
*/
$damage = 0;
}
// 返り値
return $damage;
}
/**
* 命中判定
*
* @param integer|null
* @return boolean
*/
private function checkHit($accuracy)
{
// nullの場合は命中率関係無し
if(is_null($accuracy)){
return true;
}
/**
* 0〜100からランダムで数値を取得して、それより小さければ命中
* 例:命中80%→mt_randで60が生成されたら成功、90なら失敗
*/
if($accuracy >= mt_rand(0, 100)){
return true;
}
return false;
}
/**
* ステータス(攻撃値、防御値)の取得
*
* @param string $species
* @param object $atk_pokemon
* @param object $def_pokemon
* @return array
*/
private function getStats($species, $atk_pokemon, $def_pokemon)
{
// 技種類での分岐
switch ($species) {
// 物理
case 'physical':
$a = $atk_pokemon->getStats('Attack');
$d = $def_pokemon->getStats('Defense');
break;
// 特殊
case 'special':
$a = $atk_pokemon->getStats('SpAtk');
$d = $def_pokemon->getStats('SpDef');
break;
// 変化
case 'status':
// ここに変化技の処理
break;
}
// 配列にして返却
return [
'a' => $a ?? 0,
'd' => $d ?? 0,
];
}
/**
* ダメージ計算(カッコ毎に小数点の切り捨てをする)
* floor(floor(floor(レベル×2/5+2)×威力×A/D)/50+2)*M
*
* @param integer $l レベル
* @param integer $a 攻撃値
* @param integer $d 防御値
* @param integer $p 威力
* @param integer $m 補正値
* @return integer
*/
private function calDamage($l, $a, $d, $p, $m)
{
// 計算式を当てはめる
$result = floor(floor(floor($l * 2 / 5 + 2) * $p * $a / $d) / 50 + 2) * $m;
if($result === 0){
// 計算結果が0になった場合は+1
$result++;
}
// 整数で返却
return (int)$result;
}
/****************************************************************
* 補正値の計算
****************************************************************/
/**
* タイプ相性チェック
*
* @param object $atk_type
* @param array $def_types
* @return string
*/
private function checkTypeCompatibility($atk_type, $def_types)
{
// ダメージ補正(初期値は等倍)
$m = 1;
// 補正判定
foreach($def_types as $def_type){
// 「こうかがない」かチェック
if(in_array($def_type, $atk_type->getAtkDoesntAffectTypes(), true)){
// ダメージ無し
$m = 0;
// ループ終了
break;
}
// 「こうかばつぐん」かチェック
if(in_array($def_type, $atk_type->getAtkExcellentTypes(), true)){
// 2倍
$m *= 2;
// 次の処理へスキップ
continue;
}
// 「こうかいまひとつ」かチェック
if(in_array($def_type, $atk_type->getAtkNotVeryTypes(), true)){
// 半減
$m /= 2;
}
}
// 補正によるメッセージの分岐
if($m > 1){
// 等倍超過
$message = 'こうかはばつぐんだ!';
}
if($m < 1){
// 等倍未満
$message = 'こうかはいまひとつだ';
}
// 算出した補正値を乗算
$this->m *= $m;
// メッセージを返却
return $message ?? '';
}
/**
* 急所判定
*
* @param object $move
* @return void
*/
private function checkCritical(...$rank)
{
switch (array_sum($rank)) {
// 急所ランク+0
case 0:
$chance = 4.17; #(%)
break;
// 急所ランク+1
case 0:
$chance = 12.5; #(%)
break;
// 急所ランク+2
case 0:
$chance = 50; #(%)
break;
// 急所ランク+3以上
default:
$chance = 100; #(%)
break;
}
/**
* 0〜10000からランダムで数値を取得して、それより小さければ急所
* 確率($chance)は*100して整数で比較する
*/
if(($chance * 100) >= (mt_rand(0, 10000))){
// 急所に当たっ
$this->m *= 1.5;
return true;
}
// 急所に当たらなかった
return false;
}
/**
* 乱数補正値の計算
*
* @return void
*/
private function calRandNum()
{
// 85〜100の乱数をかけ、その後100で割る
$this->m *= (mt_rand(85, 100) / 100);
}
/**
* タイプ一致補正値の計算(一致→1.5倍)
*
* @param string $move_type 技タイプ
* @param array $pokemon_types 攻撃ポケモンのタイプ
* @return void
*/
private function calMatchType($move_type, $pokemon_types)
{
if(in_array($move_type, $pokemon_types, true)){
// 攻撃ポケモンのタイプと技タイプが一致
$this->m *= 1.5;
}
}
}
まとめ
いかがだったでしょうか。
今回のPHPポケモンは「バトルシステム実装編〜補正値の計算〜」について、急所判定、乱数補正、タイプ一致補正、最小ダメージ調整の4つをご紹介しました。
ポケモンのゲームが好きな人、プログラミングに興味がある人は、ぜひ参考にしてくださいね。