feat: la semilla del dia, el mismo campo para todos los que jueguen hoy - #48
feat: la semilla del dia, el mismo campo para todos los que jueguen hoy#48leocagli wants to merge 5 commits into
Conversation
Reconstruido sobre el main de hoy. La version anterior entro en conflicto cuando se mergeo el PR #19, que saco el bloque del `fieldGate`: ahi nacia uno de los tres `new Field(...)` que esta rama sembraba. Ahora quedan dos y se siembran los dos. El campo se sembraba con el valor por defecto, asi que cada salida a la pradera era exactamente la misma pradera. Es la issue #3, y seguia viva en main: los `new Field(...)` no llevaban semilla. Ahora la semilla sale del numero de ledger de Stellar partido en bloques de un dia. Todos los que jueguen hoy caminan el mismo campo, con los mismos bichos en los mismos lugares. Manana es otro. Verificado: dos clientes independientes sacan seed 3310838056 para el dia 250, y esa semilla produce 28 bichos identicos. Por que la cadena y no un servidor: la semilla tiene que ser igual para todos, cambiar sola, y sobre todo que no la haya elegido nadie. Las dos primeras las da cualquier servidor. La tercera exige que el jugador pueda comprobarlo por su cuenta, y por eso sirve un contador publico que ni el dueno del juego puede mover. No pide billetera, ni fondos, ni transacciones. Es un GET. Sin internet cae a una semilla local derivada del jugador, que igual arregla la issue #3 para el que juega solo. La consulta no bloquea ningun cuadro y nadie la espera. @stellar/stellar-sdk no carga en Bare: pide TextDecoder y despues Event. Se usa @stellar/stellar-base empaquetado con --conditions=browser, que es lo que hace que @noble/curves deje de pedir node:crypto. Los tres globals que faltan los pone lib/stellar.js. Todo explicado en vendor/README.md. Ocho tests nuevos y ninguno toca la red: un test que pida la semilla de verdad se pone rojo cuando el RPC publico tiene un mal dia, y eso no prueba el juego sino el clima. main hoy 65 tests, 517 asserts con esto 73 tests, 532 asserts lint limpio y git diff --check limpio. Refs #3
| async dailySeed() { | ||
| try { | ||
| const l = await this.rpc('getLatestLedger') | ||
| const day = Math.floor(l.sequence / LEDGERS_PER_DAY) | ||
|
|
||
| // Dispersion. El indice del dia crece de a uno, y campos de dias vecinos | ||
| // saldrian casi calcados si se lo pasaramos crudo al generador. | ||
| let s = day >>> 0 | ||
| s = Math.imul(s ^ (s >>> 16), 2246822507) >>> 0 | ||
| s = Math.imul(s ^ (s >>> 13), 3266489909) >>> 0 | ||
| s = (s ^ (s >>> 16)) >>> 0 | ||
|
|
||
| this.day = day | ||
| this.seed = s | ||
| return { seed: s, day, sequence: l.sequence } |
There was a problem hiding this comment.
💡 Bug: dailySeed dereferences l.sequence without null guard
In dailySeed, this.rpc('getLatestLedger') can resolve to null (rpc returns r.body.result, which may be null/undefined), and the code immediately reads l.sequence. If the RPC returns a non-error body without a result, l is null and Math.floor(l.sequence / ...) throws — though it is caught and returns null, so behavior degrades gracefully. Consider guarding if (!l) return null for clarity and to avoid relying on the catch for a normal-ish response shape.
Guard against a missing ledger result.:
const l = await this.rpc('getLatestLedger')
if (!l || typeof l.sequence !== 'number') return null
const day = Math.floor(l.sequence / LEDGERS_PER_DAY)
Was this helpful? React with 👍 / 👎
| const dx = Math.sign(target.x - this.x) || -1 | ||
| const sx = this.x + dx * (BODY_HALF_WIDTH + 1) | ||
| for (const offset of [-2, -1, 0, 1, 2]) { | ||
| for (let step = 0; step <= Math.max(8, attack.reach * 2); step += 2) { |
There was a problem hiding this comment.
💡 Quality: barrido telegraph uses attack.reach*2 without Number coercion
Unlike the other telegraph branches which use Number(attack.reach) || 4, the barrido branch multiplies attack.reach * 2 directly. If a future sweep-type attack were defined without a reach (or a non-numeric one), this evaluates to NaN, Math.max(8, NaN) is NaN, and the loop step <= NaN never runs — silently dropping the warning cells for a damaging sweep. Current data defines reach:6 so it works today, but coercing keeps it robust: use Math.max(8, (Number(attack.reach) || 4) * 2).
Was this helpful? React with 👍 / 👎
El Coliseo ya existe como mapa propio y publica duelSpawns, arenaBounds y refereeSpawn. Esto no dibuja nada: se ocupa de quien va de que lado, donde vuelve cada uno al terminar, y que nadie se escape a las gradas mientras la pelea vive. Tres decisiones que ordenan el resto. Los lados se calculan, no se acuerdan. Dos jugadores sin servidor tienen que llegar al mismo reparto por su cuenta, y cualquier negociacion es un mensaje que se puede perder o contradecir. Comparar las dos identidades y que la menor sea oeste no necesita ningun mensaje: los dos hacen la misma cuenta y les da lo mismo. Hay un test que lo prueba con 72 pares. La vuelta se guarda al entrar. Un duelo termina porque alguien gano, porque se rindio, o porque se corto internet. El ultimo caso es el que manda el diseno: si el regreso dependiera de un mensaje de cierre, el que se desconecta quedaria varado en el Coliseo para siempre. Por eso `from` se guarda antes de salir y alcanza con tenerlo. Los tres motivos vuelven al mismo lugar, y terminar dos veces es inofensivo. Ninguna coordenada esta escrita aca. Todas salen de MAPS.coliseum y se devuelven copiadas, para que mover el arte no obligue a tocar la logica y para que nadie le corra los puntos al mapa sin querer. Hay un test que lo vigila. El punto 6 del traspaso sale gratis: la capa de presencia ya expone update(mapId, x, y) y others(mapId), asi que con los dos jugadores parados en el mapa `coliseum` el rival se replica sin codigo nuevo. Un test empezo rojo y el mapa tenia razon: los dos lados se ven asimetricos contra arenaBounds.center.x, que es 64 redondeado, pero el Coliseo mide 128 y su centro real cae en 63.5. Contra ese, 40 y 87 estan a 23.5 los dos. El test ahora mide contra el centro geometrico y quedo documentado por que. Este commit no toca game.js todavia: es la capa de sesion y sus pruebas. El cableado del recorrido completo va aparte para que se pueda revisar de a una cosa por vez. antes 65 tests, 515 asserts ahora 82 tests, 630 asserts (+17 tests, +115 asserts) lint limpio y git diff --check limpio.
| // PvP cooldowns share the visible game clock, but attacks never happen by | ||
| // themselves: every damaging action still comes from an ordered input. | ||
| if (this.duel && this.duel.active && this.duelCombat) this.duelCombat.tick() |
There was a problem hiding this comment.
💡 Quality: Duel cooldown ticks on wall-clock, risking cross-peer divergence
onTick() advances duelCombat.tick() on the local wall-clock game clock, decrementing both fighters' cooldownLeft. DuelCombat's documented determinism ("same moves, attacks and ticks produce the same winner") only holds if ticks are delivered in the same order/count to both peers. Since attacks are gated by cooldownLeft, two peers with different frame timing between attack inputs can disagree on whether an attack was on-cooldown and thus compute different winners — which would break the Soroban consensus/settlement the design relies on. This is latent because the network transport is still pending and current tests advance ticks explicitly. When wiring the P2P layer, treat tick advancement as part of the ordered transported input stream (or derive elapsed ticks from a shared logical clock) rather than driving it from onTick's wall clock.
Was this helpful? React with 👍 / 👎
| @@ -393,6 +477,10 @@ class Runa { | |||
| * it is dropped whole rather than retried. | |||
| */ | |||
| dropPresence() { | |||
There was a problem hiding this comment.
⚠️ Bug: Cancelling stale outgoing duel invite derefs null presence
When an outgoing duel invite is pending and the network drops, dropPresence() (lib/game.js:479-488) sets this.presence = null but never clears this.duelInvite or this.duelMessages. If the player then presses N/Esc, the out-invite branch calls this.presence.sendDuel('decline', ...) with no null guard (lib/game.js:1612-1620), throwing a TypeError. onKey is invoked from update() without a try/catch, so this crashes the render loop. The same unguarded this.presence.sendDuel exists in the busy 'challenge' branch of handleDuelMessage (lib/game.js:1230-1236), reachable when a queued challenge is processed after the drop. Fix: clear duelInvite/duelMessages in dropPresence() and/or guard the sendDuel calls with this.presence.
Clear pending invite and queued duel messages when presence is dropped, so no later handler dereferences a null presence.:
dropPresence() {
if (this.duelNetwork && this.duel && this.duelCombat) {
this.duelCombat.surrender(this.duelNetwork.rivalId)
this.finishDuel('desconexion', { broadcast: false })
}
this.presence = null
this.presenceStarted = false
this.online = false
this.arrivals = []
this.duelInvite = null
this.duelMessages = []
}
Was this helpful? React with 👍 / 👎
| this.presence.sendDuel('result', network.rivalId, { | ||
| duelId: network.duelId, | ||
| winner: result.winner, | ||
| loser: result.loser, | ||
| reason: result.reason | ||
| }) | ||
| } |
There was a problem hiding this comment.
💡 Quality: 'result' duel packet is broadcast but never handled
finishDuel broadcasts a result duel message (lib/game.js:1410-1416) and result is a valid DUEL_KIND, but handleDuelMessage has no branch for it (lib/game.js:1216-1291) — it falls through and returns false. Both peers already end the duel deterministically via the ordered step replay, so the packet is effectively dead traffic today. Either wire it up (for the intended contract/settlement path) or drop the broadcast to avoid confusion about which side is authoritative.
Was this helpful? React with 👍 / 👎
Code Review
|
| Auto-apply | Compact |
|
|
Important
Your trial ends in 6 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.
Was this helpful? React with 👍 / 👎 | Gitar
|
Cierro este porque su contenido ya está integrado en el trabajo local de Este PR ya era el reemplazo del #37, que se rompió cuando entró el #19 y desapareció el La rama |
Reemplaza al #37, que entro en conflicto cuando se mergeo el #19: ese PR saco el bloque del
fieldGate, y ahi nacia uno de los tresnew Field(...)que esta rama sembraba. Ahora quedan dos enlib/game.jsy se siembran los dos.Verificado contra el main de hoy (
263e53a): 73 tests / 532 aserciones, lint limpio,git diff --checklimpio. La base de main es 65 y 517.Primero de tres. Este trae la semilla del dia; el jefe del mundo y los duelos
necesitan contratos y van aparte.
Que cambia para el que juega
Hoy el campo se sembraba con el valor por defecto en los tres lugares donde nace
un
Field, asi que cada salida a la pradera era exactamente la misma pradera.Eso es la issue #3.
Ahora la semilla sale del numero de ledger de Stellar partido en bloques de un
dia. Todos los que jueguen hoy caminan el mismo campo, con los mismos bichos
en los mismos lugares. Manana es otro.
Lo que se compara entre jugadores deja de ser oro suelto y pasa a ser cuanto
aguantaste vos en el campo de hoy.
Por que la cadena y no un servidor
La semilla tiene que cumplir tres cosas: ser igual para todos, cambiar sola, y
que no la haya elegido nadie. Las dos primeras las da cualquier servidor. La
tercera exige que vos puedas comprobarlo por tu cuenta, y por eso sirve un
contador publico que ni el dueno del juego puede mover.
Lo que NO pide
Ni billetera, ni fondos, ni transacciones. Es un
GET. Alguien que nunca oyohablar de cripto lo juega sin enterarse.
Sin internet cae a una semilla local derivada del jugador, que igual arregla la
issue #3 para el que juega solo. La consulta no bloquea ningun cuadro y nadie la
espera.
Verificado
Dos clientes independientes, sin coordinarse:
Y esa semilla llega al mundo:
La parte tecnica que costo
@stellar/stellar-sdkno carga en Bare: pideTextDecoder, y despuesEvent, porque trae su propio cliente HTTP pensado para navegador o Node.Lo que si funciona es
@stellar/stellar-baseempaquetado con--conditions=browser. Esa bandera es la clave: hace que@noble/curveselijasu camino de WebCrypto en vez del que hace
require('node:crypto'), que es elmodulo que Bare no tiene. Faltan tres globals mas y los pone
lib/stellar.js.Todo esto queda explicado en
vendor/README.md, incluido por que el bundle estacommiteado:
npm run makearma binarios para seis plataformas, y quien clonatiene que poder jugar sin pasos extra. Se regenera con
npm run vendor:stellar.Sobre los tests
Ocho nuevos, y ninguno toca la red. Un test que pida la semilla de verdad se
pone rojo cuando el RPC publico tiene un mal dia, y eso no prueba el juego sino
el clima. El RPC se reemplaza por una funcion que devuelve el ledger que
queramos, y se prueba lo que decide runa: como se mezcla la semilla, que dos
momentos del mismo dia den el mismo campo, que dias vecinos den campos bien
distintos y no casi calcados, y que si la cadena no contesta no se rompa nada.
Un detalle que elegi a conciencia
Se usa el numero de ledger y no su hash. El hash del ledger que abrio el dia
seria impredecible, que suena mejor, pero para leerlo hay que pedirle al RPC un
ledger de hace 24 horas y eso cae justo en el borde de lo que el RPC conserva.
Un jugador lo conseguiria y otro no, y entonces no caminarian el mismo campo,
que era todo el punto.
Entre impredecible y que todos coincidan, coincidir gana: el mapa del dia se
comparte igual apenas el primero lo publique.
Refs #3