Dungeon 8. Energía y feature flags

por Fran Iglesias

Ahora que ya se puede jugar en una mazmorra, aunque sea pequeña, ha llegado el momento de empezar a ponerlo difícil. La primera dificultad para la jugadora será tener un límite de energía. Pero antes necesitamos tener una representación de esa jugadora en el juego.

Así, de primeras, se me ocurre que el rol que está teniendo Game como despachador de comandos, ahora podría ser mejor desempeñado por Player. Al fin y al cabo, los comandos son las órdenes con las que lo dirigimos, por lo que ver algo así tendría bastante sentido:

result = player.do(command)

Además, Player interceptaría el resultado y podría usarlo para que modificar su estado. Pero además, podría informar a Application o a Game de si puede continuar jugando o no.

Presentamos Player

Lo primero es tener una representación de la jugadora. Hay unas cosas básicas que necesitamos saber:

  • que está viva, y, por tanto, puede continuar en el juego
  • si está viva, queremos poder preguntarle si ha salido de la mazmorra
  • qué es lo que nos cuenta (básicamente qué mensaje ha extraído del resultado de la acción)

Nada más despertarse dentro de la mazmorra, nuestra jugadora:

  • Está viva
  • No ha salido de la mazmorra
  • Nos dice que está lista
class PlayerTestCase(unittest.TestCase):
    def test_player_should_be_ready_for_action(self):
        player = Player.awake()

        self.assertTrue(player.is_alive())
        self.assertFalse(player.has_exited())
        self.assertEqual("I'm ready", player.said())

Pues con esto ya estaría:

class Player:
    @classmethod
    def awake(cls):
        return cls()

    def is_alive(self):
        return True

    def has_won(self):
        return False

    def said(self):
        return "I'm ready"

Obviamente, queremos que pueda hacer cosas, así que vamos a pasarle un comando que pueda ejecutar. Por ejemplo, uno que equivalga a salir de la mazmorra.

class ExitDungeonCommand(Command):
    def do(self, receiver):
        return ActionResult.player_exited("You're out")

class PlayerTestCase(unittest.TestCase):
    ## Code removed for clarity

    def test_player_should_be_able_to_exit_dungeon(self):
        player = Player.awake()

        player.do(ExitDungeonCommand(""), player)

        self.assertTrue(player.is_alive())
        self.assertTrue(player.has_won())
        self.assertEqual("You're out", player.said())

Nota: paso player como dummy receiver porque lo voy a ignorar en el contexto del test.

Así que tras un rato de pensarlo:

class Player:
    def __init__(self):
        self.last_message = "I'm ready"
        self.exited = False

    @classmethod
    def awake(cls):
        return cls()

    def do(self, command, receiver):
        result = command.do(receiver)
        self.exited = result.is_finished()
        self.last_message = result.message()

    def is_alive(self):
        return True

    def has_won(self):
        return self.exited

    def said(self):
        return self.last_message

Usando Player con feature flags

En este punto puede que Player ya esté preparado para reemplazar a Game. A la vez, pienso que Application está haciendo el rol de Game, puesto que controla lo que podríamos llamar el bucle de juego. Pero, por supuesto, no quiero enfangarme en una vorágine de cambios y dejar de desarrollar nuevas prestaciones, ya que mi compromiso ahora mismo es introducir todo el tema de energía.

Pero tampoco puedo desarrollar Player en paralelo a Game y no introducirlo en ningún momento, ya que lo necesito para mi propósito.

Así que he pensado en programar un nuevo bucle de juego basado en Player y para desarrollarlo en test sin romper la versión de producción voy a usar una feature toggle o feature flag que me permita usar uno o otro dependiente de si estoy ejecutando en un entorno de producción o de test.

Una librería de feature flags es más de lo que necesita el proyecto en este momento, así que voy a crear una versión sencilla que me sirva sin muchas complicaciones y que, de alguna manera, abstraiga el uso futuro de una librería completa. Básicamente, una feature flag no es más que una condición para ejecutar un bloque de código u otro según un criterio de conveniencia. Lo ideal es que se puedan activar o desactivar de forma separada al despliegue, reduciendo los riesgos derivados de los cambios. Es decir: despliegas un cambio a producción, pero está desactivado y no se ejecuta. Lo activas para ver qué tal funciona. Si hay cualquier problema, lo desactivas y listo. Cuando decides que lo puedes consolidar, eliminar la condicional y el bloque que ya no quieres usar.

En mi caso no voy a tener esa capacidad de separar despliegue y activación, pero me basta con poder decidir si una versión del código se ejecuta en test y otra versión en producción. De este modo, puedo proteger la versión de producción mientras desarrollo en pequeños pasos.

En pocas palabras, lo que necesito es un objeto en el que pueda guardar flags y chequearlos luego.

class TogglesTestCase(unittest.TestCase):
    def test_can_activate_toggle(self):
        toggles = Toggles()
        toggles.activate('some toggle')
        
        self.assertTrue(toggles.is_active('some toggle'))
class Toggles:
    def __init__(self):
        self._toggles = dict()

    def activate(self, toggle_name):
        self._toggles[toggle_name] = True

    def is_active(self, toggle_name):
        return self._toggles[toggle_name]

Por supuesto, necesito poder desactivar un Toggle:

class TogglesTestCase(unittest.TestCase):
    def test_can_activate_toggle(self):
        toggles = Toggles()
        toggles.activate('some toggle')

        self.assertTrue(toggles.is_active('some toggle'))

    def test_can_deactivate_toggle(self):
        toggles = Toggles()
        toggles.activate('another toggle')
        toggles.deactivate('another toggle')

        self.assertFalse(toggles.is_active('another toggle'))

Algo que puedo implementar así:

class Toggles:
    def __init__(self):
        self._toggles = dict()

    def activate(self, toggle_name):
        self._toggles[toggle_name] = True

    def is_active(self, toggle_name):
        return self._toggles[toggle_name]

    def deactivate(self, toggle_name):
        self._toggles[toggle_name] = False

He pensado que si un toggle no está definido es equivalente a que está desactivado.

class TogglesTestCase(unittest.TestCase):
    def test_can_activate_toggle(self):
        toggles = Toggles()
        toggles.activate('some toggle')

        self.assertTrue(toggles.is_active('some toggle'))

    def test_can_deactivate_toggle(self):
        toggles = Toggles()
        toggles.activate('another toggle')
        toggles.deactivate('another toggle')

        self.assertFalse(toggles.is_active('another toggle'))

    def test_undefined_toggle_is_deactivated(self):
        toggles = Toggles()

        self.assertFalse(toggles.is_active('undefined toggle'))

De este modo puedo introducir Toggles para test, sin tener que tocar producción.

class Toggles:
    def __init__(self):
        self._toggles = dict()

    def activate(self, toggle_name):
        self._toggles[toggle_name] = True

    def is_active(self, toggle_name):
        if toggle_name not in self._toggles.keys():
            return False
        return self._toggles[toggle_name]

    def deactivate(self, toggle_name):
        self._toggles[toggle_name] = False

Consolido estos cambios con un commit y ahora que tenemos Toggles podemos introducirlo en Application. Para eso, tengo que inyectarlo en construcción en los lugares adecuados. De paso, refactorizo un poco los tests de Application.

class TestApplication(TestCase):

    def setUp(self) -> None:
        self.toggles = Toggles()
        self.obtain_user_command = FixedObtainUserCommand("go north")
        self.show_output = TestShowOutput()
        self.application = Application(self.obtain_user_command, self.show_output, DungeonFactory(), self.toggles)

    def test_should_show_title(self):
        self.application.run('test')
        self.assertIn("Welcome to the Dungeon", self.show_output.contents())

    def test_should_show_command_echo(self):
        self.application.run('test')
        self.assertIn("You said: go north", self.show_output.contents())

Con esto ya puedo activar un Toggle. Lo primero que hago es extraer todo el bloque de código que quiero reemplazar a un método.

class Application:
    def __init__(self, obtain_user_command, show_output, factory, toggles):
        self._toggles = toggles
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output
        self._factory = factory

    def run(self, dungeon='game'):
        self.run_with_game(dungeon)

    def run_with_game(self, dungeon):
        self._show_message("Welcome to the Dungeon")
        game = self._prepare_game_with_dungeon(dungeon)
        action_result = ActionResult.player_acted("")
        while not action_result.is_finished():
            command = self._obtain_command()
            action_result = game.do_command(command)
            self._show_message(str(command))
            self._show_message(action_result.message())

A continuación, añado el check del Toggle:

    def run(self, dungeon='game'):
        if not self._toggles.is_active('with_player'):
            self.run_with_game(dungeon)
        else:
            pass

    def run_with_game(self, dungeon):
        self._show_message("Welcome to the Dungeon")
        game = self._prepare_game_with_dungeon(dungeon)
        action_result = ActionResult.player_acted("")
        while not action_result.is_finished():
            command = self._obtain_command()
            action_result = game.do_command(command)
            self._show_message(str(command))
            self._show_message(action_result.message())

Si ahora activo el Toggle en test, provocaré que fallen todos los tests de Application, que es justo lo que me interesa:

class TestApplication(TestCase):

    def setUp(self) -> None:
        self.obtain_user_command = FixedObtainUserCommand("go north")
        self.show_output = TestShowOutput()
        self.toggles = Toggles()
        self.toggles.activate('with_player')

        self.application = Application(self.obtain_user_command, self.show_output, DungeonFactory(), self.toggles)

    ## Code removed

Para poder desplegar podría hacer esto:

class Application:
    def __init__(self, obtain_user_command, show_output, factory, toggles):
        self._toggles = toggles
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output
        self._factory = factory

    def run(self, dungeon='game'):
        if not self._toggles.is_active('with_player'):
            self.run_with_game(dungeon)
        else:
            self.run_with_game(dungeon)

    ## Code removed

De este modo tengo:

  • Todos los tests pasando, lo que podría ser necesario para superar la pipeline de integración continua
  • El Toogle en su sitio, listo para que pueda empezar a escribir el código nuevo
class Application:
    def __init__(self, obtain_user_command, show_output, factory, toggles):
        self._toggles = toggles
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output
        self._factory = factory

    def run(self, dungeon='game'):
        if not self._toggles.is_active('with_player'):
            self.run_with_game(dungeon)
        else:
            self.run_with_player(dungeon)

    def run_with_player(self, dungeon):
        pass

Y esta implementación funciona:

class Application:
    def __init__(self, obtain_user_command, show_output, factory, toggles):
        self._toggles = toggles
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output
        self._factory = factory

    def run(self, dungeon='game'):
        if not self._toggles.is_active('with_player'):
            self.run_with_game(dungeon)
        else:
            self.run_with_player(dungeon)

    def run_with_player(self, dungeon_name):
        self._show_message("Welcome to the Dungeon")
        dungeon = self._build_dungeon(dungeon_name)
        player = Player()

        while player.is_alive() and not player.has_won():
            command = self._obtain_command()
            player.do(command, dungeon)
            self._show_message(str(command))
            self._show_message(player.said())

En este momento tengo dos versiones del juego. En producción el bucle del juego se basa en Game y en tests se basa en Player.

Si quiero probar la versión con Player en producción, no tengo más que hacer este cambio en __main__.py

def main(args=None):
    if args is None:
        args = sys.argv[1:]

    toggles = Toggles()
    toggles.activate('with_player')
    application = Application(ConsoleObtainUserCommand(), ConsoleShowOutput(), DungeonFactory(), toggles)
    application.run()

Una vez comprobado que funciona correctamente, no tenemos más que borrar la condicional y deshacernos del código que ya no usamos.

from dungeon.app.domain.player import Player


class Application:
    def __init__(self, obtain_user_command, show_output, factory, toggles):
        self._toggles = toggles
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output
        self._factory = factory

    def run(self, dungeon='game'):
        self.run_with_player(dungeon)

    def run_with_player(self, dungeon_name):
        self._show_message("Welcome to the Dungeon")
        dungeon = self._build_dungeon(dungeon_name)
        player = Player()

        while player.is_alive() and not player.has_won():
            command = self._obtain_command()
            player.do(command, dungeon)
            self._show_message(str(command))
            self._show_message(player.said())

    def _obtain_command(self):
        return self._obtain_user_command.command()

    def _show_message(self, message):
        self._show_output.put(message)

    def _build_dungeon(self, dungeon):
        return self._factory.make(dungeon)

Y esto incluso Game y sus tests, así como cualquier mención al Toggle definido para la ocasión. Ver el estado del código en este punto.

Ahora podemos empezar a trabajar con el consumo de energía.

Energía

En muchos juegos de aventura la protagonista pierde energía al actuar. Esta se puede recuperar de varias formas: descansando, que suele ser lentamente, o consiguiendo algún tipo de objeto o comida. Para este juego voy a usar solo la segunda opción. Todas las acciones consumirán al menos una unidad de energía.

Si se agota la energía, la jugadora muere y pierde el juego. En el fondo es como poner un límite de tiempo para salir de la mazmorra, aunque habrá oportunidades de recuperar.

Otra regla es que la jugadora tiene un máximo de energía, y nunca se podrá superar ese máximo.

Este mecanismo puede usarse, en la narrativa del juego, para representar fuerza o incluso aire. Imagina que lo que representamos es una mazmorra sumergida.

Estas son las reglas básicas. Ahora toca pensar en cómo se van a representar en el programa.

Energy será un objeto miembro de Player. Su trabajo será:

  • Decrementar su valor cuando se realice una acción, teniendo en cuenta el coste de esa acción.
  • Decidir si la jugadora sigue viva atendiendo a su nivel de energía.
  • Incrementar su valor cuando se realice una acción que resulte en una aportación, como comer (eat).

Ahora mismo no tenemos este último tipo de acción, así que me centraré en los dos primeros objetivos. Para eso, también necesitaré que las acciones tengan un coste y que pueda inicializar Player con un valor de energía.

Algo interesante es que este desarrollo se puede hacer sin afectar al juego, pero funcionará en cuanto esté listo. Esto es, Player ya exponer un método is_alive que ahora mismo siempre devolverá True. Ese método ya se está usando para controlar el bucle de juego.

Para mí, la mejor forma de testear estas cosas sería poder crear Player con un nivel de energía arbitrario, de modo que sea fácil entender como funciona el mecanismo usando tests.

    def test_player_dies_if_action_consumes_all_energy(self):
        player = Player.awake_with_energy(EnergyUnit(10))
        player.do(KillerCommand(EnergyUnit(15)), player)
        
        self.assertFalse(player.is_alive())
        self.assertFalse(player.has_won())
        self.assertEqual("You're dead!", player.said())

En este test, una jugadora se despertaría en la mazmorra con un cierto nivel de energía y, dado que ejecuta un comando que consuma más que esa energía, tendrá que morir.

EnergyUnit es un objeto que representa la cantidad de energía. ConsumingCommand será un comando que consume una cantidad dada de energía. Solo lo usaremos para testing.

Una vez creados los nuevos objetos y el nuevo constructor vamos a ver como podemos implementar esto. Queremos que Player le pregunte a su Energy si sigue viva:

    def is_alive(self):
        return self._energy.is_alive()

La siguiente implementación de Player todavía no hace pasar el test porque no tenemos forma de tomar en cuenta el coste de la acción:

class EnergyUnit:
    def __init__(self, value):
        self._value = value

    def is_greater_than(self, other):
        return self._value > other.value()

    def value(self):
        return self._value


class Energy:
    def __init__(self, starting_energy):
        self._energy = starting_energy

    def is_alive(self):
        return self._energy.is_greater_than(EnergyUnit(0))


class Player:
    def __init__(self, starting_energy):
        self._last_message = "I'm ready"
        self._energy = Energy(starting_energy)
        self._exited = False

    @classmethod
    def awake(cls):
        return cls(EnergyUnit(100))

    @classmethod
    def awake_with_energy(cls, starting_energy):
        return cls(starting_energy)

    def do(self, command, receiver):
        result = command.do(receiver)
        self._exited = result.is_finished()
        self._last_message = result.message()

    def is_alive(self):
        return self._energy.is_alive()

    def has_won(self):
        return self._exited

    def said(self):
        return self._last_message

Necesitamos algo así:

class EnergyUnit:
    def __init__(self, value):
        self._value = value

    def is_greater_than(self, other):
        return self._value > other.value()

    def value(self):
        return self._value

    def subtract(self, delta):
        return EnergyUnit(self._value - delta.value())


class Energy:
    def __init__(self, starting_energy):
        self._energy = starting_energy

    def is_alive(self):
        return self._energy.is_greater_than(EnergyUnit(0))

    def decrease(self, delta):
        self._energy = self._energy.subtract(delta)


class Player:
    def __init__(self, starting_energy):
        self._last_message = "I'm ready"
        self._energy = Energy(starting_energy)
        self._exited = False

    @classmethod
    def awake(cls):
        return cls(EnergyUnit(100))

    @classmethod
    def awake_with_energy(cls, starting_energy):
        return cls(starting_energy)

    def do(self, command, receiver):
        result = command.do(receiver)
        self._energy.decrease(result.cost())
        self._exited = result.is_finished()
        self._last_message = result.message()

    def is_alive(self):
        return self._energy.is_alive()

    def has_won(self):
        return self._exited

    def said(self):
        return self._last_message

Esto hace que tengamos que introducir un método cost en ActionResult… y esto me hace saltar la alarma: ¿Cuántas cosas más tendremos que añadir a ActionResult? ¿Estamos ante un objeto con demasiada responsabilidad?

Quizá lo más fácil sea empezar haciéndolo así y luego veremos si es posible otro diseño. Primero hacemos pasar el test:

class ActionResult:
    @classmethod
    def player_acted(cls, message):
        return cls(message)

    @classmethod
    def player_moved(cls, message, destination):
        return cls(message, destination)

    @classmethod
    def player_exited(cls, message):
        return cls(message, None, True)

    @classmethod
    def game_started(cls):
        return cls("")

    def __init__(self, message, destination=None, exited=False):
        self._message = message
        self._destination = destination
        self._exited = exited

    def message(self):
        return self._message

    def is_finished(self):
        return self._exited

    def moved_to(self):
        return self._destination

    def cost(self):
        return EnergyUnit(15)

A continuación hacemos que Command pueda informar a ActionResult de su coste, añadiendo un parámetro a los muchos constructores, con un valor por defecto para no romper nada:

from dungeon.app.domain.player import EnergyUnit


class ActionResult:
    @classmethod
    def player_acted(cls, message, cost=EnergyUnit(1)):
        return cls(message, None, False, cost)

    @classmethod
    def player_moved(cls, message, destination, cost=EnergyUnit(1)):
        return cls(message, destination, False, cost)

    @classmethod
    def player_exited(cls, message, cost=EnergyUnit(1)):
        return cls(message, None, True, cost)

    @classmethod
    def game_started(cls):
        return cls("")

    def __init__(self, message, destination=None, exited=False, cost=EnergyUnit(1)):
        self._message = message
        self._destination = destination
        self._exited = exited
        self._cost = cost

    def message(self):
        return self._message

    def is_finished(self):
        return self._exited

    def moved_to(self):
        return self._destination

    def cost(self):
        return self._cost

De este modo, cambiamos:

class KillerCommand(Command):
    def __init__(self, energy_consumption):
        self._energy_consumption = energy_consumption

    def do(self, receiver):
        return ActionResult.player_acted("You're dead!", self._energy_consumption)

Y los tests pasan.

Vamos a añadir un test que verifique que un comando que no consume suficiente energía no nos mata.

def test_player_does_not_die_if_action_does_not_consume_all_energy(self):
    player = Player.awake_with_energy(EnergyUnit(10))
    player.do(KillerCommand(EnergyUnit(5)), player)
    self.assertTrue(player.is_alive())

Posiblemente, debería haber empezado por aquí… En cualquier caso, parece que estamos en el camino correcto. Lo único que nos falta es añadir el coste a los distintos comandos del juego. Por defecto, todos los comandos cuestan una unidad de energía, excepto GoCommand, que consumirá cinco.

Pero… ¿Dónde añadimos esto? En muchos casos, ActionResult es instanciado en el objeto que recibe el comando y no tenemos una forma de ajustar este valor a posteriori. La solución fácil es añadir un setter. La solución menos fácil es un decorador. El decorador debería tener la interfaz de ActionResult. Así que extraigo la interfaz a una clase llamada Result

class Result:
    def message(self):
        pass

    def is_finished(self):
        pass

    def moved_to(self):
        pass

    def cost(self):
        pass

Ahora implemento un decorador para llevar el coste:

    def test_action_result_with_cost(self):
        result = ActionResult.player_acted("Action")
        result = WithCost(result)
        self.assertEqual(EnergyUnit(1), result.cost())

Que se define así:

class WithCost(Result):
    def __init__(self, origin, cost):
        self._origin = origin
        self._cost = cost

    def message(self):
        return self._origin.message()

    def is_finished(self):
        return self._origin.is_finished()

    def moved_to(self):
        return self._origin.moved_to()

    def cost(self):
        return self._cost

Ahora puedo usar este decorador de los Command para añadir el coste de la acción. Y eso implica que no necesito inicializarlo en ActionResult, por lo que podría deshacer ese cambio.

class GoCommand(Command):
    def __init__(self, argument):
        super().__init__(argument)

    def do(self, receiver):
        return WithCost(receiver.go(self._argument), EnergyUnit(5))

    def _name(self):
        return "go"
class LookCommand(Command):
    def __init__(self, argument):
        super().__init__(argument)

    def do(self, receiver):
        return WithCost(receiver.look(self._argument), EnergyUnit(1))

    def _name(self):
        return "look"

¿Un comando que no se entiende debería tener un coste? ¿Por qué no?

class InvalidCommand(Command):
    def __init__(self, user_input):
        super().__init__(user_input)

    def do(self, dungeon):
        return WithCost(ActionResult.player_acted("I don't understand"), EnergyUnit(1))

    def __str__(self) -> str:
        return "You said: {}".format(self._argument)

Este diseño me gusta más. No me convence del todo todavía, pero me permite hacer algo interesante con ActionResult, como es componerla con piezas encargadas de partes específicas. Tengo que seguir manteniendo muchos métodos, pero puedo mejorar mucho la creación.

Por tanto, voy a desplegar una versión que se pueda usar con el estado actual del código y, posteriormente, voy a refactorizar para convertir a ActionResult en un objeto compuesto.

La iteración no está completa

Sin embargo, hay un detalle que me preocupa. Una iteración debería mostrar un cambio visible a las usuarias. Pero en este caso, el hecho de que se consuma energía solo va a notarse en caso de que la jugadora muera.

En otras palabras, tendríamos que mostrar información de la energía disponible tras cada acción. De este modo, la jugadora podrá tomar decisiones basadas en ello.

La forma más fácil que se me ocurre es añadir una línea en el método Player.said(). Para eso tengo que cambiar los tests de Player, de modo que no fallen al hacer el cambio:

class PlayerTestCase(unittest.TestCase):
    def test_player_should_be_ready_for_action(self):
        player = Player.awake()

        self.assertTrue(player.is_alive())
        self.assertFalse(player.has_won())
        self.assertIn("I'm ready", player.said())
        self.assertIn("Remaining energy: 100", player.said())

    def test_player_should_be_able_to_exit_dungeon(self):
        player = Player.awake()

        player.do(ExitDungeonCommand(""), player)

        self.assertTrue(player.is_alive())
        self.assertTrue(player.has_won())
        self.assertIn("You're out", player.said())
        self.assertIn("Remaining energy: 99", player.said())

    def test_player_dies_if_action_consumes_all_energy(self):
        player = Player.awake_with_energy(EnergyUnit(10))
        player.do(KillerCommand(EnergyUnit(15)), player)
        self.assertFalse(player.is_alive())
        self.assertFalse(player.has_won())
        self.assertIn("You're dead!", player.said())
        self.assertIn("Remaining energy: -5", player.said())

    def test_player_does_not_die_if_action_does_not_consume_all_energy(self):
        player = Player.awake_with_energy(EnergyUnit(10))
        player.do(KillerCommand(EnergyUnit(5)), player)
        self.assertTrue(player.is_alive())

Necesitaré una representación en string de Energy:

class Energy:
    def __init__(self, starting_energy):
        self._energy = starting_energy

    def is_alive(self):
        return self._energy.is_greater_than(EnergyUnit(0))

    def decrease(self, delta):
        self._energy = self._energy.subtract(delta)

    def __str__(self):
        return str(self._energy.value())

Y en Player me bastaría con añadir lo siguiente:

class Player:
    # Removed code

    def said(self):
        return "{message}\nRemaining energy: {energy}".format(message=self._last_message, energy=self._energy)

Ahora, podemos jugar sabiendo que gastamos energía. Probablemente, con la mazmorra actual no resulte una gran dificultad. Pero basta con incluir otro diseño de mazmorra para que la cosa se complique. En cualquier caso, el estado actual del código lo permite.

Próximos pasos

Mis próximos objetivos son los siguientes:

  • Añadir una mazmorra más compleja en la que la jugadora pueda morir por no alcanzar la salida antes de agotar la energía.
  • Mejorar el diseño de todo lo relacionado con ActionResult. Esto es algo que me gustaría tratar en un artículo dedicado.

Aparte de eso, en la siguiente iteración debería ser posible conseguir recargar energía de alguna forma. Eso implica que las celdas deben poder tener objetos accionables y la jugadora debe poder coleccionarlos y usarlos.

Esto sigue en el capítulo 9

Temas

good-practices

refactoring

php

testing

tdd

design-patterns

python

blogtober19

design-principles

tb-list

misc

bdd

legacy

golang

dungeon

ruby

tools

hexagonal

tips

software-design

ddd

books

bbdd

soft-skills

pulpoCon

oop

javascript

api

typescript

sql

ethics

agile

swift

java