Capítulo servidor de la guía Neblla — Server (tu room.js en un servidor de verdad), Fog (el servidor que vive en los dispositivos de los usuarios), No-code (herramientas de servidor que corren en Neblla), estado, hora común, anonimato y anti-cheat. El contrato base está en core.txt; el criterio de cuándo usar Neblla, en empezar.txt. # El servidor Tu servidor son los ficheros de `server/` (`lobby.js`, `gathering.js`, `room.js`). Neblla te da tres piezas que trabajan juntas, siempre las tres a la vez: - **Server:** tu `room.js` en un servidor de verdad: en la nube de Neblla o en el tuyo (abajo). Para lo que no debe correr en el dispositivo de ningún usuario. - **Fog:** tu `room.js` corre en los dispositivos de los usuarios. Neblla elige dónde, coordina y juzga. No hay servidor que montar ni pagar. - **No-code:** herramientas de servidor que corren en Neblla y no necesitan código: una baraja, un dado, sobres cerrados, zonas, contadores, límites de puntuación. Tu `room.js` las llama para lo que tiene que ser verdad aunque el dispositivo que ejecuta `room.js` sea de un usuario. El reparto es este: **Neblla guarda lo poco que tiene que ser verdad (el azar, los secretos, la hora, el veredicto) y los dispositivos hacen el trabajo.** Fog es lo que usa una app por defecto, gratis. Cuando una sala necesita un servidor que no sea de nadie —un aforo que no se puede pasar, un ranking de un juego de un jugador que no se puede falsear, una arena de muchos jugadores—, ese mismo `room.js` pasa a Server sin cambiar de código. ## Qué necesitas según tu juego Neblla trae mucho, pero cada juego usa poco. Empieza por tu caso y deja el resto para cuando lo necesites: - **Un juego casual entre amigos** (dibujar, adivinar, preguntas): `room.js` con `state.set` y `message.to('server', …)`. Nada más. - **Con información oculta** (roles, cartas, respuestas): añade la baraja y los sobres de No-code. - **Competitivo, con algo en juego** (rankings, premios): añade testigos, el azar y la hora del servicio (abajo) y, si hay roles secretos, el anonimato. - **De acción** (movimiento en tiempo real): pon la ventana a unos 200 ms y, si el mapa es grande, zonas. - **Un juego para uno con ranking** (un Wordle, reflejos, un test): sin sala, `neblla.scores` (ver identidad.txt). La partida empieza en Neblla y lo imposible no entra en el ranking. - **Lo que no debe correr en ningún jugador** (un aforo, una arena grande): Server. Lo demás de este capítulo existe para cuando lo necesites; no hace falta leerlo todo para empezar. ## Fog: el servidor que vive en los usuarios Fog es un servicio con sus propias reglas. Lo que hace por ti: - **Elige dónde corre tu `room.js`:** en tres dispositivos de la sala a la vez. Uno manda y dos le siguen. Los elige por latencia y CPU, y los reordena durante la sesión. - **Relevo sin cortes:** si el que manda se va (pestaña cerrada, túnel, batería), uno de los que le siguen ya tiene el estado al día y sigue en el acto, con sus timers. - **Compara a los tres:** los Fog se pasan entre ellos una huella de cada estado. Si no coinciden, Neblla compara los estados: gana la mayoría, el que no coincide se resincroniza en silencio y se marca a su usuario. El caso queda en tu panel, Personas › Moderación (y en `neblla_list_fog_cases`) con lo que difería y quiénes eran los Fog. Si para ti fue un bug y no una trampa, lo anulas y la marca se va; si fue un ataque de los otros dos, les pasas la marca a ellos (`neblla_decide_fog_case`). - **Hora común y orden:** todos los dispositivos comparten una hora, y los mensajes se aplican en el mismo orden en todos (abajo). - **Quórum de dos:** todo lo que destapa un secreto en No-code necesita que lo pidan dos Fog con la misma llamada. Un Fog trucado, solo, no consigue nada y queda marcado. - **Juzga Neblla, nunca un usuario:** los testigos, las huellas y los flags los resuelve el servidor de Neblla. Con menos de tres dispositivos en la sala, corre en los que haya. Lo que viaja entre dispositivos va **P2P y no gasta cuota**. Su identidad es opaca: no hay `user.isServer` ni nada que diga qué dispositivo es. Nunca escribas código que inspeccione quién es el servidor: rutea con `message.to('server', …)`. ### En el móvil: pantalla bloqueada, app en segundo plano, red que se corta - **Pantalla bloqueada o app en segundo plano.** El navegador frena los timers de una página oculta. El SDK avisa a Neblla en cuanto la página se oculta: si ese dispositivo llevaba la sala, uno de los Fog visibles sigue en el acto, con el estado al día; si era uno de los que seguían, le cede el sitio a un usuario con la pantalla encendida. Mientras haya dispositivos visibles, uno oculto no se elige como Fog. Si todos están ocultos, el que manda sigue, más lento, y al volver a primer plano todo continúa. - **El móvil pierde la conexión** (túnel, ascensor, batería). Sin red no puede avisar: Neblla lo nota cuando el socket deja de responder, como mucho en unos 45 s, y entonces el relevo es inmediato. Mientras tanto la sala espera al que manda. El usuario sigue siendo de la sala 3 minutos (12 s si llevaba la sala); si vuelve dentro de ese plazo, sigue en ella. - **Redes que cortan el P2P** (algunas wifi de empresa o de operadoras). Lo que no llega P2P a un dispositivo se lo reenvía Neblla, y ese reenvío gasta cuota (ver publicar.txt). Con el relé TURN encendido (`neblla_update_app({ p2p: { turn: true } })`), el P2P pasa por él y también cuesta cuota. Un dispositivo así funciona igual, pero rara vez es el mejor Fog: Neblla elige a los Fog por latencia. ## Las reglas de `room.js` Tres reglas, siempre: 1. **Sin DOM.** `window`, `document`, `globalThis` y `self` están sombreados en `server/`. La UI vive en `client.js`. 2. **Sin "yo".** No existen `neblla.auth` ni `neblla.room.me` en `server/`. 3. **Solo eventos.** Cliente y servidor se hablan con `message.to('server', …)`, `message.on` y `state`. **El azar y la hora: tú decides.** Con `Math.random()` y `Date.now()` tu `room.js` funciona, gratis; pero cada Fog saca su propio número y su propia hora, así que los que siguen al que manda pueden ir algo distintos, y en un relevo el siguiente sigue desde el último estado publicado. Para un juego entre amigos, suficiente. Si quieres que los tres Fog coincidan exactamente (comparar huellas, anti-trampas, relevo sin un solo salto), usa **`neblla.tools.random`** (o la baraja), que cuesta cuota por llamada, y **`neblla.room.now()`**, la hora común de la sala, que es gratis. **Los timers funcionan.** `setTimeout` y `setInterval` sirven dentro de `room.js`, y un timer es lo único que hace avanzar la sala cuando nadie actúa. Cuando salta en el Fog que manda, el SDK lo reproduce en los otros dos en el mismo punto. **Si tus reglas usan trigonometría** (`Math.sin`, `Math.atan2`…), un iPhone y un Android pueden diferir en el último decimal. Para decisiones que dependen de eso, redondea antes de comparar, o lleva la sala a Server, donde corre en una sola máquina. **Arrancar con la partida en marcha.** El código de arriba de `room.js` no corre solo al empezar: corre otra vez en cada dispositivo que pasa a ser Fog con la sala ya en marcha (un relevo, otra réplica, uno que Neblla resincroniza), con `neblla.state.get()` tal como está en ese momento. Arriba, lee el estado y apunta tus handlers; llama a herramientas solo para empezar la partida (sin estado) o para terminar un paso que quedó a medias (por ejemplo, «si el estado está en `dealing`, crea el mazo»). Neblla hace que eso sea seguro: un Fog que arranca mientras otros ya corren el fichero no sabe en qué punto van, así que sus llamadas No-code (y `callApi`) esperan a que uno de los que ya corren haga la del mismo número. Si es la misma, recibe la misma respuesta y desde ahí va al paso; si es otra (los demás ya habían pasado ese punto), la sala sigue como si nada y Neblla arranca otra vez ese Fog desde el estado y el número de llamada actuales de la sala. Si no queda nadie corriendo el fichero, sigue solo: es él quien termina el paso. Una llamada que no casa no vuelve nunca, ni con error, porque el código que venía detrás es de un arranque que se descarta; si los demás no la hacen en unos segundos (no están en ese paso), tampoco vuelve, y la consola del dispositivo lo avisa (`fog_unpaired`). ```javascript // ❌ Mal — room.js toca el DOM: document.getElementById('counter').textContent = state.turno; // ✅ Bien — room.js solo cambia estado, el cliente reacciona: // room.js neblla.state.set({ ...state, turno: state.turno + 1 }); // client.js neblla.state.on(state => { document.getElementById('counter').textContent = state.turno; }); ``` ## La hora común y el orden de los mensajes Cada mensaje que un usuario manda al servidor lleva un número (el 1, el 2, el 3… de ese usuario) y la hora común en que lo mandó. Los tres Fog los aplican en orden de hora, así que todos llegan al mismo estado aunque los mensajes les lleguen en distinto orden. - **Provisional y definitivo.** Un mensaje es provisional hasta que llega el siguiente del mismo usuario o pasa la **ventana**: 1 segundo por defecto. Pasada la ventana, es definitivo. - **Si llega tarde uno anterior,** Fog rebobina al último estado bueno y vuelve a aplicar todo en orden (rollback). Tu `room.js` no tiene que hacer nada: el SDK vuelve a ejecutar tus handlers. - **Si un mensaje se pierde,** Fog lo pide a los otros Fog. Si nadie lo tiene, es lag de ese usuario: su mensaje se aplica cuando llega, no en su hora. - **Una hora imposible** (más antigua de lo que permite la latencia real de ese usuario) se corrige a la más cercana posible. Si se repite, flag. La ventana se cambia por app con `neblla_update_app({ fog: { window: 200 } })`, y por sala en el `config` de `rooms.book`. Un juego de acción quiere unos 200 ms; uno por turnos puede dejar el segundo. **Dos flujos de eventos en el cliente.** El estado y los mensajes del servidor llegan de dos formas, y tú eliges cuál escuchas: ```javascript // Al momento. Puede rebobinarse: tu front tiene que reflejar el estado nuevo cuando llegue. neblla.state.on(state => render(state)); // Solo lo definitivo. Nunca se rebobina: para lo que no se puede deshacer ("has ganado"). neblla.state.on(state => anunciar(state), { final: true }); ``` Lo mismo con lo que manda el servidor en `neblla.message.on`. Lo visual suele ir al momento; lo importante, definitivo. ## Estado — state.set `neblla.state.set(state)` **(SERVER)** emite el estado a todos los dispositivos de la sala. Viaja **P2P y no gasta cuota**; solo hacia un dispositivo que no conecta P2P lo reenvía Neblla, y ese reenvío cuenta (ver publicar.txt). Cada dispositivo comprueba que viene de un Fog legítimo. ```javascript // room.js let state = neblla.state.get() || { tablero: initialBoard(), turno: 0 }; neblla.message.on(m => { if (m.to !== 'server') return; if (esLegal(state, m.data)) { state.tablero = aplicar(state.tablero, m.data); state.turno++; neblla.state.set(state); } }); ``` ```javascript // client.js neblla.state.on(state => render(state)); console.log(neblla.state.get()); // siempre el último valor ``` Usa `state.set`, no `message.send`, para el estado: ```javascript // ❌ Mal — mensaje genérico: neblla.message.send({ type: 'state', state: newState }); // ✅ Bien — primitivo de estado: neblla.state.set({ tablero, turno }); ``` **El estado es público:** llega a todos. Lo que no deben ver los usuarios (las respuestas correctas, el mazo, lo que ha contestado cada uno) no va en el estado: va en un **sobre** de No-code (abajo). Un usuario que pierde la conexión conserva su sesión unos **tres minutos**: si vuelve dentro de esa ventana reentra en la misma sala, con sus datos, como miembro normal. ## Lo que manda el servidor: a toda la sala o a uno Un `message.send(data)` del servidor llega a todos los clientes por `message.on`, con `from: 'server'`. Para un solo usuario, `message.to(userId, data)`: el rol de cada jugador o su mano de cartas le llegan por privado. ## message.to('server'), no broadcast filtrado ```javascript // ❌ Mal — broadcast con filtro manual: neblla.message.send({ type: 'move', square: 'e4', forServer: true }); // ✅ Bien — destino dedicado: // cliente neblla.message.to('server', { move: 'e4' }); // room.js neblla.message.on(m => { if (m.to !== 'server') return; // los mensajes para el servidor traen to: 'server' }); ``` `message.to('server', …)` no se difunde al resto de clientes: llega solo al servidor de la sala. ## Anonimato: el servidor que no sabe quién es quién En juegos con rol oculto (quién es el lobo, quién tiene qué carta) el problema es que el servidor corre en el dispositivo de un jugador. Con `await neblla.tools.anonymize()` **(SERVER)**, nadie lo sabe, ni el Fog: - **Cada jugador recibe un alias** que solo conoce él (en su cliente le llega con `neblla.room.on('alias', ({ alias }) => …)`). Es un código largo al azar, nuevo en cada partida. - **`room.js` solo ve alias.** `m.from` (y `m.user.id`) es el alias. Tus reglas funcionan igual: "el alias X es el lobo" basta para aplicarlas. - **Lo que manda el servidor va por alias.** `message.to(alias, data)` llega solo a ese jugador, sin que el servidor sepa quién es ni los demás lo vean. Va por Neblla, que lo reenvía tal cual: no está cifrado de extremo a extremo. - **Lo que un jugador tiene que decir en público** ("he muerto"): `await neblla.tools.announce(alias, data, { within: 5000 })` **(SERVER)**. Neblla se lo pide solo a ese jugador, y su móvil lo dice en público con su nombre, tras una pausa al azar de hasta 1,5 s para que nadie lo empareje con lo que pasa dentro del servidor. Si no lo dice a tiempo, lo dice Neblla por él y marca la sala. Todos lo reciben en `neblla.room.on('announce', ({ user, data, by }) => …)`, con `by: 'player' | 'neblla'`, y la llamada devuelve `{ by }`. `within` va de 1 a 30 s (5 por defecto) y `data` hasta 4 KB. Sin anonimato responde `not_anonymized`; con un alias que no está en la sala, `not_in_room`. - **Cuando un jugador nombra a otro en secreto,** usa `neblla.tools.seal(userId)` **(CLIENT)**: `neblla.message.to('server', { mata: neblla.tools.seal('bob') })`. El nombre viaja cerrado, y el propio Bob lo cambia por su alias por el camino. El servidor recibe el alias, nunca el nombre. Si Bob no lo hace, Neblla lo traduce (con quórum de dos Fog) y lo marca. - **El origen también se oculta:** los mensajes a un servidor anónimo rebotan por otros jugadores antes de llegar, así que nadie sabe quién escribió cada uno. Lo hace el SDK. - **Desenmascarar,** cuando el juego lo hace público (una votación elimina a Ana y hay que saber si era la loba): `await neblla.tools.unmask(userId)` **(SERVER)** devuelve su alias. Necesita quórum de dos Fog. Con menos de tres jugadores no se anonimiza: si el mensaje no es tuyo, es del otro. ## No-code: herramientas que corren en Neblla Viven en Neblla, por sala, y se llaman por nombre con `await`, desde `room.js` o desde el cliente según sus permisos. Reglas comunes: - **Con nombre:** puedes tener varias (una baraja de roles y otra de avatares). Sobreviven al relevo: el siguiente Fog las encuentra por su nombre. - **Los tres Fog hacen la misma llamada y reciben la misma respuesta:** Neblla las empareja por su número y el efecto ocurre una vez. Un Fog que arranca con la partida en marcha casa las suyas con las de los que ya corren (arriba, «Arrancar con la partida en marcha»). - **Permisos por papel:** `fog` (el servidor), `player` (cualquier usuario de la sala) y `owner` (el usuario al que se le asignó). Cada herramienta dice qué puede hacer cada uno. - **Destapar un secreto necesita dos Fog** (quórum). - **Coste:** cada llamada cuenta como un mensaje que entra a Neblla (ver publicar.txt). Son pocos bytes y solo en los momentos clave. ### Baraja: quién tiene qué Sacar sin repetir: cartas, roles, el orden de las preguntas. ```javascript // room.js — repartir roles sin que el servidor sepa quién tiene cuál await neblla.tools.deck.create('roles', ['lobo', 'lobo', 'vidente', 'medico', 'aldeano', 'aldeano'], { perUser: 1, fromUser: true, secret: true }); await neblla.tools.deck.shuffle('roles'); // no devuelve el orden neblla.message.send({ fase: 'roba' }); // client.js const rol = await neblla.tools.deck.draw('roles'); // robar otra vez devuelve la misma carta neblla.message.send({ visto: true }); // público: "esperando a…" // room.js, cuando todos han dicho que lo vieron const roles = await neblla.tools.deck.holders('roles'); // { alias: carta }, de golpe ``` - `perUser`: cuántas cartas por jugador; repetir `draw` devuelve las mismas (los reintentos por lag no hacen nada). - `fromUser: true`: los jugadores roban directamente, sin pasar por el servidor. - `secret: true`: ni el servidor puede descargar quién tiene qué; solo `holders`, de golpe, cuando lo pides. - `type: 'cards'` rellena una baraja estándar (`jokers`, `decks`). - **Pilas con permisos**, para juegos de cartas: `stacks: { mano: { fog: 'add|assign', owner: 'draw|muck' }, mesa: { fog: 'add|draw' } }`. `add(pila, n).assign(userId)` da cartas a un jugador; `muck(pila)` las tira (ya no las enseña nadie, nunca); `show(pila, ...userIds)` las enseña al final, con quórum de dos Fog, solo de quien las enseñó en público. ### Una baraja de cada jugador: duelos con sus cartas En un juego de cartas coleccionables cada jugador trae su mazo, hecho de sus filas de una tabla que solo escribe Neblla (identidad.txt). Neblla comprueba que son suyas y las baraja en una pila secreta de cada uno: ```javascript // room.js await neblla.tools.deck.create('duelo', [], { fromRows: { table: 'coleccion', min: 10, max: 20 } }); await neblla.tools.deck.deal('duelo', userId, 5); // la mano inicial: solo la ve ese jugador // client.js await neblla.tools.deck.bring('duelo', idsElegidos); // tus filas; los demás oyen cuántas, no cuáles const robadas = await neblla.tools.deck.draw('duelo', 1); // a tu mano; los demás oyen «robó 1» const { hand, pile, played } = await neblla.tools.deck.hand('duelo'); await neblla.tools.deck.play('duelo', robadas[0].id); // ahora sí la ve toda la sala const cuantas = await neblla.tools.deck.counts('duelo'); // { usuario: { pile, hand, played } }, para todos ``` - **Traer** pide login y solo filas propias de una tabla con `write: 'neblla'`, una vez, sin repetir y entre `min` y `max` (hasta 200). Errores: `not_owned` (con `id`), `table_not_read_only`, `repeated_card`, `invalid_deck_size` (con `min` y `max`), `already_brought`, `invalid_from_rows`. - **Nadie más ve una pila:** ni el rival ni el servidor. `draw` anuncia el número; `play` enseña la carta (`not_in_hand` si no la tiene); `deal`, solo desde el servidor, manda las cartas a ese jugador y a nadie más (evento `dealt`). Pila vacía: `deck_empty`. Sin traer: `not_brought`. - Los eventos llegan por `neblla.tools.on('event', fn)` (`{ op, user, n }`, o `card` al jugar), también al fichero de servidor: así decide la ronda con la carta que Neblla anuncia, sin ver nunca una pila. Las filas no cambian: la partida no gasta las cartas. ### Azar: el dado Sacar con repetición. Cada tirada la anuncia Neblla a toda la sala: si el servidor tira más veces de las que tocaba, todos lo ven y se marca. ```javascript await neblla.tools.random.create('dado', { min: 1, max: 6, decimals: 0, dices: 2 }); const [a, b] = await neblla.tools.random.roll('dado'); ``` Permisos: `fog`, `player`, `owner` (`roll`, `read`, `assign` para dar el dado a un jugador). **El reto del día.** Con `same: 'daily'`, la tirada es la misma para todos los jugadores de la app durante el día, y mañana otra. Nadie puede adivinar la de mañana, así que sirve de semilla para un nivel diario igual para todos; con `neblla.tools.counter` y `reset: 'daily'` limitas los intentos. ```javascript await neblla.tools.random.create('reto', { min: 0, max: 1e9, same: 'daily' }); const semilla = await neblla.tools.random.roll('reto'); // la misma para todos hoy ``` ### Sobres: quién sabe qué, y cuándo Cualquier JSON, cerrado hasta que se cumple una condición. Dos modos: - **`mode: 'p2p'`** (gratis): cada usuario cierra su sobre en su dispositivo (la llave no sale de él) y lo manda cerrado a través de Neblla, que apunta la hora a la que llega; Neblla no puede leerlo. Al abrirlo, los usuarios entregan su llave. **Un sobre sin llave, o con una llave que no sirve, no cuenta.** Para lo que mandan los usuarios: respuestas, apuestas, votos secretos. - **`mode: 'nocode'`:** la llave la tiene Neblla y abre cuando se cumple la condición. Para lo que no puede depender de ningún usuario. Un sobre que solo abre `fog` es la memoria privada de tu servidor. ```javascript await neblla.tools.envelope.create('r1', { mode: 'p2p', perUser: true, owner: 'seal', fog: 'open' }); await neblla.tools.envelope.seal('r1', 'C'); // client.js const respuestas = await neblla.tools.envelope.open('r1'); // room.js → { userId: { value, time } } ``` ### Zonas: quién está con quién Cada jugador solo recibe a los que tiene cerca, y el servidor no sabe dónde está nadie. Las posiciones exactas viajan P2P solo entre los de la misma zona. ```javascript await neblla.tools.zones.create('nave', { type: 'graph', graph: { cocina: ['cafeteria', 'baño'], cafeteria: ['cocina', 'oficina'], motores: [{ to: 'reactor', requires: 'impostor' }], // paso solo para quien tiene esa carta } }); await neblla.tools.zones.create('mundo', { type: '2d', vision: 24, release: 'onEmpty' }); // client.js const z = await neblla.tools.zones.move('mundo', { x, y }); // → { alias, members, chunks? } neblla.message.send({ x, y }, { zone: 'mundo' }); // P2P solo a los de tu zona y las vecinas ``` - **Tipos:** `graph` (salas con un mapa de vecinos), `1d` (una carrera), `2d`, `3d`. - **Saltar a una zona que no es vecina se bloquea**, en los cuatro tipos. - **El servidor ve el alias de la zona, no cuál es:** las coordenadas solo las sabe Neblla. Cuando una zona se vacía, cambia de alias, así que nadie reconstruye el mapa observando. - **En `1d`, `2d` y `3d`,** `vision` es hasta dónde ve cada jugador: ves tu celda y las vecinas, y tu juego enseña solo lo que entra en el radio. Para mundos grandes, Neblla agrupa las celdas en niveles de zoom (como los chunks de Minecraft; cada nivel junta 16×16 del de abajo) a medida que alguien entra. Cuántos: con `players` (los jugadores que esperas), Neblla los calcula: hasta 16 jugadores, 1 nivel; con 500, 3; sin `players`, 2. `levels` (de 1 a 6) los fija a mano. Con más de un nivel, `move` devuelve también `chunks`: el alias de cada chunk en el que estás, de arriba abajo, que comparten los que están dentro y cambia cuando se vacía. **`precision: true`** usa celdas de media visión: ves las dos de alrededor (5, 25 o 125 celdas) y el margen del tramposo baja; puedes moverte hasta dos celdas de golpe. Con `release: 'onEmpty'`, lo que se vacía se libera: solo existe lo que alguien está usando. ### Contador y puntuación ```javascript await neblla.tools.counter.create('intentos', { max: 3, per: 'user', reset: 'daily' }); const r = await neblla.tools.counter.add('intentos'); // → { value: 2 }, o { ok: false } si pasa del máximo await neblla.tools.score.create('nivel3', { max: 15000, maxPerSecond: 500 }); await neblla.tools.score.submit('nivel3', 12000); // por encima: rechazado y flag ``` - `counter`: intentos, vidas, partidas al día, existencias. `reset`: `'match'`, `'daily'` o nunca. Por defecto solo el Fog suma y los usuarios leen; para que el cliente sume, créalo con `player: 'add|read'`. - `score`: valida lo que se envía a un ranking desde una sala. `maxPerSecond` pilla al que hace en diez segundos lo que cuesta un minuto. Un juego para uno, sin sala, tiene `neblla.scores` (ver identidad.txt): la partida empieza en Neblla y su puntuación se comprueba antes de entrar en el ranking. ## Ejemplo: un quiz en clase La profe proyecta las preguntas; los alumnos contestan desde el móvil; acertar más rápido da más puntos. Las respuestas correctas solo las tiene el dispositivo de la profe, y las de los alumnos van en sobres, así que nadie ve nada antes de tiempo, tampoco el alumno cuyo móvil hace de servidor. ```javascript // server/room.js const PREGUNTA_MS = 20000; let state = neblla.state.get() || { fase: 'idle', q: null, inicio: 0, puntos: {} }; async function preguntar(q) { // q llega de la profe, sin la correcta await neblla.tools.envelope.create('r' + q.n, { mode: 'p2p', perUser: true, owner: 'seal', fog: 'open' }); state = { ...state, fase: 'preguntando', q, inicio: neblla.room.now() }; neblla.state.set(state); setTimeout(cerrar, PREGUNTA_MS); } async function cerrar() { const respuestas = await neblla.tools.envelope.open('r' + state.q.n); // sin llave, no cuenta state = { ...state, fase: 'corrigiendo', respuestas }; neblla.state.set(state); } function puntuar(correcta) { for (const [id, r] of Object.entries(state.respuestas)) { if (r.value !== correcta) continue; const rapidez = 1 - (r.time - state.inicio) / PREGUNTA_MS / 2; // hora común del sobre state.puntos[id] = (state.puntos[id] || 0) + Math.round(1000 * rapidez); } state = { ...state, fase: 'revelando', correcta }; neblla.state.set(state); } neblla.message.on(m => { if (m.to !== 'server' || !m.user?.isPresenter) return; if (m.data.pregunta) preguntar(m.data.pregunta); if (m.data.correcta !== undefined && state.fase === 'corrigiendo') puntuar(m.data.correcta); }); // Si los tres Fog se fueran a la vez, el siguiente arranca con neblla.state.get(): if (state.fase === 'preguntando') setTimeout(cerrar, Math.max(0, state.inicio + PREGUNTA_MS - neblla.room.now())); ``` ```javascript // client.js — alumno const contestar = opcion => neblla.tools.envelope.seal('r' + neblla.state.get().q.n, opcion); // client.js — pantalla de la profe: las correctas viven solo aquí neblla.state.on(s => { if (s.fase === 'corrigiendo') neblla.message.to('server', { correcta: correctas[s.q.n] }); }, { final: true }); ``` La profe manda la correcta solo cuando ya están todas las llaves, así que nadie la ve antes de entregar la suya. Si su pantalla se cae, la partida espera y sigue al volver: los puntos están a salvo en los Fog. ## Testigos y flags Es el módulo `anticheat`: se enciende con `neblla_update_app` y `features: { anticheat: true }`, y **juzga Neblla, nunca un jugador**. Cada cliente dice con `anticheat.confirm` lo que ha visto de un evento crítico, y Neblla comprueba que todos han visto lo mismo. **`neblla.anticheat.confirm(eventName, value)`** **(CLIENT)** — Neblla recoge los reportes durante unos 3 segundos y comprueba: - ¿todos los valores son estrictamente iguales (`===`)? - ¿reportó al menos el 75% de la sala? Si las dos respuestas son sí, silencio. Si alguna falla, Neblla marca la sala, igual que un `neblla.anticheat.flag()`. El `value` tiene que ser primitivo (string, number finito, boolean). No vale objeto, array, `null`, `undefined` ni `NaN` — cada afirmación debe ser un dato inequívoco que la sala pueda votar. ```javascript // client.js neblla.state.on(state => { if (state.ganador) neblla.anticheat.confirm('ganador-final', state.ganador); }, { final: true }); ``` ```javascript // ❌ Mal — pasa un objeto: neblla.anticheat.confirm('estado', { tablero, turno }); // el SDK lo rechaza // ✅ Bien — afirmaciones inequívocas: neblla.anticheat.confirm('ganador', userId); neblla.anticheat.confirm('score-final', 1250); ``` **`neblla.anticheat.flag(reason?)`** **(SERVER)** — marca la sala como sospechosa. Lo llaman solos los testigos que fallan, las horas imposibles, las tiradas de más y los quórums incompletos; también puedes llamarlo tú desde el archivo de servidor. Un cliente no puede: un jugador marcaría a todos los de su sala. Una vez marcada, los siguientes `anticheat.flag()` en la misma sala son no-ops (anti-troll). El contador `flags` sube en todos los usuarios de la sala (logueados lo persisten, anónimos en sesión) y se ve en el backoffice, donde se banea sin redeploy. Evento `room.on('flagged')` `{ room, reason, flaggerId, userCount }`, una vez por sala. ## Protecciones siempre activas - **Datos privados aislados.** `neblla.db.data` es por usuario y vive en su sesión. Nunca llega a otros clientes ni a los Fog. Lo público va por `publicInfo`. - **Pagos mediados.** `neblla.shop.buy()` abre un checkout de Stripe alojado por Neblla. `room.js` solo ve el resultado, nunca datos de tarjeta ni la capacidad de falsificar compras. - **Backoffice con bandera.** El dashboard muestra datos, mensajes, actividad y `flags`. Desde ahí se banea. ## Módulos: pagas por lo que usas La base va siempre y es gratis: elegir los tres Fog, el relevo, el estado P2P y la hora común. El resto se enciende solo si lo usas: ``` Pieza Se enciende con Coste ────────────────────────────────────── ────────────────────────────────── ───────────────────────────── Orden y rollback fog: { window } (por defecto, 1 s) en los dispositivos; sin cuota Vigilancia de horas imposibles fog: { latencyWatch: true } poca cuota Testigos features: { anticheat: true } por cada confirm Anonimato neblla.tools.anonymize() por partida, y las traducciones que hace Neblla Herramientas No-code usarlas por llamada (ver publicar.txt) ``` Un quiz usa sobres y testigos; un juego de acción, una ventana de 200 ms; un juego de dibujo entre amigos, solo la base. ## Server: tu `room.js` en un servidor de verdad El mismo `room.js`, con el mismo SDK de servidor (`neblla.message`, `neblla.state`, `neblla.room`, `neblla.tools`…), corriendo en un servidor que no es de ningún usuario. Los usuarios siguen conectados a Neblla como siempre: tu código de cliente no cambia. Dos sitios: **1. En la nube de Neblla** (`serverMode: 'cloud'`). **Cada app tiene una caja de 128 MB** en Cloudflare (Workers y Durable Objects), cerca de los usuarios, con todas sus salas dentro. Para texto y lógica (de juego o de app) sobra. **Dormida no cuesta nada;** se despierta sola cuando una sala necesita su servidor y vuelve a dormirse cuando no le queda ninguna sala abierta. **Despierta cuesta unos 100 créditos por hora** (unas diez horas al día con la cuota gratis de toda la cuenta, unas veinte con un proyecto Pro), y cada mensaje que procesa, 5 unidades (ver publicar.txt). Una sala abierta la mantiene despierta aunque nadie mande nada, así que cierra las salas que ya no se usan. Las reglas de la caja: - solo el SDK de servidor y JavaScript; sin ficheros, sin npm, sin `process`; - para llamar fuera, `neblla.api.call` con tus secretos; - todas las salas de la app comparten la caja y sus 128 MB; - **una sala pesada puede ir en su propia caja,** al mismo precio: `rooms.book({ config, dedicated: true })`; - **guardar un fichero de `server/` redespliega la caja** (la respuesta trae `cloud: { status: 'deployed', script, hash, files }`): las salas abiertas esperan un instante y siguen con la versión nueva. **El día compartido de la nube.** Hoy la nube de Neblla corre en el plan gratis de Cloudflare, y ese plan da un tiempo de caja despierta al día (unas 28 horas) **para todas las apps juntas**. Para que un bucle o una sala que no se cierra en una app no gaste el día de las demás, cada app tiene como mucho **4 horas de caja despierta al día** (su pro y su pre juntos; Neblla cuenta el tiempo que la caja está conectada, y la caja se desconecta unos segundos después de quedarse sin salas). La cuota gratis de la cuenta (1.000 créditos ≈ 10 horas al día, para todas sus apps) puede llegar si una sola app la gasta en su caja. Al pasar de su parte, **las salas de esa app dejan de ir a la nube hasta las 00:00 UTC**; las demás apps siguen. Y si un día se gasta el tiempo de toda la nube, pasa lo mismo con todas hasta las 00:00 UTC. En los dos casos la sala sigue en Fog con el mismo `room.js`, como cuando tu servidor se cae (la red de seguridad, abajo); con `server: { fallback: null }` espera a las 00:00 UTC y los usuarios no ejecutan tu `room.js`. Te llega un aviso de la app: `cloud_app_limit` (tu parte) o `cloud_day_full` (la de todos), que dice hasta cuándo; terminan solos a las 00:00 UTC, y entonces las salas que esperaban vuelven a su caja. **La nube llena.** Si al pasar a `cloud` (o al pasar a pro una app cuya caja aún no existe) la nube no tiene sitio para una caja más, la respuesta trae `cloud: { status: 'error', error: 'cloud_full', message }` y el aviso `cloud_full`. Neblla está avisado y hace sitio; mientras, las salas siguen en Fog (o esperan, con `fallback: null`). Vuelve a intentarlo más tarde (guardar un fichero de `server/` o volver a elegir `cloud`) o usa tu propio servidor. **2. En tu propio servidor (BYOS,** `serverMode: 'own'`). Tu `room.js` corre donde quieras —un VPS, AWS, tu ordenador, tu cuenta de Cloudflare— con el paquete `@neblla/server`, que **llama él a Neblla** con la clave de servidor de la app (`neblla_server_key`, solo en tu servidor). No abres puertos ni necesitas una URL pública: ``` npx @neblla/server --app --key $NEBLLA_SERVER_KEY ./server ``` El paquete trae los tipos del `neblla` de servidor: con `/// ` en la primera línea de `room.js`, el editor autocompleta y avisa de lo que un servidor no tiene. Y si prefieres arrancarlo desde tu propio código: `import { startServer } from '@neblla/server'`. Tu `room.js` corre en una caja aislada de Node con el mismo SDK de servidor que en la nube: sin `require`, `fs` ni `process`, y `fetch` solo llega a Neblla; para llamar fuera, `neblla.api.call` con tus secretos. Lo que ganas es el sitio: tu máquina o tu cuenta, sin las horas de la caja de Neblla. Solo cuesta el reenvío de los mensajes entre los usuarios y tu servidor, como cualquier mensaje que pasa por Neblla. Con tu cuenta de Cloudflare, el dev pega el token en el panel (Conexiones, tema «Tu servidor»: `neblla_projects` con `cloudflare_setup` da el enlace, `panelUrl`; **nunca en el chat**), que lo comprueba y lo guarda cifrado como conexión del proyecto (la de la cuenta si la app está en Explorer); después `neblla_byos({ appId, action: 'set', provider: 'cloudflare' })` pone la app en `own` y despliega la caja en tu cuenta (en tu `workers.dev`); guardar un fichero de `server/` la redespliega, y Cloudflare te lo factura a ti. El token de Cloudflare es uno para todas las cajas de la cuenta, o del proyecto que ponga el suyo, y es el mismo de los demás temas (ficheros, dominio, correo). Si la app se mueve a un proyecto con otro Cloudflare, su caja se redespliega allí. `status` nunca devuelve el token y `remove` quita el Worker (el token sigue conectado para las demás apps). Errores: `cloudflare_not_connected` (no hay token en el proyecto ni en la cuenta: viene con `panelUrl`) y `workers_subdomain_missing` (activa `workers.dev` una vez en tu cuenta). **Mientras tu servidor no está,** tu proceso recibe `needServer` para conectarse, y la sala sigue en Fog (la red de seguridad, abajo); si la apagas, la sala espera y los usuarios no ejecutan tu `room.js`. Tu servidor no ocupa plaza en la sala, no aparece en su lista y nunca es presentador. **Red de seguridad incluida, encendida por defecto.** Si tu servidor se cae la sala pasa sola a Fog con el mismo `room.js` y sigue, sin que los usuarios lo noten; cuando tu servidor vuelve, la sala vuelve a él. Si prefieres que la sala espere a tu servidor, apágala con `neblla_update_app({ server: { fallback: null } })`. **Escribe `room.js` con las reglas de Fog** (azar de `neblla.tools`, hora de `neblla.room.now()`) aunque lo pienses correr en Server: así puede pasar de un sitio a otro, y el relevo a Fog funciona. La hora común, el orden de los mensajes y el rollback funcionan igual en los tres sitios: van en el SDK de servidor, no en el transporte. ## Dos cosas que conviene saber - **Con dos jugadores no hay quórum de dos Fog,** y el servidor corre en el dispositivo del rival. Lo que el rival no debe ver va en sobres y en barajas con `secret: true`, nunca en el estado; y si mirar importa de verdad, Server. - **Lo que ningún sistema evita:** dos amigos que se cuentan las cartas por teléfono, o un bot que juega demasiado bien. Eso no rompe ninguna regla del servidor.