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}