Skip to content
Tommy CardeLo
Go back

Neo Tamagotchi — Una mascota de bolsillo para mis perros, en C sobre un RP2350

Un regalo para mi esposa: un tamagotchi de bolsillo donde viven nuestros cuatro perros en pixel art. El código del juego, en C11 puro, corre igual en un simulador de Windows y en una placa Waveshare RP2350 con pantalla AMOLED táctil, y uno de los perros es un ángel que nunca necesita nada.


El problema

Quería regalarle a mi esposa algo que no fuera una pantalla llena de notificaciones. Un tamagotchi es lo contrario: un mundo pequeño, lento y paciente al que se le echa un vistazo. La condición era que las mascotas fueran las nuestras: Maní, Aurelio, Nala y Kloe.

Kloe ya no está con nosotros, así que no podía ser una mascota a la que le da hambre. Aparece como un fantasmita translúcido con aureola dorada, un ángel guardián que nunca se pone triste, nunca come y nunca se enferma. Visita a los demás y brilla. De ahí sale todo el diseño: sin castigos, sin muerte, sin estado de fracaso. Los perros se ponen un poco tristes o con sueño si los ignoras, y nada más.

Qué hace

Hardware y stack

Una Waveshare RP2350-Touch-AMOLED-2.41: pantalla AMOLED de 2,41” y 600x450, RP2350B a 150 MHz, 520 KB de SRAM, y en la misma placa un RTC, una IMU, el controlador táctil y un cargador de LiPo.

El firmware es C con el Pico SDK. Primero miré MicroPython y lo descarté porque no estaba claro que el controlador de la pantalla tuviera soporte nativo. Del lado del PC hay una ventana Win32 mínima más un modo headless, compilados con zig cc (instalado con pip), para no depender de una toolchain completa en Windows.

Arquitectura

          game.c (lógica)      draw.c (dibujo)
          sprites_data.c       parque_data.c
              C11 puro, sin hardware, sin malloc
                     |
                  hal.h (15 líneas)
            /                      \
   sim.c (Win32 + headless)     placa/hal_placa.c
   PC, velocidad x1 .. x3600    RP2350: pantalla QSPI, táctil,
                                RTC, IMU, guardado en flash, dormant

Toda la superficie de hardware es hal.h: hal_epoch, hal_ms, hal_touch, hal_shake, hal_present, hal_load/hal_save, hal_idle. El juego nunca toca un registro. Esa decisión hizo posible todo lo demás: construí, probé y mostré el juego completo antes de que llegara la placa, y portarlo fue implementar esas funciones. El código del juego no cambió.

Decisiones de diseño

1. Dos relojes, a propósito

La pregunta más difícil de un tamagotchi es qué pasa mientras está apagado. Dividí el tiempo en dos:

Con un solo reloj, el temporizador de Kloe podría vencerse dentro de mi bolsillo y ella aparecería en el instante en que enciendo la pantalla.

2. Los indicadores se integran, no se simulan

Los indicadores van de 10 a 100 con un piso estricto, y los perros se ven tristes por debajo de 30. Al encender tras una pausa, el aparato no reproduce el hueco tick por tick: lo integra en tramos de máximo 60 segundos, cada uno con la tasa de día o de noche de su propia hora, con un tope de siete días:

const float *r = (h >= NOCHE_INI || h < NOCHE_FIN) ? TASA_NOCHE : TASA_DIA;
for (int i = 0; i < NDOGS; i++)
  for (int k = 0; k < NNEED; k++)
    s->need[i][k] = cl(s->need[i][k] + r[k] * c / 3600.f);

Así, un perro que durmió ocho horas de noche despierta descansado, y uno que pasó el día en un cajón tiene hambre pero nunca por debajo del piso. Si el reloj salta hacia atrás, no se recalcula nada.

3. Kloe es otra máquina de estados

Los perros tienen humores ponderados por la hora y por sus indicadores. Kloe no tiene ni siquiera un arreglo de indicadores, así que estructuralmente no puede estar triste. Es una pequeña rutina de visita: entra por un lado, baraja el orden de los perros, visita a cada uno, brilla cuando la tocan y se va. Su temporizador solo corre con la pantalla encendida (de 40 a 120 segundos), y hay un 35 % de probabilidad de que llegue a los pocos segundos si enciendo de día, para que las miradas rápidas también puedan encontrarse con ella. La pantalla no se apaga a media visita. De noche flota sobre los perros dormidos.

Todo eso se ajusta con las constantes KLOE_* de config.h. Los datos personales (el apodo y seis fechas) viven ahí también como simples defines, de modo que cambiar un cumpleaños nunca implica tocar la lógica.

4. Dibujar por bandas, porque 540 KB no caben

Un cuadro completo de 600x450 en RGB565 pesa 540 KB, más que los 520 KB de SRAM. Podía usar la PSRAM de la placa. En cambio, el render dibuja 10 bandas de 45 filas con doble búfer, enviadas por QSPI con DMA. Va con un tope de 15 fps, de sobra para una mascota. El parque fijo vive en una caché de colores indexados y solo se recompone cuando cambia la franja horaria, la lluvia o la posición del sol o la luna; los cambios de paleta entre franjas se funden durante los últimos 30 minutos de cada una.

5. Guardar sin confiar en la flash

El guardado alterna entre dos ranuras en los últimos sectores de la flash, cada una con número mágico, versión, tamaño y checksum. Al cargar, el juego además rechaza cualquier indicador fuera de rango, lo que también atrapa los NaN. Un guardado corrupto no tumba el programa ni se carga a medias: se vuelve a un inicio limpio.

6. Un sueño que solo interrumpe el botón de energía

Con la pantalla apagada, la CPU seguía sondeando el táctil y la IMU cada 50 ms: entre 15 y 25 mA, alrededor de un día con una batería de 600 mAh. Pasé al modo dormant del RP2350: relojes al cristal, PLL apagados, despertar por un flanco en un GPIO, controlador táctil en hibernación e IMU apagada. El táctil no lograba despertarlo de forma fiable (el pulso de interrupción parecía demasiado corto), así que la decisión fue sencilla: solo el botón de energía despierta al aparato dormido. Ya despierto, el toque y la sacudida funcionan normal.

Pipeline de pixel art

Los sprites partieron de hojas generadas a partir de fotos de los perros: caminar, correr, sentado, parpadeo, dormir, triste, más una hoja de acciones extra por perro. Un script de Python (assets.py) recorta los cuadros, quita el fondo con un relleno por inundación que parte solo del borde de la celda (para no comerse detalles interiores, como la placa rosada de Nala), alinea todos los cuadros a una misma línea base para que nadie tiemble, y escribe paletas indexadas de 8 bits en sprites_data.c, unos 1,46 MB de flash. El parque son 31 capas exportadas de una herramienta de diseño y convertidas igual a parque_data.c. Ambos archivos son generados, nunca se editan a mano.

Cómo lo probé

No escribo pruebas unitarias a posteriori. Escribí primero los modos de fallo y después un único corredor E2E (python juego/tools/e2e.py) que maneja el simulador real en modo headless y deja artefactos: una tira de capturas y un state.json. Hoy pasa 80 de 80 comprobaciones: perros dormidos de noche y despiertos de día, pisos de los indicadores, Kloe nunca triste, un saludo por día, recuperación tras un apagado, reloj hacia atrás, guardados corruptos, determinismo, y que las seis fechas reales disparan su fiesta. En la placa, un script de diagnóstico deja un reporte JSON por USB. Aun así, tuve que sentarme frente a la pantalla real para calibrar la orientación, los desplazamientos y el espejo del táctil, algo que ninguna prueba podía hacer por mí.

Lo que aprendí


Share this post on:

Artículo anterior
Deepy 2.0 — Reconstruir un asistente personal alrededor de un solo Postgres, un núcleo de WhatsApp y un dashboard privado
Artículo siguiente
Kana Practice — Aprender a leer hiragana adaptando el español a sonidos japoneses