Skip to content

Instantly share code, notes, and snippets.

@rrampage
Created June 3, 2025 07:16
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),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment