Kabosu - Creando cosas
Publicado: 2024-04-28 (actualizado 2024-08-02)
Etiquetas: Juegos, Proyectos, Reto de la Paella
Hace días me embarqué en un nuevo proyecto-reto: meter la paella en el juego Rogule. Para quien no lo conozca: es un rogue que genera una nueva mazmorra cada día. Los gráficos son simples emojis así que en mi cabeza era una tarea bastante sencilla. Modificar un poco de JavaScript y poco más. Como voy a explicar en este artículo, ha sido otro caso de ingeniero subestimando enormemente la dificultad de una tarea.
Se puede jugar al Rogule original aquí. El código fuente con licencia AGPL se encuentra en GitHub.
El primer paso como siempre es compilar el código. Según la documentación del juego simplemente hay que ejecutar dos comandos. Aquí ya se empezó a poner la cosa difícil:
En el repositorio de GitHub pone que el 95% del código es JavaScript. No tengo demasiada experiencia con el lenguaje pero me las puedo apañar para hacer pequeños cambios. El problema es que realmente todo el código está escrito en ClojureJS, un lenguaje que se parece mucho a Lisp, pero GitHub solo le da un 3.3% del código porque en el repositorio hay un par de ficheros JS autogenerados que invalidan completamente la métrica. Había leído alguna vez sobre Clojure pero era la primera vez que me enfrentaba a código en ese lenguaje.
Para compilar el código necesité instalar varias cosas:
Tras instalar todo lo necesario la compilación fue bien aunque tardó bastante. Más de media hora. Pensé que sería porque la primera vez se tiene que bajar todas las dependencias y además estaba ejecutando todo en una máquina virtual a la que no le había asignado muchos recursos así que no le di mucha importancia.
Ejecuté el servidor y cargué la página. El mapa generado por mi versión era exactamente igual al que aparecía en la web oficial así que parecía funcionar. Deduje que usa como semilla aleatoria la fecha del día. La posiciones de los monstruos y los objetos sí que eran distintas.
Una vez soy capaz de compilar un software siempre intento hacer un pequeño cambio para ver si funciona y asegurarme que el código que estoy ejecutando es realmente el que he compilado yo.
Al final el código de juego eran unos 10 ficheros no demasiados largos escritos en ClojureJS. No conozco el lenguaje pero hace unos años me introduje en el mundo Lisp y me gusta bastante su filosofía así que entiendo medianamente lo que estoy leyendo.
Abrí el fichero src/rogule/ui.cljs
porque pensé que ahí podría meter fácilmente un mensaje. A pesar de su nombre parece ser que es el punto de entrada del juego ya que define muchas constantes como el tamaño del mapa, cuánto se puede ver del mapa, etc. Además casi arriba del fichero hay una línea que da pistas porque pone:
(log "main loaded")
De paso me sirvió para descubrir cómo escribir mensajes de log. Añadí un mensaje nuevo justo debajo para ver si aparecía al cargar el juego. Al recompilar y recargar la página pude ver mi mensaje así que todo marchaba sobre ruedas. Sí que noté que la compilación tardó mucho rato...
Mientras estaba compilando encontré la función que crear al personaje principal. Se llama make-player
. No tiene pérdida.
(defn make-player [entities free-tiles]
(let [pos (rand-nth (keys free-tiles))
player {:sprite (load-sprite :elf)
:name "you"
:layer :occupy
:pos pos
:stats {:hp [10 10]
:xp player-xp}
:inventory []
:fns {:encounter :combat
:passable :make-player-passable-fn}}]
[(assoc entities :player player)
(dissoc free-tiles pos)]))
"elf" es el emoji del personaje principal. No me había fijado hasta ese momento pero es verdad que es un elfo. Decidí cambiarlo por "ninja" para ver qué pasaba.
(load-sprite :ninja)
Tras otra larga compilación y una recarga de la página no podía ver a mi ninja. Probé a borrar cachés pero seguía apareciendo el elfo. Quizá ese sprite no define el emoji que se ve durante el juego. Busqué y encontré en otro fichero otra línea (load-sprite :elf)
. Lo cambié también por ninja. Ya no había ninguna otra referencia a elfos en todo el código así que ya debería estar. Recompilo, media hora, recarga de la página y seguía saliendo el elfo. En ese momento ya empecé a notar que estaba tardando demasiado tiempo en compilar para lo pequeño que era el proyecto.
No comprendía porqué no salía mi ninja así que probé a hacer otro cambio. Encontré la función que muestra la pantalla de ayuda:
(defn component-help [show-help]
(if show-help
[:div.modal
[:button#help.key {:on-click #(trigger-key 27)} "esc"]
[:h2 "Rogule"]
[:p "Use the arrow keys to move. Press the " [:button.key "."] " key to rest."]
[:p "Move onto items and " (tile-mem (load-sprite :ghost)) " monsters to interact."]
[:p "The number above each monster's head is the maximum damage they can deal to you."]
[:p "Health bars show up at the top of the screen during combat."]
[:p "Collect all the " (tile-mem (load-sprite :mushroom)) " items."]
[:p "Shields " (tile-mem (load-sprite :shield)) " give you protection."]
[:p "Weapons " (tile-mem (load-sprite :dagger)) " add to your hits."]
[:p "Get to the shrine " (tile-mem (load-sprite :shinto-shrine) "shrine") " to ascend and win the game."]]
[:button#help.key {:on-click #(trigger-key 191)} "?"]))
Cambié el nombre de Rogule por otra cosa y tras la maldita media hora de compilación este cambio sí aparecía. ¿Por qué este texto sí y el ninja no?
Cuando voy haciendo cambios sin tener mucha idea como en este caso necesito saber rápidamente los resultados de mis cambios. No podía esperar media hora cada vez así que me puse a investigar qué pasaba. La acción que más tardaba parecía ser cuando el comando NPM se estaba bajando las dependencias pero eso solo se debería hacer una vez. Configuré npm para que mostrara más información:
npm config set loglevel info
Al recompilar vi que cada vez estaba buscando información sobre cientos de paquetes al servidor de NPM y cada petición tardaba casi un segundo. ¡Además parecía que lo hacía varias veces por cada instalación! Estuve un par de horas buscando formas de "optimizar" NPM para que no fuera tan lento pero no conseguía gran cosa. Por más cosas que probaba, la compilación siempre tardaba más de media hora.
Entonces me apareció en pantalla una notificación diciendo que me estaba quedando sin espacio en disco. Estaba usando una máquina virtual con muy poco disco pero para lo que estaba haciendo no debería ser un problema. Con el comando du
, que muestra el tamaño de los archivos, me puse a buscar porqué se había llenado el disco. Quedaban 50 megas libres.
El culpable era NPM: el directorio .npm
ocupaba casi 10 gigas. En un primer momento pensé que como cada vez estaba conectándose a su servidor se estaba bajando los mismos paquetes una y otra vez llenándome el disco pero luego vi que lo que realmente ocupaba mucho espacio eran los logs. Parece que cada compilación me había generado más de un giga de logs. Seguramente por eso estaba tardando tanto.
Abrí uno de ellos y había miles de mensajes sobre conflictos entre dependencias. No sé mucho de NPM así que no sé cómo arreglar esas cosas.
El fichero package.json
que define las dependencias no se había modificado en casi un año. El comando npm outdated
mostraba que todas las dependencias tenían versiones más nuevas disponibles. Decidía que iba a actualizar todo a ver si arreglaba los problemas de conflictos. Si no funcionaba había pensado en dejar el proyecto para no perder más tiempo.
Es peligroso pero se puede forzar una actualización de todas las dependencias con el mando:
npx npm-check-updates
Tras el cambio en el package.json
, la compilación pasó a tardar 40 segundos. ¡De 30 minutos a 40 segundos! No tenía nada de fe de que funcionase pero tras actualizar las dependencias y compilar durante menos de un minuto el juego funcionaba perfectamente.
Volví a aplicar los cambios de la sección anterior pero el ninja seguía sin salir. Al menos ahora podía probar cosas y ver qué pasaba casi inmediatamente lo cual iba a facilitarme mucho investigar pero eso ya lo veremos en el próximo artículo.