Cosas que aprendí de... dir
diciembre 15, 2015

dir es un videojuego multiplataforma escrito en Lua que utiliza el framework Löve2D. El estilo de juego es parecido a Bejeweled o Tetris: juntar piezas de un mismo color para que desaparezcan.

Es también mi intento de diseñar un juego que resulte atractivo a audiencias casuales y hardcore, sin depender de límites de tiempo o reflejos rápidos.

Tiene esta pinta:

dir

Si quieres descargarlo y echar una partida antes de continuar leyendo haz click aquí.

Características

dir podría definirse tanto por las cosas que sí tiene:

... como por las que no:

Löve2D

Antes de empezar a programar el juego, evalué múltiples frameworks como posibles candidatos. Los principales fueron Haxe, Löve2D, Monogame y pygame.

Dos de ellos, Monogame y pygame, los descarté en parte por sus dependencias (.NET y Python respectivamente) y en parte por complicaciones a la hora de hacer tareas que deberían ser realmente simples, como dibujar un cuadrado en pantalla.

En el caso de Haxe, las incompatibilidades entre la versión web y la versión escritorio fueron la principal razón de descartarlo. Basta con echar un ojo a la demo de PiratePig en el blog de Joshua Granick para darse cuenta de las diferencias (compara por ejemplo la versión HTML5 y Flash).

A día de hoy no sé si puedo decir que Löve fuese el framework "perfecto", pero volvería a utilizarlo para programar dir. La documentación es excelente. El foro también.

Lua

Utilizar Löve2D implica utilizar Lua.

Lua es en general un lenguaje muy bien diseñado con alguna que otra pega importante, aunque todas las pegas tienen sentido en el contexto donde Lua suele utilizarse.

Por ejemplo: Lua no tiene una librería standard decente, no tiene excepciones, no tiene orientación a objetos, no tiene mil cosas que normalmente son necesarias cuando no estás programando algo embebido. Löve2D suple en parte la falta de esta funcionalidad a base de módulos adicionales como "love.audio", "love.filesystem", etc...

Lua tampoco tiene un modo de distinguir "nil" de "variable no inicializada", lo cual me ha causado muchos más dolores de cabeza a la hora de programar dir que cualquier módulo aparte que haya tenido que escribir.

Arquitectura

dir está escrito de modo que cada parte es lo más independiente posible de todas las demás. Cada componente usa además su propia noción de orientación a objetos (closures en realidad), con un "self" explícito al estilo Python.

Por ejemplo, para inicializar el juego:

function Game ()
    local self = {}

    -- initialization:
    self.init = function ()
        self.grid_width = 5
        self.grid_height = 5

        self.grid = Grid(self)
        self.hud = Hud(self)
        self.menu = Menu(self)
        self.screen = Screen(self)
        self.state = State(self)
        self.theme = Theme(self)

        self.resize()
        self.restart()
    end

    -- otros "métodos"...

    self.init()
    return self
end

donde Grid, Hud, Menu, Screen, State y Theme son los módulos que se combinan para dar forma al juego final.

A más bajo nivel, cada módulo es responsable de sus propias animaciones. Por ejemplo, Grid contiene todas las definiciones necesarias para que las piezas del grid que están apareciendo, desapareciendo, moviéndose, etc mantengan su estado entre frame y frame.

Un detalle importante aquí es que en dir solo existe una animación a la vez en todo el grid. No es posible tener piezas que se muevan, mientras otras desaparecen al mismo tiempo. Esto simplifica mucho la implementación, porque puede hacerse en base al concepto de "Tween", donde cada animación se enlaza con la siguiente a través de una función puente (ver por ejemplo grid.appearing.oncomplete).

El hilo conductor final es State, que decide qué hacer en base al ranking y la puntuación actuales, cada vez que las piezas cambian de estado.

Finalmente, en la carpeta lib hay dos pequeñas librerías. Una para manipular arrays de dos dimensiones y otra con wrappers sobre la funcionalidad básica de Löve2D. Esta última es totalmente prescindible, pero hará más fácil portar dir a futuras versiones de Löve si este cambia.

Conclusiones

Estoy muy orgulloso del resultado obtenido con dir. Creo que el resultado ha merecido el esfuerzo. El record actual será además muy difícil de superar.

Bonus

Aquí puedes ver un vídeo de un usuario del foro de Löve2D jugando a dir:

Y un port a Android:

Cosas que aprendí de... MQLite
diciembre 14, 2015

MQLite es un pequeño dialecto basado en MQL, el lenguaje que se utiliza en Freebase para realizar búsquedas. Está escrito en Python 3 y es un compilador sencillo a un AST recursivo de nodos que implementan un método "match".

Aunque el núcleo de MQLite es un subset de MQL, en realidad implementa bastantes más cosas que el original, debido a la necesidad de realizar búsquedas sobre JSON arbitrario en lugar de una base de datos concreta.

Como lo mejor es un ejemplo... El siguiente comando devuelve la lista de repositorios que tengo en Github con al menos 1 fork activo.

$ curl https://api.github.com/users/Beluki/repos
| MQlite.py '[{"name": null, "forks": null, "forks >": 0}]'
[
    {
        "name": "GaGa",
        "forks": 2
    },
    {
        "name": "MQLite",
        "forks": 1
    }
]

Arquitectura

MQLite es un proyecto relativamente simple porque no necesita un parser. Utiliza la misma sintaxis que JSON. Una vez tenemos los datos a buscar y un archivo .json donde buscarlos, solo queda preprocesar la búsqueda para poder hacerla del modo más eficientemente posible.

Uno de los límites que me puse al implementarlo, es que MQLite nunca debería modificar los datos JSON sobre los que busca, ni ordenándolos ni optimizándolos. Al fin y al cabo, si realmente necesitas eso, te conviene más una base de datos real.

Si no podemos modificar los datos, solo queda representar la búsqueda. En el caso de MQLite, está hecha a partir de una serie de nodos (objetos) con un método: "match" que devuelve o bien una "respuesta" o "vacío" dependiendo de si los datos de la búsqueda coinciden con alguno existente en la base de datos.

Como "None" es un resultado válido, "vacío" se representa con una clase propia:

class _NoMatch(object):
    """
    A custom class to represent no matches.
    Needed because None is a legitimate match.
    """
    pass

NoMatch = _NoMatch()

Y "concuerda con cualquier cosa" se representa así:

class MatchAny(object):
    """
    Match any input data.
    """
    def match(self, data):
        return data

La mayoría de nodos son sencillos. Cadenas, números o booleanos solo concuerdan consigo mismos. Otros datos, como las listas o los diccionarios, permiten operaciones más complejas.

Un ejemplo de búsqueda una vez transformada a esta representación, podría ser:

[{ "name": null, "age >": 25, "__sort__": "age" }]

Que queda así:

MatchList(
    MatchDict(
        [{ "name": MatchAny() }],
        [{ "age": ConstraintBiggerThan(25) }],
        [{ "__sort__": DirectiveSort("age") }]
    )
)

Y significa:

Dame el nombre de todos los items en la base de datos que cumplan la condición de tener una edad mayor de 25 e imprímelos en orden ascencente.

Dificultades

La parte más complicada de escribir MQLite fue con bastante diferencia encontrar una sintaxis adecuada para expresar las búsquedas. En el README hay numerosos ejemplos de búsquedas complejas con directivas, constraints y todo tipo de opciones.

Dos limitaciones de MQLite son:

Es imposible escribir una cláusula "A o B" para dos claves distintas.

[{ "a:age >": 20, "b:name in" ["a", "b", "c"] }]

Algunas directivas, en particular "__limit__", son sensibles al orden.

[{ "name": null, "__sort__": "age", "__limit__": 3 }]

Esta última búsqueda ordenará PRIMERO y limitará después en lugar de hacerlo al contrario. En su API, MQLite usa OrderedDict para asegurarse de mantener el orden correcto en cada búsqueda (dado que los diccionarios de Python no garantizan un orden concreto).

Compatibilidad con Python

Aunque MQLite está pensado para JSON, puede buscar en prácticamente cualquier cosa iterable de Python. Para hacerlo, basta con usar el objeto "Pattern" de la API en lugar de "JSONPattern".

data = [
    {
        "name": "James",
        "age": 23,
        "student": False,
        "hobbies": ["chess", "football", "basketball"]
    },
    {
        "name": "John",
        "age": 35,
        "student": True,
        "grades": { "chemistry": "C", "english": "A" },
        "hobbies": ["reading", "swimming", "painting"]
    }
]

pattern = Pattern([{ "name": None, "age in": set([20, 21, 22, 23]) }])
print(pattern.match(data))

Como añadido, MQLite incluye una pequeña shell para poder experimentar con distintas búsquedas de manera rápida, explorar datasets, etc... También hay una suite de tests disponible.

Grandmaster
octubre 29, 2015

Hace unos 4 meses (concretamente el 30 de Junio) terminé dir, un pequeño juego programado en Lua, utilizando Love2D como motor. Este post no es un postmortem acerca de dir, aunque no descarto escribir más posts sobre los proyectos que he hecho después de GaGa.

Este post va de la que es probablemente la mejor partida que se ha jugado en dir hasta el momento. La siguiente screenshot me la envió mi pareja por email en plan "te dije que lo haría":

grandmaster

Si no has jugado nunca a dir, es probable que la foto no te diga gran cosa. En dir, es realmente difícil alcanzar el ranking "grandmaster". Hay de hacer un combo de 25x, llegar al nivel 20 y hacer más de un millón de puntos en la misma partida.

Si te atreves a intentarlo, dir es gratuito y puedes descargarlo aquí.

Miscelánea
marzo 24, 2015

Hace unos días me encontré con un post sobre GaGa en una página web llamada Gorgeous Machine. Estas cosas siempre hacen sonreír. Que otros hablen bien de tus proyectos es un buen alimento para el ego.

En otro orden de cosas, he terminado MQLite, un programa (y librería) escrito en Python para hacer pattern-matching al estilo MQL pero dirigido a JSON. Irónicamente, Freebase cerrará su web y APIs en junio de este año, migrando todos sus datos a Wikidata.

Cosas que aprendí de... GaGa
febrero 01, 2015

GaGa es un pequeño reproductor de radios online para Windows, escrito en C#. Es parecido a RadioTray para Linux, en el sentido en que se integra con el área de notificación del sistema.

Tiene esta pinta:

GaGa

Arquitectura

Con unas 3000 líneas de código en 18 archivos, GaGa es bastante más grande que la mayoría de proyectos que tengo en Github (MultiHash por ejemplo tiene unas 400 líneas). ¿Cómo organizarlo?.

Las pautas que seguí fueron las siguientes:

Y este fue el resultado:

GaGa

Finalmente, GaGa (GaGa.cs y GaGaSettings.cs) solo implementa el menú y la lógica de los eventos que se derivan de él, como reproducir una radio al hacer click en el stream correspondiente. Sirve para unir todo lo anterior.

Curiosidades del diseño:

Diría que la lección más importante en este punto es: organiza el programa de modo que necesites tener en tu cabeza el mínimo contenido posible para poder analizar su funcionamiento.

mINI y LowKey

mINI es una pequeña librería que sirve para leer archivos INI. Es curioso que aunque el formato INI sea predominante en Windows, .NET no tenga nada en la librería standard para usarlo.

Lo más interesante de mINI es que no guarda dato alguno del archivo que está leyendo. Es una clase abstracta con métodos como OnSection(String section) que las subclases implementan para decidir qué hacer con los datos. Este diseño es el que permite recargar el menú de GaGa con la mínima latencia posible, dado que no hay estructuras de datos intermedias.

Escribí mINI en un fin de semana y la verdad es que estoy contento con el resultado. Es simple, práctico, funciona.

LowKey es mucho más complicado. Es una librería que responde a pulsaciones de teclas desde cualquier aplicación utilizando hooks a bajo nivel. Aunque su código es relativamente corto, hay partes en las que es fácil cometer un error.

Dos ejemplos:

.NET Framework

C# es, en general, un lenguaje excelente. Un buen ejemplo es el modo en el que GaGa hace la animación en su icono de "buffering", que tiene esta pinta:

Buffering

Implementado así (fragmento):

// iconos que vamos a usar:
private readonly Icon[] bufferingIcons;

// temporizador para cambiar de icono:
private readonly DispatcherTimer bufferingIconTimer;

// índice del icono actual en el array bufferingIcons:
private Int32 currentBufferingIcon;

public Player(NotifyIcon icon)
{
    ...

    // 300 milisegundos entre icono e icono:
    bufferingIconTimer = new DispatcherTimer(DispatcherPriority.Background);
    bufferingIconTimer.Interval = TimeSpan.FromMilliseconds(300);
    bufferingIconTimer.Tick += OnBufferingIconTimerTick;
    currentBufferingIcon = 0;
}

private void OnBufferingIconTimerTick(Object sender, EventArgs e)
{
    // cambiar icono:
    notifyIcon.Icon = bufferingIcons[currentBufferingIcon];

    // ir al siguiente icono o volver el primero si es necesario:
    currentBufferingIcon++;
    if (currentBufferingIcon == bufferingIcons.Length)
    {
        currentBufferingIcon = 0;
    }
}

Un simple DispatcherTimer que se ejecuta en el thread actual con prioridad lo más baja posible (dado que no es algo que requiera precisión). Ojalá todos los lenguajes tuviesen algo similar.

Por desgracia, no puedo decir lo mismo de Windows Forms o de otras partes de la librería standard. Aunque sobre el papel es genial, en la práctica hay comportamientos extraños y bugs que hacen su uso incómodo y que llevan fácilmente a errores por parte del programador.

Algunos ejemplos (no exhaustivo):

Conclusiones

Estoy muy satisfecho con GaGa.

Casi todos mis programas son utilidades de consola diseñadas para usuarios avanzados (o para programadores), pero GaGa es útil para cualquier persona con un ordenador. Ha sido un reto interesante.

Además, una vez conseguí escuchar Radio GaGa de Queen en el propio GaGa (en la radio KissFM), lo que no tiene precio.

Repositorios en Github: GaGa - LowKey - mINI