initialization

This commit is contained in:
mb 2022-08-31 18:19:14 +03:00
commit 36a93ab177
13 changed files with 601 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

58
Cargo.lock generated Normal file
View file

@ -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",
]

9
Cargo.toml Normal file
View file

@ -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"

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# golrs
Game Of Life RuSt
---
## Description
This is a TUI for vizualising a rust implementation of the game of life

11
patterns/flower.txt Normal file
View file

@ -0,0 +1,11 @@
###
# #
# #
## ##
# # #
# # # #
# # #
## ##
# #
# #
###

3
patterns/glider.txt Normal file
View file

@ -0,0 +1,3 @@
#
#
###

3
patterns/simple.txt Normal file
View file

@ -0,0 +1,3 @@
#
#
#

44
src/main.rs Normal file
View file

@ -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<Pos> {
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::<HashedWorld>(simulation.handle());
simulation.join();
view.join();
}

163
src/sim.rs Normal file
View file

@ -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<W>
where
W: World,
{
world: W,
}
impl<W> State<W>
where
W: World,
{
pub fn actives(&self) -> Vec<Pos> {
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<Item = Pos> + '_ {
self.actives()
.into_iter()
.map(|p| self.get_neighbors(p))
.flatten()
}
pub fn get_neighbors(&self, pos: Pos) -> impl Iterator<Item = Pos> + '_ {
(-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<W>
where
W: World,
{
Snapshot(mpsc::Sender<W>),
}
pub struct SimHandle<W>
where
W: World,
{
sender: mpsc::Sender<SimCmd<W>>,
}
impl<W> SimHandle<W>
where
W: World,
{
pub fn new(sender: mpsc::Sender<SimCmd<W>>) -> 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<W>
where
W: World,
{
thread: JoinHandle<()>,
sender: mpsc::Sender<SimCmd<W>>,
}
impl<W> Sim<W>
where
W: World,
{
pub fn spawn(actives: impl IntoIterator<Item = Pos>) -> Self {
let mut state: State<W> = 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<W> {
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<W>(receiver: mpsc::Receiver<SimCmd<W>>, state: State<W>)
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<W> = 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);
}
}

28
src/utils.rs Normal file
View file

@ -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)
}
}

120
src/view.rs Normal file
View file

@ -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<W>(handle: SimHandle<W>) -> 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<InputCmd>) {
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<W>(handle: SimHandle<W>)
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<InputCmd>, 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<W>(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();
}

35
src/world.rs Normal file
View file

@ -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<Pos>;
}
pub use hashed_world::HashedWorld;
mod hashed_world;

117
src/world/hashed_world.rs Normal file
View file

@ -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<Item = Pos> + '_ {
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<ChunkPos, Chunk>,
}
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<Pos> {
self.chunks
.iter()
.map(|(ChunkPos(chunk_pos), chunk)| {
chunk.get_actives().map(|pos| chunk_pos.clone() + pos)
})
.flatten()
.collect()
}
}