// 01 - DESCRIPCIÓN
¿De qué trata?
HeartSweeper es una reinterpretación del clásico buscaminas: en vez de minas, corazones ocultos. El objetivo, descubrir todo el tablero sin destaparlos.
No es solo un ejercicio de UI. Es un proyecto que ha vivido dos vidas: nació en JavaScript vanilla como ejercicio de lógica de juego, evolucionó hacia una aplicación full-stack con ranking global persistente, y más tarde lo reconstruí por completo en React para consolidar fundamentos del framework diseñando y desplegando, de nuevo desde cero, su propia API serverless independiente.
// 02 - EL RETO
- Lógica de juego no trivial.El revelado en cascada de celdas vacías exige un algoritmo de flood fill recursivo que calcule en tiempo real los corazones vecinos y determine cuándo se cumplen las condiciones de victoria o derrota.
- Persistencia real, no local.Un ranking en
localStorage se pierde al cambiar de dispositivo. Quería puntuaciones globales, siempre disponibles, sin montar un backend tradicional con servidor y base de datos gestionados.
// 03 - LA EVOLUCIÓN: de JS vanilla a React
v1 - JS vanilla (Buscacorazones)
La primera versión nació como ejercicio de lógica pura: generación de tablero, colocación aleatoria según dificultad, apretura en cascada, detección de victoria/derrota. Con el tiempo creció hacia una app full-stack real:
- Backend inicial con Supabase (BaaS) para el leaderboard global.
- Migración completa a una API propia:Cloudflare Worker + D1 (SQLite en el edge), con validación de payloads, CORS restringido a dominio propio y migraciones SQL versionadas.
- No fue un lift and shift: implicó diseñar el esquema de datos, escribir los endpoints REST
POST/GET /api/scores y desplegar infraestructura serverless real en producción.
- Frontend con Vite + Sass, y SweetAlert2 sustituyendo los
alert/prompt nativos.
Por qué migrar a React
No fue por necesidad técnica del proyecto, la versión JS ya funcionaba en producción con su propia API. Fue una decisión de aprendizaje deliberada: quería consolidar fundamentos de React (gestión de estado, lifting state, separación de lógica pura y efectos secundarios) construyendo desde cero, sin plantillas, y aplicando de nuevo lo aprendido sobre backends serverless para diseñar y desplegar una segunda API independiente, propia de esta versión.
v1 vs v2 - Comparativa
|
v1 - JS vanilla |
v2 - React 19 |
| Renderizado |
Manipulación directa del DOM |
Componentes declarativos + estado |
| Gestión de estado |
Variables globales / closures |
Lifting state hacia Game como fuente de verdad |
| Estilos |
Sass |
Sass (SCSS) + CSS custom properties inyectadas desde JSX |
| UX feedback |
SweetAlert2 |
Modal propio + animación de corazones al ganar |
| Backend |
Supabase migrado a Cloudflare Workers + D1 (API propia) |
Cloudflare Workers (segunda API propia, independiente de la de v1) |
| Punto fuerte |
Control total, cero dependencias de framework, demuestra fundamentos puros de JS |
Código más mantenible y escalable, lógica de juego desacoplada de la UI |
| Punto débil |
Lógica y renderizado más entrelazados, refactors más costosos |
Curva de configuración inicial de React/JSX/build tooling
|
// 04 - DEMO
Prueba las dos versiones
// 05 - SOLUCIÓN TÉCNICA
Frontend (React19 + Vite)
- Tablero configurable (8x8 / 16x16) con tres niveles de dificultad.
- Flood fill recursivo separado en
expandirCelda (lógica pura) y revealCell (actualización de estado) testeable y razonable de forma independiente de React.
- Lifting state: el estado compartido (celdas abiertas, cronómetro, fin de partida, ranking) se eleva a Game que actúa como fuente de verdad.
- Cronómetro sincronizado con el ciclo de vida de la partida, modal con formulario controlado, y animación de corazones cayendo al ganar.
- Sass (SCSS) con variables y mixins, grid dinámico vía CSS custom properties inyectadas desde JSX.
Backend (Cloudflare Workers)
- API propia para el ranking:
POST /api/scores (guardar), GET /api/scores (consultar, filtrado por tamaño de tablero y dificultad).
- Consumo desacoplado en un módulo dedicado,
scoresApi.js.
- Ranking global, persistente, sin infraestructura que festionar, coste y complejidad mínimos frente a un backend tradicional.
// 06 - STACK
Tecnologías utilizadas.
v2 - React
- React 19
- Vite
- Sass (SCSS)
- Cloudflare Workers
- D1
- Vercel
v1 - JS
- JavaScript
- Vite
- Sass (SCSS)
- Cloudflare Workers
- D1
- Vercel
01
Estado compartido sin caos: lifting state bien pensado
En una app de tamaño mediano es fácil caer en prop drilling excesivo o en estado duplicado entre componentes. Elevé el estado compartido (celdas abiertas, cronómetro, fin de partida, ranking) al componente Game, que actúa como única fuente de verdad. El resultado: un flujo de datos predecible y componentes hijos que reciben solo lo que necesitan, sin acoplarse entre sí.
02
Separar lógica pura de efectos secundarios
El flood fill del tablero vive en una función pura expandirCelda completamente aislada de React; la actualización de estado vive en revealCell. Esta separación no es un detalle estético: significa que la lógica de juego se puede razonar, testear y depurar sin necesidad de montar componentes ni simular el ciclo de vida de React, algo que se nota especialmente cuando hay que localizar un bug de cálculo en el tablero.
03
Diseñar una API REST propia, no solo consumirla
Más allá de hacer fetch a un endpoint, diseñé el esquema de datos, los endpoints POST/GET /api/scores y las reglas de vvalidación de una API ligera desplegada en Cloudflare Workers. Desacoplé además la lógica de red del resto de la app en un módulo dedicado scoresApi.js, de forma que cambiar de backend en el futuro no debería tocar ni un componente de la interfaz.
04
Migrar arquitectura en producción sin romper nada
En la versión anterior del proyecto, sustituí un backend basado en Supabase por una API propia sobre Cloudflare Workers + D1, sin que el frontend que la consumía dejara de funcionar en ningún momento. No fue un simple cambio de proveedor: implicó rediseñar el esquema de datos, versionar migraciones SQL y justificar la decisión más allá de "hacer que funcione" evaluando trade-offs realies entre un BaaS y una solución serverless propia.
05
Algoritmia de juego aplicada a un problema real
Destrás de un juego aparentemente sencillo hay un algoritmo de flood fill recursivo que calcula en tiempo real los corazones vecinos de cada celda, genera el tablero de forma procedural según la dificultar elegida, y determina con precisión cuándo se cumplen las condiciones de victoria o derrota. Un buen ejemplo de cómo un problema clásico de algoritmia se traduce en código de producción.
06
Despliegue continuo, no solo código en un repositorio
El proyecto está desplegado en Vercel con integración directa desde el repositorio: cada git push actualiza la app en producción. Tener el hábito de desplegar desde el primer commit, en lugar de dejarlo para el final, permitió detectar problemas de producción pronto y tratar el despliegue como parte del desarrollo, no como un paso posterios.