code wiki / _hdl_build / nx_game_actor.nx

nx_game_actor.nx source

↩ module page · 220 lines · 8527 B

1// nx_game_actor.nx -- the character<->environment INTERACTION substrate (ws=game-interact 2026-07-27). 2// The gamebench honest audit says the emitted games are click-loops: nothing lets a character MOVE through 3// a world, be STOPPED by it, or MEET another character. This lib is that missing floor, first-byte-up: 4// - ONE walkability grid (0=open 1=wall, the exact nx_pathfind convention) shared by movement, entity 5// collision AND pf_astar => the player and every NPC agree on passability BY CONSTRUCTION -- there is 6// no second copy of the map to drift (the dual-source hazard killed at design time). 7// - an OCCUPANCY index (cell -> actor handle) kept consistent by the ONLY mover, ac_step. 8// - BUMP-TO-INTERACT: moving into an occupied cell does not move you -- it RETURNS the occupant. The 9// bump is the interaction primitive (NetHack/Crawl/CDDA idiom -- the ingested corpus's own model): 10// bump an enemy = engage, bump an NPC = talk. The GAME decides; this lib only reports truthfully. 11// Actors live in the certified nx_entity_store (handle = index+generation => stale handles detectably 12// invalid). All integer, deterministic, no globals. LIB, no main. license_tier: ORIGINAL 13import "nx_syscalls.nx" 14import "nx_entity_store.nx" 15 16// ---- world arena: [0]=w [1]=h then grid w*h then occ w*h (occ: 0=empty else actor handle) ---- 17const WD_HDR: i64 = 2 18 19func wd_words(w: i64, h: i64) -> i64 { return WD_HDR + w*h*2 } 20func wd_bytes(w: i64, h: i64) -> i64 { return wd_words(w, h) * 8 } 21 22func wd_init(wd: *i64, w: i64, h: i64) -> i64 { 23 wd[0] = w; wd[1] = h 24 var i: i64 = 0 25 let n: i64 = w*h 26 while i < n { wd[WD_HDR+i] = 0; wd[WD_HDR+n+i] = 0; i = i + 1 } 27 return 0 28} 29func wd_w(wd: *i64) -> i64 { return wd[0] } 30func wd_h(wd: *i64) -> i64 { return wd[1] } 31// the grid pf_astar consumes -- the SAME memory, not a copy (byte-offset idiom, cf. nx_swgpu sg_fb) 32func wd_grid(wd: *i64) -> *i64 { return ((wd as i64) + WD_HDR*8) as *i64 } 33 34func wd_in(wd: *i64, x: i64, y: i64) -> i64 { 35 if x < 0 { return 0 } 36 if y < 0 { return 0 } 37 if x >= wd[0] { return 0 } 38 if y >= wd[1] { return 0 } 39 return 1 40} 41func wd_set_wall(wd: *i64, x: i64, y: i64, v: i64) -> i64 { 42 if wd_in(wd, x, y) == 0 { return 0-1 } 43 wd[WD_HDR + y*wd[0] + x] = v 44 return 0 45} 46func wd_wall(wd: *i64, x: i64, y: i64) -> i64 { 47 if wd_in(wd, x, y) == 0 { return 1 } 48 return wd[WD_HDR + y*wd[0] + x] 49} 50func wd_occ(wd: *i64, x: i64, y: i64) -> i64 { 51 if wd_in(wd, x, y) == 0 { return 0 } 52 return wd[WD_HDR + wd[0]*wd[1] + y*wd[0] + x] 53} 54func wd_set_occ(wd: *i64, x: i64, y: i64, hnd: i64) -> i64 { 55 if wd_in(wd, x, y) == 0 { return 0-1 } 56 wd[WD_HDR + wd[0]*wd[1] + y*wd[0] + x] = hnd 57 return 0 58} 59 60// ---- actor components (entity-store arena, ncomp = AC_NC) ---- 61const AC_NC: i64 = 12 62const A_X: i64 = 0 63const A_Y: i64 = 1 64const A_KIND: i64 = 2 // 0 player, 1 enemy, 2 npc 65const A_STATE: i64 = 3 // agent brain writes this (see nx_game_agent) 66const A_HP: i64 = 4 67const A_MAXHP: i64 = 5 68const A_HOMEX: i64 = 6 69const A_HOMEY: i64 = 7 70const A_TICK: i64 = 8 // per-actor deterministic phase counter 71const A_SPEC: i64 = 9 // species row (links battle/companion data) 72const A_LVL: i64 = 10 73const A_SEED: i64 = 11 // per-actor rng stream seed 74 75// spawn an actor onto a FREE, OPEN cell. returns handle or -1 (cell taken/wall/oob/full). 76func ac_spawn(wd: *i64, ar: *i64, x: i64, y: i64, kind: i64, hp: i64, spec: i64, lvl: i64, seed: i64) -> i64 { 77 if wd_wall(wd, x, y) != 0 { return 0-1 } 78 if wd_occ(wd, x, y) != 0 { return 0-1 } 79 let hnd: i64 = en_spawn(ar) 80 if hnd == EN_NULL { return 0-1 } 81 en_set(ar, hnd, A_X, x); en_set(ar, hnd, A_Y, y) 82 en_set(ar, hnd, A_KIND, kind); en_set(ar, hnd, A_STATE, 0) 83 en_set(ar, hnd, A_HP, hp); en_set(ar, hnd, A_MAXHP, hp) 84 en_set(ar, hnd, A_HOMEX, x); en_set(ar, hnd, A_HOMEY, y) 85 en_set(ar, hnd, A_TICK, 0); en_set(ar, hnd, A_SPEC, spec) 86 en_set(ar, hnd, A_LVL, lvl); en_set(ar, hnd, A_SEED, seed) 87 wd_set_occ(wd, x, y, hnd) 88 return hnd 89} 90 91// remove an actor (death/capture): frees its cell, destroys the entity (generation bump). 92func ac_remove(wd: *i64, ar: *i64, hnd: i64) -> i64 { 93 if en_valid(ar, hnd) == 0 { return 0-1 } 94 wd_set_occ(wd, en_get(ar, hnd, A_X), en_get(ar, hnd, A_Y), 0) 95 en_destroy(ar, hnd) 96 return 0 97} 98 99// step-result codes 100const AC_MOVED: i64 = 0 101const AC_WALL: i64 = 1 102const AC_OOB: i64 = 2 103const AC_BUMP: i64 = 3 // out[0] = the occupant's handle 104const AC_DEAD: i64 = 4 // stale/invalid handle 105 106// THE mover. Every position change in the game goes through here -- that is what keeps the 107// occupancy index truthful. |dx|+|dy| must be 1 (4-connected, the pathfind metric). 108func ac_step(wd: *i64, ar: *i64, hnd: i64, dx: i64, dy: i64, out: *i64) -> i64 { 109 out[0] = 0 110 if en_valid(ar, hnd) == 0 { return AC_DEAD } 111 let x: i64 = en_get(ar, hnd, A_X) 112 let y: i64 = en_get(ar, hnd, A_Y) 113 let nx2: i64 = x + dx 114 let ny2: i64 = y + dy 115 if wd_in(wd, nx2, ny2) == 0 { return AC_OOB } 116 if wd_wall(wd, nx2, ny2) != 0 { return AC_WALL } 117 let occ: i64 = wd_occ(wd, nx2, ny2) 118 if occ != 0 { 119 out[0] = occ 120 return AC_BUMP 121 } 122 wd_set_occ(wd, x, y, 0) 123 wd_set_occ(wd, nx2, ny2, hnd) 124 en_set(ar, hnd, A_X, nx2) 125 en_set(ar, hnd, A_Y, ny2) 126 return AC_MOVED 127} 128 129// squared distance between two actors (perception metric; integer) 130func ac_dist2(ar: *i64, a: i64, b: i64) -> i64 { 131 let dx: i64 = en_get(ar, a, A_X) - en_get(ar, b, A_X) 132 let dy: i64 = en_get(ar, a, A_Y) - en_get(ar, b, A_Y) 133 return dx*dx + dy*dy 134} 135func ac_adjacent(ar: *i64, a: i64, b: i64) -> i64 { 136 var dx: i64 = en_get(ar, a, A_X) - en_get(ar, b, A_X) 137 var dy: i64 = en_get(ar, a, A_Y) - en_get(ar, b, A_Y) 138 if dx < 0 { dx = 0 - dx } 139 if dy < 0 { dy = 0 - dy } 140 if dx + dy == 1 { return 1 } 141 return 0 142} 143 144// integer LOS (Bresenham over the wall grid; endpoints excluded). 1 = clear line. 145func ac_los(wd: *i64, x0: i64, y0: i64, x1: i64, y1: i64) -> i64 { 146 var dx: i64 = x1 - x0 147 if dx < 0 { dx = 0 - dx } 148 var dy: i64 = y1 - y0 149 if dy < 0 { dy = 0 - dy } 150 var sx: i64 = 0-1 151 if x0 < x1 { sx = 1 } 152 var sy: i64 = 0-1 153 if y0 < y1 { sy = 1 } 154 var err: i64 = dx - dy 155 var cx: i64 = x0 156 var cy: i64 = y0 157 var guard: i64 = 0 158 let lim: i64 = (dx + dy) * 2 + 4 159 while guard < lim { 160 guard = guard + 1 161 if cx == x1 { if cy == y1 { return 1 } } 162 let e2: i64 = err * 2 163 if e2 > (0 - dy) { err = err - dy; cx = cx + sx } 164 if e2 < dx { err = err + dx; cy = cy + sy } 165 var atend: i64 = 0 166 if cx == x1 { if cy == y1 { atend = 1 } } 167 if atend == 0 { if wd_wall(wd, cx, cy) != 0 { return 0 } } 168 } 169 return 1 170} 171 172// occupancy self-check: every alive actor's (x,y) cell holds exactly its handle, and every non-zero 173// occ cell backs a live actor at that spot. Returns 0 consistent, else count of violations. 174// This is the gate's invariant walker -- in the LIB so any composer can assert it cheaply. 175func ac_occ_check(wd: *i64, ar: *i64) -> i64 { 176 var bad: i64 = 0 177 var i: i64 = 0 178 let n: i64 = en_count(ar) 179 while i < n { 180 let hnd: i64 = en_nth(ar, i) 181 let x: i64 = en_get(ar, hnd, A_X) 182 let y: i64 = en_get(ar, hnd, A_Y) 183 if wd_occ(wd, x, y) != hnd { bad = bad + 1 } 184 i = i + 1 185 } 186 var y2: i64 = 0 187 while y2 < wd[1] { 188 var x2: i64 = 0 189 while x2 < wd[0] { 190 let o: i64 = wd_occ(wd, x2, y2) 191 if o != 0 { 192 var ok: i64 = 0 193 if en_valid(ar, o) == 1 { 194 if en_get(ar, o, A_X) == x2 { if en_get(ar, o, A_Y) == y2 { ok = 1 } } 195 } 196 if ok == 0 { bad = bad + 1 } 197 } 198 x2 = x2 + 1 199 } 200 y2 = y2 + 1 201 } 202 return bad 203} 204 205// rolling checksum of world+actors (order-sensitive, GX-9 style) for determinism/save proofs 206func ac_ck(wd: *i64, ar: *i64) -> i64 { 207 var ck: i64 = 1469598103 208 let n: i64 = wd_words(wd[0], wd[1]) 209 var i: i64 = 0 210 while i < n { ck = ((ck ^ wd[i]) * 1099511) & 4611686018427387903; i = i + 1 } 211 var j: i64 = 0 212 let m: i64 = en_count(ar) 213 while j < m { 214 let hnd: i64 = en_nth(ar, j) 215 var c: i64 = 0 216 while c < AC_NC { ck = ((ck ^ en_get(ar, hnd, c)) * 1099511) & 4611686018427387903; c = c + 1 } 217 j = j + 1 218 } 219 return ck 220}