Last active
June 23, 2025 16:06
-
-
Save rrampage/2a781662645dc2fcba45784eb584cbdc to your computer and use it in GitHub Desktop.
Snake in a QR code!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Snake for Linux terminal | |
// Fits in a QR code! | |
// Compile with: | |
// zig build-exe snek.zig -O ReleaseSmall -target x86_64-linux -fstrip -flto -fsingle-threaded -femit-bin=snek_x64 | |
// zig build-exe snek.zig -O ReleaseSmall -target aarch64-linux -fstrip -flto -fsingle-threaded -femit-bin=snek_aarch64 | |
// Run `sstrip -z` from https://www.muppetlabs.com/~breadbox/software/elfkickers.html to reduce binary size even further | |
// Currently, snek_x64 is 2616 bytes and snek_aarch64 is 2941 bytes | |
// Threshold for a binary QR code is 2953 bytes | |
// Encode using: | |
// qrencode -8 -r snek_aarch64 -o qr_aarch64.png | |
// Decode using: | |
// zbarimg --raw --oneshot -Sbinary qr_aarch64.png > snek_decoded_aarch64 | |
const std = @import("std"); | |
const builtin = @import("builtin"); | |
const linux = std.os.linux; | |
const native_arch = builtin.cpu.arch; | |
const width = 40; | |
const height = 20; | |
const max_len = 63; | |
const GAME_STATE = enum { | |
GAME_OVER, | |
PLAYING | |
}; | |
var game_state: GAME_STATE = .PLAYING; | |
var snake_len: u8 = 5; | |
const direction = struct { | |
x: i8, | |
y: i8 | |
}; | |
var dir: direction = .{.x = 1, .y = 0}; | |
const point = struct { | |
x: i8, | |
y: i8 | |
}; | |
var snake : [max_len]point = undefined; | |
var food: point = .{.x = 10, .y = 10}; | |
var sscore: [4]u8 = [_]u8{'0', '0', '0', '\n'}; | |
// var hscore: [4]u8 = [_]u8{'0', '0', '0', '\n'}; | |
// var is_highscore = true; | |
var orig_termios: linux.termios = undefined; | |
const ofd: linux.pollfd = .{ | |
.fd = 0, | |
.events = linux.POLL.IN, | |
.revents = 0 | |
}; | |
var pollfds = [_]linux.pollfd{ofd}; | |
fn reset_state() void { | |
game_state = .PLAYING; | |
snake_len = 5; | |
sscore = [_]u8{'0', '0', '0', '\n'}; | |
dir = .{.x = 1, .y = 0}; | |
for (0..snake_len) |i| { | |
snake[i].x = @intCast(5 - i); | |
snake[i].y = 5; | |
} | |
} | |
fn disable_raw_mode() void { | |
_ = linux.tcsetattr(0, linux.TCSA.FLUSH, &orig_termios); | |
} | |
fn enable_raw_mode() void { | |
_ = linux.tcgetattr(0, &orig_termios); | |
var raw : linux.termios = orig_termios; | |
raw.lflag.ECHO = false; | |
raw.lflag.ICANON = false; | |
_ = linux.tcsetattr(0, linux.TCSA.FLUSH, &raw); | |
} | |
fn clear_screen() void { | |
_ = linux.write(0, "\x1b[H\x1b[J", 6); | |
} | |
fn input() void { | |
var buf: [3]u8 = undefined; | |
const n = linux.read(0, &buf, 3); | |
if (n < 0) { | |
return; | |
} | |
if (n == 1) { | |
const c = buf[0]; | |
if (game_state == .GAME_OVER) { | |
if (c == 'r') { | |
reset_state(); | |
return; | |
} | |
} | |
if (c == 'w' or c == 'W' or c == 'k' or c == 'K') { | |
dir = .{.x = 0, .y = -1}; | |
} | |
if (c == 's' or c == 'S' or c == 'j' or c == 'J') { | |
dir = .{.x = 0, .y = 1}; | |
} | |
if (c == 'a' or c == 'A' or c == 'h' or c == 'H') { | |
dir = .{.x = -1, .y = 0}; | |
} | |
if (c == 'd' or c == 'D' or c == 'l' or c == 'L') { | |
dir = .{.x = 1, .y = 0}; | |
} | |
if (c == 'q' or c == 'Q') { | |
clean_exit(); | |
} | |
} else if (n == 3 and buf[0] == '\x1b' and buf[1] == '[') { | |
const c = buf[2]; | |
if (c == 'A' or c == 'a') { | |
dir = .{.x = 0, .y = -1}; | |
} | |
if (c == 'B' or c == 'b') { | |
dir = .{.x = 0, .y = 1}; | |
} | |
if (c == 'C' or c == 'c') { | |
dir = .{.x = 1, .y = 0}; | |
} | |
if (c == 'D' or c == 'd') { | |
dir = .{.x = -1, .y = 0}; | |
} | |
} | |
} | |
fn wait_for_input(timeout_ms: i32) i32 { | |
return @intCast(linux.poll(&pollfds, 1, timeout_ms)); | |
} | |
fn update_score() void { | |
var i: u8 = 2; | |
while (i >= 0) : (i -= 1) { | |
if (sscore[i] == '9') { | |
sscore[i] = '0'; | |
continue; | |
} else { | |
sscore[i] += 1; | |
break; | |
} | |
} | |
} | |
fn update_snake() void { | |
var i: usize = snake_len - 1; | |
while (i > 0) : (i -= 1) { | |
snake[i] = snake[i - 1]; | |
} | |
snake[0].x += dir.x; | |
snake[0].y += dir.y; | |
if (snake[0].x == food.x and snake[0].y == food.y) { | |
if (snake_len < max_len) snake_len+=1; | |
update_score(); | |
food.x = @mod(food.x + 7, width); | |
food.y = @mod(food.y + 3, height); | |
} | |
if (snake[0].x < 0 or snake[0].x >= width or snake[0].y < 0 or snake[0].y >= height or check_collision()) { | |
game_state = .GAME_OVER; | |
} | |
} | |
fn check_collision() bool { | |
for (1..snake_len) |i| { | |
if (snake[0].x == snake[i].x and snake[0].y == snake[i].y) {return true;} | |
} | |
return false; | |
} | |
fn print(ptr: [*]const u8, count: usize) void { | |
_ = linux.write(1, ptr, count); | |
} | |
fn draw() void { | |
clear_screen(); | |
print("r: new\tq: quit\tArrow keys|WASD|HJKL: move\nScore:", 48); | |
print(&sscore, sscore.len); | |
print("┏", 3); | |
for (0..width) |_| { | |
print("━", 3); | |
} | |
print("┓", 3); | |
print("\n",1); | |
for (0..height) |y| { | |
print("┃", 3); | |
for (0..width) |x| { | |
var hit: i16 = 0; | |
for (0..snake_len) |i| { | |
if (snake[i].x == x and snake[i].y == y) { | |
if (i == 0) {hit = 2; } else { hit = 1;} | |
break; | |
} | |
} | |
if (hit == 2) { | |
print("\x1b[35;1m@\x1b[0m", 12); | |
} | |
else if (hit == 1) { | |
print("\x1b[31;1m⫳\x1b[0m", 14); | |
} else if (food.x == x and food.y == y ) { | |
print("\x1b[32m*\x1b[0m", 10); | |
} else { | |
print( " ", 1); | |
} | |
} | |
print( "┃", 3); | |
print( "\n", 1); | |
} | |
// Draw bottom wall | |
print("┗", 3); | |
for (0..width) |_| { | |
print("━", 3); | |
} | |
print("┛", 3); | |
print("\n",1); | |
} | |
fn clean_exit() noreturn { | |
disable_raw_mode(); | |
linux.exit(0); | |
} | |
pub fn main() callconv(.c) noreturn { | |
enable_raw_mode(); | |
while (true) { | |
const w = wait_for_input(60); | |
if (w < 0) { | |
clean_exit(); | |
} | |
if (w > 0) { input(); } | |
if (game_state == .PLAYING) { | |
update_snake(); | |
} | |
draw(); | |
} | |
} | |
pub export fn _start() callconv(.naked) noreturn { | |
asm volatile (switch (native_arch) { | |
.x86_64 => | |
\\ xorl %%ebp, %%ebp | |
\\ movq %%rsp, %%rdi | |
\\ callq %[main:P] | |
, | |
.aarch64 => | |
\\ mov fp, #0 | |
\\ mov lr, #0 | |
\\ b %[main] | |
, | |
else => @compileError("unsupported arch"), | |
} | |
: | |
: [_start] "X" (&_start), | |
[main] "X" (&main), | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
const builtin = @import("builtin"); | |
const linux = std.os.linux; | |
const native_arch = builtin.cpu.arch; | |
const width = 64; | |
const height = 32; | |
const max_len = 16; | |
const SnakeState = packed struct { | |
game_on: bool, | |
food_eaten: bool, | |
food_pos: u11, | |
head_pos: u11, | |
head_new_dir: u2, | |
score: u6, | |
dirs: u32, | |
fn getDir(state: SnakeState, i: usize) u2 { | |
const idx: u5 = @intCast(2 * i); | |
return @intCast((state.dirs >> idx) & 0b11); | |
} | |
fn setDir(state: *SnakeState, i: usize, val: u32) void { | |
const idx: u5 = @intCast(2 * i); | |
const bin: u32 = 0b11; | |
const mask: u32 = (bin << (idx)); | |
const valor: u32 = val; | |
const shifted: u32 = @intCast(valor << idx); | |
state.dirs = state.dirs & ~mask | shifted; | |
} | |
}; | |
var snake_state: SnakeState = std.mem.zeroes(SnakeState); | |
var cursor_buf: [8]u8 = undefined; | |
var score_line: [2]u8 = undefined; | |
var orig_termios: linux.termios = undefined; | |
const ofd: linux.pollfd = .{ .fd = 0, .events = linux.POLL.IN, .revents = 0 }; | |
var pollfds = [_]linux.pollfd{ofd}; | |
var prng = std.Random.Pcg.init(0x0123_45678); | |
var rand = prng.random(); | |
const LEFT = 0; | |
const RIGHT = 1; | |
const UP = 2; | |
const DOWN = 3; | |
fn reset_state() void { | |
//const old_food_pos = snake_state.food_pos; | |
snake_state = std.mem.zeroes(SnakeState); | |
snake_state.game_on = true; | |
snake_state.food_pos = rand.uintAtMost(u11, 2047); | |
//snake_state.food_pos = (old_food_pos + 7 + 3 * width) % 2047; | |
snake_state.head_pos = 15 * width + 15; | |
// tmp_dir = 0; | |
// initial len = 4 | |
snake_state.dirs = 0; | |
} | |
fn disable_raw_mode() void { | |
_ = linux.tcsetattr(0, linux.TCSA.FLUSH, &orig_termios); | |
// Disable alternate screen and enable cursor | |
_ = linux.write(0, "\x1b[?1049l", 8); | |
_ = linux.write(1, "\x1b[?25h", 6); | |
} | |
fn enable_raw_mode() void { | |
_ = linux.tcgetattr(0, &orig_termios); | |
var raw: linux.termios = orig_termios; | |
raw.lflag.ECHO = false; | |
raw.lflag.ICANON = false; | |
_ = linux.tcsetattr(0, linux.TCSA.FLUSH, &raw); | |
// Enable alternate screen and Disable cursor | |
_ = linux.write(0, "\x1b[?1049h", 8); | |
_ = linux.write(1, "\x1b[?25l", 6); | |
} | |
fn clear_screen() void { | |
_ = linux.write(0, "\x1b[H\x1b[J", 6); | |
_ = linux.write(1, "\x1b[?25l", 6); | |
} | |
fn input() void { | |
var buf: [3]u8 = undefined; | |
const n = linux.read(0, &buf, 3); | |
if (n < 0) { | |
return; | |
} | |
if (n == 1) { | |
const c = buf[0]; | |
if (!snake_state.game_on) { | |
if (c == 'r') { | |
reset_state(); | |
clear_board(); | |
return; | |
} | |
} | |
if (c == 'w' or c == 'W' or c == 'k' or c == 'K') { | |
snake_state.head_new_dir = UP; | |
//dir = .{ .x = 0, .y = -1 }; | |
} | |
if (c == 's' or c == 'S' or c == 'j' or c == 'J') { | |
snake_state.head_new_dir = DOWN; | |
// dir = .{ .x = 0, .y = 1 }; | |
} | |
if (c == 'a' or c == 'A' or c == 'h' or c == 'H') { | |
snake_state.head_new_dir = LEFT; | |
//dir = .{ .x = -1, .y = 0 }; | |
} | |
if (c == 'd' or c == 'D' or c == 'l' or c == 'L') { | |
snake_state.head_new_dir = RIGHT; | |
//dir = .{ .x = 1, .y = 0 }; | |
} | |
if (c == 'q' or c == 'Q') { | |
clean_exit(); | |
} | |
} else if (n == 3 and buf[0] == '\x1b' and buf[1] == '[') { | |
const c = buf[2]; | |
if (c == 'A' or c == 'a') { | |
snake_state.head_new_dir = UP; | |
//dir = .{ .x = 0, .y = -1 }; | |
} | |
if (c == 'B' or c == 'b') { | |
snake_state.head_new_dir = DOWN; | |
//dir = .{ .x = 0, .y = 1 }; | |
} | |
if (c == 'C' or c == 'c') { | |
snake_state.head_new_dir = RIGHT; | |
//dir = .{ .x = 1, .y = 0 }; | |
} | |
if (c == 'D' or c == 'd') { | |
snake_state.head_new_dir = LEFT; | |
//dir = .{ .x = -1, .y = 0 }; | |
} | |
} | |
} | |
fn wait_for_input(timeout_ms: i32) i32 { | |
return @intCast(linux.poll(&pollfds, 1, timeout_ms)); | |
} | |
fn update_snake() void { | |
const len: usize = @min(snake_state.score + 4, 16); | |
const old_tail = snake_state.getDir(len - 1); | |
const old_head_pos = snake_state.head_pos; | |
// update head_pos | |
const disp: i16 = switch (snake_state.head_new_dir) { | |
UP => -width, | |
DOWN => width, | |
LEFT => -1, | |
RIGHT => 1, | |
}; | |
const new_pos: i16 = @intCast(snake_state.head_pos + disp); | |
const old_x = old_head_pos % width; | |
const old_y = old_head_pos / width; | |
const new_x = @rem(new_pos, width); | |
const new_y = @divTrunc(new_pos, width); | |
if (snake_state.head_new_dir < 2) { | |
if (new_y != old_y) { | |
snake_state.game_on = false; | |
return; | |
} | |
} | |
if (snake_state.head_new_dir > 1) { | |
if (new_x != old_x) { | |
snake_state.game_on = false; | |
return; | |
} | |
} | |
// std.log.warn("NEW_POS {d} {d} {d}\n", .{ new_pos, new_x, new_y }); | |
if (new_x < 0 or new_x >= width or new_y < 0 or new_y >= height or new_pos < 0 or new_pos > 2047 or check_collision()) { | |
snake_state.game_on = false; | |
return; | |
} | |
snake_state.head_pos = @intCast(new_pos); | |
snake_state.setDir(0, snake_state.head_new_dir); | |
snake_state.dirs = snake_state.dirs << 2; // Moving snake one cell | |
snake_state.setDir(0, snake_state.head_new_dir); | |
if (snake_state.head_pos == snake_state.food_pos) { | |
snake_state.score += 1; | |
if (len < 16) { | |
snake_state.setDir(len, old_tail); | |
} | |
// update_score(); | |
// snake_state.food_pos = (snake_state.food_pos + 7 + 3 * width) % 2047; | |
snake_state.food_pos = rand.uintAtMost(u11, 2047); | |
score_line[0] = '0' + snake_state.score / 10; | |
score_line[1] = '0' + snake_state.score % 10; | |
} | |
} | |
fn gen_coord(pos: u11, dir: u2) u11 { | |
const disp: i16 = switch (dir) { | |
UP => width, | |
DOWN => -width, | |
LEFT => 1, | |
RIGHT => -1, | |
}; | |
return @intCast(pos + disp); | |
} | |
fn check_collision() bool { | |
const len: u8 = @min(snake_state.score + 4, 16); | |
var coord: u11 = snake_state.head_pos; | |
for (1..len) |i| { | |
coord = gen_coord(coord, snake_state.getDir(i)); | |
if (snake_state.head_pos == coord) { | |
return true; | |
} | |
} | |
return false; | |
} | |
fn print(ptr: [*]const u8, count: usize) void { | |
_ = linux.write(1, ptr, count); | |
} | |
fn move_cursor(coord: u11) void { | |
const x: u11 = coord % width + 2; | |
const y: u11 = coord / width + 2; | |
const x0: u8 = @intCast(x % 10 + '0'); | |
const x1: u8 = @intCast(x / 10 + '0'); | |
const y0: u8 = @intCast(y % 10 + '0'); | |
const y1: u8 = @intCast(y / 10 + '0'); | |
cursor_buf[2] = y1; | |
cursor_buf[3] = y0; | |
cursor_buf[5] = x1; | |
cursor_buf[6] = x0; | |
//std.log.warn("CURSOR {d} {d} {d}\n", .{ coord, x, y }); | |
// clean_exit(); | |
print(&cursor_buf, cursor_buf.len); | |
} | |
fn clear_snake() void { | |
var coord = snake_state.head_pos; | |
move_cursor(coord); | |
print(" ", 1); | |
const len: u8 = @min(snake_state.score + 4, 16); | |
for (1..len) |i| { | |
coord = gen_coord(coord, snake_state.getDir(i)); | |
move_cursor(coord); | |
print(" ", 1); | |
} | |
} | |
fn draw_snake() void { | |
var coord = snake_state.food_pos; | |
move_cursor(coord); | |
print("\x1b[32m*\x1b[0m", 10); | |
coord = snake_state.head_pos; | |
move_cursor(coord); | |
print("\x1b[35;1m@\x1b[0m", 12); | |
const len: u8 = @min(snake_state.score + 4, 16); | |
for (1..len) |i| { | |
coord = gen_coord(coord, snake_state.getDir(i)); | |
move_cursor(coord); | |
print("\x1b[31;1m⫳\x1b[0m", 14); | |
} | |
// Print score | |
cursor_buf[2] = (height + 3) / 10 + '0'; | |
cursor_buf[3] = (height + 3) % 10 + '0'; | |
cursor_buf[5] = '0'; | |
cursor_buf[6] = '8'; | |
print(&cursor_buf, cursor_buf.len); | |
print(&score_line, score_line.len); | |
} | |
fn clean_exit() noreturn { | |
disable_raw_mode(); | |
linux.exit(0); | |
} | |
fn clear_board() void { | |
clear_screen(); | |
// print top wall | |
const wall_line: []const u8 = "━" ** width; | |
print("┏", 3); | |
print(wall_line.ptr, wall_line.len); | |
print("┓\n", 4); | |
// const top_wall: []const u8 = "┏" ++ "━" ** width ++ "┓" ++ "\n"; | |
// print(top_wall.ptr, top_wall.len); | |
const board_line: []const u8 = "┃" ++ " " ** width ++ "┃" ++ "\n"; | |
// const bot_wall: []const u8 = "┗" ++ "━" ** width ++ "┛" ++ "\n"; | |
for (0..height) |_| { | |
print(board_line.ptr, board_line.len); | |
} | |
print("┗", 3); | |
print(wall_line.ptr, wall_line.len); | |
print("┛\n", 4); | |
print("Score: 00\n", 10); | |
} | |
pub fn main() callconv(.c) noreturn { | |
cursor_buf[0] = '\x1b'; | |
cursor_buf[1] = '['; | |
cursor_buf[4] = ';'; | |
cursor_buf[7] = 'H'; | |
enable_raw_mode(); | |
reset_state(); | |
clear_board(); | |
while (true) { | |
const w = wait_for_input(80); | |
if (w < 0) { | |
clean_exit(); | |
} | |
if (w > 0) { | |
input(); | |
} | |
clear_snake(); | |
if (snake_state.game_on) { | |
update_snake(); | |
} | |
draw_snake(); | |
} | |
} | |
pub export fn _start() callconv(.naked) noreturn { | |
asm volatile (switch (native_arch) { | |
.x86_64 => | |
\\ xorl %%ebp, %%ebp | |
\\ movq %%rsp, %%rdi | |
\\ callq %[main:P] | |
, | |
.aarch64 => | |
\\ mov fp, #0 | |
\\ mov lr, #0 | |
\\ b %[main] | |
, | |
else => @compileError("unsupported arch"), | |
} | |
: | |
: [_start] "X" (&_start), | |
[main] "X" (&main), | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment