nx_tactics_engine_lib.nx source
↩ module page · 400 lines · 15336 B
1// nx_tactics_engine_lib.nx -- the GENERIC data-driven TACTICS SIMULATION engine (G-ECO-005 rung 1):
2// the SECOND genuine Nishi game engine, structurally distinct from the dungeon-crawler engine.
3// Generalizes the proven nx_tactics_native (XCOM-style turn-based squad combat) from HARDCODED
4// constants into a runtime CONFIG array -- exactly as nx_game_engine_lib did for the dungeon.
5// All per-game variation (squad/enemy counts, grid, hp/damage/moves/range, wall/cover density,
6// the hit-chance curve, seed) arrives in cfg; the turn loop + cover/hit-chance + enemy AI are the
7// reusable engine. A generated tactics game (future nx_tactics_emit rung) just fills cfg and calls
8// teng_init + drives teng_input; render + spec-emitter are the NEXT rungs (this rung = the SIM core).
9//
10// Built native by Nishi's own toolchain (nx_cc->nxasm_x86, no gcc). HONEST AUTHORSHIP: this engine
11// library is TUTOR-authored tooling (like nx_game_engine_lib); the GAMES a future emitter writes
12// from tactics specs are the autonomy lever. license_tier: ORIGINAL
13//
14// state (i64 at st): 0=turn(0play/1enemy) 1=cur(active player unit) 2=mode(0play/1win/2lose)
15// 3=rng 4=msg 5=moves_left 6=has_shot | units i in 0..NUNITS-1 (team0=0..np-1, team1=np..np+ne-1):
16// ux=st[16+i] uy=st[22+i] hp=st[28+i] team=st[34+i] alive=st[40+i] (MAXU=6) | CONFIG copied in:
17// 64=grid_w 65=grid_h 66=n_player 67=n_enemy 68=hp0 69=dmg 70=moves0 71=range 72=n_walls
18// 73=n_cover 74=hit_base 75=hit_distdiv 76=cover_penalty | world grid bytes at (st)+2048 (0 floor
19// 1 wall 2 cover), stride grid_w.
20import "nx_syscalls.nx"
21import "nx_game_engine_lib.nx"
22const K_MAGIC_1103515245: i64 = 1103515245
23const K_MAGIC_12345: i64 = 12345
24const K_MAGIC_32767: i64 = 32767
25const K_MAGIC_2048: i64 = 2048
26const K_MAGIC_9999: i64 = 9999
27const K_MAGIC_8192: i64 = 8192
28const K_MAGIC_16384: i64 = 16384
29
30func teng_nunits(st: *i64) -> i64 {
31 var u: i64 = st[66] + st[67]
32 if u > 6 { u = 6 }
33 return u
34}
35func teng_rng(st: *i64) -> i64 {
36 var s: i64 = st[3]
37 s = s * K_MAGIC_1103515245 + K_MAGIC_12345
38 st[3] = s
39 return (s >> 16) & K_MAGIC_32767
40}
41func teng_cell_get(st: *i64, x: i64, y: i64) -> i64 {
42 if x < 0 { return 1 }
43 if y < 0 { return 1 }
44 if x >= st[64] { return 1 }
45 if y >= st[65] { return 1 }
46 let w: *u8 = ((st as i64) + K_MAGIC_2048) as *u8
47 return w[y * st[64] + x] as i64
48}
49func teng_cell_set(st: *i64, x: i64, y: i64, v: i64) -> i64 {
50 let w: *u8 = ((st as i64) + K_MAGIC_2048) as *u8
51 w[y * st[64] + x] = v as u8
52 return 0
53}
54func teng_unit_at(st: *i64, x: i64, y: i64) -> i64 {
55 let nu: i64 = teng_nunits(st)
56 var i: i64 = 0
57 while i < nu {
58 if st[40 + i] == 1 { if st[16 + i] == x { if st[22 + i] == y { return i } } }
59 i = i + 1
60 }
61 return 0 - 1
62}
63func teng_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
64func teng_dist(ax: i64, ay: i64, bx: i64, by: i64) -> i64 { return teng_abs(ax - bx) + teng_abs(ay - by) }
65func teng_team_alive(st: *i64, team: i64) -> i64 {
66 let nu: i64 = teng_nunits(st)
67 var c: i64 = 0
68 var i: i64 = 0
69 while i < nu {
70 if st[40 + i] == 1 { if st[34 + i] == team { c = c + 1 } }
71 i = i + 1
72 }
73 return c
74}
75func teng_nearest_foe(st: *i64, x: i64, y: i64, myteam: i64) -> i64 {
76 let nu: i64 = teng_nunits(st)
77 var best: i64 = 0 - 1
78 var bd: i64 = K_MAGIC_9999
79 var i: i64 = 0
80 while i < nu {
81 if st[40 + i] == 1 { if st[34 + i] != myteam {
82 let d: i64 = teng_dist(x, y, st[16 + i], st[22 + i])
83 if d < bd { bd = d; best = i }
84 } }
85 i = i + 1
86 }
87 return best
88}
89func teng_first_player(st: *i64) -> i64 {
90 let nu: i64 = teng_nunits(st)
91 var i: i64 = 0
92 while i < nu {
93 if st[40 + i] == 1 { if st[34 + i] == 0 { return i } }
94 i = i + 1
95 }
96 return 0 - 1
97}
98func teng_next_player(st: *i64, from: i64) -> i64 {
99 let nu: i64 = teng_nunits(st)
100 var i: i64 = from + 1
101 while i < nu {
102 if st[40 + i] == 1 { if st[34 + i] == 0 { return i } }
103 i = i + 1
104 }
105 return 0 - 1
106}
107func teng_place_unit(st: *i64, idx: i64, x0: i64, y0: i64, team: i64) -> i64 {
108 var tries: i64 = 0
109 while tries < 80 {
110 tries = tries + 1
111 let rx: i64 = x0 + (teng_rng(st) % 5)
112 let ry: i64 = y0 + (teng_rng(st) % 5)
113 var ok: i64 = 1
114 if teng_cell_get(st, rx, ry) != 0 { ok = 0 }
115 if teng_unit_at(st, rx, ry) >= 0 { ok = 0 }
116 if ok == 1 {
117 st[16 + idx] = rx; st[22 + idx] = ry; st[28 + idx] = st[68]
118 st[34 + idx] = team; st[40 + idx] = 1
119 return 0
120 }
121 }
122 st[16 + idx] = x0; st[22 + idx] = y0; st[28 + idx] = st[68]; st[34 + idx] = team; st[40 + idx] = 1
123 return 0
124}
125func teng_build(st: *i64) -> i64 {
126 let gw: i64 = st[64]
127 let gh: i64 = st[65]
128 var y: i64 = 0
129 while y < gh {
130 var x: i64 = 0
131 while x < gw {
132 var c: i64 = 0
133 if x == 0 { c = 1 }
134 if y == 0 { c = 1 }
135 if x == gw - 1 { c = 1 }
136 if y == gh - 1 { c = 1 }
137 teng_cell_set(st, x, y, c)
138 x = x + 1
139 }
140 y = y + 1
141 }
142 var i: i64 = 0
143 while i < st[72] {
144 let rx: i64 = 2 + (teng_rng(st) % (gw - 4))
145 let ry: i64 = 2 + (teng_rng(st) % (gh - 4))
146 teng_cell_set(st, rx, ry, 1)
147 i = i + 1
148 }
149 var j: i64 = 0
150 while j < st[73] {
151 let rx: i64 = 2 + (teng_rng(st) % (gw - 4))
152 let ry: i64 = 2 + (teng_rng(st) % (gh - 4))
153 if teng_cell_get(st, rx, ry) == 0 { teng_cell_set(st, rx, ry, 2) }
154 j = j + 1
155 }
156 // clear unit spawn anchors so placement never falls back onto a wall
157 let np: i64 = st[66]
158 let ne: i64 = st[67]
159 let nu: i64 = teng_nunits(st)
160 var p: i64 = 0
161 while p < np {
162 teng_cell_set(st, 1, 1 + p, 0)
163 teng_place_unit(st, p, 1, 1 + p, 0)
164 p = p + 1
165 }
166 var e: i64 = np
167 while e < nu {
168 teng_cell_set(st, gw - 2, 1 + (e - np), 0)
169 teng_place_unit(st, e, gw - 6, 1 + (e - np), 1)
170 e = e + 1
171 }
172 return 0
173}
174func teng_init(st: *i64, cfg: *i64) -> i64 {
175 var k: i64 = 0
176 while k < 13 { st[64 + k] = cfg[k]; k = k + 1 }
177 st[3] = cfg[13]
178 // clear ALL unit slots before placement so a reused state buffer is fully idempotent --
179 // otherwise teng_place_unit's overlap check reads stale units from a prior game and consumes
180 // RNG differently, breaking deterministic replay (caught by nx_play_parity 2026-06-14).
181 var u: i64 = 0
182 while u < 6 { st[16 + u] = 0; st[22 + u] = 0; st[28 + u] = 0; st[34 + u] = 0; st[40 + u] = 0; u = u + 1 }
183 teng_build(st)
184 st[0] = 0
185 st[1] = teng_first_player(st)
186 st[2] = 0
187 st[4] = 0
188 st[5] = st[70]
189 st[6] = 0
190 return 0
191}
192func teng_check_end(st: *i64) -> i64 {
193 if teng_team_alive(st, 1) == 0 { st[2] = 1 }
194 if teng_team_alive(st, 0) == 0 { st[2] = 2 }
195 return 0
196}
197func teng_chance(st: *i64, d: i64, cover: i64) -> i64 {
198 var c: i64 = st[74] - (d / st[75])
199 if cover == 1 { c = c - st[76] }
200 if c < 1 { c = 1 }
201 if c > 9 { c = 9 }
202 return c
203}
204func teng_move_unit(st: *i64, ui: i64, dx: i64, dy: i64) -> i64 {
205 if st[5] <= 0 { st[4] = 7; return 0 }
206 let nx: i64 = st[16 + ui] + dx
207 let ny: i64 = st[22 + ui] + dy
208 if teng_cell_get(st, nx, ny) == 1 { return 0 }
209 if teng_unit_at(st, nx, ny) >= 0 { return 0 }
210 st[16 + ui] = nx; st[22 + ui] = ny
211 st[5] = st[5] - 1
212 return 0
213}
214func teng_shoot(st: *i64, ui: i64) -> i64 {
215 let ei: i64 = teng_nearest_foe(st, st[16 + ui], st[22 + ui], st[34 + ui])
216 if ei < 0 { st[4] = 8; return 0 }
217 let d: i64 = teng_dist(st[16 + ui], st[22 + ui], st[16 + ei], st[22 + ei])
218 if d > st[71] { st[4] = 9; return 0 }
219 var cover: i64 = 0
220 if teng_cell_get(st, st[16 + ei], st[22 + ei]) == 2 { cover = 1 }
221 let ch: i64 = teng_chance(st, d, cover)
222 let roll: i64 = teng_rng(st) % 10
223 if roll < ch {
224 st[28 + ei] = st[28 + ei] - st[69]
225 if st[28 + ei] <= 0 { st[40 + ei] = 0; st[4] = 11 } else { st[4] = 12 }
226 } else { st[4] = 13 }
227 return 0
228}
229func teng_enemy_turn(st: *i64) -> i64 {
230 let np: i64 = st[66]
231 let nu: i64 = teng_nunits(st)
232 var i: i64 = np
233 while i < nu {
234 if st[40 + i] == 1 {
235 let pi: i64 = teng_nearest_foe(st, st[16 + i], st[22 + i], 1)
236 if pi >= 0 {
237 var steps: i64 = 0
238 while steps < st[70] {
239 let tx: i64 = st[16 + pi]
240 let ty: i64 = st[22 + pi]
241 var dx: i64 = 0
242 var dy: i64 = 0
243 if teng_abs(tx - st[16 + i]) >= teng_abs(ty - st[22 + i]) {
244 if tx > st[16 + i] { dx = 1 }
245 if tx < st[16 + i] { dx = 0 - 1 }
246 } else {
247 if ty > st[22 + i] { dy = 1 }
248 if ty < st[22 + i] { dy = 0 - 1 }
249 }
250 let nx: i64 = st[16 + i] + dx
251 let ny: i64 = st[22 + i] + dy
252 var moved: i64 = 0
253 if teng_cell_get(st, nx, ny) != 1 {
254 if teng_unit_at(st, nx, ny) < 0 {
255 if teng_dist(nx, ny, tx, ty) >= 1 { st[16 + i] = nx; st[22 + i] = ny; moved = 1 }
256 }
257 }
258 if moved == 0 { steps = st[70] }
259 steps = steps + 1
260 }
261 let d: i64 = teng_dist(st[16 + i], st[22 + i], st[16 + pi], st[22 + pi])
262 if d <= st[71] {
263 var cover: i64 = 0
264 if teng_cell_get(st, st[16 + pi], st[22 + pi]) == 2 { cover = 1 }
265 let ch: i64 = teng_chance(st, d, cover)
266 let roll: i64 = teng_rng(st) % 10
267 if roll < ch {
268 st[28 + pi] = st[28 + pi] - st[69]
269 if st[28 + pi] <= 0 { st[40 + pi] = 0 }
270 }
271 }
272 }
273 }
274 i = i + 1
275 }
276 return 0
277}
278func teng_end_unit(st: *i64) -> i64 {
279 let nxt: i64 = teng_next_player(st, st[1])
280 if nxt >= 0 { st[1] = nxt; st[5] = st[70]; st[6] = 0; return 0 }
281 teng_enemy_turn(st)
282 st[0] = 0
283 st[1] = teng_first_player(st)
284 st[5] = st[70]
285 st[6] = 0
286 teng_check_end(st)
287 return 0
288}
289// key: 0 up 1 down 2 left 3 right 4 shoot 5 end-unit 6 restart
290func teng_input(st: *i64, key: i64) -> i64 {
291 if st[2] != 0 { return 0 }
292 if st[0] != 0 { return 0 }
293 let ci: i64 = st[1]
294 if ci < 0 { return 0 }
295 if key == 0 { teng_move_unit(st, ci, 0, 0 - 1) }
296 if key == 1 { teng_move_unit(st, ci, 0, 1) }
297 if key == 2 { teng_move_unit(st, ci, 0 - 1, 0) }
298 if key == 3 { teng_move_unit(st, ci, 1, 0) }
299 if key == 4 { teng_shoot(st, ci); st[6] = 1; teng_check_end(st); if st[2] == 0 { teng_end_unit(st) } }
300 if key == 5 { teng_end_unit(st) }
301 return 0
302}
303
304// --- RENDER (G-ECO-005 rung 2): build an ANSI frame into buf; return its length (no fd write, so the
305// gate can inspect it headlessly). Reuses nx_game_engine_lib's bput* terminal helpers (the shared
306// render primitive, built once). A native play loop / emitter is rung 3. ---
307func teng_draw_cell(buf: *u8, off: *i64, st: *i64, x: i64, y: i64) -> i64 {
308 let ui: i64 = teng_unit_at(st, x, y)
309 if ui >= 0 {
310 if st[34 + ui] == 0 {
311 bput_bg(buf, off, 24, 40, 28)
312 if ui == st[1] { bput_fg(buf, off, 250, 240, 90) } else { bput_fg(buf, off, 90, 230, 120) }
313 bput(buf, off, "P" as *u8); bputn(buf, off, ui + 1)
314 } else {
315 bput_bg(buf, off, 40, 24, 24); bput_fg(buf, off, 230, 70, 70)
316 bput(buf, off, "E" as *u8); bputn(buf, off, ui - st[66] + 1)
317 }
318 bput_reset(buf, off)
319 return 0
320 }
321 let c: i64 = teng_cell_get(st, x, y)
322 if c == 1 { bput_bg(buf, off, 70, 80, 110); bput(buf, off, " " as *u8); bput_reset(buf, off); return 0 }
323 if c == 2 { bput_bg(buf, off, 86, 70, 40); bput(buf, off, "##" as *u8); bput_reset(buf, off); return 0 }
324 bput_bg(buf, off, 24, 22, 30); bput(buf, off, " " as *u8); bput_reset(buf, off)
325 return 0
326}
327func teng_render(buf: *u8, st: *i64) -> i64 {
328 let off: *i64 = sys_mmap(16) as *i64
329 off[0] = 0
330 bput_clear(buf, off)
331 bput(buf, off, " NISHI TACTICS Turn: " as *u8)
332 if st[0] == 0 { bput(buf, off, "PLAYER" as *u8) } else { bput(buf, off, "ENEMY" as *u8) }
333 bput(buf, off, " Squad " as *u8); bputn(buf, off, teng_team_alive(st, 0))
334 bput(buf, off, " Enemies " as *u8); bputn(buf, off, teng_team_alive(st, 1))
335 let ci: i64 = st[1]
336 if ci >= 0 { bput(buf, off, " ActiveHP " as *u8); bputn(buf, off, st[28 + ci]); bput(buf, off, " Moves " as *u8); bputn(buf, off, st[5]) }
337 bputc(buf, off, 10)
338 let m: i64 = st[4]
339 bput(buf, off, " " as *u8)
340 if m == 11 { bput(buf, off, ">> ENEMY DOWN!" as *u8) }
341 if m == 12 { bput(buf, off, ">> hit!" as *u8) }
342 if m == 13 { bput(buf, off, ">> missed" as *u8) }
343 if m == 9 { bput(buf, off, ">> out of range (move closer)" as *u8) }
344 if st[2] == 1 { bput(buf, off, " *** VICTORY -- squad cleared the map!" as *u8) }
345 if st[2] == 2 { bput(buf, off, " *** DEFEAT -- squad wiped" as *u8) }
346 bputc(buf, off, 10); bputc(buf, off, 10)
347 var y: i64 = 0
348 while y < st[65] {
349 var x: i64 = 0
350 while x < st[64] { teng_draw_cell(buf, off, st, x, y); x = x + 1 }
351 bput_reset(buf, off); bputc(buf, off, 10)
352 y = y + 1
353 }
354 bputc(buf, off, 10)
355 bput(buf, off, " w/a/s/d move active space=shoot nearest n=end unit r=restart q=quit" as *u8)
356 bputc(buf, off, 10)
357 return off[0]
358}
359
360// keyboard byte -> game key (w/a/s/d move, space shoot, n end, r restart)
361func teng_key(c: i64) -> i64 {
362 if c == 119 { return 0 }
363 if c == 115 { return 1 }
364 if c == 97 { return 2 }
365 if c == 100 { return 3 }
366 if c == 32 { return 4 }
367 if c == 110 { return 5 }
368 if c == 114 { return 6 }
369 return 9
370}
371// the NATIVE interactive play loop (mirrors nx_game_engine_lib's eng_run(cfg)): a generated tactics
372// game's main is just `return teng_run(cfg)`. Allocates state, inits from cfg, renders+reads+steps
373// until quit. Returns 0 (caller's main returns it -- no sys_exit, so it composes cleanly).
374func teng_run(cfg: *i64) -> i64 {
375 let st: *i64 = sys_mmap(K_MAGIC_8192) as *i64
376 let orig: *u8 = sys_mmap(128)
377 let raw: *u8 = sys_mmap(128)
378 let is_tty: i64 = term_raw_on(orig, raw)
379 teng_init(st, cfg)
380 let buf: *u8 = sys_mmap(K_MAGIC_16384)
381 let ib: *u8 = sys_mmap(8)
382 var running: i64 = 1
383 while running == 1 {
384 let len: i64 = teng_render(buf, st)
385 sys_write(1, buf, len)
386 let n: i64 = sys_read(0, ib, 1)
387 if n <= 0 { running = 0 }
388 if n > 0 {
389 let c: i64 = ib[0] as i64
390 if c == 113 { running = 0 }
391 if c == 81 { running = 0 }
392 if c != 113 { if c != 81 {
393 if st[2] != 0 { if c == 114 { teng_init(st, cfg) } }
394 if st[2] == 0 { teng_input(st, teng_key(c)) }
395 } }
396 }
397 }
398 if is_tty == 1 { term_raw_off(orig) }
399 return 0
400}