行動順の判定
ポケモンの行動順は以下の通りです。
- 技の優先度
- すばやさの実数値(補正有り)
- 同速の場合は50%の乱数
この順番で比較を行い、先行後攻を決めます。ただし、これは両者ともに攻撃を選択した場合のみです。アイテムの使用や交代は技よりも優先されますし(※一部技を除く)、にげるはそれよりも先に行動することになります。
現在はアイテムと交代システムは実装出来ていないため、技選択における行動順序の比較のみを行います。
相手ポケモンの行動
先手と後手を決めるためには、相手にも技の選択をして貰う必要があります。なぜなら、まず「技の優先度」を比較しなければならないからです。そのためには、簡易ながらも相手ポケモン(CP)の行動AIが必要になります。
簡易行動AIの作成
では、バトルコントローラーのアクションメソッドの最初に以下の記述を追加しましょう。
バトルコントローラー(/Classes/Controller/BattleController.php)
/**
* アクション
*
* @param string $action
* @param mixed $param
* @return void
*/
private function action($action, $param)
{
// 敵ポケモンの技をインスタンス化
$e_move = $this->getInstance($this->aiSelectMove());
相手の技については、自分がどの行動を行なったとしても基本的には実行されます。なので、最初にAIを使って技を選択させ、インスタンスを作成しておきましょう。
新しく登場したaiSelectMoveというメソッドは新しくAI用のトレイトを作成してそこに作り込んでいきましょう。
AIトレイト(/Traits/Battle/EnemyAiTrait.php)
<?php
// 敵ポケモンの行動AI
trait EnemyAiTrait
{
/**
* 技の選択
*
* @return string
*/
protected function aiSelectMove()
{
// 技の一覧を配列形式で取得
$move = $this->enemy
->getMove('array');
// ランダムで1つ返却
return $move[array_rand($move)];
}
}
初代ポケモンでは、ポケモンのタイプ相性に合わせて技が選択されていましたが、そのせいで攻撃技ではなく変化技を連打してしまうなどといった現象が起こってしまい、一種の不具合とまで言われていました。PHPポケモンではそういったことを防ぐためにも、独自のAIを組み上げていきますが、非常に作り込みが重視される部分のため本格的な作り込みは後回しにします。
今回実装した技選択のAIは、array_randを使って技をランダムで1つ選ぶというだけのものです。getMoveではインスタンス化した技の一覧を返却していましたが、すべてをインスタンス化する必要はないのでプロパティに格納された状態(配列)のまま受け取れるように引数を設定しました。
array_rand(PHP.net)
Get格納トレイト
/**
* 覚えている技の一覧を取得する
* @param string (object|array)
* @return array
*/
public function getMove($param='object')
{
switch ($param) {
case 'object':
// オブジェクトで返却
// array_mapで配列内の技クラスをインスタンス化
return array_map([$this, 'getInstance'], $this->move);
case 'array':
// 配列で返却(そのまま)
return $this->move;
default:
// オブジェクト(デフォルト)
return array_map([$this, 'getInstance'], $this->move);
}
}
※今更ですが、残り技ポイントの設定が抜けていました。こちらを変更するとポケモンの技回りの処理の大幅変更が予想されるため、後ほど実装予定です。
先行の取得
それでは行動順の判定を実装します。ダブルバトルが実装されていない関係上、先行か後攻かを判定できれば行動順を決めることができます。なので「自分が先行かどうか」を判定するためのメソッドを作成し、その結果に合わせた分岐を作成します。
バトルコントローラー(/Classes/Controller/BattleController.php)
/**
* たたかう
*/
case 'fight':
// 自ポケモンの技をインスタンス化
$p_move = $this->getInstance($param);
// 行動順の判定
if($this->checkFirstMove($p_move, $e_move)){
// 先行
// 自ポケモンの攻撃
$e_damage = $this->attack($this->pokemon, $this->enemy, $p_move);
// 敵ポケモンの攻撃
$p_damage = $this->attack($this->enemy, $this->pokemon, $e_move);
}else{
// 後攻
// 敵ポケモンの攻撃
$p_damage = $this->attack($this->enemy, $this->pokemon, $e_move);
// 自ポケモンの攻撃
$e_damage = $this->attack($this->pokemon, $this->enemy, $p_move);
}
break;
※敵ポケモン同様に、自ポケモンの技のインスタンス化もコントローラー内で実行する仕様に変更しました
checkFirstMoveというメソッドを使い、自分が「先行かどうか」をtrueまたはfalseで判定します。true(先行)が返ってくれば自→敵という順番でattackを実行、false(後攻)であれば敵→自という順番でattackを実行するという単純な仕組みです。
attackメソッドを複数回使用することになるため、ダメージはresponseに格納せず返り値として受け取っています。
では、checkFirstMoveメソッドの処理をみてみましょう。
バトルコントローラー(/Classes/Controller/BattleController.php)
/**
* 先手の判定
*
* @param object 自ポケモンの技 $p_move
* @param object 敵ポケモンの技 $e_move
* @return boolean (pokemon > enemy):true (pokemon < enemy):false
*/
private function checkFirstMove($p_move, $e_move)
{
/**
* 優先度の比較
*/
// 判定
if($p_move->getPriority() > $e_move->getPriority()){
// 優先度が高い
return true;
}elseif($p_move->getPriority() < $e_move->getPriority()){
// 優先度が低い
return false;
}
/**
* すばやさの比較
*/
// 自ポケモンの素早さ(補正あり実数値)
$p_speed = $this->pokemon
->getStats('Speed', true);
// 敵ポケモンの素早さ(補正あり実数値)
$e_speed = $this->enemy
->getStats('Speed', true);
// 判定
if($p_speed > $e_speed){
// 素早さが上回っている
return true;
}elseif($p_speed < $e_speed){
// 素早さが下回っている
return false;
}elseif($p_speed === $e_speed){
// 同速(50%の乱数)
if(random_int(0, 1)){
return true;
}else{
return false;
}
}
}
優先度ランクの比較
まず最初に、技の優先度をそれぞれgetPriorityで取得して比較します。
/**
* 優先度の比較
*/
// 判定
if($p_move->getPriority() > $e_move->getPriority()){
// 優先度が高い
return true;
}elseif($p_move->getPriority() < $e_move->getPriority()){
// 優先度が低い
return false;
}
ここで差があれば、その時点で行動順が決定します。もし優先度が同じであれば、素早さの判定をしなければならないため、elseを使用せずelseifで分岐するようにしておいてください。
素早さの比較
次に素早さの比較です。
/**
* すばやさの比較
*/
// 自ポケモンの素早さ(補正あり実数値)
$p_speed = $this->pokemon
->getStats('Speed', true);
// 敵ポケモンの素早さ(補正あり実数値)
$e_speed = $this->enemy
->getStats('Speed', true);
// 判定
if($p_speed > $e_speed){
// 素早さが上回っている
return true;
}elseif($p_speed < $e_speed){
// 素早さが下回っている
return false;
}elseif($p_speed === $e_speed){
// 同速(50%の乱数)
if(random_int(0, 1)){
return true;
}else{
return false;
}
}
それぞれの素早さを補正値込みで取得し、その差を比較しています。こちらもelseifで判定しながら、同速($p_speed === $e_speed)であれば50%の乱数で判定しました。
※最後の判定はelseでも構いません。今回はわかりやすくするためにelseifを使って比較式を記述しました。
50%の乱数の求め方は、今までの用にmt_randを使っても構いませんが、PHP7からはrandom_intという暗号化でも使用できる疑似乱数生成方法があったのでそちらを採用しました。
random_int(PHP.net)
1/2を求めるだけであれば、最小値(第1引数)を0、最大値(第2引数)を1でifに掛けるだけで判定することができます。
最終的な出力結果を確認するために、setResponseをswitch外に移動させて返却データを増やしておきましょう。
// 結果の返却
$this->setResponse([
'受けたダメージ' => $p_damage ?? 0,
'ランク' => $this->pokemon->getRank(),
], $this->pokemon->getName());
$this->setResponse([
'受けたダメージ' => $e_damage ?? 0,
'ランク' => $this->enemy->getRank(),
], $this->enemy->getName());
出力結果は以下の通りです。
敵ポケモンも素早さに合わせて攻撃を仕掛けてきましたね。ピカチュウのステータスに変化があることも確認ができます。
にげる失敗時の相手の行動
それではにげる失敗時の行動についても合わせて設定しておきましょう。もし失敗した場合、相手から技を受けることになります。なので、相手の攻撃処理を追加しましょう。
バトルコントローラー(/Classes/Controller/BattleController.php)
/**
* にげる
*/
case 'run':
// $this->run++;
if($this->checkRun()){
unset($_SESSION['enemy']);
unset($_SESSION['rank']);
unset($_SESSION['run']);
header("Location: ./home.php", true, 307);
exit;
}
$this->setMessage('逃げられない!');
// 敵ポケモンの攻撃
$p_damage = $this->attack($this->enemy, $this->pokemon, $e_move);
break;
これで「にげる」に失敗すると、相手から技を受けることになります。
以下がバトルコントローラーの最終コードになります。
バトルコントローラー(/Classes/Controller/BattleController.php)
<?php
require_once(__DIR__.'/../Controller.php');
require_once(__DIR__.'/../../Traits/Battle/AttackTrait.php');
require_once(__DIR__.'/../../Traits/Battle/EnemyAiTrait.php');
// バトル用コントローラー
class BattleController extends Controller
{
use AttackTrait;
use EnemyAiTrait;
/**
* 敵ポケモン格納用
* @var object
*/
protected $enemy;
/**
* 逃走を試みた回数
* @var integer
*/
public $run = 1;
/**
* @return void
*/
public function __construct()
{
// 親コンストラクタの呼び出し
parent::__construct();
// 自ポケモンの格納
$this->myPokemon($_SESSION['pokemon']);
// 敵ポケモンの格納
if(isset($_SESSION['enemy'])){
$this->enemyPokemon($_SESSION['enemy']);
}else{
$this->enemyPokemon();
}
// ランク(バトルステータス)の引き継ぎ
if(isset($_SESSION['rank'])){
$this->pokemon
->setRank($_SESSION['rank']['pokemon']);
$this->enemy
->setRank($_SESSION['rank']['enemy']);
}
// にげるの実行回数を引き継ぎ
if(isset($_SESSION['run'])){
$this->run = $_SESSION['run'];
}
// アクションが選択された
if(isset($_POST['action'])){
// アクションメソッドの実行
$this->action(htmlspecialchars($_POST['action']), htmlspecialchars($_POST['param'] ?? null));
return;
}
}
/**
* 自ポケモンの格納
*
* @param array $pokemon
* @return void
*/
private function myPokemon($pokemon)
{
$this->pokemon = new $pokemon['class_name']($pokemon);
}
/**
* 敵ポケモンの格納
*
* @param array|null $pokemon
* @return void
*/
private function enemyPokemon($pokemon=null)
{
if(is_null($pokemon)){
$this->enemy = new Fushigidane();
$this->setMessage('野生の'.$this->enemy->getName().'が現れた!');
}else{
$this->enemy = new $pokemon['class_name']($pokemon);
}
}
/**
* アクション
*
* @param string $action
* @param mixed $param
* @return void
*/
private function action($action, $param)
{
// 敵ポケモンの技をインスタンス化
$e_move = $this->getInstance($this->aiSelectMove());
switch ($action) {
/**
* にげる
*/
case 'run':
// $this->run++;
if($this->checkRun()){
unset($_SESSION['enemy']);
unset($_SESSION['rank']);
unset($_SESSION['run']);
header("Location: ./home.php", true, 307);
exit;
}
$this->setMessage('逃げられない!');
// 敵ポケモンの攻撃
$p_damage = $this->attack($this->enemy, $this->pokemon, $e_move);
break;
/**
* たたかう
*/
case 'fight':
// 自ポケモンの技をインスタンス化
$p_move = $this->getInstance($param);
// 行動順の判定
if($this->checkFirstMove($p_move, $e_move)){
// 先行
// 自ポケモンの攻撃
$e_damage = $this->attack($this->pokemon, $this->enemy, $p_move);
// 敵ポケモンの攻撃
$p_damage = $this->attack($this->enemy, $this->pokemon, $e_move);
}else{
// 後攻
// 敵ポケモンの攻撃
$p_damage = $this->attack($this->enemy, $this->pokemon, $e_move);
// 自ポケモンの攻撃
$e_damage = $this->attack($this->pokemon, $this->enemy, $p_move);
}
break;
}
// 結果の返却
$this->setResponse([
'受けたダメージ' => $p_damage ?? 0,
'ランク' => $this->pokemon->getRank(),
], $this->pokemon->getName());
$this->setResponse([
'受けたダメージ' => $e_damage ?? 0,
'ランク' => $this->enemy->getRank(),
], $this->enemy->getName());
}
/**
* 敵ポケモン情報の取得
*
* @return object
*/
public function getEnemy()
{
return $this->enemy;
}
/**
* にげる判定
* F = (A × 128 / B) + 30 × C
* Fを256で割った値 → 逃走成功率
* @var A 味方ポケモンのすばやさ(ランク補正有り)
* @var B 相手ポケモンのすばやさ(ランク補正無し)
* @var C 逃走を試みた回数
* @return boolean
*/
private function checkRun()
{
// 味方の素早さを取得(ランク補正有り)
$a = $this->pokemon
->getStats('Speed', true);
// 相手の素早さを取得(ランク補正無し)
$b = $this->enemy
->getStats('Speed');
// 逃走を試みた回数
$c = $this->run;
// 計算式への当てはめ
$f = ($a * 128 / $b) + 30 * $c;
// 確率計算
if(round($f / 256, 2) * 100 >= mt_rand(0, 100)){
return true; # 逃走成功
}else{
return false; # 逃走失敗
}
}
/**
* 先手の判定
*
* @param object 自ポケモンの技 $p_move
* @param object 敵ポケモンの技 $e_move
* @return boolean (pokemon > enemy):true (pokemon < enemy):false
*/
private function checkFirstMove($p_move, $e_move)
{
/**
* 優先度の比較
*/
// 判定
if($p_move->getPriority() > $e_move->getPriority()){
// 優先度が高い
return true;
}elseif($p_move->getPriority() < $e_move->getPriority()){
// 優先度が低い
return false;
}
/**
* すばやさの比較
*/
// 自ポケモンの素早さ(補正あり実数値)
$p_speed = $this->pokemon
->getStats('Speed', true);
// 敵ポケモンの素早さ(補正あり実数値)
$e_speed = $this->enemy
->getStats('Speed', true);
// 判定
if($p_speed > $e_speed){
// 素早さが上回っている
return true;
}elseif($p_speed < $e_speed){
// 素早さが下回っている
return false;
}elseif($p_speed === $e_speed){
// 同速(50%の乱数)
if(random_int(0, 1)){
return true;
}else{
return false;
}
}
}
}
まとめ
いかがだったでしょうか。
今回のPHPポケモンは「行動判定」と「敵ポケモンの攻撃」についてご紹介しました。
現在はランダムの技選択のため戦略性は乏しいかも知れませんが、AIを作り込んでいけばそれだけでかなり高難度のバトルシステムを再現することも可能です。
ゲームづくりに興味がある人は、ぜひ参考にしてくださいね。