nx_game_engine_compose_test.nx source
↩ module page · 131 lines · 5408 B
1// nx_game_engine_compose_test.nx -- the full game-loop scaffold.
2//
3// All 4 new primitives composed with the prior Minecraft-MCU
4// substrate. Realistic game-loop shape:
5// 1. world_seed generates 16x16x8 terrain from seed=42
6// 2. font + 1bpp framebuffer prepared
7// 3. raycast + dither pattern renders the world to fb
8// 4. text_render overlays "Z42" coordinate
9// 5. input drives camera movement
10// 6. q14_math normalizes camera direction
11//
12// This proves the substrate-side game engine works end-to-end on a
13// hardware-floor that maps cleanly to ESP32-class silicon.
14
15import "nx_syscalls.nx"
16import "nx_tier.nx"
17import "nx_palette.nx"
18import "nx_tissue.nx"
19import "nx_raycast_voxel.nx"
20import "nx_fb_1bpp.nx"
21import "nx_input_gpio.nx"
22import "nx_q14_math.nx"
23import "nx_font_1bpp.nx"
24import "nx_text_render.nx"
25import "nx_world_seed.nx"
26
27func main() -> i64 {
28 // ===== Step 1: deterministic world generation ===================
29 let world: *NxTissue = nx_tissue_new(16, 16, 8, NX_BPP_3)
30 let cells_filled: nx_int = nx_world_seed_generate(world, 42)
31 if cells_filled <= 0 { return 1 }
32 // Stone floor at z=0 should be all stone
33 if nx_tissue_get(world, 0, 0, 0) != NX_WS_PAL_STONE { return 2 }
34
35 // Verify determinism: regenerate, byte-identical
36 let world2: *NxTissue = nx_tissue_new(16, 16, 8, NX_BPP_3)
37 nx_world_seed_generate(world2, 42)
38 if nx_tissue_get(world, 8, 8, 4) != nx_tissue_get(world2, 8, 8, 4) { return 3 }
39
40 // ===== Step 2: framebuffer + font ===============================
41 let fb: *NxFb1bpp = nx_fb_1bpp_new(32, 16)
42 nx_fb_1bpp_clear(fb)
43
44 // Tiny 3-char "Z42" font (5 chars including space + minus)
45 let glyph_data: *u8 = (sys_mmap(64)) as *u8
46 // Just need glyphs for '4', '2', 'Z' = decimal 52, 50, 90
47 // Easiest: use base_char='2' and glyph_count=89 won't fit...
48 // For test, base_char='0', glyph_count=43 (0..Z range 48..90)
49 // Each glyph: 3 cols x 1 byte = 3 bytes. 43 * 3 = 129 bytes data buffer
50 let big_data: *u8 = (sys_mmap(256)) as *u8
51 // Initialize the digits 0-9 ('0'..'9' = 48..57) with stub patterns
52 var i: nx_size = 0
53 while i < 129 {
54 big_data[i] = (i & 31) as u8 // arbitrary distinct patterns
55 i = i + 1
56 }
57 let font: *NxFont = nx_font_new(big_data, 3, 5, 43, 48)
58 if (font as i64) == 0 { return 4 }
59 if nx_font_has_char(font, 50) != 1 { return 5 } // '2'
60 if nx_font_has_char(font, 90) != 1 { return 6 } // 'Z'
61
62 // ===== Step 3: render text "42" to framebuffer ==================
63 let buf: *u8 = (sys_mmap(8)) as *u8
64 buf[0] = 52 as u8 // '4'
65 buf[1] = 50 as u8 // '2'
66 let adv: nx_size = nx_text_render_string(fb, font, 0, 0, buf, 2)
67 if adv != 6 { return 7 }
68 // Some pixels lit (depends on stub glyph patterns)
69 if nx_fb_1bpp_count_on(fb) <= 0 { return 8 }
70
71 // ===== Step 4: q14_math camera setup ============================
72 // Camera at (3.0, 3.0, 4.0) looking +X (normalized)
73 let cam_x_q14: nx_int = 3 * NX_Q14
74 let cam_y_q14: nx_int = 3 * NX_Q14
75 let cam_z_q14: nx_int = 4 * NX_Q14
76
77 // Initial dir = (1, 0, 0); verify it normalizes to length 1
78 let len: nx_int = nx_q14_vec3_length(NX_Q14, 0, 0)
79 if nx_q14_abs(len - NX_Q14) > 500 { return 9 }
80
81 // Rotate dir by 45° around Z axis using sin/cos
82 let cos45: nx_int = nx_q14_cos(NX_Q14_2PI / 8)
83 let sin45: nx_int = nx_q14_sin(NX_Q14_2PI / 8)
84 // dx' = dx*cos - dy*sin = 1 * cos - 0 * sin = cos45
85 // dy' = dx*sin + dy*cos = 1 * sin + 0 * cos = sin45
86 let dir_x_rotated: nx_int = cos45
87 let dir_y_rotated: nx_int = sin45
88 // length of (cos45, sin45) = 1
89 let rot_len: nx_int = nx_q14_vec3_length(dir_x_rotated, dir_y_rotated, 0)
90 if nx_q14_abs(rot_len - NX_Q14) > 1000 { return 10 }
91
92 // ===== Step 5: raycast in the rotated direction ================
93 let ray: *NxRay = nx_ray_new(cam_x_q14, cam_y_q14, cam_z_q14,
94 dir_x_rotated, dir_y_rotated, 0, 32)
95 let hit: *NxRayHit = (sys_mmap(56)) as *NxRayHit
96 let v: nx_int = nx_raycast_voxel(ray, world, hit)
97 // Depending on world content the ray will hit something at this angle
98 // through the terrain. Either HIT or MAX_STEPS / OUT_OF_BOX is OK.
99 if v == NX_RV_HIT {
100 // dither the hit voxel as bright
101 nx_fb_1bpp_dither_4(fb, 10, 8, 6, 4, 3)
102 }
103
104 // ===== Step 6: input drives the camera ==========================
105 let input: *NxInputGpio = nx_input_gpio_new(10000, 500000)
106 // Press LEFT at t=15000 (past 10ms debounce on first poll)
107 let edge: nx_int = nx_input_gpio_poll(input, NX_BTN_LEFT, 1, 15000)
108 if edge != NX_BE_PRESSED { return 11 }
109 if nx_input_gpio_is_pressed(input, NX_BTN_LEFT) != 1 { return 12 }
110
111 // ===== Step 7: forensic assertions ==============================
112 // World was generated with cells > 0
113 if cells_filled <= 0 { return 13 }
114 // Framebuffer has SOMETHING rendered
115 if nx_fb_1bpp_count_on(fb) <= 0 { return 14 }
116 // Camera dir normalized properly
117 if nx_q14_abs(rot_len - NX_Q14) > 1000 { return 15 }
118
119 // Stone floor consistency: every (x,y) at z=0 is stone
120 var fx: nx_size = 0
121 while fx < 16 {
122 var fy: nx_size = 0
123 while fy < 16 {
124 if nx_tissue_get(world, fx, fy, 0) != NX_WS_PAL_STONE { return 16 }
125 fy = fy + 1
126 }
127 fx = fx + 1
128 }
129
130 return 0
131}