// snake — ncurses snake game in ww. // // What this proves: // 1. ww the language can express an interactive program. // 2. The @symbol FFI mechanism binds to libncurses and libc. // 3. 6l's new dynamic linker (PT_INTERP + PT_DYNAMIC + PLT/GOT) // reaches a real .so at runtime — `ldd snake` shows // libncurses.so.6 and libc.so.6 as NEEDED entries. // // Build: see ./Makefile. Pipeline is the ww toolchain (6c → 6a → 6l) // with -l ncurses -l c. No cc. // // Controls: w/a/s/d to steer, q to quit. // ---- ncurses bindings ------------------------------------------------- // // mvaddstr is preferred over mvaddch because chtype's width depends on // whether ncurses was built with --enable-widec. A NUL-terminated // 1-byte string sidesteps the question and works on either build. @symbol("initscr") fn nc_initscr() *void; @symbol("endwin") fn nc_endwin() i32; @symbol("cbreak") fn nc_cbreak() i32; @symbol("noecho") fn nc_noecho() i32; @symbol("curs_set") fn nc_curs_set(v: i32) i32; @symbol("timeout") fn nc_timeout(d: i32) void; @symbol("getch") fn nc_getch() i32; @symbol("clear") fn nc_clear() i32; @symbol("refresh") fn nc_refresh() i32; @symbol("mvaddstr") fn nc_mvaddstr(y: i32, x: i32, s: *u8) i32; // ---- libc bindings ---------------------------------------------------- @symbol("rand") fn c_rand() i32; @symbol("srand") fn c_srand(s: u32) void; @symbol("time") fn c_time(t: *i64) i64; // ---- board sizing ----------------------------------------------------- def W: i32 = 40; def H: i32 = 20; def CAP: i32 = 200; // max snake length, plenty for 40x20 def DIR_UP: i32 = 0; def DIR_RIGHT: i32 = 1; def DIR_DOWN: i32 = 2; def DIR_LEFT: i32 = 3; def K_W: i32 = 119; def K_A: i32 = 97; def K_S: i32 = 115; def K_D: i32 = 100; def K_Q: i32 = 113; // ---- game state ------------------------------------------------------- // // Snake body lives in a circular buffer of (x, y) pairs addressed by // (tail, len). Head index is cached as `head`. Arrays live in main's // frame and are passed in as *i32; struct-with-array codegen still // segfaults today (probe11.ww). type state = struct { head: i32, tail: i32, len: i32, dx: i32, dy: i32, dir: i32, fx: i32, fy: i32, score: i32, dead: bool, }; fn next_idx(i: i32) i32 = { let n: i32 = i + 1; if (n >= CAP) { n = 0; }; return n; }; fn occupied(s: *state, xs: *i32, ys: *i32, x: i32, y: i32) bool = { let i: i32 = s.tail; let k: i32 = 0; for (k < s.len) { if (xs[i] == x) { if (ys[i] == y) { return true; }; }; i = next_idx(i); k += 1; }; return false; }; fn place_food(s: *state, xs: *i32, ys: *i32) void = { let tries: i32 = 0; for (tries < 1000) { let r1: i32 = c_rand(); let r2: i32 = c_rand(); if (r1 < 0) { r1 = -r1; }; if (r2 < 0) { r2 = -r2; }; let x: i32 = r1 % W; let y: i32 = r2 % H; if (!occupied(s, xs, ys, x, y)) { s.fx = x; s.fy = y; return; }; tries += 1; }; }; fn init_state(s: *state, xs: *i32, ys: *i32) void = { s.dx = 1; s.dy = 0; s.dir = DIR_RIGHT; s.dead = false; s.score = 0; let cx: i32 = W / 2; let cy: i32 = H / 2; xs[0] = cx - 2; ys[0] = cy; xs[1] = cx - 1; ys[1] = cy; xs[2] = cx; ys[2] = cy; s.tail = 0; s.head = 2; s.len = 3; place_food(s, xs, ys); }; fn handle_key(s: *state, k: i32) void = { if (k == K_W) { if (s.dir != DIR_DOWN) { s.dx = 0; s.dy = -1; s.dir = DIR_UP; }; }; if (k == K_S) { if (s.dir != DIR_UP) { s.dx = 0; s.dy = 1; s.dir = DIR_DOWN; }; }; if (k == K_A) { if (s.dir != DIR_RIGHT) { s.dx = -1; s.dy = 0; s.dir = DIR_LEFT; }; }; if (k == K_D) { if (s.dir != DIR_LEFT) { s.dx = 1; s.dy = 0; s.dir = DIR_RIGHT; }; }; }; fn step(s: *state, xs: *i32, ys: *i32) void = { let nx: i32 = xs[s.head] + s.dx; let ny: i32 = ys[s.head] + s.dy; if (nx < 0) { s.dead = true; return; }; if (ny < 0) { s.dead = true; return; }; if (nx >= W) { s.dead = true; return; }; if (ny >= H) { s.dead = true; return; }; let grow: bool = false; if (nx == s.fx) { if (ny == s.fy) { grow = true; }; }; let scan: i32 = s.len; if (!grow) { scan = s.len - 1; }; let i: i32 = s.tail; let k: i32 = 0; for (k < scan) { if (xs[i] == nx) { if (ys[i] == ny) { s.dead = true; return; }; }; i = next_idx(i); k += 1; }; let h: i32 = next_idx(s.head); xs[h] = nx; ys[h] = ny; s.head = h; s.len += 1; if (grow) { s.score += 1; place_food(s, xs, ys); } else { s.tail = next_idx(s.tail); s.len -= 1; }; }; // itoa: write decimal of n into NUL-terminated buf (room >= 12). Returns // number of digits written, not counting the NUL. fn itoa(buf: *u8, n: i32) i32 = { if (n == 0) { buf[0] = '0': u8; buf[1] = 0u8; return 1; }; let tmp: [16]u8; let i: i32 = 0; let v: i32 = n; for (v > 0) { tmp[i] = ((v % 10) + 48): u8; v = v / 10; i += 1; }; let j: i32 = 0; let cnt: i32 = i; for (j < cnt) { i -= 1; buf[j] = tmp[i]; j += 1; }; buf[cnt] = 0u8; return cnt; }; // ---- rendering -------------------------------------------------------- fn draw_walls() void = { let x: i32 = 0; for (x < W + 2) { nc_mvaddstr(0, x, "#".ptr); nc_mvaddstr(H + 1, x, "#".ptr); x += 1; }; let y: i32 = 0; for (y < H + 2) { nc_mvaddstr(y, 0, "#".ptr); nc_mvaddstr(y, W + 1, "#".ptr); y += 1; }; }; fn render(s: *state, xs: *i32, ys: *i32) void = { nc_clear(); draw_walls(); nc_mvaddstr(s.fy + 1, s.fx + 1, "*".ptr); let i: i32 = s.tail; let k: i32 = 0; for (k < s.len - 1) { nc_mvaddstr(ys[i] + 1, xs[i] + 1, "o".ptr); i = next_idx(i); k += 1; }; nc_mvaddstr(ys[s.head] + 1, xs[s.head] + 1, "@".ptr); nc_mvaddstr(H + 2, 0, "score: ".ptr); let buf: [16]u8; itoa(buf.ptr, s.score); nc_mvaddstr(H + 2, 7, buf.ptr); nc_mvaddstr(H + 2, 14, " (wasd to move, q to quit)".ptr); nc_refresh(); }; // ---- main ------------------------------------------------------------- export fn main() i32 = { c_srand((c_time(nil)): u32); nc_initscr(); nc_cbreak(); nc_noecho(); nc_curs_set(0); nc_timeout(120); let s: state; let xs: [200]i32; let ys: [200]i32; init_state(&s, xs.ptr, ys.ptr); let quit: bool = false; for (!s.dead) { render(&s, xs.ptr, ys.ptr); let k: i32 = nc_getch(); if (k == K_Q) { quit = true; break; }; if (k != -1) { handle_key(&s, k); }; step(&s, xs.ptr, ys.ptr); }; if (!quit) { render(&s, xs.ptr, ys.ptr); nc_mvaddstr(H + 3, 0, "game over - press any key".ptr); nc_refresh(); nc_timeout(-1); nc_getch(); }; nc_endwin(); return 0; };