Cosas que aprendí de... Tileboard
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:
Básicamente, lo que buscaba era un programa que:
- Pudiese generar cualquier tablero "parecido" al de ajedrez.
- Con o sin coordenadas.
- Con cualquier tipo de piezas (othello, reversi, etc...).
- En cualquier tamaño, tanto de imagen como de casillas.
- Personalizable en cuestión de colores, fuentes, etc...
- Usase poca memoria para ello (para poder imprimir el tablero real, en grande).
¿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:
- Parsear posiciones FEN.
- Representar el tablero a dibujar.
- Conversiones de base para los bordes (base26).
- Parsear notación algebraica (A1, H2...)
- Cargar imágenes y fuentes.
- Cálculos de tamaño de imagen, borde, etc...
- Funciones utilidad de ayuda para dibujar.
- Funciones que realmente dibujan el tablero.
- El parser de línea de comandos.
- Dibujado del tablero en si.
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:
- Dibujar siempre todo por separado.
- Partir de (0, 0) al dibujar. Es mucho más fácil razonar con coordenadas absolutas.
- Separar funciones con conocimiento específico y utilidades, especialmente de dibujo.
- Tener modo de desactivar cada elemento para poder depurar cómodamente.
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):
Como con todos los proyectos que he estado revisando, he hecho release.
Releases, Colorama y otras historias
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:
- Asumir por defecto que la terminal no soporta color alguno.
- Dos parámetros: "--ansi-colors" y "--colorama" para el usuario pueda elegir.
- Usar siempre los códigos de escape ANSI y cerrarlos a mano, sin autoreset en
colorama.
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:
Windows, cmd.exe:
Windows, 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
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:
Aunque ya existen muchos programas parecidos (como LaunchBox o mGalaxy) no pude
encontrar ninguno que cumpliese los siguientes requisitos:
- Fuese portable (funcione desde un pincho USB).
- No utilice una base de datos interna, solo carpetas y archivos.
- Sea fácil de configurar, preferiblemente en un solo archivo.
- Solo ejecute los juegos, delegando el resto del trabajo a cada emulador.
Arquitectura
Yava es prácticamente idéntico a GaGa en diseño.
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:
-
"path" es una ruta relativa o absoluta a la carpeta donde están las roms de Wii.
-
"executable" es una ruta relativa o absoluta a la carpeta donde reside el emulador, Dolphin.
-
"parameters" contiene los argumentos a pasarle a Dolphin, donde el %FILEPATH% es la ROM a usar.
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
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:
Algunas características interesantes del programa:
- Singleplayer o multiplayer (cualquier número de jugadores, por turnos).
- Incluye 7600 películas populares en la base de datos.
- Dos modos de juego: "película aleatoria" o "un jugador escoge y el resto adivinan fecha".
- Usa OMDB para buscar películas que desconoce y actualiza la DB con las entradas nuevas.
- Soporte de colores opcional, utilizando Colorama (Windows) o colores ansi (Unix, MSYS).
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
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.