Login to bookmark this video
Buy Access to Course
03.

Adding Actions to the Game

|

Share this awesome video!

|

Our investors have asked for a new feature: "make the game more interactive". Those stakeholders are so funny... Ok so, instead of running battles automatically, they want the player to be able to choose which action to perform at the start of each turn. Right now, our game only supports the Attack action, so, to make it more real we'll also add a couple more actions: Heal and Surrender.

This is a great opportunity to use the command pattern!

Applying the Command Pattern

The first step is to identify the code that needs to change and encapsulate it into its own command class. Open up GameApplication and find the play() method. Inside the while loop there's a comment telling us where the player's turn starts. Select the code right before checking if the player won and cut it, and since we're already here let's write the code we want. We know that we want to instantiate an AttackCommand object and call execute() on it, so let's do that. Create a variable called $playerAction and set it to new AttackCommand(), this class does not exist yet. Then, write $playerAction->execute().

The next thing is to handle the arguments of the command. To be able to attack we need both character objects and the FightResultSet, but where should we set them, in the constructor or the execute method? The answer may depend on your application's needs, but passing them to the constructor is usually a better idea because it allows you to decouple instantiation from execution. You can create your command objects at some point, and then later, if the conditions are met, execute them without worrying about their arguments.

187 lines | src/GameApplication.php
// ... lines 1 - 10
class GameApplication
{
// ... lines 13 - 24
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void
{
while (true) {
// ... lines 28 - 33
// Player's turn
$attackCommand = new AttackCommand($player, $ai, $fightResultSet);
$attackCommand->execute();
// ... lines 37 - 53
}
}
// ... lines 56 - 185
}

Alright! Let's create this class, I'll press "Option + Enter", then select Create class. I'll put it in the App\ActionCommand namespace instead of simply Command because we don't want to confuse these with Symfony commands. Press enter and, voila! There is our AttackCommand class. Remove the annotations on top of the constructor because they're redundant. Next, I'll split the arguments into multiple lines and shorten the namespaces. While doing so, I'll add private readonly to leverage constructor property promotion, and rename the $ai variable to $opponent because it makes more sense in this context.

21 lines | src/ActionCommand/AttackCommand.php
// ... lines 1 - 4
use App\Character\Character;
use App\FightResultSet;
class AttackCommand implements ActionCommandInterface
{
public function __construct(
private readonly Character $player,
private readonly Character $opponent,
private readonly FightResultSet $fightResultSet,
) {
}
// ... lines 16 - 19
}

Next, we need to implement the execute method. Write public function execute(), and paste! Say yes to add the GameApplication import statement, and now we just need to refactor the local variables with $this. Oh, and don't forget to change ai to opponent.

36 lines | src/ActionCommand/AttackCommand.php
// ... lines 1 - 17
public function execute(): void
{
$damage = $this->player->attack();
if ($damage === 0) {
GameApplication::$printer->printFor($this->player)->exhaustedMessage();
$this->fightResultSet->of($this->player)->addExhaustedTurn();
return;
}
$damageDealt = $this->opponent->receiveAttack($damage);
$this->fightResultSet->of($this->player)->addDamageDealt($damageDealt);
GameApplication::$printer->printFor($this->player)->attackMessage($damageDealt);
GameApplication::$printer->writeln('');
usleep(300000);
}
// ... lines 35 - 36

Perfect! Our AttackCommand is ready. Now, let's go back to GameApplication and we'll do the same thing for the AI's turn. Scroll down a bit until you see the comment "AI's turn", select all that code and replace with $aiAction = new AttackCommand() where the player argument is $ai, the opponent is $player and $fightResultSet at the end. Then, write $aiAction->execute().

187 lines | src/GameApplication.php
// ... lines 1 - 10
class GameApplication
{
// ... lines 13 - 24
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void
{
while (true) {
// ... lines 28 - 42
// AI's turn
$aiAttackCommand = new AttackCommand($ai, $player, $fightResultSet);
$aiAttackCommand->execute();
// ... lines 46 - 53
}
}
// ... lines 56 - 185
}

Phew, finally! We're ready to give it a try. Spin over to your terminal and run:

php bin/console app:game:play

It's running! I'll be a fighter, and... yes! We won! This is great! Well, nothing changed, but it is using our commands under the hood.

We're ready to add more commands and ask the player which action to perform. That's next!