From 78ddc360aa2f734528aea467ae3d9426e1560bbb Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 11 May 2026 09:42:42 +0900 Subject: [PATCH] examples/snake: ncurses snake demo Small interactive demo wired through the new dynamic linker. Uses libncurses and libc via @symbol bindings; the Makefile runs the unmodified `ww build` pipeline with -l ncurses -l c -L /usr/lib. Resulting binary has libncurses.so.6 + libc.so.6 in NEEDED. Workaround: state struct keeps no array fields and threads the xs/ys body buffers through main()'s frame, because the current 6c codegen segfaults on `&struct.field`. Once that's fixed the arrays can move into state and place_food / step / render lose their *i32 parameters. --- examples/snake/Makefile | 16 +++ examples/snake/snake.ww | 289 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 examples/snake/Makefile create mode 100644 examples/snake/snake.ww diff --git a/examples/snake/Makefile b/examples/snake/Makefile new file mode 100644 index 00000000..1fed3119 --- /dev/null +++ b/examples/snake/Makefile @@ -0,0 +1,16 @@ +# examples/snake — built with the ww toolchain only (6c → 6a → 6l). +# +# `ww build` does the whole pipeline. -l ncurses -l c routes through +# 6l's dynamic linker (PT_INTERP + PT_DYNAMIC + PLT/GOT), so the +# resulting binary maps libncurses.so.6 + libc.so.6 at runtime via +# the system loader. No cc. + +WW := $(shell cd ../..; pwd)/out/bin/ww + +snake: snake.ww + $(WW) build snake.ww -L /usr/lib -l ncurses -l c + +clean: + rm -f snake snake.o snake.s snake.combined.ww + +.PHONY: clean diff --git a/examples/snake/snake.ww b/examples/snake/snake.ww new file mode 100644 index 00000000..e5044892 --- /dev/null +++ b/examples/snake/snake.ww @@ -0,0 +1,289 @@ +// 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; +};