Dungeon 5. Laberintos y deuda técnica

por Fran Iglesias

Tenemos que empezar a hacer un juego interesante, pero también abordar los problemas de diseño que arrastrábamos. Esta iteración se entrega en dos partes.

Mazmorras laberínticas

AHora que las jugadoras pueden interaccionar con el juego lo que piden es que sea mínimamente desafiante. Como mínimo, la mazmorra tienen que ser, o parecer, un laberinto del que resulte lo más difícil posible salir, pero no imposible.

Trampas, puertas cerradas, enemigos, power-ups, etc., no tienen ningún valor si no existe una base que suponga por sí misma una dificultad que haga interesante el juego.

Para hacer un juego de mazmorras complicado hay varios factores que considerar:

  • El tamaño: cuanto más grande sea la mazmorra, más difícil.
  • El nivel de ramificación: cuantas más conexiones haya entre mazmorras, más difícil será orientarse, aunque hasta un cierto límite, ya que si todas las celdas se conectan será más fácil encontrar caminos a la salida.
  • Aleatoridad: si el laberinto cambia en cada partida, la jugadora tendrá que descubrir los caminos de nuevo.

En cuanto a modalidades de implementación podríamos optar entre mazmorras creadas manualmente o algorítmicamente. Esta última me parece más compleja, pero no debería ser un problema diseñar el sistema de tal modo que sea fácil enchufar un generador de mazmorras.

Las mazmorras pueden tener distintas estructuras. Por ejemplo, pueden ser del estilo de una matriz de dos dimensiones. Pero también podrían tener una estructura en árbol.

El punto de partida es que la mazmorra está compuesta por celdas o habitaciones, que pueden tener cuatro paredes. Las paredes pueden tener una puerta que conecta con otra celda. Una cierta celda puede tener una pared con una puerta especial que es la salida.

Aquí podemos ver un ejemplo muy simple de lo que sería una mazmorra matricial. El símbolo @ nos muestra donde empezaría la jugadora. Los símbolos | y --- indican paredes, mientras que los espacios en blanco ` ` son puertas. En algún momento, estas puertas podrían estar cerradas o abiertas.


+---+---+---+
|           |
+   +---+   +
|       |   |
+   +   +   +
|   | @ |   
+---+---+---+

Como se puede ver, necesitaríamos mantener información sobre la posición de la jugadora y actualizarla cada vez que se mueve. Así mismo, tendremos que saber cuando ha encontrado la salida.

En el estado actual del código hay algunos problemas para implementar esto:

  • La mazmorra se inicializa dentro de Game, tenemos que cambiar esto para que se le pueda pasar una mazmorra (Dungeon) que habremos creado con algún tipo de Factory/Builder.
  • El único estado que tenemos acerca de lo que pasa cuando la jugadora actúa es el mensaje que se genera. Esto es insuficiente y entronca con el problema que comentamos en la entrega anterior de fragilidad de los tests. Necesitaremos alguna forma de representar ese estado tras la acción.

En resumidas cuentas, antes de implementar la posibilidad de tener mazmorras más complicadas, tendremos que refactorizar un poco.

¿Recuerdas la regla de Calistenia de no tener tipos primitivos, sino encapsularlos en objetos? Exacto. No la hemos desarrollado por completo. En parte por eso tenemos ahora la necesidad de hacer estos cambios antes.

Los tipos primitivos que todavía estamos usando son:

  • Los mensajes que devuelven los objetos del juego cuando la jugadora actúa sobre ellos, que en algunas partes del programa está en la variable result.
  • El comando tecleado por la jugadora, que llamamos instruction y que usaremos para obtener el objeto Command.
  • Algunos mensajes que están hard-coded.

Vamos a ver cómo empezar a realizar estos cambios mediante refactors, sin romper la funcionalidad.

Del input del usuario al comando

Veamos el código:

    instruction = self._obtain_user_command.command()
    command = Command.from_user_input(instruction)
    result = game.execute(instruction)

instruction es una variable temporal. Tendría sentido que obtain_user_command nos devuelva el comando ya instanciado, el cual podríamos pasar directamente a game, que ya no necesitaría instanciarlo.

Para esto necesitamos hacer varios cambios. Voy a empezar por Game. Primero cambiaré el nombre de la variable c a command, para que sea más explicativa. Por cierto, que es otra regla de Calistenia el no usar abreviaturas en los nombres:

class Game:
    def __init__(self):
        self.dungeon = None

    def start(self):
        self.dungeon = Dungeon()

    def execute(self, instruction):
        command = Command.from_user_input(instruction)

        result = command.do(self.dungeon)

        return result

Ahora extraemos a un nuevo método la parte que nos interesa:

class Game:
    def __init__(self):
        self.dungeon = None

    def start(self):
        self.dungeon = Dungeon()

    def execute(self, instruction):
        command = Command.from_user_input(instruction)
        return self.do_command(command)

    def do_command(self, command):
        return command.do(self.dungeon)

Y en Application, llamamos directamente a este nuevo método:

class Application:
    def __init__(self, obtain_user_command, show_output):
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            instruction = self._obtain_user_command.command()
            command = Command.from_user_input(instruction)
            result = game.do_command(command)
            self._show_output.put(str(command))
            self._show_output.put(result)
            finished = result == "Congrats. You're out"

Podríamos deshacernos del método execute, pero tenemos que cambiar los tests de Game.

class OneRoomDungeonTestCase(unittest.TestCase):
    def setUp(self):
        self.game = Game()
        self.game.start()

    def test_player_finds_easy_way_out(self):
        self.assertEqual("Congrats. You're out", self.game.do_command(Command.from_user_input("go north")))

    def test_player_tries_closed_wall(self):
        self.assertEqual("You hit a wall", self.game.do_command(Command.from_user_input("go south")))

    def test_player_tries_another_closed_wall(self):
        self.assertEqual("You hit a wall", self.game.do_command(Command.from_user_input("go east")))

    def test_unknown_command(self):
        self.assertEqual("I don't understand", self.game.do_command(Command.from_user_input("foo bar")))

    def test_player_can_look_around(self):
        description = """North: There is a door
East: There is a wall
South: There is a wall
West: There is a wall
That's all
"""
        self.assertEqual(description, self.game.do_command(Command.from_user_input("look around")))

Una opción sería hacerlos un poco más sencillos, extrayendo algunas partes repetitivas.

class OneRoomDungeonTestCase(unittest.TestCase):
    def setUp(self):
        self.game = Game()
        self.game.start()

    def execute_user_action(self, action):
        return self.game.do_command(Command.from_user_input(action))

    def test_player_finds_easy_way_out(self):
        self.assertEqual("Congrats. You're out", self.execute_user_action("go north"))

    def test_player_tries_closed_wall(self):
        self.assertEqual("You hit a wall", self.execute_user_action("go south"))

    def test_player_tries_another_closed_wall(self):
        self.assertEqual("You hit a wall", self.execute_user_action("go east"))

    def test_unknown_command(self):
        self.assertEqual("I don't understand", self.execute_user_action("foo bar"))

    def test_player_can_look_around(self):
        description = """North: There is a door
East: There is a wall
South: There is a wall
West: There is a wall
That's all
"""
        self.assertEqual(description, self.execute_user_action("look around"))

Ahora sí que podemos eliminar el método execute.

class Game:
    def __init__(self):
        self.dungeon = None

    def start(self):
        self.dungeon = Dungeon()

    def do_command(self, command):
        return command.do(self.dungeon)

Y podemos hacer un commit con estos cambios.

En este punto me planteo si Command es un buen nombre, porque recuerda demasiado al patrón del mismo nombre. El caso es que nuestro Command tiene que devolver un resultado representando el estado del juego y suena poco natural. No tengo muy claro si el nuevo nombre podría ser PlayerAction, PlayerMove o similar. Lo dejaré así, de momento, hasta que la evolución de código me lo vaya aclarando.

La segunda fase del refactor busca llevar la instanciación del comando a ObtainUserInput. Para hacer este cambio, primero voy a cambiar el nombre del método que existe:

class ObtainUserCommand:
    def command_old(self):
        pass


class ConsoleObtainUserCommand(ObtainUserCommand):
    def command_old(self):
        raw = input("What should I do? >")
        return " ".join(raw.lower().strip().split())

Este cambio no rompe los tests. El siguiente paso es introducir una nueva versión del método command que devuelva el Command instanciado, utilizando el viejo. Esta técnica se llama wrap.

from dungeon.command.command import Command


class ObtainUserCommand:
    def command_old(self):
        pass

    def command(self):
        pass


class ConsoleObtainUserCommand(ObtainUserCommand):
    def command_old(self):
        raw = input("What should I do? >")
        return " ".join(raw.lower().strip().split())

    def command(self):
        return Command.from_user_input(self.command_old())

De nuevo, este cambio no debería afectar a los tests, pero hay que asegurarse de que lo añadimos a todas las implementaciones, incluyendo las de test.

A continuación empezamos a usarlo:

class Application:
    def __init__(self, obtain_user_command, show_output):
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            command = self._obtain_user_command.command()
            result = game.do_command(command)
            self._show_output.put(str(command))
            self._show_output.put(result)
            finished = result == "Congrats. You're out"

Y lo reemplazamos también en los tests, aunque en este caso tenemos que cambiar un poco la forma de hacer la aserción.

class TestConsoleObtainUserCommand(TestCase):
    def test_should_show_a_prompt(self):
        with patch('builtins.input', return_value="go north") as mock_input:
            obtain_user_command = ConsoleObtainUserCommand()
            command = obtain_user_command.command()
            self.assertIn("go north", str(command))
            self.assertEqual("What should I do? >", mock_input.call_args.args[0])

    def test_should_normalize_case_to_lowercase(self):
        with patch('builtins.input', return_value="go NORTH"):
            obtain_user_command = ConsoleObtainUserCommand()
            command = obtain_user_command.command()
            self.assertIn("go north", str(command))

    def test_should_trim_spaces(self):
        with patch('builtins.input', return_value="  go north   "):
            obtain_user_command = ConsoleObtainUserCommand()
            command = obtain_user_command.command()
            self.assertIn("go north", str(command))

    def test_should_normalize_middle_spaces(self):
        with patch('builtins.input', return_value="go      north"):
            obtain_user_command = ConsoleObtainUserCommand()
            command = obtain_user_command.command()
            self.assertIn("go north", str(command))

Nos llevamos el código del command_old a command:

class ObtainUserCommand:
    def command_old(self):
        pass

    def command(self):
        pass


class ConsoleObtainUserCommand(ObtainUserCommand):
    def command_old(self):
        raw = input("What should I do? >")
        return " ".join(raw.lower().strip().split())

    def command(self):
        raw = input("What should I do? >")
        user_input = " ".join(raw.lower().strip().split())
        return Command.from_user_input(user_input)

Comprobamos que lo podemos eliminar (hay una implementación para tests que también debemos controlar).

class ObtainUserCommand:

    def command(self):
        pass


class ConsoleObtainUserCommand(ObtainUserCommand):

    def command(self):
        raw = input("What should I do? >")
        user_input = " ".join(raw.lower().strip().split())
        return Command.from_user_input(user_input)

Y consolidamos este cambio en otro commit.

Encapsulando el resultado

Cambiar la interfaz pública es siempre un gran engorro. En proyectos tan pequeños como este, no es demasiado problemático porque probablemente no tiene muchos usos. Sin embargo, en proyectos grandes puede ser bastante complejo hacerlo.

En nuestro caso, el problema es consecuencia de no haber usado objetos desde el principio. Incluso aunque fuesen un simple wrapper alrededor de un string, como es el caso de los mensajes, hubiera bastado para que el objeto pudiese evolucionar sin necesidad de romper las interfaces en las que participase.

Lo primero es introducir un objeto que represente el resultado de las acciones. De momento, solo va a llevar el mensaje, pero su misión a largo plazo será mantenernos al tanto de las consecuencias de la acción de la jugadora.

class ActionResult:
    def __init__(self, message):
        self._message = message
        
    def message(self):
        return self._message
    

Haré un commit con esta clase para tener un punto de partida para los siguientes pasos.

La dificultad está ahora en decidir dónde empezar a reescribir. En principio los objetos del juego que se accionan (como Wall, Room, etc) son los que tendrían que devolver este tipo de objeto, que se usa (y usará) en otras partes para realizar algunas acciones, como finalizar el juego.

Cambiar la interfaz no siempre es fácil, ya que estamos cambiando el contrato mediante el que se relacionan unos objetos con otros. En este proyecto y en este momento, bastaría con cambiar todo el código en un commit. Pero esto puede ser complicado en proyectos que sean tan solo un poco más grandes.

Una técnica que podríamos usar es el método Mikado. Este método consiste en lo siguiente:

  • Introducimos un cambio que nos interesa tener, como podría ser usar un objeto ActionResult en Application.
  • Ejecutamos los tests para ver si tenemos algún fallo (que los tendremos)
  • Tomamos nota del fallo y cómo arreglarlo. Deshacemos el cambio y modificamos el código para evitarlo.
  • Repetimos hasta que todo funciona como queremos.

El método Mikado es útil cuando trabajamos con un código que no conocemos bien. Sin embargo, vamos a probar a usarlo aquí para aprender y porque es una buena forma de hacer cambios que no son realmente refactors.

Por ejemplo, yo quiero que ActionResult me ofrezca un método is_finished() para saber si el efecto de la última acción ha provocado que tenga que terminar el juego. O sea, quiero algo así:

class Application:
    def __init__(self, obtain_user_command, show_output):
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            command = self._obtain_user_command.command()
            result = game.do_command(command)
            self._show_output.put(str(command))
            self._show_output.put(result)
            finished = result.is_finished()

Al ejecutar los tests tengo que ver un fallo:

Error
Traceback (most recent call last):
  File "/Users/frankie/Projects/dungeon/dungeon/tests/test_application.py", line 43, in test_should_show_command_echo
    app.run()
  File "/Users/frankie/Projects/dungeon/dungeon/application.py", line 20, in run
    finished = result.is_finished()
AttributeError: 'str' object has no attribute 'is_finished'

Me dice algo que ya sabía, que es que result sigue siendo un objeto string y no tiene el método que queremos.

Así que, deshago el cambio y añado código que pueda resolver el problema, pero sin romper los tests ni generar nuevos errores con el código en su estado actual. En este caso, instancio el objeto que quiero usar en lugar de result.

class Application:
    def __init__(self, obtain_user_command, show_output):
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            command = self._obtain_user_command.command()
            result = game.do_command(command)
            action_result = ActionResult(result)
            self._show_output.put(str(command))
            self._show_output.put(result)
            finished = result == "Congrats. You're out"

Ahora vuelvo a introducir el cambio que quería. La única diferencia es que ahora usaré action_result.

class Application:
    def __init__(self, obtain_user_command, show_output):
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            command = self._obtain_user_command.command()
            result = game.do_command(command)
            action_result = ActionResult(result)
            self._show_output.put(str(command))
            self._show_output.put(result)
            finished = action_result.is_finished()

Ejecutamos de nuevo. Como es de esperar, tengo el siguiente error:

Error
Traceback (most recent call last):
  File "/Users/frankie/Projects/dungeon/dungeon/tests/test_application.py", line 34, in test_should_show_title
    app.run()
  File "/Users/frankie/Projects/dungeon/dungeon/application.py", line 22, in run
    finished = action_result.is_finished()
AttributeError: 'ActionResult' object has no attribute 'is_finished'

Deshago el cambio y añado el método necesario en ActionResult.

class ActionResult:
    def __init__(self, message):
        self._message = message

    def message(self):
        return self._message

    def is_finished(self):
        return self._message == "Congrats. You're out"

Y ahora vuelven a pasar los tests. Pero tengo que seguir cambiando cosas, como mostrar el mensaje contenido en action_result.

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            command = self._obtain_user_command.command()
            result = game.do_command(command)
            action_result = ActionResult(result)
            self._show_output.put(str(command))
            self._show_output.put(action_result.message())
            finished = action_result.is_finished()

Este cambio funciona sin necesidad de más, por lo que puedo consolidarlo.

El siguiente cambio que quiero es que game.do_command() me devuelva un objeto ActionResult.

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            command = self._obtain_user_command.command() 
            action_result = game.do_command(command)
            self._show_output.put(str(command))
            self._show_output.put(action_result.message())
            finished = action_result.is_finished()

Pero como sigue devolviendo un string, me pasa esto:

Error
Traceback (most recent call last):
  File "/Users/frankie/Projects/dungeon/dungeon/tests/test_application.py", line 34, in test_should_show_title
    app.run()
  File "/Users/frankie/Projects/dungeon/dungeon/application.py", line 20, in run
    self._show_output.put(action_result.message())
AttributeError: 'str' object has no attribute 'message'

Así que deshago el cambio. Esta vez tengo que usar la técnica de wrap, por lo que cambio el nombre del método actual y creo que nuevo método con el nombre original.

class Game:
    def __init__(self):
        self.dungeon = None

    def start(self):
        self.dungeon = Dungeon()

    def do_command_deprecate(self, command):
        return command.do(self.dungeon)
    
    def do_command(self, command):
        result = self.do_command_deprecate(command)
        return ActionResult(result)

Este cambio no afecta a los tests, por lo que puedo consolidarlo y, a continuación, reintroduzco el cambio que deseaba hacer originalmente.

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        finished = False
        while not finished:
            command = self._obtain_user_command.command()
            action_result = game.do_command(command)
            self._show_output.put(str(command))
            self._show_output.put(action_result.message())
            finished = action_result.is_finished()

Y todo sigue funcionando. El siguiente paso es eliminar los usos de la versión deprecated, que en este caso ocurren en los tests. En un proyecto donde se emplee en muchos lugares, simplemente lo vamos reemplazando paso a paso hasta eliminar todos sus casos.

class OneRoomDungeonTestCase(unittest.TestCase):
    def setUp(self):
        self.game = Game()
        self.game.start()

    def execute_user_action(self, action):
        return self.game.do_command(Command.from_user_input(action)).message()

    def test_player_finds_easy_way_out(self):
        self.assertEqual("Congrats. You're out", self.execute_user_action("go north"))

    def test_player_tries_closed_wall(self):
        self.assertEqual("You hit a wall", self.execute_user_action("go south"))

    def test_player_tries_another_closed_wall(self):
        self.assertEqual("You hit a wall", self.execute_user_action("go east"))

    def test_unknown_command(self):
        self.assertEqual("I don't understand", self.execute_user_action("foo bar"))

    def test_player_can_look_around(self):
        description = """North: There is a door
East: There is a wall
South: There is a wall
West: There is a wall
That's all
"""
        self.assertEqual(description, self.execute_user_action("look around"))

Y nos queda así:

class Game:
    def __init__(self):
        self.dungeon = None

    def start(self):
        self.dungeon = Dungeon()

    def do_command(self, command):
        result = command.do(self.dungeon)
        return ActionResult(result)

Con esto, ya tenemos parte de nuestros objetivos. Pero queremos avanzar más y que todos los elementos del juego que tengan que hacerlo devuelvan objetos ActionResult. Básicamente, queremos que pase esto en Game

    def do_command(self, command):
        return command.do(self.dungeon)

Para ello, tenemos que hacer que Command.do devuelva el objeto que necesitamos. Usando la técnica wrap, cambiamos el nombre del método do a do_deprecated, introducimos el nuevo do y empezamos a hacer los cambios necesarios.

    def do_deprecated(self, dungeon):
        result = ""

        if self._command == "go":
            result = dungeon.go(self._argument)
        if self._command == "look":
            result = dungeon.look(self._argument)

        return result

    def do(self, dungeon):
        result = self.do_deprecated(dungeon)
        return ActionResult(result)

Añadir código no rompe los tests, por lo que podemos hacer commit con este cambio y empezar a usarlo en donde se necesita:

class Game:
    def __init__(self):
        self.dungeon = None

    def start(self):
        self.dungeon = Dungeon()

    def do_command(self, command):
        return command.do(self.dungeon)

Así queda Command:

class Command:

    def __init__(self, command, argument):
        self._argument = argument
        self._command = command

    @staticmethod
    def from_user_input(user_input):
        command, argument = user_input.split(" ", 1)
        if command != "go" and command != "look":
            return InvalidCommand(user_input)

        return Command(command, argument)

    def do(self, dungeon):
        result = ""

        if self._command == "go":
            result = dungeon.go(self._argument)
        if self._command == "look":
            result = dungeon.look(self._argument)
            
        return ActionResult(result)

    def __str__(self) -> str:
        return "You said: {} {}".format(self._command, self._argument)
class InvalidCommand(Command):
    def __init__(self, user_input):
        self._user_input = user_input

    def do(self, dungeon):
        return ActionResult("I don't understand")

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

Para finalizar la transformación, lo que querríamos es que los objetos Dungeon, Room y Wall, devuelvan sus correspondientes ActionResult. Dungeon no necesita mucho trabajo, ya que simplemente devuelve lo que venga de Room. Esta, por su parte, recoge lo que viene de las paredes (Wall), lo que hace el cambio un poco más complejo.

Voy a hacer este cambio en un solo paso para no alargar este capítulo.

class Command:

    def __init__(self, command, argument):
        self._argument = argument
        self._command = command

    # Removed code

    def do(self, dungeon):
        if self._command == "go":
            return dungeon.go(self._argument)
        if self._command == "look":
            return dungeon.look(self._argument)

    # Removed code

Esto rompe todos los tests, así que vamos corrigiendo allí donde es necesario, hasta conseguir hacerlos pasar. Nos basta cambiar el archivo wall.py para resolver la mayoría de los problemas:

from dungeon.command.action_result import ActionResult


class Wall:
    def go(self):
        return ActionResult("You hit a wall")

    def look(self):
        return ActionResult("There is a wall")


class Exit(Wall):
    def go(self):
        return ActionResult("Congrats. You're out")

    def look(self):
        return ActionResult("There is a door")

Room tiene un poco más de trabajo:

class Room:
    def __init__(self):
        self.north = Exit()
        self.south = Wall()
        self.east = Wall()
        self.west = Wall()

    def go(self, direction):
        wall = getattr(self, direction)
        return wall.go()

    def look(self, argument):
        response = ""
        response += "North: " + self.north.look().message() + "\n"
        response += "East: " + self.east.look().message() + "\n"
        response += "South: " + self.south.look().message() + "\n"
        response += "West: " + self.west.look().message() + "\n"

        response += "That's all" + "\n"
        return ActionResult(response)

Para rematar, un refactor que nos permite deshacernos de una variable temporal:

class Application:
    def __init__(self, obtain_user_command, show_output):
        self._obtain_user_command = obtain_user_command
        self._show_output = show_output

    def run(self):
        self._show_output.put("Welcome to the Dungeon")
        game = Game()
        game.start()
        action_result = ActionResult("")
        while not action_result.is_finished():
            command = self._obtain_user_command.command()
            action_result = game.do_command(command)
            self._show_output.put(str(command))
            self._show_output.put(action_result.message())

A las mazmorras

Este refactor ha sido largo, consecuencia de un planteamiento inicial incorrecto de trabajar con primitivos y no con objetos. Los objetos nos permiten ocultar los cambios en su implementación, con tal de no alterar las interfaces establecidas. Eso no ocurre cuando usamos primitivos (aunque sean objetos).

En cualquier caso, ya casi estamos listas para empezar a trabajar en la creación de mazmorras. Lo primero de todo es hacer que Game pueda aceptar una Dungeon instanciada y que no la tenga que instanciar.

class Game:
    def __init__(self):
        self.dungeon = None

    def start(self, dungeon=Dungeon()):
        self.dungeon = dungeon

    def do_command(self, command):
        return command.do(self.dungeon)

Con esto ya podríamos empezar a pensar en una forma de crear mazmorras. Esta parte la vamos a dejar para la próxima entrega, ya que la presente nos está quedando muy larga.

El estado actual del proyecto

Próximos pasos

Ahora que hemos hecho este refactor y nos hemos librado de buena parte de la deuda técnica que arrastrábamos, vamos a empezar a implementar una mazmorra jugable.

El principal aprendizaje de esta parte de la iteración es la importancia de empezar usando buenas prácticas desde el primer día. Esto no implica optar por diseños complejos. Al contrario, se trata de hacer las cosas simples, pero flexibles.

También hay que considerar lo que significa simple. Este juego lo estoy programando en orientación a objetos. Sin embargo, he empezado con tipos primitivos, que aunque sean objetos, han resultado poco flexibles para cambiar. A veces, el hecho de que tengamos algo a mano, no quiere decir que sea lo más sencillo.

Siguiente paso

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