diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2024-01-14 16:01:32 -0800 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2024-01-14 16:01:32 -0800 |
| commit | 680fdffb350b2409ac1709774d3cd81144d808a4 (patch) | |
| tree | ea9b7c2a321a89a2919bbbf1d7012a9e97f73c58 | |
| download | snake-680fdffb350b2409ac1709774d3cd81144d808a4.tar.zst | |
Write a simple snake game.
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 16 | ||||
| -rw-r--r-- | Cargo.toml | 9 | ||||
| -rw-r--r-- | src/main.rs | 216 | ||||
| -rw-r--r-- | src/rand.rs | 17 | ||||
| -rw-r--r-- | src/sc.rs | 134 |
6 files changed, 393 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6a2206f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "snake" +version = "1.0.0" +dependencies = [ + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0cc9539 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "snake" +version = "1.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libc = "0.2" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..37adc6f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,216 @@ +mod rand; +mod sc; + +use rand::Gen; +use sc::{Key, Screen}; +use std::error::Error; +use std::io::{stderr, stdout, Write}; +use std::time::{Duration, SystemTime}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Pos { + row: u16, + col: u16, +} + +#[derive(Debug)] +struct Snake { + body: Vec<Pos>, + direction: Key, + growing: i64, +} + +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, + } + pos + } + + fn erase(&self, screen: &Screen) -> Result<(), Box<dyn Error>> { + let pos = self.next(); + if screen.in_bounds(pos.row, pos.col) { + sc::move_cursor(pos.row, pos.col)?; + write!(stdout(), " ")?; + } + for &pos in self.body.iter() { + sc::move_cursor(pos.row, pos.col)?; + write!(stdout(), " ")?; + } + Ok(()) + } + + fn print(&self, g: &mut Gen, screen: &Screen) -> Result<(), Box<dyn Error>> { + let next = self.next(); + if (self.direction == Key::Left || self.direction == Key::Right) + && screen.in_bounds(next.row, next.col) + && g.rand(40) == 0 + { + sc::move_cursor(next.row, next.col)?; + write!(stdout(), "~")?; + } + for (i, &pos) in self.body.iter().enumerate() { + sc::move_cursor(pos.row, pos.col)?; + 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; + } +} + +struct Fruit { + pos: Pos, +} + +impl Fruit { + fn print(&self) -> Result<(), Box<dyn Error>> { + sc::move_cursor(self.pos.row, self.pos.col)?; + 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 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 snake = Snake { + body: vec![Pos { + row: screen.rows / 2, + col: screen.cols / 2, + }], + direction: Key::Right, + growing: 0, + }; + let mut fruit = Fruit { + pos: Pos { row: 0, col: 0 }, + }; + fruit.new_pos(&mut g, &screen); + let mut score: i64 = 0; + let mut real_score: i64 = 0; + let mut delay = Duration::from_millis(200); + + loop { + sc::move_cursor(0, 3)?; + write!(stdout(), " Score: {} ", score)?; + snake.print(&mut g, &screen)?; + fruit.print()?; + + sc::move_cursor(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); + + 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.step(); + + if snake.body[0] == fruit.pos { + snake.growing += 6; + loop { + fruit.new_pos(&mut g, &screen); + 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 !screen.in_bounds(snake.body[0].row, snake.body[0].col) + || snake.body[1..].contains(&snake.body[0]) + { + return Ok(score); + } + } +} + +fn main() { + let score = match snake() { + Ok(s) => s, + Err(err) => { + let _ = writeln!(stderr(), "FAIL: {}", err); + std::process::exit(1); + } + }; + let _ = sc::clear(); + let _ = sc::move_cursor(0, 0); + let _ = writeln!(stdout(), "Score: {}", score); +} diff --git a/src/rand.rs b/src/rand.rs new file mode 100644 index 0000000..f9aee46 --- /dev/null +++ b/src/rand.rs @@ -0,0 +1,17 @@ +pub struct Gen { + state: i64, +} + + +pub fn new(seed: i64) -> Gen { + Gen{ + state: seed, + } +} + +impl Gen { + pub fn rand(&mut self, max: i32) -> i32 { + self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1); + (self.state >> 33 & 0x7fffffff) as i32 % max + } +} diff --git a/src/sc.rs b/src/sc.rs new file mode 100644 index 0000000..cb20c68 --- /dev/null +++ b/src/sc.rs @@ -0,0 +1,134 @@ +use libc::{termios, winsize}; +use std::error::Error; +use std::io::{stdin, stdout, Read, Write}; +use std::sync::mpsc::{Receiver, SyncSender, TryRecvError}; + +pub fn move_cursor(row: u16, col: u16) -> Result<(), Box<dyn Error>> { + write!(stdout(), "\x1b[{};{}H", row + 1, col + 1)?; + Ok(()) +} + +pub fn clear() -> Result<(), Box<dyn Error>> { + write!(stdout(), "\x1b[2J")?; + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Key { + Up, + Down, + Left, + Right, +} + +pub struct Screen { + pub rows: u16, + pub cols: u16, + old_attr: termios, + cancel: SyncSender<()>, + keys: Receiver<Key>, +} + +impl Screen { + pub fn init() -> Result<Screen, Box<dyn Error>> { + let mut size: winsize; + unsafe { + size = std::mem::zeroed(); + if libc::ioctl(1, libc::TIOCGWINSZ, &mut size) < 0 { + return Err(Box::from("ioctl failed")); + } + } + if size.ws_col > 60 { + size.ws_col = 60; + } + if size.ws_row > 30 { + size.ws_row = 30; + } + + let mut attr; + unsafe { + attr = std::mem::zeroed(); + if libc::tcgetattr(1, &mut attr) < 0 { + return Err(Box::from("tcgetattr failed")); + } + } + + unsafe { + let mut raw = std::mem::zeroed(); + libc::cfmakeraw(&mut raw); + raw.c_cc[libc::VMIN] = 0; + raw.c_cc[libc::VTIME] = 1; + if libc::tcsetattr(1, libc::TCSANOW, &raw) < 0 { + return Err(Box::from("tcsetattr failed")); + } + } + + clear()?; + move_cursor(0, 0)?; + for _ in 0..size.ws_col { + write!(stdout(), "-")?; + } + for i in 1..size.ws_row - 1 { + move_cursor(i, 0)?; + write!(stdout(), "|")?; + move_cursor(i, size.ws_col - 1)?; + write!(stdout(), "|")?; + } + move_cursor(size.ws_row - 1, 0)?; + for _ in 0..size.ws_col { + write!(stdout(), "-")?; + } + + let (cancel_tx, cancel_rx) = std::sync::mpsc::sync_channel(0); + let (key_tx, key_rx) = std::sync::mpsc::sync_channel(4); + std::thread::spawn(move || { + 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' => Key::Up, + b'B' => Key::Down, + b'C' => Key::Right, + b'D' => Key::Left, + _ => { + continue; + } + }; + let _ = key_tx.try_send(key); + } + }); + + Ok(Screen { + rows: size.ws_row, + cols: size.ws_col, + old_attr: attr, + cancel: cancel_tx, + keys: key_rx, + }) + } + + pub fn read_key(&self) -> Option<Key> { + if let Ok(key) = self.keys.try_recv() { + return Some(key); + } + None + } + + pub fn in_bounds(&self, row: u16, col: u16) -> bool { + (1..self.rows - 1).contains(&row) && (1..self.cols - 1).contains(&col) + } +} + +impl Drop for Screen { + fn drop(&mut self) { + self.cancel.send(()).unwrap(); + unsafe { + libc::tcsetattr(1, libc::TCSANOW, &self.old_attr); + } + } +} |
