use libc::termios; use std::error::Error; use std::io::{stderr, stdin, stdout, Read, Write}; use std::time::{Duration, SystemTime}; fn rand(max: i32) -> i32 { unsafe { libc::rand() % max } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct Pos { row: u16, col: u16, } fn pos(row: u16, col: u16) -> Pos { Pos { row, col } } // https://en.wikipedia.org/wiki/ANSI_escape_code fn move_cursor(pos: Pos) -> Result<(), Box> { write!(stdout(), "\x1b[{};{}H", pos.row + 1, pos.col + 1)?; Ok(()) } fn clear() -> Result<(), Box> { write!(stdout(), "\x1b[2J")?; Ok(()) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Dir { Up, Down, Left, Right, } impl Dir { fn opposite(self) -> Dir { match self { Dir::Up => Dir::Down, Dir::Down => Dir::Up, Dir::Left => Dir::Right, Dir::Right => Dir::Left, } } } struct Screen { old_attr: termios, } impl Screen { const ROWS: u16 = 25; const COLS: u16 = 50; fn init() -> Result> { // Get current terminal attrs. let mut attr; unsafe { attr = std::mem::zeroed(); if libc::tcgetattr(1, &mut attr) < 0 { return Err(Box::from("tcgetattr failed")); } } // Put the terminal into raw mode. unsafe { let mut raw = std::mem::zeroed(); libc::cfmakeraw(&mut raw); raw.c_cc[libc::VMIN] = 0; raw.c_cc[libc::VTIME] = 0; if libc::tcsetattr(1, libc::TCSANOW, &raw) < 0 { return Err(Box::from("tcsetattr failed")); } } clear()?; move_cursor(pos(0, 0))?; for _ in 0..Screen::COLS { write!(stdout(), "-")?; } for i in 1..Screen::ROWS - 1 { move_cursor(pos(i, 0))?; write!(stdout(), "|")?; move_cursor(pos(i, Screen::COLS - 1))?; write!(stdout(), "|")?; } move_cursor(pos(Screen::ROWS - 1, 0))?; for _ in 0..Screen::COLS { write!(stdout(), "-")?; } Ok(Screen { old_attr: attr }) } } impl Drop for Screen { fn drop(&mut self) { unsafe { libc::tcsetattr(1, libc::TCSANOW, &self.old_attr); } } } fn read_key() -> Result> { let mut buf = [0; 3]; let n = stdin().read(&mut buf)?; if n < 3 || buf[0] != 27 || buf[1] != b'[' { return Err(Box::from("unexpected input")); } let key = match buf[2] { b'A' => Dir::Up, b'B' => Dir::Down, b'C' => Dir::Right, b'D' => Dir::Left, _ => { return Err(Box::from("unexpected input")); } }; Ok(key) } fn in_bounds(pos: Pos) -> bool { (1..Screen::ROWS - 1).contains(&pos.row) && (1..Screen::COLS - 1).contains(&pos.col) } #[derive(Debug)] struct Snake { body: Vec, direction: Dir, growing: i64, } impl Snake { fn next(&self) -> Pos { let mut pos = self.body[0]; match self.direction { Dir::Up => pos.row -= 1, Dir::Down => pos.row += 1, Dir::Left => pos.col -= 1, Dir::Right => pos.col += 1, } pos } fn erase(&self) -> Result<(), Box> { let pos = self.next(); if in_bounds(pos) { move_cursor(pos)?; write!(stdout(), " ")?; } for &pos in self.body.iter() { move_cursor(pos)?; write!(stdout(), " ")?; } Ok(()) } fn print(&self) -> Result<(), Box> { let next = self.next(); if (self.direction == Dir::Left || self.direction == Dir::Right) && in_bounds(next) && rand(40) == 0 { move_cursor(next)?; write!(stdout(), "~")?; } for (i, &pos) in self.body.iter().enumerate() { move_cursor(pos)?; if i == 0 { write!(stdout(), "O")?; continue; } let prev = self.body[i - 1]; if i == self.body.len() - 1 { if prev.row < pos.row { write!(stdout(), "v")?; continue; } if prev.row > pos.row { write!(stdout(), "^")?; continue; } if prev.col < pos.col { write!(stdout(), ">")?; continue; } write!(stdout(), "<")?; continue; } let next = self.body[i + 1]; if prev.col == next.col { write!(stdout(), "|")?; continue; } if prev.row == next.row { write!(stdout(), "=")?; continue; } let (a, b) = if prev.row == pos.row { (prev, next) } else { (next, prev) }; if (a.col < pos.col) == (b.row < pos.row) { write!(stdout(), "/")?; continue; } write!(stdout(), "\\")?; } Ok(()) } fn step(&mut self) { let pos = self.next(); if self.growing > 0 { self.body.push(self.body[self.body.len() - 1]); self.growing -= 1; } for i in (1..self.body.len()).rev() { self.body[i] = self.body[i - 1]; } self.body[0] = pos; } } #[derive(Debug)] struct Fruit { pos: Pos, } impl Fruit { fn print(&self) -> Result<(), Box> { move_cursor(self.pos)?; write!(stdout(), "@")?; Ok(()) } fn new_pos(&mut self) { self.pos.row = rand(i32::from(Screen::ROWS) - 4) as u16 + 2; self.pos.col = rand(i32::from(Screen::COLS) - 4) as u16 + 2; } } fn snake() -> Result> { let _screen = Screen::init()?; let mut snake = Snake { body: vec![pos(Screen::ROWS / 2, Screen::COLS / 2)], direction: Dir::Right, growing: 0, }; let mut fruit = Fruit { pos: pos(0, 0) }; fruit.new_pos(); let mut score: i64 = 0; let mut real_score: i64 = 0; let mut delay = Duration::from_millis(200); loop { move_cursor(pos(0, 3))?; write!(stdout(), " Score: {} ", score)?; snake.print()?; fruit.print()?; move_cursor(pos(Screen::ROWS - 1, Screen::COLS - 1))?; stdout().flush()?; std::thread::sleep(delay); snake.erase()?; if let Ok(dir) = read_key() { if dir != snake.direction.opposite() || snake.body.len() == 1 { snake.direction = dir; } }; snake.step(); if snake.body[0] == fruit.pos { snake.growing += 6; loop { fruit.new_pos(); if !snake.body.contains(&fruit.pos) { break; } } let h = real_score / 2; score += h * h * h * h * h - h + 1; real_score += 1; delay = delay * 14 / 15; } if !in_bounds(snake.body[0]) || snake.body[1..].contains(&snake.body[0]) { return Ok(score); } } } fn main() { unsafe { libc::srand( SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_nanos() as u32, ); } let score = match snake() { Ok(s) => s, Err(err) => { let _ = writeln!(stderr(), "FAIL: {}", err); std::process::exit(1); } }; let _ = clear(); let _ = move_cursor(pos(0, 0)); let _ = writeln!(stdout(), "Score: {}", score); }