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