Introducción
¡Hola amigos! ¡Bienvenidos de nuevo al segundo episodio de nuestra serie Patrones de diseño! En este episodio, continuaremos nuestro viaje para construir el mejor juego de rol de línea de comandos de la historia. Para ello, aplicaremos no uno, ni dos, sino cinco nuevos patrones de diseño.
Aprenderemos tres nuevos patrones de comportamiento: Comando, Cadena de Responsabilidad y Estado. Estos patrones ayudan a organizar el código en clases separadas que luego pueden interactuar entre sí.
También conoceremos el patrón Fábrica, que es un patrón de creación. Este tipo de patrón sirve para ayudar a instanciar objetos, igual que el patrón Constructor del episodio uno. Y, como extra, trataremos uno de mis favoritos: el patrón NullObject.
Para más información sobre los tipos de patrones de diseño, consulta el primer capítulo del episodio uno.
Recordatorio Sobre los Patrones de Diseño
Antes de entrar en materia, recapitulemos qué son los patrones de diseño y lo que hemos tratado hasta ahora.
En pocas palabras, los patrones de diseño son soluciones probadas en batalla a problemas de diseño de software. Cuando te encuentres con un problema, puedes consultar el catálogo de patrones de diseño y encontrar el patrón ideal para tu caso de uso. Y recuerda que siempre puedes modificar el patrón de la forma que mejor se adapte a tu aplicación.
En el episodio uno, cubrimos cinco patrones de diseño: Estrategia, Constructor, Observador, PubSub y Decorador. Seguimos utilizando esos patrones en nuestro juego, pero no es necesario que los entiendas para seguir este tutorial.
Configuración del Proyecto
Bien, ¡vamos a hacerlo! Te recomiendo encarecidamente que descargues el código del curso desde esta página y codifiques conmigo. El código base ha cambiado bastante desde el episodio uno, así que si estás utilizando el código de ese tutorial, asegúrate de descargar esta nueva versión. Después de descomprimirlo, encontrarás un directorio start/
con el mismo código que ves aquí. El archivo README.md
contiene todos los detalles de configuración que necesitarás.
Ésta no puede ser más fácil. Ejecuta
composer install
y, para jugar, ejecuta:
php bin/console app:game:play
Tenemos unos cuantos personajes para elegir. Yo seré un luchador. Y... ¡qué bien! ¡Ganamos! Hubo cuatro rondas de lucha, hicimos 79 puntos de daño, recibimos 0, ¡y ganamos 30 puntos de XP! Estupendo. Y aquí arriba, puedes ver cómo se desarrolló el combate. ¡Esto sí que es emocionante!
Entonces... ¿cómo funciona esto? Abre GameCommand.php
. Este es un comando de Symfony que prepara las cosas, inicializa este objeto global $printer
, que es muy práctico para imprimir información donde la necesitemos, y luego nos pregunta qué personaje queremos ser.
// ... lines 1 - 14 | |
'app:game:play') | (|
class GameCommand extends Command | |
{ | |
// ... lines 18 - 24 | |
protected function execute(InputInterface $input, OutputInterface $output): int | |
{ | |
$io = new SymfonyStyle($input, $output); | |
// Static field so we can print messages from anywhere | |
GameApplication::$printer = new MessagePrinter($io); | |
$io->section('Welcome to the game where warriors fight against each other for honor and glory... and 🍕!'); | |
$characters = $this->game->getCharactersList(); | |
$playerChoice = $io->choice('Select your character', $characters); | |
$playerCharacter = $this->game->createCharacter($playerChoice); | |
$playerCharacter->setNickname($playerChoice); | |
GameApplication::$printer->initPlayerPrinters($playerCharacter->getId()); | |
$this->play($playerCharacter); | |
return Command::SUCCESS; | |
} | |
// ... lines 46 - 114 | |
} |
A continuación, inicia la batalla llamando a play()
en la propiedad GameApplication
, imprime los resultados y nos permite seguir jugando.
// ... lines 1 - 46 | |
private function play(Character $player): void | |
{ | |
GameApplication::$printer->writeln(sprintf('Alright %s! It\'s time to fight!', | |
$player->getNickname() | |
)); | |
// ... lines 52 - 54 | |
do { | |
// ... lines 56 - 65 | |
$aiCharacter = $this->game->createAiCharacter(); | |
// ... lines 67 - 72 | |
$this->game->play($player, $aiCharacter, $fightResultSet); | |
// ... lines 74 - 80 | |
$this->printResult($fightResultSet, $player); | |
// ... lines 82 - 86 | |
$answer = GameApplication::$printer->choice('Want to keep playing?', [ | |
1 => 'Fight!', | |
2 => 'Exit Game', | |
]); | |
} while ($answer === 'Fight!'); | |
} | |
// ... lines 93 - 116 |
Esto no es nada del otro mundo. Todo el trabajo pesado ocurre en el método play()
de GameApplication
. Si mantienes pulsada la tecla "CMD" + "B" para ir a la definición, podemos ver que este método toma dos objetos personaje -el jugador, que somos nosotros, y la IA- y hace que se ataquen mutuamente hasta que uno de ellos gane.
// ... lines 1 - 9 | |
class GameApplication | |
{ | |
// ... lines 12 - 23 | |
public function play(Character $player, Character $ai, FightResultSet $fightResultSet): void | |
{ | |
while (true) { | |
// ... lines 27 - 32 | |
// Player's turn | |
$playerDamage = $player->attack(); | |
// ... lines 35 - 39 | |
$damageDealt = $ai->receiveAttack($playerDamage); | |
// ... lines 41 - 46 | |
if ($this->didPlayerDie($ai)) { | |
$this->endBattle($fightResultSet, $player, $ai); | |
return; | |
} | |
// ... lines 51 - 72 | |
} | |
} | |
// ... lines 75 - 204 | |
} |
Si exploramos un poco más esta clase, encontraremos algunos lugares en los que ya hemos aplicado algunos patrones de diseño. Si buscas el método createCharacter()
, podrás ver cómo hemos utilizado el patrón Constructor para crear y configurar objetos personaje.
// ... lines 1 - 121 | |
public function createCharacter(string $character, int $extraBaseDamage = 0, int $extraHealth = 0, int $level = 1): Character | |
{ | |
return match (strtolower($character)) { | |
'fighter' => $this->characterBuilder | |
->setMaxHealth(60 + $extraHealth) | |
->setBaseDamage(12 + $extraBaseDamage) | |
->setAttackType('sword') | |
->setArmorType('shield') | |
->setLevel($level) | |
->buildCharacter(), | |
'archer' => $this->characterBuilder | |
->setMaxHealth(50 + $extraHealth) | |
->setBaseDamage(10 + $extraBaseDamage) | |
->setAttackType('bow') | |
->setArmorType('leather_armor') | |
->setLevel($level) | |
->buildCharacter(), | |
// ... lines 140 - 157 | |
}; | |
} | |
// ... lines 160 - 206 |
Y, aquí abajo, estamos utilizando el patrón Observador, añadiendo o eliminando observadores, y notificándoles una vez finalizado el combate.
// ... lines 1 - 9 | |
class GameApplication | |
{ | |
// ... lines 12 - 182 | |
public function subscribe(GameObserverInterface $observer): void | |
{ | |
if (!in_array($observer, $this->observers, true)) { | |
$this->observers[] = $observer; | |
} | |
} | |
public function unsubscribe(GameObserverInterface $observer): void | |
{ | |
$key = array_search($observer, $this->observers, true); | |
if ($key !== false) { | |
unset($this->observers[$key]); | |
} | |
} | |
private function notify(FightResultSet $fightResultSet): void | |
{ | |
foreach ($this->observers as $observer) { | |
$observer->onFightFinished($fightResultSet); | |
} | |
} | |
} |
¡Muy bien! Es hora de conocer el patrón Comando y hacer que nuestro juego sea más interactivo. Eso a continuación.