diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2024-01-15 09:36:15 -0800 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2024-01-15 09:36:15 -0800 |
| commit | 3603d42087fa9399fa7f3730a0d3c7c714d6dccc (patch) | |
| tree | da6ea53261f52ba754a21c3b5e2590ca633207fb /src/main.rs | |
| parent | 680fdffb350b2409ac1709774d3cd81144d808a4 (diff) | |
| download | snake-3603d42087fa9399fa7f3730a0d3c7c714d6dccc.tar.zst | |
Clean up and simplify the code.
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 254 |
1 files changed, 192 insertions, 62 deletions
diff --git a/src/main.rs b/src/main.rs index 37adc6f..ffa6320 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,172 @@ -mod rand; -mod sc; - -use rand::Gen; -use sc::{Key, Screen}; +use libc::termios; use std::error::Error; -use std::io::{stderr, stdout, Write}; +use std::io::{stderr, stdout, Read, Write}; +use std::sync::mpsc::{Receiver, SyncSender, TryRecvError}; use std::time::{Duration, SystemTime}; +#[derive(Debug)] +struct Gen { + state: u64, +} + +impl Gen { + fn new() -> Gen { + Gen { + state: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64, + } + } + + fn rand(&mut self, max: i32) -> i32 { + // https://en.wikipedia.org/wiki/Linear_congruential_generator + self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1); + (self.state >> 33 & 0x7fffffff) as i32 % 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<dyn Error>> { + write!(stdout(), "\x1b[{};{}H", pos.row + 1, pos.col + 1)?; + Ok(()) +} + +fn clear() -> Result<(), Box<dyn Error>> { + 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, + keys: Receiver<Dir>, + cancel: SyncSender<()>, +} + +impl Screen { + const ROWS: u16 = 25; + const COLS: u16 = 50; + + fn init() -> Result<Screen, Box<dyn Error>> { + // 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] = 1; // 100ms timeout + if libc::tcsetattr(1, libc::TCSANOW, &raw) < 0 { + return Err(Box::from("tcsetattr failed")); + } + } + + let (key_tx, key_rx) = std::sync::mpsc::sync_channel(4); + let (cancel_tx, cancel_rx) = std::sync::mpsc::sync_channel(0); + std::thread::spawn(move || { + let mut stdin = std::io::stdin().lock(); + let mut buf = [0; 3]; + while let Err(TryRecvError::Empty) = cancel_rx.try_recv() { + let Ok(n) = stdin.read(&mut buf) else { + continue; + }; + if n < 3 || buf[0] != 27 || buf[1] != b'[' { + continue; + } + let key = match buf[2] { + b'A' => Dir::Up, + b'B' => Dir::Down, + b'C' => Dir::Right, + b'D' => Dir::Left, + _ => { + continue; + } + }; + let _ = key_tx.try_send(key); + } + }); + + 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, + keys: key_rx, + cancel: cancel_tx, + }) + } + + fn read_key(&self) -> Option<Dir> { + self.keys.try_recv().ok() + } +} + +impl Drop for Screen { + fn drop(&mut self) { + self.cancel.send(()).unwrap(); + unsafe { + libc::tcsetattr(1, libc::TCSANOW, &self.old_attr); + } + } +} + +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<Pos>, - direction: Key, + direction: Dir, growing: i64, } @@ -24,38 +174,38 @@ impl Snake { fn next(&self) -> Pos { let mut pos = self.body[0]; match self.direction { - Key::Up => pos.row -= 1, - Key::Down => pos.row += 1, - Key::Left => pos.col -= 1, - Key::Right => pos.col += 1, + Dir::Up => pos.row -= 1, + Dir::Down => pos.row += 1, + Dir::Left => pos.col -= 1, + Dir::Right => pos.col += 1, } pos } - fn erase(&self, screen: &Screen) -> Result<(), Box<dyn Error>> { + fn erase(&self) -> Result<(), Box<dyn Error>> { let pos = self.next(); - if screen.in_bounds(pos.row, pos.col) { - sc::move_cursor(pos.row, pos.col)?; + if in_bounds(pos) { + move_cursor(pos)?; write!(stdout(), " ")?; } for &pos in self.body.iter() { - sc::move_cursor(pos.row, pos.col)?; + move_cursor(pos)?; write!(stdout(), " ")?; } Ok(()) } - fn print(&self, g: &mut Gen, screen: &Screen) -> Result<(), Box<dyn Error>> { + fn print(&self, g: &mut Gen) -> Result<(), Box<dyn Error>> { let next = self.next(); - if (self.direction == Key::Left || self.direction == Key::Right) - && screen.in_bounds(next.row, next.col) + if (self.direction == Dir::Left || self.direction == Dir::Right) + && in_bounds(next) && g.rand(40) == 0 { - sc::move_cursor(next.row, next.col)?; + move_cursor(next)?; write!(stdout(), "~")?; } for (i, &pos) in self.body.iter().enumerate() { - sc::move_cursor(pos.row, pos.col)?; + move_cursor(pos)?; if i == 0 { write!(stdout(), "O")?; continue; @@ -113,70 +263,52 @@ impl Snake { } } +#[derive(Debug)] struct Fruit { pos: Pos, } impl Fruit { fn print(&self) -> Result<(), Box<dyn Error>> { - sc::move_cursor(self.pos.row, self.pos.col)?; + move_cursor(self.pos)?; write!(stdout(), "@")?; Ok(()) } - fn new_pos(&mut self, g: &mut Gen, screen: &Screen) { - self.pos.row = g.rand(i32::from(screen.rows) - 4) as u16 + 2; - self.pos.col = g.rand(i32::from(screen.cols) - 4) as u16 + 2; + fn new_pos(&mut self, g: &mut Gen) { + self.pos.row = g.rand(i32::from(Screen::ROWS) - 4) as u16 + 2; + self.pos.col = g.rand(i32::from(Screen::COLS) - 4) as u16 + 2; } } fn snake() -> Result<i64, Box<dyn Error>> { let screen = Screen::init()?; - let mut g = rand::new( - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_nanos() as i64, - ); + let mut g = Gen::new(); let mut snake = Snake { - body: vec![Pos { - row: screen.rows / 2, - col: screen.cols / 2, - }], - direction: Key::Right, + body: vec![pos(Screen::ROWS / 2, Screen::COLS / 2)], + direction: Dir::Right, growing: 0, }; - let mut fruit = Fruit { - pos: Pos { row: 0, col: 0 }, - }; - fruit.new_pos(&mut g, &screen); + let mut fruit = Fruit { pos: pos(0, 0) }; + fruit.new_pos(&mut g); let mut score: i64 = 0; let mut real_score: i64 = 0; let mut delay = Duration::from_millis(200); loop { - sc::move_cursor(0, 3)?; + move_cursor(pos(0, 3))?; write!(stdout(), " Score: {} ", score)?; - snake.print(&mut g, &screen)?; + snake.print(&mut g)?; fruit.print()?; - sc::move_cursor(screen.rows - 1, screen.cols - 1)?; + move_cursor(pos(Screen::ROWS - 1, Screen::COLS - 1))?; stdout().flush()?; - let mut d = delay; - if snake.direction == Key::Up || snake.direction == Key::Down { - d = d * 5 / 4; - } - std::thread::sleep(d); + std::thread::sleep(delay); - snake.erase(&screen)?; - if let Some(key) = screen.read_key() { - if !(snake.direction == Key::Up && key == Key::Down - || snake.direction == Key::Down && key == Key::Up - || snake.direction == Key::Left && key == Key::Right - || snake.direction == Key::Right && key == Key::Left) - || snake.body.len() == 1 - { - snake.direction = key; + snake.erase()?; + if let Some(dir) = screen.read_key() { + if dir != snake.direction.opposite() || snake.body.len() == 1 { + snake.direction = dir; } }; snake.step(); @@ -184,7 +316,7 @@ fn snake() -> Result<i64, Box<dyn Error>> { if snake.body[0] == fruit.pos { snake.growing += 6; loop { - fruit.new_pos(&mut g, &screen); + fruit.new_pos(&mut g); if !snake.body.contains(&fruit.pos) { break; } @@ -194,9 +326,7 @@ fn snake() -> Result<i64, Box<dyn Error>> { real_score += 1; delay = delay * 14 / 15; } - if !screen.in_bounds(snake.body[0].row, snake.body[0].col) - || snake.body[1..].contains(&snake.body[0]) - { + if !in_bounds(snake.body[0]) || snake.body[1..].contains(&snake.body[0]) { return Ok(score); } } @@ -210,7 +340,7 @@ fn main() { std::process::exit(1); } }; - let _ = sc::clear(); - let _ = sc::move_cursor(0, 0); + let _ = clear(); + let _ = move_cursor(pos(0, 0)); let _ = writeln!(stdout(), "Score: {}", score); } |
