Ricochet Robot es un juego web de lógica. Cuatro robots —rojo, verde, azul y amarillo— se deslizan por un tablero de 16 × 16 casillas y solo se detienen cuando chocan con una pared, con el borde o con otro robot. La tarea consiste en llevar el robot correcto hasta la ficha objetivo en el menor número de movimientos posible y, en los retos diarios, desempatar por tiempo.

El proyecto recupera y reinterpreta uno de mis proyectos antiguos. Está publicado en https://ricochetrobot.net/game, y los retos pasados se consultan en https://ricochetrobot.net/game/retos.html. Hoy es un prototipo funcional que sigue en desarrollo. El nombre puede recordar a otros juegos de mesa o productos con nombres parecidos, pero esto es un proyecto personal: no hay ninguna relación oficial ni comercial con ellos.

Cómo se juega

El tablero se forma con cuatro cuadrantes de 8 × 8 casillas que se colocan en una cuadrícula de 16 × 16 aplicando rotaciones fijas. Hay paredes exteriores y un bloque central de 2 × 2 casillas, y las posiciones iniciales de los robots no pueden caer en las cuatro casillas centrales.

La mecánica central no es mover un robot una casilla cada vez: cada movimiento lo desliza en línea recta hasta que algo lo detiene. Eso hace que la lectura del tablero importe más que la rapidez, porque un desplazamiento mal elegido puede dejar el robot en un sitio del que cuesta salir.

Los objetivos se dibujan con cinco tipos visuales —estrella, luna, planeta, engranaje y vórtice— en cuatro colores. El vórtice multicolor lo puede alcanzar cualquiera de los robots; el resto, solo el robot de su color. También hay reflectores diagonales: si un robot entra en la casilla de un reflector de otro color, su trayectoria se desvía 90 grados, mientras que atraviesa los reflectores de su propio color. Se gana cuando el robot correcto llega exactamente a la casilla objetivo.

Los controles son deliberadamente redundantes. Se elige robot haciendo clic sobre él o con los botones numerados, se mueve con las flechas del teclado y, en pantallas pequeñas, con una cruceta táctil. Los movimientos se pueden deshacer con el botón correspondiente o con las teclas Q y U. La partida muestra el número de pasos y un cronómetro con centésimas; el cronómetro se detiene mientras la pantalla de ayuda está abierta. En la primera visita se pide un nick, se guarda en el navegador, admite como máximo 20 caracteres y no se puede cambiar desde la interfaz.

Modos de juego

  • Reto diario: se carga desde un fichero con la fecha del día. Se puede recuperar un reto diario anterior escribiendo una fecha en formato AAAAMMDD.
  • Práctica: un tablero aleatorio, sin clasificación, que combina cuadrantes, objetivo y posiciones de robots. Se puede repetir el tablero o generar otro.
  • Tablero compartido: una configuración guardada que se abre con un identificador o mediante un parámetro de la URL.

La interfaz distingue visualmente la práctica, el reto diario, un reto diario anterior y un tablero compartido. Un reto diario ya resuelto no se puede volver a jugar con el mismo nickname, y cuando se empieza un reto diario se registra también como abandono si la persona sale sin resolverlo, de modo que salir de la página no permite reiniciarlo. Es un mecanismo sencillo apoyado en los datos guardados en PocketBase, no un sistema de seguridad.

Resultados, clasificación y reproducción

Los resultados de los retos diarios se guardan en una colección de PocketBase. La clasificación ordena primero por número de movimientos y después por tiempo, muestra hasta diez resultados de un tablero, indica la posición alcanzada y el total de resultados terminados, y deja los abandonos por detrás de los resultados válidos.

De cada solución diaria se guarda la secuencia de movimientos. Al terminar un reto —o después de haberlo jugado— se puede elegir a otra persona de la clasificación y ver su partida: el tablero vuelve a la posición inicial y se reproduce automáticamente movimiento a movimiento, con opción de pausar, continuar y retroceder un paso, además de la lista de movimientos. No hay una grabación de vídeo: se vuelve a ejecutar la secuencia sobre el mismo tablero.

Un tablero no diario se puede guardar y compartir mediante su identificador. Cuando el navegador lo permite, el identificador se copia al portapapeles; si no, hay una alternativa para navegadores antiguos.

Portada y retos pasados

La portada pública da acceso al reto del día, muestra su identificador, cuántas personas lo han terminado y el mejor resultado disponible. También hay un podio del reto anterior y podios mensuales y anuales calculados con un sistema de puntos: 10, 8, 6, 5, 4, 3 y 2 puntos para las siete primeras posiciones, y 1 punto para el resto de resultados terminados.

La página de retos pasados busca los retos disponibles de los últimos 365 días y los presenta en miniaturas. Carga 16 inicialmente y permite cargar más. Al abrir uno, un modal muestra la miniatura, la clasificación y un enlace para jugarlo. En las miniaturas no se destaca cuál es la ficha objetivo, para no dar pistas.

Cómo está hecho

La aplicación usa React 19 sobre Vite 7, con JavaScript y JSX. Los estilos se apoyan en Tailwind CSS 3, PostCSS y Autoprefixer, los iconos de interfaz vienen de Lucide React y las pruebas automatizadas se ejecutan con Vitest. Los ficheros de los retos diarios se generan con Python, PocketBase se ocupa de los tableros compartidos y de las clasificaciones, y nginx sirve la portada, la aplicación compilada y los ficheros de retos.

La representación del tablero es una de las decisiones que más condiciona el resto. Las plantillas describen paredes y objetivos dentro de cuadrantes de 8 × 8 casillas; la aplicación coloca cuatro cuadrantes en una cuadrícula de 16 × 16 y aplica rotaciones fijas. Así se reutilizan plantillas y se generan combinaciones distintas sin guardar una imagen completa por tablero. Además, el tablero lógico y la posición de los robots se mantienen separados: en la práctica aleatoria las posiciones se regeneran, mientras que en un tablero compartido se guardan las posiciones iniciales reales para que otra persona pueda jugar la misma configuración.

Los retos diarios se almacenan como ficheros JSON con nombres del tipo RRboard_YYYYMMDD.dat. Se generan mediante un proceso programado, fuera de la carpeta compilada y del repositorio, y la aplicación los carga por fecha. Si no encuentra el fichero del día, utiliza un tablero de respaldo incluido en el código.

El despliegue usa nginx con dos zonas: la portada en la raíz y la aplicación compilada bajo /game/. Un script de despliegue actualiza el clon del proyecto, instala dependencias y hace una compilación de Vite en una carpeta temporal antes de sustituir la versión anterior. En la última revisión local, las 53 pruebas automatizadas pasan y la compilación de producción termina correctamente, con avisos de actualización de datos de navegadores pero sin errores.

Detalles técnicos que merecen la pena

La parte que más he usado para experimentar es el movimiento. Cada desplazamiento obliga a comprobar muros, límites, otros robots y cambios de dirección en el orden correcto, y a protegerse contra bucles de reflectores. La estela temporal que deja el robot después de cada movimiento hace visible ese trabajo.

Otras cosas que el proyecto ha permitido probar:

  • Reproducir una solución guardando una secuencia compacta de movimientos en lugar de un vídeo.
  • Usar la misma representación de datos para las miniaturas y para el tablero jugable.
  • Construir una interfaz que funcione con teclado, ratón y controles táctiles.
  • Desplegar una aplicación Vite bajo la subruta /game/ y mantener los ficheros diarios fuera del repositorio y de la compilación.
  • Evitar que un reto diario se repita con el mismo nickname y registrar el abandono como estado.

El historial reciente recoge correcciones relacionadas con la repetición de retos diarios, el registro de movimientos y soluciones, la clasificación al terminar, la reproducción de jugadas, el modal de retos de otras fechas y los podios mensuales y anuales.