Implementing More Actions
All right! We're ready to add more actions to our game and allow players to choose their actions.
First, we need to create an interface for our commands. To do that, inside the ActionCommand
directory, let's create a new PHP file and call it ActionCommandInterface
. Inside, we'll add a single method called execute()
with no arguments.
// ... lines 1 - 4 | |
interface ActionCommandInterface | |
{ | |
public function execute(): void; | |
} |
Interface done! Next, let's open AttackCommand
and make it implement ActionCommandInterface
. The execute()
method is already implemented here, so this is ready to go. Nice!
If you downloaded the course code, we can save some time by grabbing the rest of the actions we need in our tutorial
directory at the root of our project. Copy the HealCommand
and SurrenderCommand
files into the ActionCommand
directory.
Let's check those out. Inside HealCommand
, we can see that it has a constructor that only cares about the player object.
// ... lines 1 - 4 | |
use App\Character\Character; | |
// ... lines 6 - 8 | |
class HealCommand implements ActionCommandInterface | |
{ | |
public function __construct(private readonly Character $player) | |
{ | |
} | |
// ... lines 14 - 28 | |
} |
And in the execute()
method, we have some code that calculates how much damage the player will heal, and then sets the player's health to the new amount (not exceeding their max health). Finally, it prints a message on the screen.
// ... lines 1 - 14 | |
public function execute(): void | |
{ | |
$healAmount = Dice::roll(20) + $this->player->getLevel() * 3; | |
$newAmount = $this->player->getCurrentHealth() + $healAmount; | |
$newAmount = min($newAmount, $this->player->getMaxHealth()); | |
$this->player->setHealth($newAmount); | |
$this->player->setStamina(Character::MAX_STAMINA); | |
GameApplication::$printer->writeln(sprintf( | |
'You healed %d damage', | |
$healAmount | |
)); | |
} |
If we take a look at the SurrenderCommand
, the constructor here is the same as the one in HealCommand
- it only cares about the player object. And in the execute()
method, I cheated a little bit because there's no proper way to end a battle, so I just set the player's health to 0
. Cool, right?
// ... lines 1 - 4 | |
use App\Character\Character; | |
use App\GameApplication; | |
class SurrenderCommand implements ActionCommandInterface | |
{ | |
public function __construct(private readonly Character $player) | |
{ | |
} | |
public function execute(): void | |
{ | |
$this->player->setHealth(0); | |
GameApplication::$printer->block('You\'ve surrendered! Better luck next time!'); | |
} | |
} |
Asking the Player to Choose an Action
All right! It's time to ask the player to choose an action! I'll close a few files first. Okay, head back to the GameApplication
... and right before we define $playerAction
, write $actionChoice
and set it to GameApplication::$printer->choice()
, where the question is Your Turn
, and the choices are Attack
, Heal
, and Surrender
.
// ... lines 1 - 12 | |
class GameApplication | |
{ | |
// ... lines 15 - 26 | |
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void | |
{ | |
while (true) { | |
// ... lines 30 - 35 | |
// Player's turn | |
$actionChoice = GameApplication::$printer->choice('Your turn', [ | |
'Attack', | |
'Heal', | |
'Surrender', | |
]); | |
// ... lines 42 - 66 | |
} | |
} | |
// ... lines 69 - 198 | |
} |
Then, we'll replace the AttackCommand
instantiation with a match
expression, but copy this first, because we'll need it in a moment. Now write match ($actionChoice)
. Inside, the first case we want to add is Attack
, and now... paste. For the second case, write Heal
and set it to new HealCommand($player)
. The third and final case is Surrender
, and we'll set that to new SurrenderCommand($player)
. Perfect!
// ... lines 1 - 26 | |
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void | |
{ | |
while (true) { | |
// ... lines 30 - 42 | |
$playerAction = match ($actionChoice) { | |
'Attack' => new AttackCommand($player, $ai, $fightResultSet), | |
'Heal' => new HealCommand($player), | |
'Surrender' => new SurrenderCommand($player), | |
}; | |
// ... lines 48 - 66 | |
} | |
} | |
// ... lines 69 - 200 |
Let's give this a try. Spin over to your terminal and run:
php bin/console app:game:play
This time, I'll be a "mage archer", and... look at that! It's asking us what to do! Let's attack first, and... cool! We did 12
points of damage and received 10
, so our current health is 40
out of 50
. Let's try healing next. And... check it out! We healed 8
points, and the AI did 0
points of damage because we blocked its attack, so our current health is 48
. The "heal" action is working as expected! Lastly, let's try to surrender. Choose option 2
and... awesome! We surrendered and lost the match. Giving up isn't something we'd normally celebrate, but in this case, it means our "surrender" action is working like it's supposed to.
High five your rubber duck because we've successfully made our game more interactive! But... wouldn't it be awesome if we could undo our last action if, say... the AI got a little too lucky? That's next!
Looks really great!
But there's something bugging me: What if we need services in these commands? Since we instanciate them in the (symfony) command, we can't use the usual autowiring. So do we have to autowire these services in the symfony command (not really a fan of this, because it means I must load all services of the commands inside my symfony command... not very clean imho), or is there anotherway to achieve it?