El juego Gems of War tiene un modo de caza tesoros bastante adictivo. El problema con este juego es que debes disponer de un recurso limitado para poder jugar. Al final o pagas o solo puedes jugar un par de veces al día con suerte. ¿Qué tal si nos hacemos el juego entre Elixir y JavaScript?
Una de las cosas que más me gusta de volver a tener tiempo libre son la cantidad de cosas que se me ocurre hacer con mis hijos. Finalmente ellos se suelen aburrir un poco y me quedo por la noche dándole al código para enseñarles al día siguiente lo que he hecho y ver su cara entre asombro e ilusión por tener disponible un nuevo juguete (o videojuego en este caso).
En principio me interesaba resolver el problema planteado por el juego a nivel lógico. Lo plantee todo como una aplicación de servidor y con una interfaz muy rudimentaria en modo texto. Me serví de la capacidad de las nuevas terminales de poder representar emojis y de IO.ANSI
para darle un poco de imagen y color pero aún así seguía imprimiendo en consola.
Las reglas del juego
Este juego se basa en la presentación de un tablero de 8x8 donde aparecen inicialmente unas piezas agregadas de forma aleatoria. El juego consiste en mover una pieza para intercambiarla con su vecina horizontal (a derecha o izquierda) o vertical (abajo o arriba) pero no en diagonal. Una dinámica muy parecida a otros juegos más conocidos como Candy Crush.
Lo que difiere en este juego es la organización jerárquica de las piezas:
La combinación de 3 o más piezas iguales origina una pieza de nivel superior consiguiendo por cada pieza eliminada el total de puntos que estas tuviesen. Al inicio en el tablero solo se sitúan 4 de las posibles piezas, el resto se van descubriendo a medida que se combinan las de mayor orden en el tablero.
El juego comienza con 10 turnos. Solo hay 10 movimientos. Cada vez que hacemos una combinación de 3 piezas por tanto se resta un turno. Pero si hacemos una combinación de 4 movimientos el turno no se pierde y sí la combinación es de 5 o más elementos iguales se incrementa un turno.
Si quieres probar puedes intentar hacer tantos puntos como puedas. El juego está abierto y es accesible a través de la URL: leprechaun.altenwald.com.
Además si tienes curiosidad por saber cómo se ve todo por dentro también puedes ver el código fuente en la págian de github de leprechaun.
El juego desde el código
A diferencia de otros juegos que tienen toda su lógica o gran parte de ella donde se ejecuta el juego, este juego tiene toda su lógica en el servidor. Cada movimiento que se realiza es notificado al código del servidor y el servidor envía el resultado.
El módulo Leprechaun.Board
es el responsable de toda la lógica del juego. Tanto es así que la versión en consola y la versión de WebSocket comparten ese mismo módulo sin ninguna modificación.
El juego comienza iniciando un proceso donde tenemos varios datos:
-
cells
: las celdas del tablero. Cada celda contiene un número entre 1 y 8 representando cualquiera de las posibles piezas del juego. -
score
: la puntuación obtenida por el jugador hasta el momento. -
turns
: los turnos restantes. -
played_turns
: los turnos ya jugados. -
extra_turns
: los turnos extra obtenidos. -
username
: el nombre elegido por el usuario para la entrada en la tabla de récords.
Las acciones que podemos realizar con el tablero son:
-
show
: mostrar el tablero. La forma de mostrarlo es como una lista de listas conteniendo las celdas. Es lo suficientemente completo para poder ser trasladado a modo texto o HTML, según el caso. -
move
: mueve una pieza. Indica el punto (x1, y1) de la pieza origen a mover y el punto (x2, y2) de la pieza destino. En realidad el nombre más acertado seríaswap
porque se intercambian de posición. -
hiscore
: facilitamos nuestro nombre para la tabla de récords. -
score
: facilita la puntuación conseguida hasta el momento. -
turns
: facilita los turnos restantes en el momento. -
stats
: obtiene los datos estadísticos de cuántos turnos se han jugado y cuántos turnos extra ha obtenido el jugador.
El juego produce una serie de eventos enviados al proceso que solicita el movimiento. Estos eventos son generados por el motor del juego al momento de hacer un match
de una combinación así como cuando se consigue un extra_turn
o cuando se puede enviar el siguiente movimiento play
.
De esta forma el controlador de juego de consola puede esperar hasta recibir el evento play
y ante cada evento match
o extra_turn
recibido, escribir cualquier información por pantalla. De la misma forma el controlador de WebSocket transforma estos eventos y los envía directamente al navegador para ser manejados por JavaScript.
Después de todo un fin de semana pude completar la lógica completa y el domingo ya pudimos jugar a nuestro juego en lugar de seguir jugando o esperando jugar al otro. No obstante, aunque la interfaz de texto no se veía mal había muchas mejoras que introducir:
El paso a Interfaz Web
En principio tenía que ceder mi portátil para poder jugar porque el resto de dispositivos de la casa eran o tablets o dispositivos móviles. No es que no me guste compartir mi portátil, es que es mejor no dejar una herramienta de trabajo para jugar a los niños. Es mejor trasladar el juego a los otros dispositivos y así además ahorramos discusiones de a quién le toca jugar.
Tuve claro desde el principio hacer el código lo más modular posible para cuando hubiese que hacer la interfaz web o una API de alguna forma tener presente que la comunicación con los dispositivos se debía llevar fuera del núcleo y la lógica del juego.
Así pues instalé las dependencias normales a cubrir para programar de forma fácil WebSockets y envío de ficheros estáticos al navegador (HTML, CSS, JavaScript e imágenes). Convertí los eventos en mensajes a enviar a la conexión WebSocket y la información o peticiones recibidas desde WebSocket eran ejecutadas o llevadas al tablero de la misma forma.
Quizás en este punto el mayor problema fue el código en el navegador. Programo en JavaScript pero no tengo una gran experiencia en el desarrollo de interfaz ni con muchas de las librerías de frontend más empleadas. Me limité a instalar jquery como muchas otras veces y encontré además la librería jquery-swapsies que me permitía realizar el cambio de piezas con una animación. No obstante quedan otras animaciones como las de un tablero previo al siguiente para eliminar las piezas de match y hacer que caigan con gravedad las nuevas. Queda como futura mejora.
En producción
No es un secreto cómo se puede poner un proyecto Elixir en producción. Si tienes dudas o quieres saber cómo hacerlo puedes seguir los capítulos 11 y 12 de mi libro de Elixir donde se indica cómo crear un proyecto, realizar una liberación y hacer despliegues.
De hecho, en el servidor de producción yo suelo tenerlo todo bastante automatizado. La construcción del paquete y su liberación sin parada está completamente automatizada.
La primera versión fue bastante buena y al compartirla en el sitio web de repente muchos amigos y familiares comenzaron a jugar y nos deban ideas o hacían preguntas que sugerían cambios necesarios. Fue un proceso gratificante y divertido. Una de las mejoras fue la tabla de récords.
High Scores
Lo comento como apartado porque fue una característica que tuvo bastantes cambios. En las plataformas Erlang normalmente la mayoría de programadores (y sobre todo de entornos Elixir) terminan empleando un almacén de datos como Redis, MySQL, PostgreSQL, MongoDB, ... todos externos agregando un SPOF (Single Point of Failure o Punto Único de Fallo) de forma innecesaria. En Erlang tenemos Mnesia.
Además, la gente de Ecto ha trabajado duro en tener un sistema de consulta a base de datos que nos permite no solo configurar una base de datos SQL, sino también otras NoSQL como en este caso Mnesia.
Tuve algunos problemas a la hora de liberar la primera versión. Crear una base de datos Mnesia al estilo Ecto empleando una liberación con Distillery es todo un desafío. Finalmente decidí instalar la base de datos manualmente en el directorio de destino donde estaba configurado que estaría finalmente y me aseguré de que cuando iniciase el nodo de Erlang (o Elixir) con ese directorio para Mnesia todo se iniciara sin problemas. ¡Y así fue!
Gracias a este snippet se simplificó todo muchísimo.
Solo tuve que crear el directorio de Mnesia con el proyecto en mi sistema. Cambiar con el código de Erlang el nombre del nodo al que tendría definitivamente y copiar el directorio a producción donde el sistema esperaría encontrarlo.
Por el resto, cree un módulo para gestionar los hi-scores, hacer las consultas generando tan solo tres funciones para el sistema para poder almacenar un nuevo elemento en la tabla, obtener el orden de un elemento y por último listar las 20 primeras posiciones de la tabla.
Los gráficos
Una de las cosas que más juego dio fueron los gráficos. Inicialmente elegí un conjunto de gráficos por Internet empleando el conjunto de gráficos del propio Gems of War para las piezas y una alegre animación de Leprechaun para acompañar:
Ana María me indicó que habría que utilizar unas imágenes sin copyright y que podríamos hacerlas nosotros. Yo le dije que si ella las dibujaba yo las pasaba al ordenador y así fue. En poco tiempo ella me presentó una libreta con las imágenes pintadas a mano de las 8 piezas. Además mucho más próximas al nombre del juego al ser pepitas de cobre, plata y oro, el saco de pepitas, un cofre pequeño, otro mayor, un caldero de oro y por último el caldero de oro con un arcoíris.
Recordé un poco cómo usaba el Gimp con las capas, máscaras y me puse a rotular, colorear, sombrear y obtener cada una de las piezas:
Por último solo nos quedaba nuestro pequeño y danzarín amigo. Me temo que en este caso ninguno de los dos sabíamos exactamente cómo conseguir una animación decente y preferimos hacer un dibujo estático en su lugar para situarlo junto al tablero. Quizás en un futuro dediquemos un poco más de tiempo al duendecillo y le demos algunas emociones dependiendo de lo que vaya consiguiendo.
Estadísticas, Google y Addthis
Sentí curiosidad por saber cómo se estaría comportando la web. Después de ver tanto movimiento en la tabla de récords suponía que habría bastantes jugadores pero no sabía a ciencia cierta cuántos. A través de la consola de Elixir y teniendo en cuenta de que emplee Registry podría haber obtenido el número de juegos activos cada vez, pero no era lo que buscaba. La interfaz de Google Analytics es bastante completa y el cambio es mínimo en la web del proyecto. De la misma forma, para fomentar un poco más la participación agregué los botones para compartir en redes sociales.
Después de un par de días pude ver que el tráfico a la web del juego era superior incluso que el tráfico que recibía en la web de venta de libros. Lo cual no está nada mal. Sin embargo aún sigue siendo menor que el tráfico que se recibe en este blog.
Cambios en Caliente
Desde hace un tiempo decidí dejar de usar Docker en mis despliegues para poder emplear toda la potencia de los sistemas Erlang y hacer cambios en caliente consiguiendo que el sistema no se detuviese ante nada.
Uno de los desafíos llegó hoy cuando tuve que modificar la tabla de rutas de cowboy. Los comportamientos de esta librería no contemplan los cambios en caliente y por tanto tuve que preparar una función que al ser llamada se asegurase de configurar correctamente las rutas HTTP.
Obviamente esta función no sería llamada de forma automática por sí sola al realizar la actualización, había que personalizar esa actualización y agregar la llamada a la función.
Por suerte distillery permite generar ficheros appup dentro del directorio rel
y ahí especifica lo que hará por defecto para que podamos agregar (especificando a modo Erlang) qué queremos que haga. Puedes ver el fichero generado: 0.6.0_to_0.6.1.appup.
Puedes leer más acerca de appups, cambios en caliente y el entorno OTP en el libro Erlang/OTP Volumen II: Las bases de OTP.
Conclusiones
Me he divertido mucho y sé que mis hijos también se han divertido en este proceso. Les ha servido para acercarse un poco más en cómo se realiza un videojuego y han visto en qué consiste la digitalización (aunque un poco chapucera) de una imagen. Me quedo con lo primero no obstante, nos hemos divertido. Y tanto que incluso estamos pensando en otros juegos aprovechando las potencias de la plataforma de Erlang y de Elixir para hacer algo que permita a varios jugadores jugar entre ellos. Iremos viendo.
¿Y tú? ¿Has pensado en hacer tus propios juegos? ¿Te animas a programar un poco en backend? ¿Sabrías hacer un Tetris usando ANSI? ¿Tienes alguna experiencia positiva o negativa en este tema? ¡Déjanos tu comentario!