OPENU5 Portada EN

Estas páginas se generan a partir de la documentación interna del proyecto. Las referencias a ficheros de código y a offsets del binario se conservan como procedencia —para que se vea de dónde sale cada afirmación—, pero todavía no se pueden seguir: el motor aún no está publicado. Cuando lo esté, pasarán a ser enlaces.

1 · Mejoras de imagen

Todo lo que cambia lo que se ve: el suavizado, las dos pieles, la transparencia de sprites y tiles, la tipografía, y las líneas y contornos.

Etiquetas: 🎯 FIDELIDAD (el «antes» era un fallo nuestro) · ✨ AÑADIDO (el original no lo hacía) · ⚖️ DIVERGENCIA DECLARADA · 🗑️ RETIRADO. Ver README.


Índice

  1. Las dos pieles: «1988» y «smooth»
  2. El suavizado: xBR en la GPU
  3. Transparencia I — el cuerpo a alpha .55
  4. Transparencia II — el recorte de contorno
  5. La regla que costó dos capturas del usuario: «nunca fondo de pared»
  6. Tiles: el cadáver que era desierto 🎯
  7. Tipografía I — los iconos de equipo salían de la fuente equivocada 🎯
  8. Tipografía II — el cuerpo de los letreros: latín o runas ⚖️
  9. Tipografía III — la fuente del panel, dos maquetas descartadas
  10. Líneas y contornos I — los remates de la banda de scroll 🎯
  11. Líneas y contornos II — la flecha que era un rombo partido 🎯
  12. El zodíaco transpuesto 🎯

1 · Las dos pieles: «1988» y «smooth»

AÑADIDO · Código: game/src/skin/fiel/skin.ts, game/src/skin/shader/skin.ts, game/src/skin/manager.ts · Cambio en vivo con F9 o desde ⚙ → Vídeo.

Antes

El original tiene una sola presentación: EGA de 16 colores, 320×200, píxeles gordos. No hay nada que elegir.

Ahora

Hay dos pieles intercambiables en caliente, y una es un calco de la otra:

Por qué importa

Que la shader aloje a la fiel en vez de duplicarla es la decisión que sostiene todo lo demás: el reloj de animación, los overlays de combate, la moongate, el terremoto, el modal de Ztats y el audio siguen siendo los de la fiel. Cuando se arregla un defecto en la fiel, la shader lo hereda sin tocar una línea. El calco de 1988 no se bifurca nunca.

Y la elección de filtrar sólo el visor no es pereza: el texto y el chrome viven fuera de ese rectángulo, así que el filtro no puede tocarlos. Un upscaler pasado sobre la fuente 8×8 la habría convertido en papilla.

Contrapartida


2 · El suavizado: xBR en la GPU

AÑADIDO · Código: game/src/skin/shader/xbr-gl.ts, upscaler.ts

Antes

Vecino más cercano. Un píxel del original es un cuadrado de N×N píxeles de pantalla. Es lo que hace la piel «1988», y es lo correcto para ella.

Ahora

El visor pasa por xBR nivel 1 con la regla de detección de bordes de Hyllian (diferencia ponderada en YUV, pesos 48/7/6), portada a GLSL y ejecutada en WebGL1. El pipeline encadena dos pases de 2× (= 4× interno) y deja el estirón 4→6 al blit bilineal.

Por qué importa

Un filtro de bordes no «desenfoca»: reconstruye las diagonales que la retícula de 16 píxeles no podía representar. La medición del carril smooth-partido-c sobre el layout partido dio 12,8× más colores distintos en el visor que la piel fiel (lote-c-smooth-partido, 28-07-2026) — el filtro está trabajando, no adornando.

Contrapartidas, y son varias

  1. Licencia: el kernel es un port propio del extractor/src/upscale/xbr.ts del proyecto, no el .glsl GPL de RetroArch que recomendaba la spec. La desviación está declarada en la cabecera de xbr-gl.ts y quedó reportada al carril de publicación.
  2. Sin WebGL no hay filtro: existe un NearestUpscaler de degradado que sube a 6× por vecino. La piel shader sigue montada y conmutable, pero se ve idéntica a la fiel. Es un degradado silencioso: nada avisa al jugador.
  3. Lo recortado pierde el suavizado. Los sprites que se recomponen desde el atlas EGA (§3 y §4) se blitean con vecino, así que el sprite recortado no lleva xBR. Está anotado como caveat heredado en docs/verdicts/contour-transp/README.md, no como regresión.
  4. Backbuffer colapsado si el anfitrión mide 0×0. Alojada dentro de otro layout sin tamaño, la shader calcula ceil(320/320) = 1 y se queda en 320×200: cero suavizado, sin error. Diagnosticado en diagnostico-smooth-portrait y resuelto en el lote C. Es el modo de fallo más traicionero de todos, porque el juego funciona perfectamente y sólo está mal la única cosa por la que se eligió esta piel.

3 · Transparencia I — el cuerpo a alpha .55

AÑADIDO · Piel shader · Kill-switch ?transp=off · Actas: docs/verdicts/sprites-transparencia/candidatos.md (censo), docs/verdicts/transp-wire/README.md (1ª ola), docs/verdicts/transp-wave2/README.md (2ª ola)

Antes

Nada en el port aplicaba alpha < 1 al cuerpo de un tile o de un actor. Los actores ya tenían el fondo negro recortado, pero el cuerpo era sólido: un fantasma tapaba el suelo igual que un troll.

El censo primero, el cableado después

Antes de tocar un píxel se hizo un censo de candidatos sobre los 529 tiles de TileData.json, con impacto / coherencia / coste y —la columna que decide— si la referencia original ya lo insinúa (DOS) o si es puro añadido estético (Licencia). La maqueta A/B se renderizó del atlas HD real antes de elegir:

Candidatos a transparencia — A opaco / B alpha .55 / C glass-tint

El usuario eligió la columna B (alpha .55 del cuerpo), y eso es lo cableado.

Ahora — 1ª ola

FamiliaTilesVía
Force fields (Poison/Magic/Fire/Electric)488-491terreno: suelo sintetizado + cuerpo a .55
Fantasmas412-415actor: ActorTransparency.bodyAlpha
Wisps468-471actor
Shadowlords508-511actor

Y en la 2ª ola, el boundary del shadowlord (112-127, como «bruma») y la llama azul (222).

🗑️ Lo que este apartado enseñaba, y por qué ya no

Aquí había una comparación A/B de la moongate translúcida. Se ha retirado, porque el efecto ya no existe.

La moongate se cableó en la 1ª ola y el usuario la mandó quitar el 22-07-2026 («El moongate no debe tener transparencia dentro», con dos capturas). Está en el código, en el sitio exacto donde antes se pintaba:

// MOONGATE: RETIRADA del censo de translucidez por VEREDICTO DEL USUARIO

La puerta se queda con el pintado opaco de la capa fiel. El tile 220 sigue en isCensusTerrainTile, pero sólo como exclusión de suelo-donante para los fields vecinos.

Cómo se cazó, que es lo reutilizable: capturando las dos ramas del A/B —por defecto y ?transp=off— y comparando el sha. Salieron byte-idénticas. Un kill-switch que no cambia ni un byte significa que la cosa que apaga ya no está. No hizo falta leer código para sospecharlo; sólo para confirmarlo.

Las capturas originales siguen en su carpeta de veredicto: son ciertas de su fecha, y son el acta de un carril. Lo que no puede hacerse es citarlas en presente.

Por qué importa

Es el uso canónico de la translucidez: un fantasma que se ve sólido es un fantasma que no da miedo. Pero la decisión relevante no es esa, sino la de al lado —

Contrapartidas y descartes


4 · Transparencia II — el recorte de contorno

AÑADIDO · Piel shader · Kill-switch ?contourTransp=off · Actas: docs/verdicts/contour-transp/README.md, docs/verdicts/contour-fix/README.md

Es el otro tratamiento, y no es el mismo que el anterior. Aquí no se baja el alpha del cuerpo: se sustituyen por transparencia los píxeles negros exteriores del sprite (los conectados al borde de la celda, por flood-fill), y los negros internos se conservan. El sprite sigue opaco; lo que desaparece es su cuadrado de fondo.

Antes / después — la fuente del castillo de Lord British

?contourTransp=offpor defecto
fuente con su cuadro negrofuente recortada

A la izquierda, el suelo de rejilla se corta en negro en las esquinas del tile. A la derecha, la rejilla continúa por las esquinas y sólo queda el agua y la basa.

El problema de fondo, y su solución

La fuente la hornea la piel fiel sin un hook por tile: no hay dónde engancharse. La salida fue sintetizar el suelo de debajo con dominantFloorNeighbor — de los cuatro vecinos ortogonales, el tile de suelo que más aparece, copiando sus píxeles ya renderizados (xBR-exactos) antes de blitear el recorte.

La regresión que apareció, y por qué es instructiva

Al cablear el recorte, la fuente dejó de animarse. terrainWindow lleva el tile crudo del mapa (frame base fijo, 0xd8); la fiel anima la fuente al dibujar, con animatedFrame(tile, phase, groups). El recorte blitaba el tile crudo, así que tapaba la fuente animada de la capa mundo con un frame estático. Un fix visual que congela lo que estaba vivo.

El arreglo: el recorte deriva el frame vivo con la misma fase de la fiel (FaithfulSkin.animPhase). Los cuatro frames, ya ciclando con el recorte activo:

Cuatro frames de la fuente ciclando

Y quedó una guarda de regresión medida (SAD entre frames distintos = 133 454, o sea ≠ 0) en game/tests/contour-transp.test.ts.

Extensión a los fuegos, que no se animan igual

Braseros (0xb2), antorchas (0xb0/0xb1), hogueras (0xb3), hogar (0xbc–0xbf) y llama azul (0xde) no ciclan su id: su titileo es ruido procedural fn32 sobre un canvas vivo. El recorte los trata distinto — sintetiza el suelo, y luego recorta el canvas vivo de la llama por la silueta estática del tile (destination-in), de modo que el parpadeo sobrevive y el fondo se corta.

?contourTransp=offpor defecto
brasero con cuadro negrobrasero recortado

Lo que NO se toca: su aportación de luz (lightLevel / visMask). Sólo el pintado. Un fix de presentación que hubiera movido la iluminación habría cambiado el juego.

Contrapartidas


5 · La regla que costó dos capturas del usuario: «nunca fondo de pared»

AÑADIDO (corrección de un añadido) · Código: game/src/render/floor-underlay.ts · Acta: docs/verdicts/transp-wave2/README.md

El «vecino dominante» de §4 tenía un defecto que sólo se ve mirando: si los vecinos de la celda eran paredes, sintetizaba pared como suelo, y el recorte pintaba ladrillo donde el jugador ve piedra.

Dos reglas, las dos con un testigo visual del usuario detrás:

  1. Las paredes no son candidatas a suelo. isFloorUnderlayCandidate: un candidato es suelo si es pisable a pie o navegable (hierba, ladrillo, madera, alfombra, puente, escaleras, lava, agua). Muros, piedra seca, puertas y ventanas quedan fuera. Con tres paredes y un suelo alrededor, gana el suelo: las paredes ni cuentan. Todo pared ⇒ null ⇒ celda intacta. Jamás pared de fondo.
  2. Los fuegos montados en pared no se recortan. Los apliques izquierdo/derecho (0xb0/0xb1) y el hogar (0xbc) ya llevan la pared en su gráfico; recortarlos pintaba ladrillo detrás. Se dejan con su celda entera horneada, y el titileo lo sigue poniendo la fiel. Los fuegos de suelo (brasero 0xb2, hoguera 0xb3, farola 0xbd, llama azul 0xde) sí se recortan. Los ambiguos (0xbe vela sobre mesa, 0xbf cocina) se excluyeron por el mismo principio: el fondo debe ser lo que realmente hay detrás.

Verificación — interior del keep «Calma»

?contourTransp=offcon el fix
Calma, contorno apagadoCalma, contorno arreglado

El brasero se funde con el ladrillo (no con la piedra blanca del borde) y los apliques quedan idénticos entre las dos capturas, porque están excluidos del recorte.

Por qué importa fuera de su carril

dominantFloorNeighbor es un helper compartido por fuente, braseros, fields, moongate y boundary. Arreglarlo aquí corrigió también lo ya aterrizado del carril anterior. Un bug en una función compartida no tiene un dueño: los tiene a todos.

Contrapartida: la mejora queda deliberadamente DESIGUAL

El precio de la regla 2 es que dos fuegos contiguos se ven distintos. En una sala con un brasero de suelo y un aplique de pared, el brasero se funde con el ladrillo y el aplique conserva su cuadro horneado. A ojos de quien no conoce la razón, parece que el efecto «falla a veces».

Se aceptó a sabiendas, y es la elección correcta: la alternativa —recortar también los fuegos de pared— era peor y además falsa, porque pintaba ladrillo donde el jugador ve piedra. Entre un efecto uniforme que miente y uno desigual que dice la verdad sobre lo que hay detrás, se eligió el segundo. Cuando el fondo real es la pared, la respuesta correcta es no recortar.


6 · Tiles: el cadáver que era desierto

🎯 FIDELIDAD · Código: game/src/skin/coreview.ts (arenaLoot) · Acta: docs/verdicts/corpse-tile/README.md

Antes

En la celda de un troll recién muerto sobre un puente no salía el cadáver, sino «una mezcla de hierba con transparencia»: motas verdes y amarillas sobre los tablones.

La causa

COMBAT:0x1574 guarda en la tabla de objetos de la arena el byte bajo del resto: cadáver 0x1E, sangre 0x1F, cofre 0x01. El render los blitea desde el banco alto, con +0x100. arenaLoot pintaba el byte crudo, sin ese offset — así que salían sus gemelos del banco bajo:

bytebanco bajo (el bug)banco alto (lo fiel)
0x1ELeftDesert2 (desierto)0x11E DeadBody (cadáver)
0x1FRightDesert2 (desierto)0x11F Splat (sangre)
0x01Water1 (agua)0x101 Chest (cofre)

Tiles de desierto —motas verdes sobre negro— pintados como entidad con fondo transparente sobre tablones. Exactamente lo que el usuario describió.

Antes / después

Banco bajo vs banco alto

Fila superior, el banco bajo (el bug); fila inferior, el banco alto.

Por qué importa

Es el género de defecto que no parece un defecto: parece un efecto raro. El testigo no dijo «falta el cadáver», dijo «sale un desierto con transparencia» — y ese es precisamente el síntoma que produce un offset de 0x100 perdido. Sin la descripción literal del usuario, la búsqueda habría empezado en el sitio equivocado.

Contrapartida: el arreglo es de tres bytes, no de la familia

lootTiles() conserva su contrato de bytes bajos porque lo consumen los tests y open_chest: el +0x100 se añade sólo en el punto de pintado. Es lo correcto —cambiar el contrato habría roto a los otros consumidores— pero deja la asimetría viva: hay dos convenciones de banco en juego y hay que saber en cuál se está.

Y el arreglo no cubre la familia entera: el gargoyle (0x4C) sigue diferido, porque es otra capa (terreno del mapa, no objeto de la tabla de la arena) y no se soluciona con el mismo offset. Está anotado como diferido en el acta, no como resuelto.


7 · Tipografía I — los iconos de equipo salían de la fuente equivocada

🎯 FIDELIDAD · Código: game/src/skin/fiel/skin.ts (drawReadyPicker) · Acta: docs/verdicts/font-glyphs/verdict.md

El reporte

«los iconos de fuente de la plantilla NO son los del juego, hay que revisar eso bien, tenemos algún error»

Lo que NO estaba roto (descartado byte a byte)

Antes de tocar nada se verificó lo obvio, y lo obvio estaba bien:

El bug real

El original imprime el glifo de clase de cada ítem equipado como carácter de atributo desde la fuente rúnica, no desde IBM. La banda de control de RUNES.CH lleva dingbats de equipo (casco, escudo, coraza, armas, anillo); la de IBM.CH lleva triángulos y piezas de caja. El port escribía el código en una celda que la piel rasteriza con IBM ⇒ los 48/48 ítems renderizaban un glifo distinto del que muestra el DOS.

El corolario lo confirma: 0x09 (Thrwng Axe) es un glifo vacío en IBM.CH —de ahí el «BLANK!» que había desconcertado a un carril anterior— pero tiene glifo real en RUNES.CH. Ningún código de la tabla es vacío en la fuente correcta.

Y lo que sigue en IBM, verificado

Las flechas de scroll del picker (0x18/0x19/0x12) sí son IBM en el DOS real: en la referencia es un ↓ limpio = IBM.CH[0x19]. El marco del pergamino y el texto de la fila también. El fix usa el highlightCols que layoutReadyPicker ya calculaba y que se ignoraba, para dibujar sólo las celdas del glifo de clase desde el atlas rúnico.

Contrapartida: bajo la piel smooth, estos glifos no se suavizan

La piel shader hereda el fix por el blit nearest de la base fiel, no repintándolo. O sea: los iconos de equipo salen ya correctos, pero sin vectorizar — al contrario que las flechas de scroll de §11, que sí se repintan en vectorial. En la misma pantalla conviven un elemento suave y otro de píxel duro.

Es la elección barata y es defendible (un dingbat de 8×8 vectorizado a mano por 30 códigos es mucho trabajo para un icono diminuto), pero es una desigualdad real de la piel smooth, no una casualidad.

🔴 Nota de estado del material

El veredicto citaba una imagen comparativa.png (columna roja = IBM, verde = RUNES) que nunca llegó a commitearse: la carpeta contiene sólo verdict.md. La adjudicación no depende de ella —se sostiene en el careo byte a byte de arriba—, pero la referencia estaba rota. Queda anotada en el propio veredicto (04-08-2026) para que nadie vuelva a buscarla.


8 · Tipografía II — el cuerpo de los letreros: latín o runas

🎯 FIDELIDAD · Código: game/src/skin/fiel/sign-box.ts (layoutSignBox, runicBodyCells)

Antes (latín)Ahora, por defecto (runas)
letrero en latínletrero en runas

El DOS real pinta el cuerpo de los carteles en runas, con los dígrafos colapsados (TH=0x5b, EA=0x5e, ST=0x5f, NG=0x5d, EE=0x5c) — por eso «NORTH», «EAST» y «TRINSIC» salen más compactos que en latín. El port lo hacía en latín; ahora lo hace en runas.

Se decidió a favor de la fidelidad (veredicto #25): layoutSignBox lleva const runicBody = opts?.runicBody !== false — o sea rúnico salvo que te opongas, y el latín sobrevive únicamente para los tests de la variante legible.

Lo que salva la legibilidad

El log de consola conserva el latín. La caja va fiel en runas y el texto sigue leyéndose en el historial. Fidelidad máxima donde se mira, legibilidad donde se lee — y por eso la decisión no costó nada que doliera.

Detalle de método que conviene no perder: la caja se dimensiona al cuerpo ya compuesto y se capa al ancho del panel (interior 14, derivado de SIGNS.DAT). Como la runa colapsa dígrafos, el mismo texto ocupa menos: «PRIVATE ISLAND» llena el panel justo, sin aire.

🔴 Esta sección decía lo contrario hasta el 04-08-2026. Afirmaba que el cuerpo rúnico era una «maqueta no aterrizada» tras un flag ?signRunic=1, y que el port «se queda en latín por defecto». Las dos cosas eran falsas: el flag no existe en el árbol y el rúnico es el default. No era una cifra caducada — era el resultado invertido de una decisión que sí se tomó. Lo destapó el censo de este documento contra el código.

9 · Tipografía III — la fuente del panel, dos maquetas descartadas

Maquetas de la rama fiel/mockups-verdicts, sin aterrizar · Acta: docs/verdicts/fuente-transparente/README.md

La idea de «fuente transparente» del panel bajo la piel shader admitía dos lecturas, y se maquetaron las dos como flags reversibles antes de decidir:

Estado de hoy?mockFont=nobg?mockFont=alpha
glifos opacos sobre panel negroglifo sin fondo de celdaglifos translúcidos

Apagados, el render es byte-idéntico al de hoy (no-op verificado). El acta anota que en C la banda de vientos («Calm Winds») se pinta por otra ruta y queda opaca en la maqueta: si esta vía se eligiera, habría que unificarla al cablear.


10 · Líneas y contornos I — los remates de la banda de scroll

🎯 FIDELIDAD · Actas: docs/verdicts/chevrones/README.md (shader), docs/verdicts/ready-arrows/README.md (fiel)

Cuando una lista del picker de Ready desborda sus 7 filas visibles, el original pinta una banda ► flecha ◄ bajo el pergamino. En el port, los remates quedaban separados de la banda azul por una línea negra de 1 píxel lógico.

antesdespués
remate separado por una línea negraremate pegado a la banda

Y la prueba a ×8 de la junta, que es donde de verdad se adjudica:

Junta banda–chevrón a ×8

Dos causas distintas para el mismo síntoma

Esto es lo que hace la entrada interesante: el mismo defecto tenía una causa distinta en cada piel, y arreglar una no arregló la otra.

El resultado de la piel fiel es byte-idéntico al recorte de referencia del DOS (original-ORIG_3-band.png).


11 · Líneas y contornos II — la flecha que era un rombo partido

🎯 FIDELIDAD · Código: game/src/skin/shader/skyband.ts · Acta: docs/verdicts/ready-arrows/README.md

La piel shader repinta el chrome en vectorial, y su flecha de scroll dibujaba triángulos rellenos ▲/▼. Para el estado «hay más arriba y abajo» (↕) pintaba dos triángulos con un hueco entre ellos, que leía como un rombo partido.

antes / después, piel fielantes / después, piel shader
fiel antes y despuésshader antes y después

El glifo del DOS no es un triángulo: es una flecha fina con asta (↑ 0x18 / ↓ 0x19 / ↕ 0x12 de IBM.CH). El fix vectoriza esa forma midiendo el bitmap real de font-ibm.png — cabeza de semiancho 3 (cols 1..7), asta de semiancho 1 (cols 3..5) — y el ↕ pasa a ser una silueta continua de doble punta unida por el asta, no dos piezas.

La guarda quedó escrita en el término que importa: el test shader-skyband.test.ts afirma que el ↕ es 1 fill + 1 stroke, no 2+2. Un test que cuenta operaciones de dibujo, porque el defecto era precisamente que había dos piezas donde debía haber una.

El censo que vino detrás

El mismo indicador faltaba en las listas de Ztats, y ahí el hallazgo fue de método (ui-scroll-audit): la banda no la dibuja cada lista, es un kernel único (0x6c0a) sin coordenadas —o sea, banda fija en pantalla— que varios comandos invocan. Las derivaciones se habían hecho por comando, en vertical; nadie barrió el consumidor compartido. Ready lo implementó cuando le tocó y ahí se quedó.

De las tres bandas de scroll de lista del binario, el port cubre dos. La tercera es la lista de mercancías del herrero, y no aplica: el port sirve las tiendas como flujo de consola, no como el pergamino enmarcado del original. Eso es una divergencia de presentación anotada, no una flecha que falte.

🔴 Y hay un cuarto sitio donde esa banda aparece sin deber aparecer

En el port, el picker de (R)eady tiene un cliente más: el selector de reactivos de (M)ix, que reusa drawReadyPicker con variant:"mix". Así que el arreglo de arriba le llegó gratis. Y con él, algo que no debía llegarle:

Picker de reactivos de (M)ix

7 reactivos y una . Los del juego son 8: falta Mandrake Root.

En el binario, el panel de reactivos de Mix ni siquiera es el picker de Ready: es una rutina propia de otro overlay, CMDS.OVL @0x18be, y no pagina nada.

El 7 del port sale de skin/fiel/ready.ts:56READY_VISIBLE_ROWS, derivado de la ventana de Ready (draw_list_frame(8) @ZSTATS 0x12ee) y aplicado a Mix por el solo hecho de compartir renderizador.

La misma banda es el ARREGLO en Ztats y Ready, y el DEFECTO en Mix. Y el mecanismo no es «se heredó una constante»: es que el port hizo de Mix un cliente del picker de Ready, y el binario los tiene separados en dos overlays distintos. Al reutilizar el renderizador viajó con él su geometría —la cota de página, y la banda que anuncia esa cota— hasta un caso que no la tiene. Un widget compartido no comparte sólo el dibujo: comparte sus supuestos, y nadie decide si valen también para el cliente nuevo.

Sigue abierto: la ventana de Mix son 9 filas, así que con 8 de contenido hay que desplazarla, y el renderizador es el de Ready — radio de explosión. Detalle en docs/verdicts/mix-flow/README.md y mix-flow-acta §6b.

Corolario, y matiza la frase de arriba: de las tres bandas de lista del binario el port cubre dos y la tercera no aplica. Pero «cubierto» no es «fiel» — y hay un cuarto sitio, Mix, donde el port pinta una banda que el binario no pinta. El censo contó las bandas que FALTABAN; nadie contó las que SOBRAN.

⚠️ Discrepancia anotada, para adjudicar fuera de este documento: re/notes/ui-scroll-audit.md:34 lista el «selector de reactivos de (M)ix» como consumidor de la banda de ZSTATS item_page_controller @0x0f2e, en una tabla cuyo sujeto declarado es el BINARIO. Esa rutina no puede servir a Mix: despacha sobre [bp+4] y sólo tiene dos ramas0x52 ('R', tabla de equipo, 48 entradas) y el caso contrario (otra tabla, 38 entradas)—, ninguna es la lista de 8 reactivos, y el único llamador que le pasa argumento pasa 0x52. Lo que esa fila describe es la implementación del PORT. Corresponde al carril del censo, no a este documento.

estadocaptura
7 ítems (cabe justo) — sin flechasin flecha
8 ítems, tope — sólo ▼flecha abajo
8 ítems, scrolleado — ▲flecha arriba
piel shader — vectorialflecha vectorial

12 · El zodíaco transpuesto

🎯 FIDELIDAD · Capturas: docs/verdicts/zodiaco-70/

La lámina del zodíaco (el cielo estrellado con los signos) salía con las estrellas en las posiciones equivocadas:

antesdespués
zodíaco transpuestozodíaco fiel

Y el detalle a zoom de un signo, que es donde se ve la diferencia de forma:

antesdespués
signo, antessigno, después

La carpeta no tiene acta, así que se midió

zodiaco-70/ no lleva README. Lo único que había era el nombre de un fichero afirmando «transpuesto», y un nombre de fichero no es una derivación. En vez de repetirlo como si lo fuera, se contrastó la hipótesis contra las dos imágenes (04-08-2026).

Método: las dos láminas son 528×528 y ~98 % negro, así que comparar píxel a píxel da 99 % de acuerdo con cualquier transformación — el fondo domina. Se comparan sólo los píxeles encendidos, por intersección sobre unión (IoU), contra las ocho simetrías del cuadrado:

Hipótesis: DESPUÉS = … de ANTESIoU
transpuesta (aᵀ)0,4897
identidad (ningún cambio)0,0242
rotación 90°0,0160
rotación 180°0,0059
espejo vertical0,0040
rotación 270°0,0020
espejo horizontal0,0000

La transposición gana por 20× sobre la siguiente candidata. La hipótesis del nombre de fichero queda corroborada: lo que estaba mal era el intercambio de los dos índices al leer la lámina.

Lo que esta medición NO establece

El IoU es 0,49, no 1,0, y hay que decirlo: la transposición explica el grueso de la diferencia, pero no toda. DESPUÉS tiene más píxeles encendidos que ANTES (2 394 frente a 2 178), así que el arreglo no fue una transposición pura — hubo algo más, o los sprites de estrella no son simétricos y transponerlos no los mapea exactamente.

Cuál de las dos cosas, no lo sé y no lo invento: haría falta el código del carril. Lo que aquí queda es una hipótesis medida con su residuo declarado, que es bastante más de lo que había.

Las cuatro capturas estaban sin commitear hasta hoy; entraron en la rama de este documento junto con las otras ocho sueltas de feat-anim/ y gem-glyphs/.


Lo que este documento no cubre