Cosas que aprendí de... Tileboard
febrero 12, 2016

Tileboard es un programa escrito en Python 3 que genera diagramas de juegos de mesa, como por ejemplo ajedrez. Lo escribí porque hace un tiempo inventé una variante de ajedrez que utiliza un tablero distinto al habitual y no encontré ningún otro programa que pudiese dibujarlo.

Sobre el juego en si, de momento no diré nada, pero he aquí una foto el tablero en cuestión, que tiene 5x7 casillas en lugar de las habituales:

Duchess

Básicamente, lo que buscaba era un programa que:

¿No parece difícil no?

Arquitectura

Como librería para manipular imágenes, decidí escoger Pillow. Ya la usé en su momento para otros proyectos con buenos resultados. Es rápida y práctica.

En cuestión de código, Tileboard tiene unas 900 líneas, divididas más o menos en las siguientes tareas:

La mayoría del código es simple (y podría serlo más), excepto quizá los cálculos de cómo dibujar las letras/números de los bordes y del tamaño de la imagen final, que depende del tamaño de las piezas del tablero y de los elementos que la componen (borde, líneas adyacentes...)

Lo que también es, es repetitivo. Una buena muestra de ello podría ser:

# outer outline:
if not options.outer_outline_disable:
    draw_rectangle_outline(image,
                              x1 = 0,
                              y1 = 0,
                              x2 = image.width - 1,
                              y2 = image.height - 1,
                           width = outer_outline_size - 1,
                           color = options.outer_outline_color)

# border:
if not options.border_disable:
    draw_rectangle_outline(image,
                              x1 = outer_outline_size,
                              y1 = outer_outline_size,
                              x2 = image.width - outer_outline_size - 1,
                              y2 = image.height - outer_outline_size - 1,
                           width = border_size - 1,
                           color = options.border_color)

Y así para cada elemento del tablero, incluyendo línea exterior, borde, línea interior, casillas, piezas y cruces y puntos para señalar posiciones. Es tedioso pero diría sin duda que volvería a escribirlo igual si tuviese que hacerlo.

Algunas lecciones aprendidas

Me quedo con:

Y la más importante: leer la documentación de las librerías que uso.

De la de ImageDraw:

PIL.ImageDraw.Draw.rectangle(xy, fill=None, outline=None)

Draws a rectangle.
Parameters:

    xy: Four points to define the bounding box.
        Sequence of either [(x0, y0), (x1, y1)] or [x0, y0, x1, y1].
        The second point is just outside the drawn rectangle.

        ...

"The second point is just outside the drawn rectangle." De ahí el -1 en casi todas las funciones de dibujado de Tileboard y varias horas de trabajo de depuración.

Es curioso que al final lo más fácil de todo el asunto fuese dibujar las piezas.

Conclusiones

La verdad es que estoy bastante satisfecho con el resultado. Quizá me pasé un poco en la cantidad de argumentos que tiene, pero es flexible y práctico. En el README hay ejemplos de otros juegos como las Damas o incluso de un Bejeweled con sus gemas.

Dejo una imagen de bonus, mostrando el proceso de dibujado de un tablero normal de ajedrez. Faltan algunos frames que estarían en otras imágenes, dado que este tablero no tiene huecos, cruces o puntos, pero la idea es la misma: ir construyendo todo de manera independiente.

(la imagen comienza siendo completamente transparente):

Tileboard GIF

Como con todos los proyectos que he estado revisando, he hecho release.

Releases, Colorama y otras historias
febrero 11, 2016

Esta semana he estado revisando todos mis proyectos en Github y testeándolos un poco con las versiones nuevas de los compiladores y librerías que usan. No hay cambios en los Changelog más allá de que compilan con VS 2015 o el último MinGW, Python 3.5... este tipo de cosas.

He hecho release de todos, así que si quieres tener los últimos ejecutables de dir, GaGa, Yava, etc... están calentitos salidos del horno.

22 proyectos. Y aún me parece que empecé ayer. Solo hay un proyecto con cambios reales y necesarios, del que ya hice un postmortem en su momento: MovieWar.

Colorama y MovieWar

Colorama es la librería que uso en MovieWar para poder escribir con colores en la terminal. Es portable y funciona correctamente en Windows, Linux, etc... O eso se supone.

Probando en MSYS2 en lugar de MSYS, me di cuenta de que MovieWar no mostraba color alguno. ¿Uh?. La razón es que mintty no se comporta como una consola normal sino "no interactiva" y que además, ya es capaz por si misma de reconocer los códigos de colores ANSI sin necesidad de usar librería alguna.

Como es imposible saber bajo que consola se está ejecutando MovieWar o las cosas soporta, mi solución fue la siguiente:

El código tiene esta pinta (nota: no se pueden poner ambos parámetros a la vez):

# initialize colors as empty strings until we know we want them:
player_colors = ['', '', '', '', '']
game_color = ''

enable_colors = False

if options.colorama and HAVE_COLORAMA:
    colorama.init()
    enable_colors = True

if options.ansi_colors:
    enable_colors = True

if enable_colors:
    # red, green, yellow, magenta, cyan:
    player_colors = ['31m', '32m', '33m', '35m', '36m']

    # white:
    game_color = '37m'

Y la función para imprimir en color:

def print_color(color, message = ''):
    """
    Print a message to stdout, possibly in color.
    When the message is empty, print a newline.

    When color is an ansi code: use it for coloring.
    When color is an empty string: behave like the regular print().
    Auto-reset color back to default after printing.
    """
    if color == '':
        print(message, flush = True)
    else:
        print('\033[1;' + color + message + '\033[0m', flush = True)

Y ahora todo funciona como debería en cualquier terminal:

Debian:

MovieWar Debian

Windows, cmd.exe:

MovieWar cmd.exe

Windows, MSYS2:

MovieWar MSYS2

Otras historias

En medio de todo el embrollo, también he terminado un proyecto "nuevo" que ya está disponible en Github: Tileboard. Quizá haga un postmortem sobre él en el futuro. Es uno de esos programas que parecen triviales a simple vista, pero no lo son. De momento el README tiene bastante información.

Dejo también una curiosidad de la que mi pareja me ha hablado y que me ha gustado mucho: Astropix o "Astronomy Picture of the Day", donde hay una foto nueva sobre el cosmos cada día con explicación y todo.

Si les visitas, no dejes de echar un ojo al archivo, porque llevan haciendo esto desde el 16 de Junio del 95. Hay muchas preciosas. Me encanta ésta.

Cosas que aprendí de... Yava
diciembre 19, 2015

Yava es un lanzador de aplicaciones escrito en C#, pensado para ser usado principalmente con emuladores como Dolphin, Mame, Snes9x, etc... Es un pequeño menú desde el que ejecutar los juegos de una manera cómoda y rápida.

Tiene esta pinta:

Yava

Aunque ya existen muchos programas parecidos (como LaunchBox o mGalaxy) no pude encontrar ninguno que cumpliese los siguientes requisitos:

Arquitectura

Yava es prácticamente idéntico a GaGa en diseño.

Yava

Usa las mismas librerías: mINI y LowKey. Los componentes son también los mismos, excepto que no existe el concepto de Player (no hay reproductor) y StreamsFile es ahora FoldersFile, que es el fichero donde reside la configuración de todos los juegos/emuladores.

Las únicas partes complicadas del código de Yava son probablemente el código que se encarga de recordar qué rom está seleccionada para cada carpeta (líneas 276 en adelante en Yava.cs) y lo relativo a ejecutar el emulador final pasándole la ruta correcta de la carpeta donde está.

Paths

¿No es tan complicado, no? Solo es ejecutar el emulador + la rom. El problema es que no hay una ruta correcta, sino múltiples rutas que pueden ser absolutas o relativas.

Por ejemplo, si el archivo Folders.ini contiene...

[Wii]
path = Romset\Wii
executable = Emulators\Dolphin 4.0.3\Play\Dolphin.exe
parameters = --batch --exec "%FILEPATH%"
extensions = iso, wbfs

Entonces:

Dos curiosidades

¿Cuál es el rendimiento que cabe esperar de un programa como Yava? Según mis pruebas tarda menos de 1 segundo en cargar las más de 45.000 roms de Mame directamente del sistema de archivos, sin base de datos ni nada. Buscar es instantáneo.

¿Qué ocurre si configuramos el archivo Folders.ini así?

[Snes]
path = Romset\Snes
executable = %FILEPATH%
extensions = smc

Yava intentará abrir las roms con extensión .smc con la aplicación asociada a ellas por defecto (sea el emulador que sea). Es posible hacer esto para cualquier extensión.

Cosas que aprendí de... MovieWar
diciembre 18, 2015

MovieWar es un trivial multijugador para consola escrito en Python. Es parecido a "El precio justo", donde los jugadores intentan adivinar la fecha de salida de una película a partir de su título.

He aquí una foto:

MovieWar

Algunas características interesantes del programa:

La base de datos

MovieWar es un programa simple, secuencial, basado en turnos. La entrada/salida de cada paso está claramente definida. El código son unas 700 líneas. La parte complicada consiste en responder a la siguiente pregunta:

¿Cómo genero una buena base de datos de películas populares para poder jugar?

Mi respuesta fue: utilizando múltiples bases de datos y verificando que todo coincide antes de añadir ninguna película. Para ello, creé una serie de scripts, MovieWarDBGen en un repositorio aparte que se encargan de todo el proceso.

The Movie List

El punto de partida para la base de datos es una lista de 9200 películas que Brad Bourland recopiló durante años. Esta lista es un "top 9200" y solo contiene películas anteriores al año 2000. Esto es ideal porque permite verificar fácilmente el año de salida de cada película con respecto a múltiples bases de datos y son películas razonablemente populares (no hay Bollywood ni nada por el estilo).

El primer script de MovieWarDBGen, usa la API de Freebase para descargar la lista completa. El segundo, convierte los datos a un formato más simple, validando el título y fecha en el proceso.

El tercero, utiliza OMDB para verificar que todas las fechas de salida y títulos de Freebase coinciden con el esperado en OMDB. De 9169 películas, 7601 concuerdan exactamente en ambas bases de datos. Este script puede tardar tranquilamente 2 horas en ejecutarse, pero mejor calidad que cantidad.

Un cuarto script se encarga de colapsar los años de películas que tienen el mismo título en una sola entrada JSON. Por ejemplo, convierte esto:

{"name": "Jane Eyre", "year": "2011"}
{"name": "Jane Eyre", "year": "1996"}

En esto otro:

{"name": "Jane Eyre", "years": ["2011", "1996"]}

También comprueba años duplicados.

Puntuando

Una vez la base de datos está lista, implementar el juego es fácil. La única decisión curiosa que toma MovieWar está en cómo puntuar las respuestas de los jugadores:

MovieWar da 50 puntos por una respuesta exacta y (20 puntos - años de error) por un fallo. Esto garantiza que la cosa esté un poco reñida al final, debido a penalizaciones y que cada jugador suele ser experto en diferentes géneros de cine.

Cosas que aprendí de... 404
diciembre 17, 2015

404 es un pequeño programa escrito en Python que permite comprobar el estado de una serie de enlaces en una página web.

Un ejemplo:

$ 404.py http://beluki.github.io --threads 10 --internal follow --external check
404: http://cdimage.debian.org/debian-cd/7.8.0/i386/iso-cd/
Checked 181 total links in 10.4 seconds.
55 internal, 126 external.
0 network/parsing errors, 1 link errors.

Todo empezó probando un lenguaje de programación llamado Nim, en el que implementé un script parecido para testear la librería standard. Frustrado con múltiples bugs, decidí plantarme y reescribirlo en Python en lugar de Nim y 404 fue el resultado.

Arquitectura

404 sigue la misma arquitectura exacta que MultiHash. Usa el mismo código para crear su threadpool y mantener los hilos. Ya escribí un post sobre ello en este blog. Una vez creado, el resto es cuestión de utilizar BeautifulSoup para leer el HTML y Requests para ir acumulando el resto de los enlaces.

Lo cierto es que la mayoría del trabajo lo hacen las dos librerías. BeautifulSoup ya se encarga de escoger el mejor parser de HTML existente en el sistema y es flexible con respecto al código en si.

Por eficiencia, 404 solo tiene en cuenta enlaces <a href... e <img src...

link_strainer = SoupStrainer(lambda name, attrs: name == 'a' or name == 'img')

soup = BeautifulSoup(response.content, 'html.parser', parse_only = link_strainer,
                        from_encoding = response.encoding)

# a href...
for tag in soup.find_all('a', href = True):
    ...

# img src...
for tag in soup.find_all('img', src = True):
    ...

Del mismo modo, Requests se encarga de todo lo necesario para establecer la comunicación entre 404 y los servidores. Soporta SSL, redirecciones, Timeouts y un montón de cosas más que 404 no necesita. Sin estas dos librerías, es probable que el código de 404 fuese tranquilamente diez o quince veces más largo y complejo.

La única optimización que hace 404 es ignorar la parte fragment de los enlaces antes de pasar cada link al resto del programa, dado que solo tiene sentido para el navegador.

Una pregunta interesante es: ¿qué se considera un error al comprobar un enlace? ¿estado 404? ¿estado 301 (movido permanentemente)?. Para 404 la respuesta es: hay un error cuando por problemas de conexión o de lectura (imposible leer el HTML) no se puede obtener un código de estado válido en un tiempo aceptable.