nx_tactics_native.nx source
↩ module page · 372 lines · 13384 B
1// nx_tactics_native.nx -- PLAYABLE XCOM-style turn-based TACTICS game (a 2nd game MODE,
2// proving the Nishi game ecosystem generalizes beyond the dungeon-crawler). Native binary
3// via Nishi's own toolchain (nx_cc->nxasm_x86, no gcc), terminal-rendered, hostable in the
4// arcade. Reuses the engine library's terminal helpers (bput*/term_raw_*); the tactics
5// logic (squads, action points, cover/hit-chance, enemy AI) is new here.
6//
7// HONEST AUTHORSHIP: tutor (Claude) authored this game module + the engine lib helpers it
8// reuses. Generalizing tactics into a spec-driven emitter (like nx_game_emit does for
9// dungeons) is the named next rung (G-ECO-005 more modes). license_tier: ORIGINAL
10//
11// state region (i64 at st): 0=turn(0=player 1=enemy) 1=cur(active player unit) 2=mode(0play
12// 1win 2lose) 3=rng 4=msg 5=moves_left 6=has_shot
13// units (NUNITS=4: 0,1 player / 2,3 enemy): ux=st[16+i] uy=st[22+i] uhp=st[28+i]
14// uteam=st[34+i] ualive=st[40+i]. world grid 16x12 bytes at st+2048 (0 floor 1 wall 2 cover).
15import "nx_syscalls.nx"
16import "nx_game_engine_lib.nx"
17const K_MAGIC_1103515245: i64 = 1103515245
18const K_MAGIC_12345: i64 = 12345
19const K_MAGIC_32767: i64 = 32767
20const K_MAGIC_2048: i64 = 2048
21const K_MAGIC_9999: i64 = 9999
22const K_MAGIC_8192: i64 = 8192
23const K_MAGIC_16384: i64 = 16384
24
25func tac_rng(st: *i64) -> i64 {
26 var s: i64 = st[3]
27 s = s * K_MAGIC_1103515245 + K_MAGIC_12345
28 st[3] = s
29 return (s >> 16) & K_MAGIC_32767
30}
31func tac_cell_get(st: *i64, x: i64, y: i64) -> i64 {
32 if x < 0 { return 1 }
33 if y < 0 { return 1 }
34 if x >= 16 { return 1 }
35 if y >= 12 { return 1 }
36 let w: *u8 = ((st as i64) + K_MAGIC_2048) as *u8
37 return w[y * 16 + x] as i64
38}
39func tac_cell_set(st: *i64, x: i64, y: i64, v: i64) -> i64 {
40 let w: *u8 = ((st as i64) + K_MAGIC_2048) as *u8
41 w[y * 16 + x] = v as u8
42 return 0
43}
44func tac_unit_at(st: *i64, x: i64, y: i64) -> i64 {
45 var i: i64 = 0
46 while i < 4 {
47 if st[40 + i] == 1 {
48 if st[16 + i] == x { if st[22 + i] == y { return i } }
49 }
50 i = i + 1
51 }
52 return 0 - 1
53}
54func tac_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
55func tac_dist(ax: i64, ay: i64, bx: i64, by: i64) -> i64 { return tac_abs(ax - bx) + tac_abs(ay - by) }
56func tac_team_alive(st: *i64, team: i64) -> i64 {
57 var c: i64 = 0
58 var i: i64 = 0
59 while i < 4 {
60 if st[40 + i] == 1 { if st[34 + i] == team { c = c + 1 } }
61 i = i + 1
62 }
63 return c
64}
65// nearest alive unit of the OTHER team to (x,y); -1 if none
66func tac_nearest_foe(st: *i64, x: i64, y: i64, myteam: i64) -> i64 {
67 var best: i64 = 0 - 1
68 var bd: i64 = K_MAGIC_9999
69 var i: i64 = 0
70 while i < 4 {
71 if st[40 + i] == 1 {
72 if st[34 + i] != myteam {
73 let d: i64 = tac_dist(x, y, st[16 + i], st[22 + i])
74 if d < bd { bd = d; best = i }
75 }
76 }
77 i = i + 1
78 }
79 return best
80}
81func tac_first_player(st: *i64) -> i64 {
82 var i: i64 = 0
83 while i < 4 {
84 if st[40 + i] == 1 { if st[34 + i] == 0 { return i } }
85 i = i + 1
86 }
87 return 0 - 1
88}
89func tac_next_player(st: *i64, from: i64) -> i64 {
90 var i: i64 = from + 1
91 while i < 4 {
92 if st[40 + i] == 1 { if st[34 + i] == 0 { return i } }
93 i = i + 1
94 }
95 return 0 - 1
96}
97// place a unit on a random free floor cell within a corner box
98func tac_place_unit(st: *i64, idx: i64, x0: i64, y0: i64, team: i64) -> i64 {
99 var tries: i64 = 0
100 while tries < 80 {
101 tries = tries + 1
102 let rx: i64 = x0 + (tac_rng(st) % 5)
103 let ry: i64 = y0 + (tac_rng(st) % 5)
104 var ok: i64 = 1
105 if tac_cell_get(st, rx, ry) != 0 { ok = 0 }
106 if tac_unit_at(st, rx, ry) >= 0 { ok = 0 }
107 if ok == 1 {
108 st[16 + idx] = rx; st[22 + idx] = ry; st[28 + idx] = 10
109 st[34 + idx] = team; st[40 + idx] = 1
110 return 0
111 }
112 }
113 st[16 + idx] = x0; st[22 + idx] = y0; st[28 + idx] = 10; st[34 + idx] = team; st[40 + idx] = 1
114 return 0
115}
116func tac_build(st: *i64) -> i64 {
117 var y: i64 = 0
118 while y < 12 {
119 var x: i64 = 0
120 while x < 16 {
121 var c: i64 = 0
122 if x == 0 { c = 1 }
123 if y == 0 { c = 1 }
124 if x == 15 { c = 1 }
125 if y == 11 { c = 1 }
126 tac_cell_set(st, x, y, c)
127 x = x + 1
128 }
129 y = y + 1
130 }
131 var i: i64 = 0
132 while i < 12 {
133 let rx: i64 = 2 + (tac_rng(st) % 12)
134 let ry: i64 = 2 + (tac_rng(st) % 8)
135 tac_cell_set(st, rx, ry, 1)
136 i = i + 1
137 }
138 var j: i64 = 0
139 while j < 10 {
140 let rx: i64 = 2 + (tac_rng(st) % 12)
141 let ry: i64 = 2 + (tac_rng(st) % 8)
142 if tac_cell_get(st, rx, ry) == 0 { tac_cell_set(st, rx, ry, 2) }
143 j = j + 1
144 }
145 tac_place_unit(st, 0, 1, 1, 0)
146 tac_place_unit(st, 1, 1, 6, 0)
147 tac_place_unit(st, 2, 11, 2, 1)
148 tac_place_unit(st, 3, 11, 7, 1)
149 return 0
150}
151func tac_init(st: *i64, seed: i64) -> i64 {
152 st[3] = seed
153 tac_build(st)
154 st[0] = 0
155 st[1] = tac_first_player(st)
156 st[2] = 0
157 st[4] = 0
158 st[5] = 5
159 st[6] = 0
160 return 0
161}
162func tac_check_end(st: *i64) -> i64 {
163 if tac_team_alive(st, 1) == 0 { st[2] = 1 }
164 if tac_team_alive(st, 0) == 0 { st[2] = 2 }
165 return 0
166}
167// hit chance (out of 10) for shooter at distance d vs target standing on `cover` (1/0)
168func tac_chance(d: i64, cover: i64) -> i64 {
169 var c: i64 = 8 - (d / 3)
170 if cover == 1 { c = c - 3 }
171 if c < 1 { c = 1 }
172 if c > 9 { c = 9 }
173 return c
174}
175func tac_move_unit(st: *i64, ui: i64, dx: i64, dy: i64) -> i64 {
176 if st[5] <= 0 { st[4] = 7; return 0 }
177 let nx: i64 = st[16 + ui] + dx
178 let ny: i64 = st[22 + ui] + dy
179 if tac_cell_get(st, nx, ny) == 1 { return 0 }
180 if tac_unit_at(st, nx, ny) >= 0 { return 0 }
181 st[16 + ui] = nx; st[22 + ui] = ny
182 st[5] = st[5] - 1
183 return 0
184}
185func tac_end_unit(st: *i64) -> i64 {
186 let nxt: i64 = tac_next_player(st, st[1])
187 if nxt >= 0 { st[1] = nxt; st[5] = 5; st[6] = 0; return 0 }
188 // no more player units -> enemy turn
189 tac_enemy_turn(st)
190 st[0] = 0
191 st[1] = tac_first_player(st)
192 st[5] = 5
193 st[6] = 0
194 tac_check_end(st)
195 return 0
196}
197func tac_shoot(st: *i64, ui: i64) -> i64 {
198 let ei: i64 = tac_nearest_foe(st, st[16 + ui], st[22 + ui], st[34 + ui])
199 if ei < 0 { st[4] = 8; return 0 }
200 let d: i64 = tac_dist(st[16 + ui], st[22 + ui], st[16 + ei], st[22 + ei])
201 if d > 9 { st[4] = 9; return 0 }
202 var cover: i64 = 0
203 if tac_cell_get(st, st[16 + ei], st[22 + ei]) == 2 { cover = 1 }
204 let ch: i64 = tac_chance(d, cover)
205 let roll: i64 = tac_rng(st) % 10
206 if roll < ch {
207 st[28 + ei] = st[28 + ei] - 5
208 if st[28 + ei] <= 0 { st[40 + ei] = 0; st[4] = 11 } else { st[4] = 12 }
209 } else { st[4] = 13 }
210 return 0
211}
212func tac_enemy_turn(st: *i64) -> i64 {
213 var i: i64 = 2
214 while i < 4 {
215 if st[40 + i] == 1 {
216 let pi: i64 = tac_nearest_foe(st, st[16 + i], st[22 + i], 1)
217 if pi >= 0 {
218 var steps: i64 = 0
219 while steps < 4 {
220 let tx: i64 = st[16 + pi]
221 let ty: i64 = st[22 + pi]
222 var dx: i64 = 0
223 var dy: i64 = 0
224 if tac_abs(tx - st[16 + i]) >= tac_abs(ty - st[22 + i]) {
225 if tx > st[16 + i] { dx = 1 }
226 if tx < st[16 + i] { dx = 0 - 1 }
227 } else {
228 if ty > st[22 + i] { dy = 1 }
229 if ty < st[22 + i] { dy = 0 - 1 }
230 }
231 let nx: i64 = st[16 + i] + dx
232 let ny: i64 = st[22 + i] + dy
233 var moved: i64 = 0
234 if tac_cell_get(st, nx, ny) != 1 {
235 if tac_unit_at(st, nx, ny) < 0 {
236 if tac_dist(nx, ny, tx, ty) >= 1 { st[16 + i] = nx; st[22 + i] = ny; moved = 1 }
237 }
238 }
239 if moved == 0 { steps = 4 }
240 steps = steps + 1
241 }
242 let d: i64 = tac_dist(st[16 + i], st[22 + i], st[16 + pi], st[22 + pi])
243 if d <= 9 {
244 var cover: i64 = 0
245 if tac_cell_get(st, st[16 + pi], st[22 + pi]) == 2 { cover = 1 }
246 let ch: i64 = tac_chance(d, cover)
247 let roll: i64 = tac_rng(st) % 10
248 if roll < ch {
249 st[28 + pi] = st[28 + pi] - 5
250 if st[28 + pi] <= 0 { st[40 + pi] = 0 }
251 }
252 }
253 }
254 }
255 i = i + 1
256 }
257 return 0
258}
259// key: 0 up 1 down 2 left 3 right 4 shoot 5 end-unit 6 restart
260func tac_input(st: *i64, key: i64) -> i64 {
261 if st[2] != 0 {
262 if key == 6 { tac_init(st, st[3] + 7) }
263 return 0
264 }
265 if st[0] != 0 { return 0 }
266 let ci: i64 = st[1]
267 if ci < 0 { return 0 }
268 if key == 0 { tac_move_unit(st, ci, 0, 0 - 1) }
269 if key == 1 { tac_move_unit(st, ci, 0, 1) }
270 if key == 2 { tac_move_unit(st, ci, 0 - 1, 0) }
271 if key == 3 { tac_move_unit(st, ci, 1, 0) }
272 if key == 4 { tac_shoot(st, ci); st[6] = 1; tac_check_end(st); if st[2] == 0 { tac_end_unit(st) } }
273 if key == 5 { tac_end_unit(st) }
274 return 0
275}
276func tac_draw_cell(buf: *u8, off: *i64, st: *i64, x: i64, y: i64) -> i64 {
277 let ui: i64 = tac_unit_at(st, x, y)
278 if ui >= 0 {
279 if st[34 + ui] == 0 {
280 bput_bg(buf, off, 24, 40, 28)
281 if ui == st[1] { bput_fg(buf, off, 250, 240, 90) } else { bput_fg(buf, off, 90, 230, 120) }
282 bput(buf, off, "P" as *u8); bputn(buf, off, ui + 1)
283 } else {
284 bput_bg(buf, off, 40, 24, 24); bput_fg(buf, off, 230, 70, 70); bput(buf, off, "E" as *u8); bputn(buf, off, ui - 1)
285 }
286 bput_reset(buf, off)
287 return 0
288 }
289 let c: i64 = tac_cell_get(st, x, y)
290 if c == 1 { bput_bg(buf, off, 70, 80, 110); bput(buf, off, " " as *u8); bput_reset(buf, off); return 0 }
291 if c == 2 { bput_bg(buf, off, 86, 70, 40); bput(buf, off, "##" as *u8); bput_reset(buf, off); return 0 }
292 bput_bg(buf, off, 24, 22, 30); bput(buf, off, " " as *u8); bput_reset(buf, off)
293 return 0
294}
295func tac_render(buf: *u8, st: *i64) -> i64 {
296 let off: *i64 = sys_mmap(16) as *i64
297 off[0] = 0
298 bput_clear(buf, off)
299 bput(buf, off, " NISHI TACTICS Turn: " as *u8)
300 if st[0] == 0 { bput(buf, off, "PLAYER" as *u8) } else { bput(buf, off, "ENEMY" as *u8) }
301 bput(buf, off, " Squad " as *u8); bputn(buf, off, tac_team_alive(st, 0))
302 bput(buf, off, " Enemies " as *u8); bputn(buf, off, tac_team_alive(st, 1))
303 let ci: i64 = st[1]
304 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]) }
305 bputc(buf, off, 10)
306 let m: i64 = st[4]
307 bput(buf, off, " " as *u8)
308 if m == 11 { bput(buf, off, ">> ENEMY DOWN!" as *u8) }
309 if m == 12 { bput(buf, off, ">> hit!" as *u8) }
310 if m == 13 { bput(buf, off, ">> missed" as *u8) }
311 if m == 9 { bput(buf, off, ">> out of range (move closer)" as *u8) }
312 if m == 7 { bput(buf, off, ">> no moves left (space=shoot, n=end)" as *u8) }
313 if st[2] == 1 { bput(buf, off, " *** VICTORY -- squad cleared the map! (r = new battle)" as *u8) }
314 if st[2] == 2 { bput(buf, off, " *** DEFEAT -- squad wiped (r = new battle)" as *u8) }
315 bputc(buf, off, 10); bputc(buf, off, 10)
316 var y: i64 = 0
317 while y < 12 {
318 var x: i64 = 0
319 while x < 16 {
320 tac_draw_cell(buf, off, st, x, y)
321 x = x + 1
322 }
323 bput_reset(buf, off); bputc(buf, off, 10)
324 y = y + 1
325 }
326 bputc(buf, off, 10)
327 bput(buf, off, " w/a/s/d move active unit space = shoot nearest foe n = end unit r restart q quit" as *u8)
328 bputc(buf, off, 10)
329 bput(buf, off, " green P# = your squad (yellow = active) red E# = enemy ## = cover (harder to hit)" as *u8)
330 bputc(buf, off, 10)
331 sys_write(1, buf, off[0])
332 return 0
333}
334func tac_key(c: i64) -> i64 {
335 if c == 119 { return 0 }
336 if c == 115 { return 1 }
337 if c == 97 { return 2 }
338 if c == 100 { return 3 }
339 if c == 32 { return 4 }
340 if c == 110 { return 5 }
341 if c == 114 { return 6 }
342 return 9
343}
344func main() -> i64 {
345 let st: *i64 = sys_mmap(K_MAGIC_8192) as *i64
346 let orig: *u8 = sys_mmap(128)
347 let raw: *u8 = sys_mmap(128)
348 let is_tty: i64 = term_raw_on(orig, raw)
349 tac_init(st, sys_now_ms())
350 let buf: *u8 = sys_mmap(K_MAGIC_16384)
351 let ib: *u8 = sys_mmap(8)
352 var running: i64 = 1
353 while running == 1 {
354 tac_render(buf, st)
355 let n: i64 = sys_read(0, ib, 1)
356 if n <= 0 { running = 0 }
357 if n > 0 {
358 let c: i64 = ib[0] as i64
359 if c == 113 { running = 0 }
360 if c == 81 { running = 0 }
361 if c != 113 { if c != 81 { tac_input(st, tac_key(c)) } }
362 }
363 }
364 if is_tty == 1 { term_raw_off(orig) }
365 let off: *i64 = sys_mmap(16) as *i64
366 off[0] = 0
367 bput_clear(buf, off)
368 bput(buf, off, "thanks for playing -- NISHI TACTICS\n" as *u8)
369 sys_write(1, buf, off[0])
370 sys_exit(0)
371 return 0
372}