code wiki / _hdl_build / nx_wasm_diablo_gate.nx
nx_wasm_diablo_gate.nx source
↩ module page · 127 lines · 8472 B
1import "nx_gate_gn.nx"
2import "nx_gate_base.nx"
3// nx_wasm_diablo_gate.nx -- NATIVE gate for the playable isometric ARPG (nx_wasm_diablo). Drives click()/tick()
4// the way the browser shim will, and verifies the game + the Diablo-generation render BEFORE lowering to wasm.
5// T1 MOVE: a click sets a destination; the hero walks toward it.
6// T2 COMBAT: chasing a foe and striking it kills it and pays gold.
7// T3 LOOT: walking onto the gold collects it.
8// T4 RENDER + LIGHTING: the torch follows the hero (hero lit, far corner in gloom). PNG.
9// expect_exit: 0 license_tier: ORIGINAL
10import "nx_wasm_diablo.nx"
11import "nx_png.nx"
12import "nx_syscalls.nx"
13import "nx_gate_verdict.nx"
14
15func grow(name: *u8, ok: i64) -> i64 { if ok==1 { gw(" PASS " as *u8) } else { gw(" FAIL " as *u8) } gw(name); gw("
16" as *u8); return ok }
17func lum(c: i64) -> i64 { return (c & 255) + ((c>>8)&255) + ((c>>16)&255) }
18func red_over_green(c: i64) -> i64 { return (c & 255) - ((c>>8)&255) }
19
20// ★DERIVED FROM THE ACTUAL PALETTE, NOT PICKED. Read out of nx_wasm_diablo's own render:
21// dungeon floor gx_rgb(92+nz, 86+nz, 76+nz) -> red-over-green = 6 (nz shifts all three equally)
22// dungeon walls gx_rgb(106,98,88) / (94,88,80) -> red-over-green = 8
23// hero body gx_rgb(160,44,40) -> red-over-green = 116
24// hero skin gx_rgb(230,186,150) -> red-over-green = 44
25// foe body gx_rgb(70,120,64) -> red-over-green = -50 (green-dominant, not confusable)
26// So the stone is barely warm and the hero is strongly red. 24 sits clear above the stone's 8 and
27// comfortably below the hero's weakest part (skin, 44). The radial torch scales all three channels,
28// which scales the DIFFERENCE too -- that is why the margin is stated against the widest gap in the
29// palette rather than against one lighting condition. The neg-control below proves the stone really
30// does fall under it in the frame as rendered, so this is measured rather than asserted.
31const DBL_RED_MARGIN: i64 = 24
32// far enough sideways to clear the hero sprite, near enough to stay inside the torch radius (116)
33const DBL_FLOOR_DX: i64 = 40
34// ★READ OUT OF gx_sprite_humanoid, NOT GUESSED. With fy at the feet, that sprite lays its TORSO --
35// the body-coloured part -- across fy-41 .. fy-13, so any offset in that band is on the hero.
36// 38 is deliberately near the SHOULDERS rather than mid-chest: this gate captures a frame RIGHT
37// AFTER A STRIKE, and render_impl paints the slash FX at ((hx+fx)/2, (hy+fy)/2 - 20) in near-white
38// (255,248,205 at alpha 215). A first cut sampled fy-28, landed under that flash, and measured
39// red-over-green 13 -- LESS than the 16 of the stone beside it, because white has no red dominance
40// at all. The tooth caught its own bad sample point by printing both numbers.
41const DBL_TORSO_DY: i64 = 38
42
43func main() -> i64 {
44 gw("=== nx_wasm_diablo_gate: playable isometric ARPG (Diablo generation), verified NATIVE ===\n" as *u8)
45 var pass: i64 = 0; var total: i64 = 0
46 let base: i64 = sys_mmap(4*1024*1024) as i64
47 let mx: *i64 = (base + O_MX) as *i64
48 let my: *i64 = (base + O_MY) as *i64
49 let ma: *i64 = (base + O_MA) as *i64
50 let st: *i64 = (base + O_ST) as *i64
51
52 // ---- T1: move ----
53 init_impl(base)
54 let hx0: i64 = st[0]
55 click_impl(base, st[0] + 50, st[1])
56 var t: i64=0; while t < 30 { tick_impl(base, 0); t=t+1 }
57 var t1ok: i64=1; if st[0] <= hx0 + 8 {t1ok=0}
58 total=total+1; if t1ok==1 { pass=pass+1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) }
59 gw("T1 click-to-move: hero walked toward the destination (x " as *u8); gn(hx0); gw("->" as *u8); gn(st[0]); gw(")\n" as *u8)
60
61 // ---- T2: combat ----
62 init_impl(base)
63 let g0: i64 = st[4]
64 t=0; while t < 500 { if ma[0] == 0 { t = 500 } else { click_impl(base, mx[0], my[0]); tick_impl(base, 0); t=t+1 } }
65 var t2ok: i64=1; if ma[0] != 0 {t2ok=0} if st[4] < g0 + 5 {t2ok=0}
66 total=total+1; if t2ok==1 { pass=pass+1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) }
67 gw("T2 combat: chased + slew a foe (alive=" as *u8); gn(ma[0]); gw(", gold+=" as *u8); gn(st[4]-g0); gw(")\n" as *u8)
68
69 // ---- T3: loot ----
70 init_impl(base)
71 t=0; while t < 500 { if st[10] == 1 { t = 500 } else { click_impl(base, st[8], st[9]); tick_impl(base, 0); t=t+1 } }
72 var t3ok: i64=1; if st[10] != 1 {t3ok=0}
73 total=total+1; if t3ok==1 { pass=pass+1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) }
74 gw("T3 loot: walked onto the gold and grabbed it (taken=" as *u8); gn(st[10]); gw(", gold=" as *u8); gn(st[4]); gw(")\n" as *u8)
75
76 // ---- render + lighting (capture a combat frame right after a strike: walk pose + attack FX visible) ----
77 init_impl(base)
78 t=0; while t < 300 { click_impl(base, mx[2], my[2]); tick_impl(base, 0); if st[11] > 3 { t = 300 } else { t = t + 1 } }
79 render_impl(base)
80 let fb: *i64 = (base + O_FB) as *i64
81 write_png(fb, W, H, "knowledge/nx_wasm_diablo_native.png" as *u8)
82 gw(" (live evidence: PNG knowledge/nx_wasm_diablo_native.png -- iso dungeon, hero, foes, torch-follow lighting)\n" as *u8)
83
84 // ---- T4: render + lighting ----
85 // ⚠SAMPLED ABOVE THE ATTACK FX. This tooth claims the TORCH lit the hero, but render_impl paints
86 // the slash flash AFTER lighting "so they pop", so a pixel under it is bright because of the FX
87 // and not because of the torch -- which would make this tooth pass with the torch removed. Using
88 // the shoulder offset puts it on torch-lit hero only. (Found while fixing T5, which measured the
89 // same contaminated pixel and said so out loud.)
90 let hero: i64 = lum(fb[(st[1]-DBL_TORSO_DY)*W + st[0]])
91 let corner: i64 = lum(fb[(H-10)*W + 8])
92 var t4ok: i64=1; if hero <= corner + 100 {t4ok=0}
93 total=total+1; if t4ok==1 { pass=pass+1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) }
94 gw("T4 render+lighting: hero lit by the torch (lum=" as *u8); gn(hero); gw(") vs far gloom (lum=" as *u8); gn(corner); gw(")\n" as *u8)
95
96 // ---- T5/T6: THE HERO IS ACTUALLY IN THE PICTURE ----
97 // ★T4 ABOVE TESTS LIGHTING, NOT PRESENCE, AND CANNOT BE MADE TO DO BOTH. The torch FOLLOWS the
98 // hero, so the floor where he stands is the brightest part of the frame whether or not he is
99 // drawn -- delete the hero sprite and T4 very likely still passes on lit stone. Found 2026-08-14
100 // by nx_vacuity_census; same class as five sibling game gates fixed the same day.
101 // A brightness difference cannot separate hero from lit floor here, so this uses COLOUR instead:
102 // the hero is strongly red-dominant and the stone is barely warm (see DBL_RED_MARGIN above).
103 let heropx: i64 = fb[(st[1]-DBL_TORSO_DY)*W + st[0]]
104 let floorpx: i64 = fb[(st[1]-22)*W + (st[0] + DBL_FLOOR_DX)]
105 total=total+1
106 if red_over_green(heropx) > DBL_RED_MARGIN { pass=pass+1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) }
107 gw("T5 hero in frame: the hero cell is red-dominant like his body, not stone (r-g=" as *u8); gn(red_over_green(heropx)); gw(" need >" as *u8); gn(DBL_RED_MARGIN); gw(")\n" as *u8)
108
109 // neg-control: LIT STONE beside him must NOT satisfy the same predicate. Without this, T5 would
110 // be an unproven claim about the palette rather than a measurement of this frame.
111 total=total+1
112 if red_over_green(floorpx) <= DBL_RED_MARGIN { pass=pass+1; gw(" [PASS] " as *u8) } else { gw(" [FAIL] " as *u8) }
113 gw("T6 neg-control-lit-stone-is-not-red: floor beside the hero, under the same torch, fails T5's test (r-g=" as *u8); gn(red_over_green(floorpx)); gw(")\n" as *u8)
114
115 gw("\n=== nx_wasm_diablo_gate " as *u8); gn(pass); gw("/" as *u8); gn(total)
116 // D001 ANCHOR: the rich line above stays as this gate's public signature; the canonical verdict
117 // goes LAST because the estate's judge anchors by POSITION, taking only the final line.
118 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check
119 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled
120 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify.
121 let ctr__dry: *i64 = gv_ctr()
122 ctr__dry[0] = pass
123 ctr__dry[1] = total
124 let rc__dry: i64 = gv_verdict("WASM-DIABLO-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8)
125 sys_exit(rc__dry)
126 return rc__dry
127}