commit 36a93ab1772439dea159c6f66fd14a6e83484c81 Author: mb Date: Wed Aug 31 18:19:14 2022 +0300 initialization 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..3e5b386 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,58 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "golrs" +version = "0.1.0" +dependencies = [ + "termion", +] + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3e7ca97 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "golrs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +termion = "1.5.6" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8bba6d --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# golrs + +Game Of Life RuSt + +--- + +## Description + +This is a TUI for vizualising a rust implementation of the game of life diff --git a/patterns/flower.txt b/patterns/flower.txt new file mode 100644 index 0000000..02e81d4 --- /dev/null +++ b/patterns/flower.txt @@ -0,0 +1,11 @@ + ### + # # + # # + ## ## +# # # +# # # # +# # # + ## ## + # # + # # + ### \ No newline at end of file diff --git a/patterns/glider.txt b/patterns/glider.txt new file mode 100644 index 0000000..b284458 --- /dev/null +++ b/patterns/glider.txt @@ -0,0 +1,3 @@ + # + # +### \ No newline at end of file diff --git a/patterns/simple.txt b/patterns/simple.txt new file mode 100644 index 0000000..98f6f22 --- /dev/null +++ b/patterns/simple.txt @@ -0,0 +1,3 @@ + # + # + # \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..84fc242 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,44 @@ +use std::{env::args, fs, process::exit}; + +pub use utils::Pos; +mod utils; + +pub use world::{Cell, HashedWorld, World}; +pub mod world; + +pub use sim::{Sim, SimHandle}; +mod sim; + +pub use view::View; +mod view; + +fn deserialize(str: &str) -> Vec { + let mut result = vec![]; + let mut pos = pos!(0, 0); + for c in str.chars() { + match c { + '#' => { + result.push(pos); + pos.x += 1 + } + '\n' => pos = pos!(0, pos.y + 1), + _ => pos.x += 1, + } + } + result +} + +pub fn main() { + let path = args().nth(1).unwrap_or_else(|| { + eprintln!("[error] must provide a path argument"); + exit(1); + }); + + let content = fs::read_to_string(path).unwrap(); + let actives = deserialize(&content); + let simulation = Sim::spawn(actives); + let view = View::spawn::(simulation.handle()); + + simulation.join(); + view.join(); +} diff --git a/src/sim.rs b/src/sim.rs new file mode 100644 index 0000000..389f776 --- /dev/null +++ b/src/sim.rs @@ -0,0 +1,163 @@ +use std::{ + sync::mpsc, + thread::{self, JoinHandle}, + time::{Duration, SystemTime}, +}; + +use crate::{pos, Cell, Pos, World}; + +#[derive(Debug, Default)] +pub struct State +where + W: World, +{ + world: W, +} + +impl State +where + W: World, +{ + pub fn actives(&self) -> Vec { + self.world.actives() + } + + pub fn get(&self, pos: Pos) -> Cell { + self.world.get(pos) + } + + pub fn set(&mut self, pos: Pos, cell: Cell) { + self.world.set(pos, cell) + } + + fn possible_change_pos(&self) -> impl Iterator + '_ { + self.actives() + .into_iter() + .map(|p| self.get_neighbors(p)) + .flatten() + } + + pub fn get_neighbors(&self, pos: Pos) -> impl Iterator + '_ { + (-1..=1) + .map(|x| (-1..=1).map(move |y| pos!(x, y))) + .flatten() + .map(move |p| pos + p) + } + + pub fn is_cell_alive(&self, pos: Pos) -> bool { + self.get(pos.clone()).is_active() + } + + pub fn get_neighbor_count(&self, pos: Pos) -> usize { + self.get_neighbors(pos) + .filter(|pos| self.is_cell_alive(pos.clone())) + .count() + } + + pub fn snapshot(&self) -> W { + self.world.clone() + } +} + +pub enum SimCmd +where + W: World, +{ + Snapshot(mpsc::Sender), +} + +pub struct SimHandle +where + W: World, +{ + sender: mpsc::Sender>, +} + +impl SimHandle +where + W: World, +{ + pub fn new(sender: mpsc::Sender>) -> Self { + Self { sender } + } + + pub fn snapshot(&self) -> W { + let (sender, receiver) = mpsc::channel(); + self.sender.send(SimCmd::Snapshot(sender)).unwrap(); + receiver.recv().unwrap() + } +} + +#[derive(Debug)] +pub struct Sim +where + W: World, +{ + thread: JoinHandle<()>, + sender: mpsc::Sender>, +} + +impl Sim +where + W: World, +{ + pub fn spawn(actives: impl IntoIterator) -> Self { + let mut state: State = State::default(); + for active in actives.into_iter() { + state.set(active, Cell::active()); + } + + let (sender, receiver) = mpsc::channel(); + let thread = thread::spawn(move || sim_loop(receiver, state)); + + Self { sender, thread } + } + + pub fn handle(&self) -> SimHandle { + let sender = self.sender.clone(); + SimHandle { sender } + } + + pub fn join(self) { + self.thread.join().unwrap(); + } +} + +const EVT_CHECK_TIMEOUT: Duration = Duration::from_millis(10); +const SIM_TICK_INTERVAL: Duration = Duration::from_millis(200); + +fn sim_loop(receiver: mpsc::Receiver>, state: State) +where + W: World, +{ + let mut current_state = state; + let mut last_update = SystemTime::now(); + + loop { + if let Some(cmd) = receiver.try_recv().ok() { + match cmd { + SimCmd::Snapshot(sender) => sender.send(current_state.snapshot()).unwrap(), + } + } + + if SystemTime::now().duration_since(last_update).unwrap() > SIM_TICK_INTERVAL { + let old_state = current_state; + let mut new_state: State = State::default(); + + for pos in old_state.possible_change_pos() { + let is_active = old_state.is_cell_alive(pos); + let neighbor_count = old_state.get_neighbor_count(pos); + match (is_active, neighbor_count) { + (true, count) if count < 3 || count > 4 => (), // die + (true, _) => new_state.set(pos, Cell::active()), // stay + (false, 3) => new_state.set(pos, Cell::active()), // becomes alive + _ => (), // stays dead + } + } + current_state = new_state; + last_update = SystemTime::now(); + } + + thread::sleep(EVT_CHECK_TIMEOUT); + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..0c5995e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,28 @@ +use std::ops::{Add, Sub}; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct Pos { + pub x: i32, + pub y: i32, +} + +#[macro_export] +macro_rules! pos { + ($x:expr, $y:expr) => { + crate::Pos { x: $x, y: $y } + }; +} + +impl Add for Pos { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + pos!(self.x + rhs.x, self.y + rhs.y) + } +} + +impl Sub for Pos { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + pos!(self.x - rhs.x, self.y - rhs.y) + } +} diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 0000000..eaf1ef0 --- /dev/null +++ b/src/view.rs @@ -0,0 +1,120 @@ +use std::{ + io::{stdin, stdout, Write}, + process::exit, + sync::mpsc, + thread::{self, JoinHandle}, + time::Duration, +}; + +use termion::{event::Key, input::TermRead, raw::IntoRawMode}; + +use crate::{pos, Pos, SimHandle, World}; + +pub struct View { + thread: JoinHandle<()>, +} +impl View { + pub fn spawn(handle: SimHandle) -> Self + where + W: World, + { + let thread = thread::spawn(|| view_loop(handle)); + Self { thread } + } + + pub fn join(self) { + self.thread.join().unwrap(); + } +} + +#[derive(Debug)] +pub enum Dir { + Up, + Down, + Left, + Right, +} + +#[derive(Debug)] +pub enum InputCmd { + Exit, + Move(Dir), + Accelerate, + Decelerate, +} + +fn input_loop(sender: mpsc::Sender) { + let stdout = stdout().into_raw_mode().unwrap(); + for c in stdin().keys() { + let command = match c.unwrap() { + Key::Char('q') => InputCmd::Exit, + Key::Up => InputCmd::Move(Dir::Up), + Key::Down => InputCmd::Move(Dir::Down), + Key::Left => InputCmd::Move(Dir::Left), + Key::Right => InputCmd::Move(Dir::Right), + _ => continue, + }; + + sender.send(command).unwrap(); + } + drop(stdout); +} + +const VIEW_REFRESH_INTERVAL: Duration = Duration::from_millis(100); + +fn view_loop(handle: SimHandle) +where + W: World, +{ + let (sender, receiver) = mpsc::channel(); + let _input_handle = thread::spawn(|| input_loop(sender)); + + let mut view_origin = pos!(0, 0); + loop { + handle_inputs(&receiver, &mut view_origin); + let world = handle.snapshot(); + display_world(view_origin, world); + thread::sleep(VIEW_REFRESH_INTERVAL); + } + drop(handle); +} + +fn handle_inputs(receiver: &mpsc::Receiver, view_origin: &mut Pos) { + if let Some(cmd) = receiver.try_recv().ok() { + match cmd { + InputCmd::Exit => exit(0), + InputCmd::Move(direction) => { + *view_origin = *view_origin + + match direction { + Dir::Up => pos!(0, -4), + Dir::Down => pos!(0, 4), + Dir::Left => pos!(-4, 0), + Dir::Right => pos!(4, 0), + } + } + InputCmd::Accelerate => todo!(), + InputCmd::Decelerate => todo!(), + } + } +} + +fn display_world(view_origin: Pos, world: W) +where + W: World, +{ + let (width, height) = termion::terminal_size().unwrap(); + let mut result = String::new(); + + for ly in 0..(height) { + let next_line = termion::cursor::Goto(1, ly + 1); + result += &format!("{next_line}"); + for lx in 0..width { + let pos = view_origin + pos!(lx as i32, ly as i32); + let char = &if world.get(pos).is_active() { "#" } else { " " }; + result += char + } + } + let clear = termion::clear::All; + print!("{clear}{result}"); + stdout().flush().unwrap(); +} diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..7c614ae --- /dev/null +++ b/src/world.rs @@ -0,0 +1,35 @@ +use crate::Pos; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Cell { + active: bool, +} + +impl Cell { + pub fn active() -> Self { + Self { active: true } + } + + pub fn inactive() -> Self { + Self { active: false } + } + + pub fn is_active(&self) -> bool { + self.active + } +} + +impl Default for Cell { + fn default() -> Self { + Cell { active: false } + } +} + +pub trait World: Default + Clone + Send + 'static { + fn get(&self, pos: Pos) -> Cell; + fn set(&mut self, pos: Pos, cell: Cell); + fn actives(&self) -> Vec; +} + +pub use hashed_world::HashedWorld; +mod hashed_world; diff --git a/src/world/hashed_world.rs b/src/world/hashed_world.rs new file mode 100644 index 0000000..c9cba70 --- /dev/null +++ b/src/world/hashed_world.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; + +use crate::{pos, Cell, Pos, World}; + +const CHUNK_SIZE: usize = 16; + +#[derive(Debug, Default, Clone)] +struct Chunk { + cells: [[Cell; CHUNK_SIZE]; CHUNK_SIZE], +} + +impl Chunk { + fn get(&self, pos: Pos) -> Cell { + let pos = HashedWorld::get_local_pos(pos); + self.cells[pos.x as usize][pos.y as usize].clone() + } + + fn set(&mut self, pos: Pos, cell: Cell) { + let pos = HashedWorld::get_local_pos(pos); + self.cells[pos.x as usize][pos.y as usize] = cell; + } + + fn get_actives(&self) -> impl Iterator + '_ { + self.cells + .iter() + .enumerate() + .map(|(x, row)| { + row.iter().enumerate().filter_map(move |(y, cell)| { + cell.is_active().then_some(pos!(x as i32, y as i32)) + }) + }) + .flatten() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ChunkPos(Pos); + +#[derive(Debug, Clone, Default)] +pub struct HashedWorld { + chunks: HashMap, +} + +impl HashedWorld { + /// gets the position of a chunk containing the passed position + fn get_chunk_pos(Pos { x, y }: Pos) -> ChunkPos { + let x = snap(x, CHUNK_SIZE as i32); + let y = snap(y, CHUNK_SIZE as i32); + ChunkPos(pos!(x, y)) + } + + /// gets the position of a cell local to it's parent chunk. + fn get_local_pos(pos: Pos) -> Pos { + let ChunkPos(chunk_pos) = Self::get_chunk_pos(pos); + pos - chunk_pos + } + + fn get_chunk(&self, pos: Pos) -> Option<&Chunk> { + let pos = Self::get_chunk_pos(pos); + self.chunks.get(&pos) + } + + fn get_chunk_mut(&mut self, pos: Pos) -> Option<&mut Chunk> { + let pos = Self::get_chunk_pos(pos); + self.chunks.get_mut(&pos) + } + + fn push_chunk(&mut self, pos: Pos, chunk: Chunk) { + let chunk_pos = Self::get_chunk_pos(pos); + self.chunks.insert(chunk_pos, chunk); + } +} + +pub fn snap(n: i32, step: i32) -> i32 { + // frankly, I forgot how it works, but somehow it passes tests + let rem = ((n % step) + step) % step; + n - rem +} + +#[test] +fn test_snap() { + assert_eq!(snap(0, 10), 0); + assert_eq!(snap(1, 10), 0); + assert_eq!(snap(-1, 10), -10); + assert_eq!(snap(10, 10), 10); + assert_eq!(snap(11, 10), 10); +} + +impl World for HashedWorld { + fn get(&self, pos: Pos) -> Cell { + if let Some(chunk) = self.get_chunk(pos) { + chunk.get(pos) + } else { + Cell::inactive() + } + } + + fn set(&mut self, pos: Pos, cell: Cell) { + if let Some(chunk) = self.get_chunk_mut(pos) { + chunk.set(pos, cell) + } else { + let mut chunk = Chunk::default(); + chunk.set(pos, cell); + self.push_chunk(pos, chunk) + } + } + + fn actives(&self) -> Vec { + self.chunks + .iter() + .map(|(ChunkPos(chunk_pos), chunk)| { + chunk.get_actives().map(|pos| chunk_pos.clone() + pos) + }) + .flatten() + .collect() + } +}