initialization
This commit is contained in:
commit
36a93ab177
13 changed files with 601 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
58
Cargo.lock
generated
Normal file
58
Cargo.lock
generated
Normal 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
9
Cargo.toml
Normal 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
9
README.md
Normal 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
11
patterns/flower.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
###
|
||||
# #
|
||||
# #
|
||||
## ##
|
||||
# # #
|
||||
# # # #
|
||||
# # #
|
||||
## ##
|
||||
# #
|
||||
# #
|
||||
###
|
3
patterns/glider.txt
Normal file
3
patterns/glider.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
#
|
||||
#
|
||||
###
|
3
patterns/simple.txt
Normal file
3
patterns/simple.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
#
|
||||
#
|
||||
#
|
44
src/main.rs
Normal file
44
src/main.rs
Normal 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
163
src/sim.rs
Normal 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
28
src/utils.rs
Normal 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
120
src/view.rs
Normal 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
35
src/world.rs
Normal 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
117
src/world/hashed_world.rs
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue