Skip to content

Instantly share code, notes, and snippets.

@rrampage
Last active June 23, 2025 16:06
Show Gist options
  • Save rrampage/2a781662645dc2fcba45784eb584cbdc to your computer and use it in GitHub Desktop.
Save rrampage/2a781662645dc2fcba45784eb584cbdc to your computer and use it in GitHub Desktop.
Snake in a QR code!
// 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),
);
}
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