Dungeon 1. Empezando por algún sitio

por Fran Iglesias

En esta serie de artículos voy a ir contando el proceso de desarrollo de un juego. No será gran cosa, pero al menos será entretenido.

Mi objetivo con este proyecto es profundizar en ideas sobre desarrollo ágil, construyendo un proyecto de forma iterativa e incremental.

El juego

Se llama Dungeon (original, ¿eh?) y, como su nombre indica, consiste en que el jugador comienza el juego dentro de una mazmorra laberíntica y tiene que encontrar la salida. La forma de interactuar es conversacional. En cada ciclo del juego, el ordenador describe la situación actual, que es básicamente la habitación de la mazmorra en la que se encuentra el jugador, y espera un comando de texto por porte del usuario.

Por el momento estos comandos son:

  • go north|east|south|west: para moverse
  • look around: para obtener la descripción de la ubicación actual

El plan es que haya otros comandos que permitan coleccionar objetos, interactuar con personajes, accionar cosas, etc. También que las mazmorras sean lo suficientemente complejas como para resultar interesantes, que las paredes puedan tener puertas cerradas, que haya algún tipo de power-ups, etc.

Primera iteración

La primera iteración ha sido más o menos un spike para hacerse una idea de los elementos básicos del proyecto. He empezado con un test muy simple asumiendo que interactuamos con un objeto Game, que puede procesar los comandos de la jugadora y devolver un resultado. Un spike sería una iteración que se hace básicamente para probar ideas, averiguar si son factibles o si funciona un enfoque para resolver un problema específico. En general, el código creado durante un spike no se va a utilizar porque probablemente se ha creado de forma poco cuidadosa o sin un mínimo de diseño.

Para esta primera iteración únicamente hay una habitación o celda en la mazmorra, una de cuyas cuatro puertas es la salida. Así que, básicamente los tests que iba introduciendo probaban que la jugadora podría salir y que no podría atravesar paredes.

Aquí se pueden ver los tests ya refactorizados:

import unittest

from dungeon.game import 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.execute("go north"))

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

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

    def test_unknown_command(self):
        self.assertEqual("I don't understand", self.game.execute("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.execute("look around"))


if __name__ == '__main__':
    unittest.main()

La primera aproximación tenía todo el código en Game, lo que fue suficiente para tener un prototipo muy tosco. Teniendo tests suficientes, comencé a refactorizar la solución para descargar a Game de las responsabilidades de gestionar la mazmorra y sus celdas. De este modo, surgieron los objetos Dungeon, Room y Wall. Los aprendizajes de esta fase, en lo que toca a diseño de la aplicación, fueron:

  • Game se encarga de gestionar el bucle del juego, la interpretación de comandos.
  • Dungeon es una colección de Rooms, aunque de momento solo tiene una. Parece claro que tendrá que llevar cuenta de cuál es la habitación en la que se encuentra la jugadora.
  • Room contiene cuatro paredes, una en cada punto cardinal, y podrá contener objetos o personajes con los que la jugadora interactuará.
  • Wall representa las paredes, que podrán tener o no puertas. Exit es un tipo especial de pared que contiene la puerta de salida.

El resultado de este primer sprint/spike ha sido un pequeño sistema que funciona y está completamente testeado. Ahora bien, no está aportando valor.

Se podría decir que está adoptando una arquitectura de ports and adapters, ya que Game actúa como la aplicación, que expone un puerto, el método execute, por el cual se le pueden pasar comandos, devolviendo un resultado.

Próximos pasos

La cuestión ahora es decidir qué es lo más importante que necesitamos. Y hay una cosa obvia. Por el momento, el juego no se puede jugar. O dicho de otra forma, no está aportando valor a usuarias finales, que lo que esperan es poder jugar con el juego.

Por tanto, el siguiente objetivo será crear una especie de Walking Skeleton que permita interactuar con el juego en su estado actual.

Podría argumentarse que al fin y al cabo el juego ahora no es interesante y que deberíamos desarrollarlo más. Solo tenemos una pequeña mazmorra y la solución es muy fácil de descubrir. Pero un juego trivial o poco interesante aporta más valor que ningún juego.

Esta es una idea que a veces resulta difícil vender. Sin embargo, aplicarla podría ahorrar más de un disgusto. Entregar algún valor ahora, aunque sea a un grupo de usuarias limitado o cubriendo un único caso de uso, es mejor que entregar mucho valor dentro de varias semanas o meses. En primer lugar, porque nos permite ir teniendo feedback y asegurando que cuestiones básicas están resueltas. Y, sobre todo, porque de esta forma, después de algunas semanas o meses estaremos entregando muchísimo más valor del previsto.

Código de esta iteración

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