code wiki / (root) / nx_tactics_engine_lib.nx

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}