Documentación técnica

Bule
Bule

Juego multijugador online de póker de dados con física 3D, invitaciones por push y modo bot IA.

85+
~4.500
Firebase Hosting
Railway (Node.js)
6 días activos
v0.1 · junio 2026

1. Resumen del proyecto

Bule Bule es una webapp multijugador en tiempo real que implementa el juego tradicional de póker de dados (caras AS, K, Q, J, 8, 7). Los jugadores se reúnen en salas con código, turnan para tirar dados, descartar y quedarse con la mejor mano posible. Quien pierde cada ronda rompe un palillo; sin palillos queda en capilla y una derrota más lo elimina. Un repóker (quintilla) libera al jugador de la partida.

Multijugador real

Salas con código de 4 caracteres. Hasta 10 jugadores. Lobby de espera con vista en tiempo real vía WebSocket.

Física 3D

Dados con Three.js y Rapier3D. Sombras PCF, rebotes y colisiones deterministas por semilla de tirada.

Bot IA

Modo 1 vs Bot. La IA evalúa la mano, decide qué dados guardar y cuándo plantarse según probabilidad.

Sistema de vidas

Palillos visuales por jugador. Capilla, liberación por quintilla y modo de juego limitado por rondas.

Push & invitaciones

Web Push VAPID para invitar amigos aunque la app esté cerrada. Búsqueda de usuarios por nombre.

Google OAuth

Login opcional con Google. Perfil editable (nombre, avatar). Los no registrados juegan como invitado.

Diseño sonoro

Cubilete, rebotes, descarte, palillo roto, música de lobby y partida. Todo sincronizado con animaciones.

Mobile-first PWA

Agitar el móvil para tirar, gestos táctiles para descartar dados. Service worker instalable.

Manos implementadas

ManoRangoDescripción
Quintilla75 iguales → libera al jugador
Póker64 iguales
Full53 + 2 iguales
Escalera45 valores consecutivos
Trío33 iguales
Dobles parejas22 pares distintos
Pareja12 iguales
Carta alta0Sin combinación

2. Arquitectura técnica

CLIENTE React 18 · Vite 5 Three.js + Rapier3D Socket.io-client Web Audio API @react-oauth/google Service Worker WS SERVIDOR Node.js · Express 4 Socket.io 4.7 web-push (VAPID) Estado en memoria gameLogic.js railway.json · Dockerfile INFRAESTRUCTURA Cloud · deploy manual Firebase Hosting (frontend) Railway (backend) GitHub (main branch) HTTPS automático CDN global

Arquitectura de tres capas

Stack completo

CapaTecnologíaVersiónRol
UI frameworkReact18.3Gestión de estado y render
BundlerVite5.4Build, HMR, dev server
3D renderThree.js0.184Escena WebGL, materiales, sombras PCFSoft
FísicaRapier3D (WASM)0.19Simulación de cuerpos rígidos de dados
Tiempo realSocket.io4.7Comunicación bidireccional cliente-servidor
Auth@react-oauth/google0.13OAuth 2.0 con Google
Backend HTTPExpress4.18API REST mínima + VAPID endpoint
Pushweb-push3.6Notificaciones push VAPID/WebPush
Hosting frontendFirebase Hosting—CDN global, HTTPS automático
Hosting backendRailway—Contenedor Node.js, auto-SSL

Flujo de comunicación WebSocket

CLIENTE SERVIDOR SALA create_room / join_room room_state (broadcast) TIRADA roll { keptIndices } room_state report_faces { faces } TURNO stand / continue_turn room_state BOT bot_ready room_state

Protocolo WebSocket cliente ↔ servidor

Estructura de directorios

bule-bule/
├── backend/
│   ├── server.js          # Lógica de servidor (659 líneas)
│   ├── gameLogic.js       # Evaluación y comparación de manos (51 líneas)
│   ├── Dockerfile
│   └── railway.json
└── frontend/
    ├── public/
    │   └── assets/        # Imágenes, audios (.mp3)
    └── src/
        ├── App.jsx            # Routing, música, auth (controlador raíz)
        ├── socket.js          # Singleton Socket.io
        ├── index.css          # Todos los estilos (1.544 líneas)
        └── components/
            ├── GameBoard.jsx          # Pantalla de juego principal
            ├── DiceRollerScene.jsx    # Escena 3D Three.js + Rapier
            ├── AnimacionNextPlayer.jsx
            ├── AnimacionPalilloRoto.jsx
            ├── WaitingRoom.jsx
            ├── RoomList.jsx / CreateRoom.jsx
            ├── UserSettings.jsx
            └── Die.jsx / AlaCaidaToast.jsx

3. Histórico de commits — línea de vida

5 junio 2026 — Día 1: Fundamentos y primer deploy
ecf1070featInitial commit — estructura base, lógica de juego (manos, comparación), servidor Socket.io, cliente React con dados 2D.
6e95c0f–1145085infraDeploy Railway — PORT env var, CORS producción, health check, URL frontend apuntando al backend.
d30910afeatNavbar persistente, scoreboard con 5 dados, preservación de posiciones al re-tirar.
8e46001–3c8b832featSonido sintético en tirada + shake-to-roll en móvil. Primer prototipo jugable end-to-end.
8 junio 2026 — Día 2: Multiplayer completo + rama 3D mergeada
046d7d8featGoogle OAuth — login con Google, perfil con avatar.
d4cf675featSistema de salas — lobby, listado de partidas públicas, creación con nombre y código.
2c53366featAjustes de usuario — pantalla de perfil, edición de nombre, gestión de notificaciones.
a08e34bfeatBot IA — modo 1 vs Bot con lógica de decisión serverside (keeps, stand-off).
086052f–ea601d7featGestos táctiles — swipe y tap para descartar dados. Mínimo 1 descarte para re-tirar.
5f4d086–46bd548featWeb Push + invitaciones — VAPID auto-generado, búsqueda de amigos, invitación en tiempo real y por push offline.
60fba58featMerge rama 3D — primera versión con Three.js + Rapier física en el dice-box.
9 junio 2026 — Día 3: Física 3D completa + polish visual
f196beb–440cb77featDados 3D con Rapier — física real, gravedad, rebotes, detección de asentamiento. Caras generadas por canvas procedural. Layout 2+3.
143ecbbfeatOrientación física — los dados se alinean al slot con la cara correcta arriba (calculada por cuaternión).
5ee7db7featCaras físicas — report_faces sincroniza la cara visible con el servidor.
92660eefeatImagen de fondo A Coruña, fade-in inicial, corrección caras K/Q.
e44a65b–912a111featMúsica de lobby en bucle con pausa al entrar en partida y fade controlado.
e7f193bfeatZona de descarte 3D — dados descartados al fondo con borde rojo y opacidad 50%.
f71a87cfixDouble-fire touchend+click — preventDefault en touchend para móvil.
10 junio 2026 — Día 4: Vidas + sonido completo + sombras
1a9c09efeatSistema de palillos — 3 vidas por jugador, capilla, liberación por quintilla. Lógica backend + UI visual.
8712917featPausa entre turnos — overlay "jugada a superar", awaitContinue serverside, contador 30s.
1ae8507featAnimación palillo roto — overlay al final de cada ronda con efecto visual.
9a687cc–c61273cfeatAnimación cambio de jugador — dos imágenes ("ÉS TU TURNO" / "ÉS EL TURNO DE"), nombre + palillos. Botón solo al activo.
79c2081featBot auto-continúa en 3s — BOT_CONTINUE_TIMEOUT separado del humano (30s).
c589639featSonido cubilete — la física espera al evento ended del mp3.
de29e9cfeatSincronización eliminar_dados — fade α→0 de la duración exacta del mp3. Luego cubilete → física.
82e55c4featSonido de botones — listener global, excluye "Tirar dados".
774e8b7featMúsica de partida (Dice Lemonlight) — volumen 0.2, en bucle.
21832b2featSombras 3D — PCFSoftShadowMap, directional light con frustum ajustado, ShadowMaterial en suelo.
740eaffperfFísica +15% — world.timestep × 1.15. Ghost colliders fijos para dados quietos durante nueva tirada.

4.1 Backend — server.js · gameLogic.js

Estado global

rooms{} — diccionario en memoria indexado por código de sala. Cada sala es un objeto mutable con jugadores, fase de juego, índice del jugador activo, timers y metadatos de ronda.

registeredUsers{} — usuarios OAuth en sesión. Mapea userId → { name, email, picture, socketId, pushSubscription }.

Error potencial: Todo el estado vive en memoria. Un reinicio de Railway (deploy, crash, sleep) destruye todas las salas activas y las suscripciones push.

gameLogic.js — Evaluación de manos

Módulo puro sin efectos secundarios. Exporta rollDie(), evaluateHand(dice[]) y compareHands(h1, h2).

Atención: rollDie() usa Math.random() serverside. La semilla (rollSeed) es solo para animación visual. Si report_faces diverge, la mano mostrada puede no coincidir con la evaluada.
lobby start_game playing ronda fin results next_round playing alguien pierde finished

Ciclo de vida de una sala

Gestión de turnos — timers

TimerDuraciónAcción al expirar
turnTimerId30sSi no ha tirado: tira automático. Si ya tiró: planta.
continueTimerId30s (human) / 3s (bot)Llama a proceedTurn, arrancando el turno.
Error potencial: Los timers usan setTimeout único por sala. Una reconexión con ID diferente durante el turno puede ejecutar finishTurn sobre el jugador equivocado si el índice se modificó mientras tanto.

Desconexión de jugadores

El jugador desconectado permanece en sala 20 segundos para permitir reconexiones. Si era el host, el siguiente jugador hereda el rol.

Error potencial: Durante esos 20s puede ser el currentPlayer. Si le toca tirar, el turno queda bloqueado hasta que expire el turnTimer (hasta 30s adicionales). Total: hasta 50s de partida congelada sin notificación.

Bot IA — protocolo de sincronización

botPhase 'rolled' cliente anima 3D bot_ready botPhase 'picking' cliente muestra descartes bot_ready botPhase null

Fases del bot — sincronización servidor ↔ cliente

Atención: Si el cliente nunca emite bot_ready (fallo de render, tab en segundo plano), el bot queda bloqueado indefinidamente. Hay un fallback de 8s en el cliente, pero no existe timeout serverside para botPhase.

report_faces — superficie de ataque

Tras cada tirada, el cliente reporta las caras físicas visibles al servidor. El servidor valida que sean 5 valores del conjunto válido.

Error potencial: Un cliente modificado podría enviar la mano que quisiera (p.ej. siempre Quintilla). No existe verificación de coherencia con la semilla ni con los dados generados serverside para esa tirada.

Push Notifications — VAPID

Si no se configuran VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY, el servidor genera nuevas claves en cada inicio.

Error potencial: Cada reinicio/redeploy de Railway invalida todas las suscripciones push existentes (fallarán silenciosamente con error 410). Es crítico persistir estas claves como env vars fijas en Railway.

CORS

Configurado con origin: '*' tanto en Express como en Socket.io.

Error potencial: CORS completamente abierto. Debería restringirse al dominio de Firebase Hosting.

Recursos no acotados

Error potencial: No hay límite de salas simultáneas ni de registeredUsers{}. Sin rate limiting en ningún evento Socket.io. Un uso abusivo puede agotar la memoria del proceso.

4.2 Frontend — Componentes y casos de uso

App.jsx — Controlador raíz

Gestiona la pantalla activa ('list' | 'create' | 'settings'), estado de usuario, música, service worker y reconexión por URL (?join=CODE). Singleton de socket importado de socket.js — una sola conexión por sesión.

  • Si llega room_state, muestra WaitingRoom o GameBoard según room.phase.
  • Dos instancias de Audio precargadas: musicRef (lobby) y gameMusicRef (partida).
Atención: La música solo arranca tras el primer gesto del usuario (política de autoplay). Al entrar directamente vía URL sin interacción previa, la música de partida puede no reproducirse.

GameBoard.jsx — Pantalla de juego

Componente principal. Gestiona pendingDiscards[], sceneValues / rollingIndices / rollSeed, botDiscards[], detección de nueva tirada, animaciones de cambio de jugador y palillo roto, shake-to-roll e intercepción de pull-to-refresh.

Atención: toggleDiscard actualiza estado local sin confirmación del servidor. Si la conexión cae justo antes de emitir roll, los descartes marcados localmente persisten hasta la reconexión.
Error potencial: El timer (turnDeadline) se calcula como diferencia entre Date.now() cliente y timestamp de servidor. Una desincronización de reloj puede mostrar 0s aunque el turno no haya expirado.

DiceRollerScene.jsx — Escena 3D (componente más complejo)

Gestiona el ciclo completo: sonidos → física → animación → callback.

  • Init (useEffect []) — renderer, cámara, luces, 5 meshes de dados. Rapier se carga de forma asíncrona (WASM).
  • rollWithSounds() — orquesta: exit fade + eliminar_dados.mp3 → cubilete.mp3 → física.
  • step() — loop de render: física, settle detection, placing tween, move tween, exit tween.
  • Ghost colliders — dados en fase idle reciben un cuerpo fijo temporal durante doRoll para que los dados nuevos reboten contra ellos.
Error potencial: Si el servidor emite la primera tirada antes de que Rapier termine de inicializar (WASM), se almacena en ctx.pendingRoll. Si el componente se desmontó mientras cargaba, la tirada se pierde silenciosamente.
Atención: La duración del fade se lee de _eliminarAudio.duration. Si el audio no ha cargado (primera tirada muy rápida), duration es NaN y se usa el fallback de 800ms, que puede no coincidir con el mp3 real.

Sistema de audio

ArchivoTriggerImplementación
dado1/2/3.mp3Rebote físico dadoRotación cíclica, volumen proporcional al impacto
cubilete.mp3Antes de lanzar dadosLa física espera al evento ended
eliminar_dados.mp3Fade-out de dadosDuración del mp3 = duración del tween α→0
cogerdado.mp3Marcar dado para descartarClick táctil/ratón + bot picking
romper_palillo.mp3Fin de ronda con perdedorSimultáneo con AnimacionPalilloRoto
button_press.mp3Cualquier botónListener global en document, excluye "Tirar dados"
bule-escaleira.mp3Pantalla de inicioLoop, pausa al entrar en partida
dice-lemonlight.mp3Durante la partidaLoop vol 0.2, para al volver al lobby

5. Roadmap y mejoras futuras

Estado actual: MVP funcional y jugable. Las áreas en rojo de las secciones anteriores son las prioridades inmediatas antes de cualquier crecimiento de usuarios.

Deuda técnica prioritaria

  1. Persistencia de estado — Migrar rooms{} y registeredUsers{} a Redis. Permite reiniciar el servidor sin perder partidas y escalar con Socket.io adapter.
  2. Validación serverside de caras — Verificar que las caras reportadas son consistentes con la semilla, o ignorar report_faces y calcular la orientación en servidor.
  3. CORS restringido — origin: 'https://bulebule.web.app' en Express y Socket.io.
  4. VAPID keys persistidas — Configurar como env vars fijas en Railway para no invalidar suscripciones en cada deploy.
  5. Rate limiting — Limitar eventos Socket.io por socket/IP (p.ej. máx. 10 roll/s).
  6. Notificación de desconexión — Informar a la sala cuando un jugador se desconecta.

Features pendientes

Juego

  • Chat en sala
  • Espectadores (solo lectura)
  • Revancha con todos los jugadores
  • Variantes de reglas configurables
  • Historial de manos de la partida
  • Estadísticas de jugador (W/L, manos)

Social

  • Lista de amigos persistente
  • Ranking global / tabla de líderes
  • Torneos bracket
  • Compartir resultado de partida
  • Avatares personalizados

Infraestructura

  • Base de datos (PostgreSQL)
  • Redis para estado + Socket.io adapter
  • CI/CD automático (GitHub Actions)
  • Monitoring y alertas (Sentry)
  • Tests unitarios (gameLogic) e integración
  • Logs estructurados (pino / winston)

Cliente

  • Modo offline (PWA completa)
  • App nativa iOS/Android (Capacitor)
  • Haptic feedback en móvil
  • Accesibilidad (ARIA, alto contraste)
  • i18n (castellano / galego / inglés)

Arquitectura escalable propuesta

Firebase Hosting HTTPS / WSS Load Balancer Node.js instancia 1 Node.js instancia 2 Redis rooms{} + Socket.io adapter PostgreSQL — users, stats, historial

Arquitectura propuesta para escalar a múltiples instancias

Tecnologías a evaluar

ÁreaOpción actualAlternativaMotivo
Estado servidorIn-memoryRedisPersistencia, multi-instancia
Base de datos—PostgreSQL + PrismaHistorial, stats, usuarios
Deploy backendRailwayFly.io / RenderMejor pricing a escala, regiones
AuthOAuth manualClerk / Auth.jsSesiones, múltiples proveedores
Tests—Vitest + PlaywrightUnit + E2E multijugador
Monitoring—Sentry + Uptime RobotErrores + disponibilidad
PhysicsRapier WASM—Rapier es ya la mejor opción en JS