Buy Access to Course
08.

La clase observadora

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ahora que hemos terminado nuestra clase objeto - GameApplication - en la que podemos llamar a subscribe() si queremos que nos avisen cuando termine un combate - pasemos a crear un observador que calcule cuánta XP debe ganar el ganador y si el personaje debe subir de nivel o no.

Pero antes, tenemos que añadir algunas cosas a la clase Character para ayudar. En la parte superior, añade private int $level que será por defecto 1 y un private int $xp que será por defecto 0:

87 lines | src/Character/Character.php
// ... lines 1 - 8
class Character
{
// ... lines 11 - 15
private int $level = 1;
private int $xp = 0;
// ... lines 18 - 85
}

Aquí abajo un poco, añade public function getLevel(): int que seráreturn $this->level... y otro método de conveniencia llamado addXp()que aceptará el nuevo $xpEarned y devolverá el nuevo número XP. Dentro digamos$this->xp += $xpEarned... y return $this->xp:

87 lines | src/Character/Character.php
// ... lines 1 - 8
class Character
{
// ... lines 11 - 65
public function getLevel(): int
{
return $this->level;
}
public function addXp(int $xpEarned): int
{
$this->xp += $xpEarned;
return $this->xp;
}
// ... lines 77 - 85
}

Por último, justo después, voy a pegar un método más llamado levelUp(). Lo llamaremos cuando un personaje suba de nivel: aumenta los $level, $maxHealth, y $baseDamage:

99 lines | src/Character/Character.php
// ... lines 1 - 8
class Character
{
// ... lines 11 - 65
public function levelUp(): void
{
// +%15 bonus to stats
$bonus = 1.15;
$this->level++;
$this->maxHealth = floor($this->maxHealth * $bonus);
$this->baseDamage = floor($this->baseDamage * $bonus);
// todo: level up attack and armor type
}
// ... lines 77 - 97
}

También podríamos subir de nivel los tipos de ataque y armadura si quisiéramos.

Creación de la clase observadora

Bien, ahora vamos a crear el observador. Dentro del directorio src/Observer/, añade una nueva clase PHP. Llamémosla XpEarnedObserver. Y todos nuestros observadores necesitanimplement el GameObserverInterface. Ve a "Generar código", o Command+Nen un Mac para implementar el método onFightFinished():

14 lines | src/Observer/XpEarnedObserver.php
// ... lines 1 - 2
namespace App\Observer;
use App\FightResult;
class XpEarnedObserver implements GameObserverInterface
{
public function onFightFinished(FightResult $fightResult): void
{
// TODO: Implement onFightFinished() method.
}
}

Para las tripas de onFightFinished(), voy a delegar el trabajo real en un servicio llamado XpCalculator.

Si has descargado el código del curso, deberías tener un directorio tutorial/ conXpCalculator.php dentro. Copia eso, en src/, crea un nuevo directorioService/ y pégalo dentro. Puedes comprobarlo si quieres, pero no es nada del otro mundo:

59 lines | src/Service/XpCalculator.php
// ... lines 1 - 2
namespace App\Service;
use App\Character\Character;
class XpCalculator
{
public function addXp(Character $winner, int $enemyLevel): void
{
$xpEarned = $this->calculateXpEarned($winner->getLevel(), $enemyLevel);
$totalXp = $winner->addXp($xpEarned);
$xpForNextLvl = $this->getXpForNextLvl($winner->getLevel());
if ($totalXp >= $xpForNextLvl) {
$winner->levelUp();
}
}
private function calculateXpEarned(int $winnerLevel, int $loserLevel): int
{
$baseXp = 30;
$rawXp = $baseXp * $loserLevel;
$levelDiff = $winnerLevel - $loserLevel;
return match (true) {
$levelDiff === 0 => $rawXp,
// You get less XP when the opponent is lower level than you
$levelDiff > 0 => $rawXp - floor($loserLevel * 0.20),
// You get extra XP when the opponent is higher level than you
$levelDiff < 0 => $rawXp + floor($loserLevel * 0.20),
};
}
private function getXpForNextLvl(int $currentLvl): int
{
$baseXp = 100;
$xpNeededForCurrentLvl = $this->fibonacciProgressionFormula($baseXp, $currentLvl);
$xpNeededForNextLvl = $this->fibonacciProgressionFormula($baseXp, $currentLvl + 1);
// Since the character holds the total amount of XP earned we need to include
// the XP needed for the current level.
return $xpNeededForCurrentLvl + $xpNeededForNextLvl;
}
private function fibonacciProgressionFormula(int $baseXp, int $currentLvl): int
{
$currentLvl--;
if ($currentLvl === 0) {
return 0;
}
return $baseXp * ($currentLvl-1) + ($baseXp * ($currentLvl));
}
}

Toma el Character que ganó, el nivel del enemigo, y calcula cuánta XP debe otorgar al ganador. Luego, si son elegibles para subir de nivel, sube de nivel a ese personaje.

En XpEarnedObserver, podemos utilizar esto. Crea un constructor para que podamos autoinstalar un private readonly (readonly sólo para estar súper a la moda) XpCalculator $xpCalculator:

23 lines | src/Observer/XpEarnedObserver.php
// ... lines 1 - 5
use App\Service\XpCalculator;
class XpEarnedObserver implements GameObserverInterface
{
public function __construct(
private readonly XpCalculator $xpCalculator
) {
}
// ... lines 14 - 21
}

A continuación, pongamos el $winner en una variable - $fightResult->getWinner() - y$loser en $fightResult->getLoser(). Por último, digamos $this->xpCalculator->addXp()y pasemos $winner y $loser->getLevel():

23 lines | src/Observer/XpEarnedObserver.php
// ... lines 1 - 7
class XpEarnedObserver implements GameObserverInterface
{
// ... lines 10 - 14
public function onFightFinished(FightResult $fightResult): void
{
$winner = $fightResult->getWinner();
$loser = $fightResult->getLoser();
$this->xpCalculator->addXp($winner, $loser->getLevel());
}
}

Conectando el sujeto y el observador

¡Qué bien! El sujeto y el observador ya están hechos. El último paso es instanciar el observador y hacer que se suscriba al sujeto: GameApplication. Vamos a hacerlo manualmente dentro de GameCommand.

Abre src/Command/GameCommand.php, y busca execute(), que es donde actualmente estamos inicializando todo el código dentro de nuestra aplicación. En unos minutos, veremos una forma más Symfony de conectar todo esto. Por ahora, digamos$xpObserver = new XpEarnedObserver()... y pasemos que un servicio new XpCalculator() para que esté contento. Entonces, podemos decir $this->game (que es el GameApplication)->subscribe($xpObserver):

105 lines | src/Command/GameCommand.php
// ... lines 1 - 7
use App\Observer\XpEarnedObserver;
use App\Service\XpCalculator;
// ... lines 10 - 16
class GameCommand extends Command
{
// ... lines 19 - 25
protected function execute(InputInterface $input, OutputInterface $output): int
{
$xpObserver = new XpEarnedObserver(
new XpCalculator()
);
$this->game->subscribe($xpObserver);
// ... lines 32 - 47
}
// ... lines 49 - 103
}

Así que estamos suscribiendo el observador antes de ejecutar nuestra aplicación aquí.

Esto significa que... ¡estamos listos! Pero, para que sea un poco más evidente si esto funciona, vuelve a Character y añade una función más aquí llamada getXp(), que devolverá int a través de return $this->xp:

104 lines | src/Character/Character.php
// ... lines 1 - 8
class Character
{
// ... lines 11 - 89
public function getXp(): int
{
return $this->xp;
}
// ... lines 94 - 102
}

Esto nos permitirá, dentro de GameCommand... si te desplazas un poco hacia abajo hastaprintResults()... aquí vamos... añadir algunas cosas como$io->writeIn('XP: ' . $player->getXp())... y lo mismo para Final Level, con $player->getLevel():

107 lines | src/Command/GameCommand.php
// ... lines 1 - 16
class GameCommand extends Command
{
// ... lines 19 - 78
private function printResult(FightResult $fightResult, Character $player, SymfonyStyle $io)
{
// ... lines 81 - 99
$io->writeln('Damage received: ' . $fightResult->getDamageReceived());
$io->writeln('XP: ' . $player->getXp());
$io->writeln('Final Level: ' . $player->getLevel());
// ... lines 103 - 104
}
}

Bien, equipo, ¡es hora de probar! Gira, corre

./bin/console app:game:play

y juguemos como el fighter, porque sigue siendo uno de los personajes más difíciles y... ¡impresionante! Como hemos ganado, hemos recibido 30 XP. Seguimos siendo de nivel 1, así que luchemos unas cuantas veces más. Aw... perdimos, así que no hay XP. Ahora tenemos 60 XP... 90 XP... ¡guau! ¡Hemos subido de nivel! Dice Final Level: 2. ¡Está funcionando!

Lo bueno de esto es que GameApplication no necesita saber ni preocuparse por la XP y la lógica de la subida de nivel. Sólo avisa a sus suscriptores y ellos pueden hacer lo que quieran.

A continuación, vamos a ver cómo podríamos conectar todo esto utilizando el contenedor de Symfony. También hablaremos de las ventajas de este patrón y de las partes de SOLID a las que ayuda.