nx_game_engine_lib.nx source
↩ module page · 432 lines · 14842 B
1// nx_game_engine_lib.nx -- the GENERIC Nishi dungeon-crawler ENGINE (reusable library).
2//
3// This is the "engine" half of autonomous game-gen: it is written ONCE and reused by
4// every generated game. All per-game variation (title, difficulty, enemy/loot counts,
5// damage model, and the COMBAT FSM transition table) arrives at runtime in a config
6// array -- nothing is hardcoded. A generated per-game module just fills the config +
7// FSM table and calls eng_run(cfg); the emitter (nx_game_emit) writes that tiny module
8// from a data game-spec. Built native by Nishi's own toolchain (nx_cc->nxasm_x86).
9//
10// Combat is fully DATA-DRIVEN: g_fsm_step reads the supplied transition table (the
11// STATE_MACHINE shape generalized to runtime data). The combat EVENT CONVENTION the
12// spec's table must follow (same as the emitter-authored _pe_dungenc):
13// states 0=START 1=PLAYER_TURN 2=ENEMY_TURN 3=WIN 4=FLEE 5=END
14// events 0=enter 1=attack 2=enemy_act 3=defeat 4=flee 5=resolve
15//
16// HONEST AUTHORSHIP: this engine library + the emitter are TUTOR-authored tooling
17// (Claude). The GAMES the emitter produces from specs are emitter-authored output =
18// the autonomy lever (feed specs -> games hands-off). The combat FSM tables come from
19// the team's STATE_MACHINE specs. license_tier: ORIGINAL
20//
21// state st[]: 0=px 1=py 2=hp 3=score 4=level 5=mode 6=fsm 7=enemy_hp 8=rng 9=foes
22// 10=msg 11=enemy_x 12=enemy_y | config: 13=walls 14=enemy_base 15=enemy_per_lvl
23// 16=ehp_base 17=ehp_per_lvl 18=loot 19=dmg_base 20=dmg_rand 21=title_ptr
24// 22=fsm_table_ptr 23=fsm_nstates 24=fsm_nevents | world grid 16x12 bytes at st+256
25import "nx_syscalls.nx"
26const K_MAGIC_1103515245: i64 = 1103515245
27const K_MAGIC_12345: i64 = 12345
28const K_MAGIC_32767: i64 = 32767
29const K_MAGIC_8192: i64 = 8192
30const K_MAGIC_16384: i64 = 16384
31
32func g_rng(st: *i64) -> i64 {
33 var s: i64 = st[8]
34 s = s * K_MAGIC_1103515245 + K_MAGIC_12345
35 st[8] = s
36 return (s >> 16) & K_MAGIC_32767
37}
38func g_cell_get(st: *i64, x: i64, y: i64) -> i64 {
39 if x < 0 { return 1 }
40 if y < 0 { return 1 }
41 if x >= 16 { return 1 }
42 if y >= 12 { return 1 }
43 let w: *u8 = ((st as i64) + 256) as *u8
44 return w[y * 16 + x] as i64
45}
46func g_cell_set(st: *i64, x: i64, y: i64, v: i64) -> i64 {
47 let w: *u8 = ((st as i64) + 256) as *u8
48 w[y * 16 + x] = v as u8
49 return 0
50}
51// generic data-driven FSM step (reads the supplied transition table)
52func g_fsm_step(st: *i64, s: i64, e: i64) -> i64 {
53 let ns: i64 = st[23]
54 let ne: i64 = st[24]
55 if s < 0 { return 0 - 1 }
56 if e < 0 { return 0 - 1 }
57 if s >= ns { return 0 - 1 }
58 if e >= ne { return 0 - 1 }
59 let t: *i64 = st[22] as *i64
60 return t[s * ne + e]
61}
62func g_place(st: *i64, kind: i64, n: i64) -> i64 {
63 var placed: i64 = 0
64 var tries: i64 = 0
65 while placed < n {
66 if tries > 400 { return placed }
67 tries = tries + 1
68 let rx: i64 = 1 + (g_rng(st) % 14)
69 let ry: i64 = 1 + (g_rng(st) % 10)
70 var ok: i64 = 1
71 if g_cell_get(st, rx, ry) != 0 { ok = 0 }
72 if rx < 3 { if ry < 3 { ok = 0 } }
73 if ok == 1 { g_cell_set(st, rx, ry, kind); placed = placed + 1 }
74 }
75 return placed
76}
77func g_build_level(st: *i64) -> i64 {
78 var y: i64 = 0
79 while y < 12 {
80 var x: i64 = 0
81 while x < 16 {
82 var c: i64 = 0
83 if x == 0 { c = 1 }
84 if y == 0 { c = 1 }
85 if x == 15 { c = 1 }
86 if y == 11 { c = 1 }
87 g_cell_set(st, x, y, c)
88 x = x + 1
89 }
90 y = y + 1
91 }
92 var i: i64 = 0
93 while i < st[13] {
94 let rx: i64 = 1 + (g_rng(st) % 14)
95 let ry: i64 = 1 + (g_rng(st) % 10)
96 var ok: i64 = 1
97 if rx < 3 { if ry < 3 { ok = 0 } }
98 if ok == 1 { g_cell_set(st, rx, ry, 1) }
99 i = i + 1
100 }
101 g_cell_set(st, 1, 1, 0)
102 let lvl: i64 = st[4]
103 var ne: i64 = st[14] + lvl * st[15]
104 if ne > 10 { ne = 10 }
105 st[9] = g_place(st, 2, ne)
106 g_place(st, 3, 1)
107 g_place(st, 5, st[18])
108 st[0] = 1
109 st[1] = 1
110 st[5] = 0
111 st[6] = 0
112 return 0
113}
114func gi_init(st: *i64, seed: i64) -> i64 {
115 st[8] = seed
116 st[2] = 100
117 st[3] = 0
118 st[4] = 1
119 st[5] = 0
120 st[6] = 0
121 st[7] = 0
122 st[10] = 0
123 g_build_level(st)
124 return 0
125}
126func gi_next(st: *i64) -> i64 {
127 st[4] = st[4] + 1
128 st[3] = st[3] + 100
129 st[2] = st[2] + 25
130 if st[2] > 100 { st[2] = 100 }
131 g_build_level(st)
132 return 0
133}
134func g_try_move(st: *i64, dx: i64, dy: i64) -> i64 {
135 let nx: i64 = st[0] + dx
136 let ny: i64 = st[1] + dy
137 let c: i64 = g_cell_get(st, nx, ny)
138 if c == 1 { return 0 }
139 if c == 2 {
140 st[5] = 1
141 st[6] = g_fsm_step(st, 0, 0)
142 st[7] = st[16] + st[4] * st[17]
143 st[11] = nx
144 st[12] = ny
145 st[10] = 1
146 return 1
147 }
148 st[0] = nx
149 st[1] = ny
150 if c == 3 { st[5] = 2; st[10] = 2; return 2 }
151 if c == 5 { g_cell_set(st, nx, ny, 0); st[3] = st[3] + 25; st[10] = 3 }
152 return 0
153}
154func g_attack(st: *i64) -> i64 {
155 if st[5] != 1 { return 0 }
156 let dmg: i64 = st[19] + (g_rng(st) % st[20])
157 st[7] = st[7] - dmg
158 if st[7] <= 0 {
159 st[6] = g_fsm_step(st, 1, 3)
160 st[6] = g_fsm_step(st, st[6], 5)
161 g_cell_set(st, st[11], st[12], 0)
162 st[3] = st[3] + 50
163 st[9] = st[9] - 1
164 st[5] = 0
165 st[6] = 0
166 st[10] = 4
167 return 1
168 }
169 st[6] = g_fsm_step(st, 1, 1)
170 let edmg: i64 = 4 + (g_rng(st) % (6 + st[4]))
171 st[2] = st[2] - edmg
172 st[6] = g_fsm_step(st, 2, 2)
173 if st[2] <= 0 { st[2] = 0; st[5] = 3; st[10] = 5 }
174 return 0
175}
176func g_flee(st: *i64) -> i64 {
177 if st[5] != 1 { return 0 }
178 st[6] = g_fsm_step(st, 1, 4)
179 st[6] = g_fsm_step(st, st[6], 5)
180 st[5] = 0
181 st[6] = 0
182 st[10] = 6
183 return 0
184}
185func gi_input(st: *i64, key: i64) -> i64 {
186 let mode: i64 = st[5]
187 if mode == 3 {
188 if key == 4 { gi_init(st, st[8] + 7) }
189 if key == 5 { gi_init(st, st[8] + 7) }
190 return 0
191 }
192 if mode == 2 {
193 if key == 4 { gi_next(st) }
194 return 0
195 }
196 if mode == 1 {
197 if key == 4 { g_attack(st) }
198 if key == 0 { g_flee(st) }
199 if key == 1 { g_flee(st) }
200 if key == 2 { g_flee(st) }
201 if key == 3 { g_flee(st) }
202 return 0
203 }
204 if key == 0 { g_try_move(st, 0, 0 - 1) }
205 if key == 1 { g_try_move(st, 0, 1) }
206 if key == 2 { g_try_move(st, 0 - 1, 0) }
207 if key == 3 { g_try_move(st, 1, 0) }
208 return 0
209}
210func bputc(buf: *u8, off: *i64, c: i64) -> i64 { buf[off[0]] = c as u8; off[0] = off[0] + 1; return 0 }
211func bput(buf: *u8, off: *i64, s: *u8) -> i64 {
212 var i: i64 = 0
213 while s[i] != (0 as u8) { buf[off[0] + i] = s[i]; i = i + 1 }
214 off[0] = off[0] + i
215 return 0
216}
217func bputn(buf: *u8, off: *i64, v: i64) -> i64 {
218 var m: i64 = v
219 if m < 0 { m = 0 }
220 if m == 0 { bputc(buf, off, 48); return 0 }
221 let tmp: *u8 = sys_mmap(32)
222 var k: i64 = 0
223 while m > 0 { tmp[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
224 var j: i64 = 0
225 while j < k { bputc(buf, off, tmp[k - 1 - j] as i64); j = j + 1 }
226 return 0
227}
228func bput_bg(buf: *u8, off: *i64, r: i64, g: i64, b: i64) -> i64 {
229 bputc(buf, off, 27); bput(buf, off, "[48;2;" as *u8)
230 bputn(buf, off, r); bputc(buf, off, 59)
231 bputn(buf, off, g); bputc(buf, off, 59)
232 bputn(buf, off, b); bputc(buf, off, 109)
233 return 0
234}
235func bput_fg(buf: *u8, off: *i64, r: i64, g: i64, b: i64) -> i64 {
236 bputc(buf, off, 27); bput(buf, off, "[38;2;" as *u8)
237 bputn(buf, off, r); bputc(buf, off, 59)
238 bputn(buf, off, g); bputc(buf, off, 59)
239 bputn(buf, off, b); bputc(buf, off, 109)
240 return 0
241}
242func bput_reset(buf: *u8, off: *i64) -> i64 { bputc(buf, off, 27); bput(buf, off, "[0m" as *u8); return 0 }
243func bput_clear(buf: *u8, off: *i64) -> i64 {
244 bputc(buf, off, 27); bput(buf, off, "[2J" as *u8); bputc(buf, off, 27); bput(buf, off, "[H" as *u8); return 0
245}
246func draw_cell(buf: *u8, off: *i64, d: i64) -> i64 {
247 if d == 1 { bput_bg(buf, off, 70, 80, 110); bput(buf, off, " " as *u8); bput_reset(buf, off); return 0 }
248 bput_bg(buf, off, 24, 22, 30)
249 if d == 0 { bput(buf, off, " " as *u8) }
250 if d == 2 { bput_fg(buf, off, 90, 230, 120); bput(buf, off, "()" as *u8) }
251 if d == 3 { bput_fg(buf, off, 220, 60, 60); bput(buf, off, "gg" as *u8) }
252 if d == 4 { bput_fg(buf, off, 240, 220, 72); bput(buf, off, ">>" as *u8) }
253 if d == 5 { bput_fg(buf, off, 248, 176, 44); bput(buf, off, "**" as *u8) }
254 bput_reset(buf, off)
255 return 0
256}
257func render_hud(buf: *u8, off: *i64, st: *i64) -> i64 {
258 bput(buf, off, " " as *u8); bput(buf, off, st[21] as *u8)
259 bput(buf, off, " HP " as *u8); bputn(buf, off, st[2])
260 bput(buf, off, "/100 Score " as *u8); bputn(buf, off, st[3])
261 bput(buf, off, " Depth " as *u8); bputn(buf, off, st[4])
262 bput(buf, off, " Foes " as *u8); bputn(buf, off, st[9])
263 if (st[25] & 1) != 0 { bput(buf, off, " Best " as *u8); bputn(buf, off, st[27]) }
264 bputc(buf, off, 10); bputc(buf, off, 10)
265 return 0
266}
267func render_grid(buf: *u8, off: *i64, st: *i64) -> i64 {
268 let px: i64 = st[0]
269 let py: i64 = st[1]
270 var y: i64 = 0
271 while y < 12 {
272 var x: i64 = 0
273 while x < 16 {
274 var d: i64 = 0
275 let cell: i64 = g_cell_get(st, x, y)
276 if cell == 1 { d = 1 }
277 if cell == 2 { d = 3 }
278 if cell == 3 { d = 4 }
279 if cell == 5 { d = 5 }
280 if x == px { if y == py { d = 2 } }
281 draw_cell(buf, off, d)
282 x = x + 1
283 }
284 bput_reset(buf, off); bputc(buf, off, 10)
285 y = y + 1
286 }
287 return 0
288}
289func render_bar(buf: *u8, off: *i64, val: i64, max: i64, r: i64, g: i64) -> i64 {
290 var n: i64 = val * 20 / max
291 if n < 0 { n = 0 }
292 if n > 20 { n = 20 }
293 bputc(buf, off, 91); bput_fg(buf, off, r, g, 60)
294 var i: i64 = 0
295 while i < 20 { if i < n { bputc(buf, off, 35) } else { bputc(buf, off, 45) } i = i + 1 }
296 bput_reset(buf, off)
297 bputc(buf, off, 93); bputc(buf, off, 32); bputn(buf, off, val); bputc(buf, off, 10)
298 return 0
299}
300func render_combat(buf: *u8, off: *i64, st: *i64) -> i64 {
301 bputc(buf, off, 10)
302 bput(buf, off, " -- COMBAT (data-driven FSM) --" as *u8); bputc(buf, off, 10); bputc(buf, off, 10)
303 bput(buf, off, " YOU " as *u8); render_bar(buf, off, st[2], 100, 90, 230)
304 var emax: i64 = st[16] + st[4] * st[17]
305 if emax < 1 { emax = 1 }
306 var ehp: i64 = st[7]
307 if ehp < 0 { ehp = 0 }
308 bput(buf, off, " FOE " as *u8); render_bar(buf, off, ehp, emax, 220, 60)
309 bputc(buf, off, 10)
310 return 0
311}
312func render_foot(buf: *u8, off: *i64, st: *i64) -> i64 {
313 let mode: i64 = st[5]
314 bputc(buf, off, 10)
315 if mode == 0 { bput(buf, off, " move: w a s d reach >> to descend quit: q" as *u8) }
316 if mode == 1 { bput(buf, off, " space = attack w/a/s/d = flee quit: q" as *u8) }
317 if mode == 2 { bput(buf, off, " LEVEL CLEAR! space = descend deeper quit: q" as *u8) }
318 if mode == 3 { bput(buf, off, " YOU DIED. r = new run quit: q" as *u8) }
319 bputc(buf, off, 10)
320 return 0
321}
322func eng_render(buf: *u8, st: *i64) -> i64 {
323 let off: *i64 = sys_mmap(16) as *i64
324 off[0] = 0
325 bput_clear(buf, off)
326 render_hud(buf, off, st)
327 if st[5] == 1 { render_combat(buf, off, st) } else { render_grid(buf, off, st) }
328 render_foot(buf, off, st)
329 sys_write(1, buf, off[0])
330 return 0
331}
332func map_key(c: i64) -> i64 {
333 if c == 119 { return 0 }
334 if c == 115 { return 1 }
335 if c == 97 { return 2 }
336 if c == 100 { return 3 }
337 if c == 32 { return 4 }
338 if c == 102 { return 4 }
339 if c == 13 { return 4 }
340 if c == 10 { return 4 }
341 if c == 114 { return 5 }
342 return 9
343}
344func term_raw_on(orig: *u8, raw: *u8) -> i64 {
345 let g: i64 = sys_ioctl(0, 0x5401, orig as i64)
346 if g != 0 { return 0 }
347 var i: i64 = 0
348 while i < 60 { raw[i] = orig[i]; i = i + 1 }
349 var lf: i64 = raw[12] as i64
350 lf = lf & (255 - 2)
351 lf = lf & (255 - 8)
352 raw[12] = lf as u8
353 raw[17 + 6] = 1 as u8
354 raw[17 + 5] = 0 as u8
355 sys_ioctl(0, 0x5402, raw as i64)
356 return 1
357}
358func term_raw_off(orig: *u8) -> i64 { sys_ioctl(0, 0x5402, orig as i64); return 0 }
359
360// ---- COMPOSED PART: persistence (needs save). cfg[25]=needs-mask (bit0=save),
361// cfg[26]=save-file path, st[27]=best score. Demonstrates the parts-composition
362// doctrine: a game declares `needs save` in its spec -> the emitter wires this part ->
363// the engine persists across runs. Same wiring path that networking/audio/accounts use.
364func eng_load_best(st: *i64) -> i64 {
365 if (st[25] & 1) == 0 { return 0 }
366 let lenp: *i64 = sys_mmap(16) as *i64
367 let buf: *u8 = sys_read_file(st[26] as *u8, lenp)
368 if buf == (0 as i64) as *u8 { return 0 }
369 let n: i64 = lenp[0]
370 var v: i64 = 0
371 var i: i64 = 0
372 var go: i64 = 1
373 while go == 1 {
374 if i >= n { go = 0 }
375 if go == 1 {
376 let c: i64 = buf[i] as i64
377 if c < 48 { go = 0 }
378 if c > 57 { go = 0 }
379 if go == 1 { v = v * 10 + (c - 48); i = i + 1 }
380 }
381 }
382 st[27] = v
383 return 0
384}
385func eng_save_best(st: *i64) -> i64 {
386 if (st[25] & 1) == 0 { return 0 }
387 let fd: i64 = sys_openat_wr(st[26] as *u8, 0x1a4)
388 if fd < 0 { return 0 }
389 let off: *i64 = sys_mmap(16) as *i64
390 off[0] = 0
391 let b: *u8 = sys_mmap(32)
392 bputn(b, off, st[27])
393 sys_write(fd, b, off[0])
394 sys_close(fd)
395 return 0
396}
397
398// the engine entry point: caller fills cfg[13..24] params + cfg[25..26] composed-part
399// config (needs-mask + save path) + the fsm table, then calls this.
400func eng_run(cfg: *i64) -> i64 {
401 let st: *i64 = sys_mmap(K_MAGIC_8192) as *i64
402 var i: i64 = 13
403 while i < 27 { st[i] = cfg[i]; i = i + 1 }
404 let orig: *u8 = sys_mmap(128)
405 let raw: *u8 = sys_mmap(128)
406 let is_tty: i64 = term_raw_on(orig, raw)
407 gi_init(st, sys_now_ms())
408 st[27] = 0
409 eng_load_best(st)
410 let buf: *u8 = sys_mmap(K_MAGIC_16384)
411 let ib: *u8 = sys_mmap(8)
412 var running: i64 = 1
413 while running == 1 {
414 eng_render(buf, st)
415 let n: i64 = sys_read(0, ib, 1)
416 if n <= 0 { running = 0 }
417 if n > 0 {
418 let c: i64 = ib[0] as i64
419 if c == 113 { running = 0 }
420 if c == 81 { running = 0 }
421 if c != 113 { if c != 81 { gi_input(st, map_key(c)) } }
422 }
423 if st[3] > st[27] { st[27] = st[3]; eng_save_best(st) }
424 }
425 if is_tty == 1 { term_raw_off(orig) }
426 let off: *i64 = sys_mmap(16) as *i64
427 off[0] = 0
428 bput_clear(buf, off)
429 bput(buf, off, "thanks for playing -- " as *u8); bput(buf, off, st[21] as *u8); bputc(buf, off, 10)
430 sys_write(1, buf, off[0])
431 return st[3]
432}