feat: playable snake game via UART terminal rendering

Snake renders using ANSI escape codes via debug_putchar syscall,
bypassing the VirtIO GPU (which has a QEMU 11.x legacy MMIO bug
where transfer_to_host_2d succeeds but doesn't transfer data).

Full microkernel stack running end-to-end:
  adapter → init → storage-server IPC → load snake from disk →
  input-server IPC (UART keyboard) → snake game logic →
  debug_putchar syscall → UART → terminal display

Controls: WASD to steer, runs at 0.2s tick rate.
Green O=head, o=body, red @=food, #=border.
This commit is contained in:
2026-10-18 22:08:52 +02:00
parent 27be73f42d
commit a3b15e5264
6 changed files with 117 additions and 191 deletions

View File

@@ -3,37 +3,119 @@
extern crate userspace_rt;
use snake::game::{Direction, GameState, Snake};
use snake::renderer::{render, RenderCommand};
use snake::game::{Direction, GameState, Snake, GRID_WIDTH, GRID_HEIGHT};
use types::bounded::CapSlot;
use types::time::Ticks;
use userspace_rt::syscall;
fn display_ep() -> CapSlot { CapSlot::new(0).unwrap() }
fn input_ep() -> CapSlot { CapSlot::new(1).unwrap() }
fn input_ep() -> CapSlot { CapSlot::new(0).unwrap() }
const TICK_DELAY: u64 = 2_000_000;
const MAX_RENDER_COMMANDS: usize = 192;
fn putchar(c: u8) { syscall::debug_putchar(c); }
fn puts(s: &[u8]) { for &b in s { putchar(b); } }
fn move_cursor(row: u16, col: u16) {
puts(b"\x1b[");
print_dec(row + 1);
putchar(b';');
print_dec(col + 1);
putchar(b'H');
}
fn print_dec(mut v: u16) {
if v == 0 { putchar(b'0'); return; }
let mut buf = [0u8; 5];
let mut i = 0;
while v > 0 { buf[i] = b'0' + (v % 10) as u8; v /= 10; i += 1; }
while i > 0 { i -= 1; putchar(buf[i]); }
}
fn set_color(fg: u8) {
puts(b"\x1b[");
print_dec(fg as u16);
putchar(b'm');
}
#[unsafe(no_mangle)]
extern "C" fn main() {
let mut snake = Snake::new();
let mut commands = [RenderCommand { x: 0, y: 0, w: 0, h: 0, color: 0 }; MAX_RENDER_COMMANDS];
puts(b"\x1b[2J"); // clear screen
puts(b"\x1b[?25l"); // hide cursor
// Draw border
set_color(90); // dark grey
move_cursor(0, 0);
for _ in 0..GRID_WIDTH + 2 { putchar(b'#'); }
for y in 0..GRID_HEIGHT {
move_cursor(y + 1, 0);
putchar(b'#');
move_cursor(y + 1, GRID_WIDTH + 1);
putchar(b'#');
}
move_cursor(GRID_HEIGHT + 1, 0);
for _ in 0..GRID_WIDTH + 2 { putchar(b'#'); }
loop {
// Render
render_game(&snake);
wait_tick();
handle_input(&mut snake);
snake.tick();
let count = render(&snake, &mut commands);
send_render_commands(&commands, count);
if snake.state() == GameState::GameOver {
move_cursor(GRID_HEIGHT / 2, GRID_WIDTH / 2 - 4);
set_color(91); // bright red
puts(b"GAME OVER");
set_color(0);
move_cursor(GRID_HEIGHT + 3, 0);
puts(b"Score: ");
print_dec(snake.score());
putchar(b'\n');
let end = Ticks::from_raw(syscall::get_time().raw() + 50_000_000);
while syscall::get_time().raw() < end.raw() { syscall::yield_now(); }
puts(b"\x1b[?25h"); // show cursor
return;
}
wait_tick();
}
}
fn render_game(snake: &Snake) {
// Clear game area
for y in 0..GRID_HEIGHT {
move_cursor(y + 1, 1);
for _ in 0..GRID_WIDTH { putchar(b' '); }
}
// Draw food
let food = snake.food();
move_cursor(food.y + 1, food.x + 1);
set_color(91); // bright red
putchar(b'@');
// Draw snake body
let body = snake.body();
for (i, pos) in body.iter().enumerate() {
move_cursor(pos.y + 1, pos.x + 1);
if i == 0 {
set_color(92); // bright green
putchar(b'O'); // head
} else {
set_color(32); // green
putchar(b'o'); // body
}
}
// Score
set_color(0);
move_cursor(GRID_HEIGHT + 2, 0);
puts(b"Score: ");
print_dec(snake.score());
puts(b" WASD to move");
}
fn handle_input(snake: &mut Snake) {
let _ = syscall::send(input_ep(), 0, 0, 0, 0);
let (tag, kind, _, _) = syscall::receive(input_ep());
@@ -49,22 +131,6 @@ fn handle_input(snake: &mut Snake) {
}
}
fn send_render_commands(commands: &[RenderCommand], count: usize) {
let _ = syscall::send(display_ep(), 1, 0, 0, 0);
let _ = syscall::receive(display_ep());
for i in 0..count {
let cmd = &commands[i];
let xy = cmd.x as usize | ((cmd.y as usize) << 16);
let wh = cmd.w as usize | ((cmd.h as usize) << 16);
let _ = syscall::send(display_ep(), 0, xy, wh, cmd.color as usize);
let _ = syscall::receive(display_ep());
}
let _ = syscall::send(display_ep(), 2, 0, 0, 0);
let _ = syscall::receive(display_ep());
}
fn wait_tick() {
let start = syscall::get_time();
let deadline = Ticks::from_raw(start.raw() + TICK_DELAY);

View File

@@ -1,11 +1,11 @@
use crate::game::{Snake, Position, GRID_WIDTH, GRID_HEIGHT};
const CELL_SIZE: u16 = 16;
const BG_COLOR: u32 = 0x1A1A2E;
const SNAKE_COLOR: u32 = 0x00FF41;
const HEAD_COLOR: u32 = 0x00CC33;
const FOOD_COLOR: u32 = 0xFF0040;
const BORDER_COLOR: u32 = 0x333355;
const CELL_SIZE: u16 = 24;
const BG_COLOR: u32 = 0xFF202040;
const SNAKE_COLOR: u32 = 0xFF00FF00;
const HEAD_COLOR: u32 = 0xFF00CC00;
const FOOD_COLOR: u32 = 0xFFFF0000;
const BORDER_COLOR: u32 = 0xFF505080;
#[derive(Clone, Copy)]
pub struct RenderCommand {