Construyendo un clon web de Balatro con React - Rommel Romero
←Volver al blog
Proyecto completo

Construyendo un clon web de Balatro con React

Cómo construí un juego web inspirado en Balatro con React, TypeScript y Vite, separando las reglas del dominio del estado y la interfaz.

ReactTypeScriptViteReact RouterVideojuegos

Crear un juego de cartas no consiste únicamente en mostrar cartas en pantalla. Hay que modelar el mazo, las manos, los turnos, la puntuación, las tiendas y todos los estados intermedios que hacen que una partida pueda continuar sin romperse.

Por eso construí Balatro Web Clone, un juego web inspirado en Balatro y desarrollado con React, TypeScript y Vite. El objetivo fue reproducir el flujo principal de una partida y, al mismo tiempo, practicar cómo separar una lógica de dominio compleja de los componentes visuales.

El flujo de una partida

La aplicación sigue el flujo principal del juego:

Menú
  ↓
Elegir baraja
  ↓
Seleccionar blind
  ↓
Jugar manos o descartar cartas
  ↓
Superar el objetivo
  ↓
Comprar jokers en la tienda
  ↓
Avanzar al siguiente blind

Cada fase tiene acciones y reglas diferentes. El jugador puede seleccionar una baraja, jugar manos de póker para conseguir chips y multiplicador, comprar jokers y avanzar por los distintos niveles de dificultad.

Separar el estado de la interfaz

El estado completo de la partida se centraliza en el hook useGameState. Los componentes reciben el estado que necesitan y callbacks como playHand, discardCards, buyJoker o advanceToNextBlind.

Esto evita que cada pantalla tenga que conocer cómo se modifica internamente la partida. Por ejemplo, RoundPanel no calcula por su cuenta qué ocurre al jugar una mano: comunica la acción al hook y recibe el nuevo estado cuando React vuelve a renderizar.

La separación también facilita añadir nuevas fases o reglas sin convertir los componentes visuales en bloques de lógica difícil de mantener.

Las reglas viven en módulos independientes

La lógica del juego está separada en varios módulos:

La fórmula principal de puntuación es:

(chips base de la mano + chips de las cartas) × multiplicador

Los jokers pueden modificar los chips, añadir multiplicador o aplicar multiplicadores adicionales. Mantener estas reglas fuera de la interfaz permite probarlas y modificarlas sin depender de cómo se dibuja cada pantalla.

Un estado persistente

Una partida puede durar bastante, así que añadí persistencia con localStorage. El módulo storage/localStorage.ts guarda y recupera el estado serializado de la partida.

El hook solo guarda durante fases reales de juego: blindSelect, playing y shop. No tiene sentido persistir el menú o una pantalla finalizada, y limitar las fases evita recuperar estados incompletos o irrelevantes.

Al volver a montar la pantalla de juego, la aplicación intenta cargar la partida guardada. Si no existe, crea un estado inicial y comienza desde el menú.

Evitar repetir bosses demasiado pronto

Un detalle que necesitaba algo más de cuidado era la selección de bosses. En lugar de elegirlos completamente al azar en cada nivel, la partida mantiene una lista serializable de bosses pendientes.

Cuando aparece un boss se elimina de esa lista. Cuando todos han aparecido, la lista vuelve a barajarse. De esta forma no se repite un boss hasta completar el ciclo y el estado se puede guardar junto al resto de la partida.

Lo que me llevo del proyecto

El reto principal no fue construir una pantalla concreta, sino coordinar muchas reglas pequeñas sin perder el control del estado. El proyecto me sirvió para practicar:

El juego está disponible en la demo web y el código completo en GitHub.


← Más artículos