-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add script for command intro #15
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
f9e8f4f
Add script for command intro
MolloKhan af4c891
add behavioral patterns definition
MolloKhan aadf185
wip command coding script
MolloKhan f5ce46a
add script command_coding.md
MolloKhan ef1f0ae
Finish command_intro.md script
MolloKhan 195b682
wip
MolloKhan e28a0f3
some tweaks to command intro chapter
MolloKhan de8a435
Add command intro script
MolloKhan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
# Adding Actions to the Game - Chapter 3 | ||
|
||
The game development department asked for a new feature "make the game more interactive". | ||
Instead of running battles automatically, they want the player to be able to choose | ||
what action to perform at the start of the 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** action. | ||
|
||
This is a great **opportunity** to put the command pattern in action! | ||
|
||
## 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 the `GameApplication` class and | ||
go to 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 know we need. 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()`. We'll create this class soon. | ||
then, say `$playerAction->execute()`. | ||
|
||
```php | ||
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void | ||
{ | ||
while (true) { | ||
... | ||
$playerAction = new AttackCommand(); | ||
$playerAction->execute(); | ||
} | ||
} | ||
``` | ||
|
||
Yes, I know I'm missing some arguments, to be able to attack we need to know about | ||
the player and AI objects, but we'll get to that in a moment. | ||
|
||
I'll press `Alt+Enter`, then select `Create class`, and | ||
I'll put it on the `App\ActionCommand` namespace because `Command` it's already taken by Symfony. I'll leave | ||
the empty constructor for now, and I'll implement the `execute()` method, then paste! | ||
|
||
```php | ||
public function execute() | ||
{ | ||
$damage = $player->attack(); | ||
if ($damage === 0) { | ||
self::$printer->printFor($player)->exhaustedMessage(); | ||
$fightResultSet->of($player)->addExhaustedTurn(); | ||
} | ||
|
||
$damageDealt = $ai->receiveAttack($damage); | ||
$fightResultSet->of($player)->addDamageDealt($damageDealt); | ||
|
||
self::$printer->printFor($player)->attackMessage($damageDealt); | ||
self::$printer->writeln(''); | ||
usleep(300000); | ||
} | ||
``` | ||
|
||
Perfect! It's now obvious what arguments we're missing, the `$player`, `$ai`, and `$fightResult`. | ||
So now, the next question is, should we pass them to the method or to the constructor? Usually, | ||
the `execute()` method of the command pattern does not have any arguments because we want to | ||
decouple the instantiation from the execution. By setting them to the constructor | ||
we're decoupling **when** this command is going to be run from when it's created. Also, | ||
it will make our life easier when implementing the undo operation. The arguments won't have | ||
to be available at that specific moment. | ||
|
||
Alright! Let's add these three arguments to the constructor. | ||
|
||
```php | ||
class AttackCommand | ||
{ | ||
public function __construct( | ||
private readonly Character $player, | ||
private readonly Character $opponent, | ||
private readonly FightResultSet $fightResultSet | ||
) { | ||
} | ||
} | ||
``` | ||
|
||
Next, in the `execute()` method we need to replace the local variables with `$this`. | ||
|
||
```php | ||
class AttackCommand | ||
{ | ||
public function execute() | ||
{ | ||
$damage = $this->player->attack(); | ||
if ($damage === 0) { | ||
GameApplication::$printer->printFor($this->player)->exhaustedMessage(); | ||
$this->fightResultSet->of($this->player)->addExhaustedTurn(); | ||
} | ||
|
||
$damageDealt = $this->opponent->receiveAttack($damage); | ||
$this->fightResultSet->of($this->player)->addDamageDealt($damageDealt); | ||
|
||
GameApplication::$printer->printFor($this->player)->attackMessage($damageDealt); | ||
GameApplication::$printer->writeln(''); | ||
usleep(300000); | ||
} | ||
} | ||
``` | ||
|
||
Perfect! Our `AttackCommand` is ready. Now, let's go back to the `GameApplication`. | ||
Let's add the arguments to the `AttackCommand` object. `$player`, `$opponent`, and `$fightResultSet`. | ||
|
||
```php | ||
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void | ||
{ | ||
while (true) { | ||
$playerAction = new AttackCommand($player, $ai, $fightResultSet); | ||
$playerAction->execute(); | ||
} | ||
} | ||
``` | ||
|
||
And, scroll down a bit, there's the AI's turn, let's use our command there too. | ||
|
||
```php | ||
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void | ||
{ | ||
while (true) { | ||
// AI's turn | ||
$aiAttackCommand = new AttackCommand($ai, $player, $fightResultSet); | ||
$aiAttackCommand->execute(); | ||
} | ||
} | ||
``` | ||
|
||
Great! We're ready to give it a try. Let's run the game and play a match, everything | ||
should work exactly as before. Exiting, right?! | ||
|
||
## Implementing More Actions | ||
|
||
* Create other actions. Heal and Surrender | ||
* First create an interface `ActionCommandInterface` | ||
```php | ||
interface ActionInterface | ||
{ | ||
public function execute(): void; | ||
} | ||
``` | ||
|
||
* Make `AttackCommand` implement it | ||
```php | ||
class AttackCommand implements ActionCommandInterface | ||
``` | ||
|
||
* Create HealAction class | ||
* copy-paste implementation | ||
```php | ||
class HealCommand implements ActionCommandInterface | ||
{ | ||
public function __construct(private readonly Character $player) | ||
{ | ||
} | ||
|
||
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 | ||
)); | ||
} | ||
} | ||
``` | ||
* Explain constructor: | ||
- Benefits of putting the arguments on the constructor instead of on the interface | ||
The constructor for this action has fewer arguments than the `AttackCommand`, it only cares about the player. | ||
And this is another benefit of putting the arguments in the constructor instead of the interface method | ||
because this help us keeping the interface simple and avoid having arguments that are not | ||
used by all the commands. | ||
* Explain `execute()` method: | ||
- Rolling a dice to add some randomness | ||
- Set the player's health to the new amount but not more than the max health | ||
|
||
* Create SurrenderCommand + interface | ||
```php | ||
class SurrenderCommand implements ActionCommandInterface | ||
``` | ||
* copy-paste implementation | ||
```php | ||
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!'); | ||
} | ||
} | ||
``` | ||
|
||
* Explanation: For this action I'll cheat a bit because there's no proper way to end the match, so I'll | ||
set the player's health to 0 | ||
|
||
* ask player to choose an action | ||
* add `match` to create command object based on selection | ||
* try it | ||
|
||
Now, let's go back to the `GameApplication` and ask the player to choose an action. | ||
|
||
```php | ||
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void | ||
{ | ||
$actionChoice = GameApplication::$printer->choice('Your turn', [ | ||
'Attack', | ||
'Heal', | ||
'Surrender', | ||
]); | ||
|
||
$playerAction = match ($actionChoice) { | ||
'Attack' => new AttackCommand($player, $ai, $fightResultSet), | ||
'Heal' => new HealCommand($player), | ||
'Surrender' => new SurrenderCommand($player), | ||
}; | ||
} | ||
``` | ||
|
||
Perfect! It's time of the truth! Let's play a few rounds. | ||
|
||
`bin/console app:game:play` | ||
|
||
Sweet! it's asking us what to do |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Adding Actions to the Game - Chapter 3 | ||
|
||
The game development department asked for a new feature "make the game more interactive". | ||
Instead of running battles automatically, they want the player to be able to choose | ||
what action to perform at the start of the 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** action. | ||
|
||
This is a great **opportunity** to put the command pattern in action! | ||
|
||
## Applying the Command Pattern | ||
- first identify code that will change | ||
- copy and replace it by the code we want to have | ||
- Use IDE to create the command class and paste | ||
- add the missing arguments. Where to put them? | ||
- constructor vs method | ||
- Fix GameApp code | ||
- Run the game | ||
|
||
# Implement other actions | ||
- Create CommandInterface | ||
- make AttackCommand implement it | ||
- copy/paste Heal action | ||
- add Surrender | ||
|
||
|
||
# Ask player to choose an action | ||
- go back to GameApp and paste code to ask the player what action to do | ||
- replace dummy code with real | ||
- run the game |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# Command Pattern Definition | ||
|
||
Are you ready for a new design patterns episode? Go grab a cup of coffee and sit tight | ||
because we'll take a deep dive into the command pattern. | ||
|
||
Let's start with the boring part first, the definition and theory, and later we'll | ||
have some fun applying it into our game application. | ||
|
||
The command pattern is a behavioral pattern. In case you already forgot what it means | ||
here's a quick reminder. Behavioral patterns are those that help you design classes | ||
with specific responsibilities that can then work together, instead of putting | ||
all of that code into one giant class. | ||
|
||
The official definition says that the command pattern **encapsulates** a request | ||
as a stand-alone object, allowing parameterization of clients with different requests, | ||
queueing or logging requests, and support undoable operations. | ||
|
||
Uhm, whaat?. Ok, let's try it again with a more friendly definition. | ||
|
||
The command pattern encapsulates a task into an object, **decoupling** what it does, | ||
how it does it, and when it gets done. Also, it allows to undo its actions | ||
by **remembering** enough information of what things need to be reverted. | ||
|
||
Ha! I don't know if it got any better but don't worry, it will make more sense | ||
when we see it in action. | ||
|
||
## Pattern Anatomy | ||
|
||
The pattern is composed by three main parts: | ||
- It has a Command Interface with a single public method that's usually called `execute()`. | ||
- There are concrete commands which implement the Command Interface and hold the task's logic. | ||
- And third, there's an **Invoker** object which holds a command reference and calls `execute()` at some point. | ||
|
||
If you look over the internet you'll discover that I didn't mention two other parts, | ||
the **receiver** and **client**. The receiver is the object that contains the business logic, | ||
and the client is the one that's on charge of **creating** command objects. | ||
|
||
In my opinion, those parts increases the complexity of the design and are not totally required. | ||
They might be useful in a heavy **object-oriented** application so the responsibilities are split better, | ||
but in our case, we would be **over-engineering** our solution, so we'll | ||
keep it simple and just ignore them. | ||
|
||
## Imaginary Example | ||
|
||
Ok, that's enough theory. Let's see an example. Suppose that we want to implement a remote control for a TV. | ||
It has many buttons to interact with it, it can turn the volume up or down, power it on or off, and so on. | ||
|
||
Without much thinking, we could easily implement it with a `switch-case` statement, where each `case` | ||
would represent a button's action containing all the logic. | ||
|
||
```php | ||
public function pressButton(string $button) | ||
{ | ||
switch ($button) { | ||
case 'turnOn': | ||
$this->powerOnDevice($this->tv->getAddress()) | ||
$this->tv->initializeSettings(); | ||
$this->tv->turnOnScreen(); | ||
$this->tv->openDefaultChannel(); | ||
break; | ||
case 'turnOff': | ||
$this->tv->closeApps(); | ||
$this->tv->turnOffScreen(); | ||
$this->powerOffDevice($this->tv->getAddress()) | ||
break; | ||
case 'mute': | ||
$this->tv->toggleMute(); | ||
break; | ||
... | ||
} | ||
} | ||
``` | ||
|
||
As we keep implementing more buttons this approach will become a mess pretty quickly. Also, | ||
it would be a maintenance nightmare and impossible to reuse the code. | ||
|
||
A better approach would be to... you guessed it! Implement the command pattern. We could encapsulate | ||
the logic of each button into their own **command** object. | ||
|
||
And, in our `pressButton` method we would just call `execute()` on | ||
the **right** command instance. | ||
|
||
Our code would look like this: | ||
|
||
The `commands` property holds a list of command objects, and if the button name | ||
is in the list, we call `execute()`. | ||
|
||
Now, you might be wondering, how do I instantiate the commands? | ||
Well, creating objects can be addressed in many ways but there are excellent patterns | ||
that deal with instantiation in a fashion way, for example, my favorite, the **factory** pattern. | ||
In our example, we could introduce a `CommandFactory` and delegate it all the instantiation logic, | ||
but that's a topic for a future chapter. | ||
|
||
From this very moment you can start notice the benefits of the command pattern. We can add or remove buttons | ||
without touching the code of the `pressButton` method. Also, all the logic is encapsulated | ||
into their own class, making it easier to maintain and reuse. And... as a nice side effect, | ||
we've successfully applied the **Open-Close** principle. This method is now open for extension | ||
but closed for modification. | ||
|
||
Next, let's see the command pattern in action and implement it into our game application! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Undo feature - Chapter 4 | ||
|
||
implement undo feature | ||
|
||
## Using Command for queuing actions | ||
|
||
## Where do we see this in the real world? | ||
|
||
## Conclusion |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Design Patterns Episode 2 - Chapter 1 | ||
|
||
- Salute audience | ||
- A quick reminder about what patterns are and what we've covered so far | ||
- What patterns we'll cover in this tutorial and what kind of patterns they are | ||
- Recap of the current state of the game | ||
- Play a round, show GameCommand quickly and then GameApplication::play() | ||
- show where we already introduced patterns | ||
|
||
- Coming next: Command Pattern |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
chapters: [] | ||
chapters: | ||
- intro | ||
- command-intro | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MolloKhan Btw, the actual file name is
sfcasts/command_intro.md
, i.e. with the underscore instead of dash... I think we should match style in both cases, probablysfcasts/command-intro.md
is the correct one.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, I already fixed that. Thanks anyways