improved canvas display and added controls
This commit is contained in:
parent
36a93ab177
commit
9cf5a5b56d
6 changed files with 157 additions and 52 deletions
|
@ -17,12 +17,14 @@ fn deserialize(str: &str) -> Vec<Pos> {
|
||||||
let mut pos = pos!(0, 0);
|
let mut pos = pos!(0, 0);
|
||||||
for c in str.chars() {
|
for c in str.chars() {
|
||||||
match c {
|
match c {
|
||||||
'#' => {
|
' ' => {
|
||||||
|
pos.x += 1;
|
||||||
|
}
|
||||||
|
'\n' => pos = pos!(0, pos.y + 1),
|
||||||
|
_ => {
|
||||||
result.push(pos);
|
result.push(pos);
|
||||||
pos.x += 1
|
pos.x += 1
|
||||||
}
|
}
|
||||||
'\n' => pos = pos!(0, pos.y + 1),
|
|
||||||
_ => pos.x += 1,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
|
|
36
src/sim.rs
36
src/sim.rs
|
@ -33,24 +33,22 @@ where
|
||||||
fn possible_change_pos(&self) -> impl Iterator<Item = Pos> + '_ {
|
fn possible_change_pos(&self) -> impl Iterator<Item = Pos> + '_ {
|
||||||
self.actives()
|
self.actives()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| self.get_neighbors(p))
|
.flat_map(|p| self.get_neighbors(p))
|
||||||
.flatten()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_neighbors(&self, pos: Pos) -> impl Iterator<Item = Pos> + '_ {
|
pub fn get_neighbors(&self, pos: Pos) -> impl Iterator<Item = Pos> + '_ {
|
||||||
(-1..=1)
|
(-1..=1)
|
||||||
.map(|x| (-1..=1).map(move |y| pos!(x, y)))
|
.flat_map(|x| (-1..=1).map(move |y| pos!(x, y)))
|
||||||
.flatten()
|
|
||||||
.map(move |p| pos + p)
|
.map(move |p| pos + p)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_cell_alive(&self, pos: Pos) -> bool {
|
pub fn is_cell_alive(&self, pos: Pos) -> bool {
|
||||||
self.get(pos.clone()).is_active()
|
self.get(pos).is_active()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_neighbor_count(&self, pos: Pos) -> usize {
|
pub fn get_neighbor_count(&self, pos: Pos) -> usize {
|
||||||
self.get_neighbors(pos)
|
self.get_neighbors(pos)
|
||||||
.filter(|pos| self.is_cell_alive(pos.clone()))
|
.filter(|pos| self.is_cell_alive(*pos))
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +62,8 @@ where
|
||||||
W: World,
|
W: World,
|
||||||
{
|
{
|
||||||
Snapshot(mpsc::Sender<W>),
|
Snapshot(mpsc::Sender<W>),
|
||||||
|
SetDelay(u64),
|
||||||
|
Delay(mpsc::Sender<usize>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SimHandle<W>
|
pub struct SimHandle<W>
|
||||||
|
@ -86,6 +86,16 @@ where
|
||||||
self.sender.send(SimCmd::Snapshot(sender)).unwrap();
|
self.sender.send(SimCmd::Snapshot(sender)).unwrap();
|
||||||
receiver.recv().unwrap()
|
receiver.recv().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delay(&self) -> usize {
|
||||||
|
let (sender, receiver) = mpsc::channel();
|
||||||
|
self.sender.send(SimCmd::Delay(sender)).unwrap();
|
||||||
|
receiver.recv().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_delay(&self, delay_ms: u64) {
|
||||||
|
self.sender.send(SimCmd::SetDelay(delay_ms)).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -124,23 +134,25 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
const EVT_CHECK_TIMEOUT: Duration = Duration::from_millis(10);
|
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>)
|
fn sim_loop<W>(receiver: mpsc::Receiver<SimCmd<W>>, state: State<W>)
|
||||||
where
|
where
|
||||||
W: World,
|
W: World,
|
||||||
{
|
{
|
||||||
|
let mut tick_interval = Duration::from_millis(200);
|
||||||
let mut current_state = state;
|
let mut current_state = state;
|
||||||
let mut last_update = SystemTime::now();
|
let mut last_update = SystemTime::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(cmd) = receiver.try_recv().ok() {
|
if let Ok(cmd) = receiver.try_recv() {
|
||||||
match cmd {
|
match cmd {
|
||||||
SimCmd::Snapshot(sender) => sender.send(current_state.snapshot()).unwrap(),
|
SimCmd::Snapshot(sender) => sender.send(current_state.snapshot()).unwrap(),
|
||||||
|
SimCmd::SetDelay(delay) => tick_interval = Duration::from_millis(delay),
|
||||||
|
SimCmd::Delay(sender) => sender.send(tick_interval.as_millis() as usize).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if SystemTime::now().duration_since(last_update).unwrap() > SIM_TICK_INTERVAL {
|
if SystemTime::now().duration_since(last_update).unwrap() > tick_interval {
|
||||||
let old_state = current_state;
|
let old_state = current_state;
|
||||||
let mut new_state: State<W> = State::default();
|
let mut new_state: State<W> = State::default();
|
||||||
|
|
||||||
|
@ -148,10 +160,10 @@ where
|
||||||
let is_active = old_state.is_cell_alive(pos);
|
let is_active = old_state.is_cell_alive(pos);
|
||||||
let neighbor_count = old_state.get_neighbor_count(pos);
|
let neighbor_count = old_state.get_neighbor_count(pos);
|
||||||
match (is_active, neighbor_count) {
|
match (is_active, neighbor_count) {
|
||||||
(true, count) if count < 3 || count > 4 => (), // die
|
(true, count) if !(3..=4).contains(&count) => (), // die
|
||||||
(true, _) => new_state.set(pos, Cell::active()), // stay
|
(true, _) => new_state.set(pos, Cell::active()), // stay
|
||||||
(false, 3) => new_state.set(pos, Cell::active()), // becomes alive
|
(false, 3) => new_state.set(pos, Cell::active()), // becomes alive
|
||||||
_ => (), // stays dead
|
_ => (), // stays dead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current_state = new_state;
|
current_state = new_state;
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub struct Pos {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! pos {
|
macro_rules! pos {
|
||||||
($x:expr, $y:expr) => {
|
($x:expr, $y:expr) => {
|
||||||
crate::Pos { x: $x, y: $y }
|
Pos { x: $x, y: $y }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
101
src/view.rs
101
src/view.rs
|
@ -1,11 +1,14 @@
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdin, stdout, Write},
|
io::{stdin, stdout},
|
||||||
process::exit,
|
process::exit,
|
||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
thread::{self, JoinHandle},
|
thread::{self, JoinHandle},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use canvas::Canvas;
|
||||||
|
mod canvas;
|
||||||
|
|
||||||
use termion::{event::Key, input::TermRead, raw::IntoRawMode};
|
use termion::{event::Key, input::TermRead, raw::IntoRawMode};
|
||||||
|
|
||||||
use crate::{pos, Pos, SimHandle, World};
|
use crate::{pos, Pos, SimHandle, World};
|
||||||
|
@ -52,6 +55,8 @@ fn input_loop(sender: mpsc::Sender<InputCmd>) {
|
||||||
Key::Down => InputCmd::Move(Dir::Down),
|
Key::Down => InputCmd::Move(Dir::Down),
|
||||||
Key::Left => InputCmd::Move(Dir::Left),
|
Key::Left => InputCmd::Move(Dir::Left),
|
||||||
Key::Right => InputCmd::Move(Dir::Right),
|
Key::Right => InputCmd::Move(Dir::Right),
|
||||||
|
Key::Char('+') => InputCmd::Accelerate,
|
||||||
|
Key::Char('-') => InputCmd::Decelerate,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,6 +67,7 @@ fn input_loop(sender: mpsc::Sender<InputCmd>) {
|
||||||
|
|
||||||
const VIEW_REFRESH_INTERVAL: Duration = Duration::from_millis(100);
|
const VIEW_REFRESH_INTERVAL: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
fn view_loop<W>(handle: SimHandle<W>)
|
fn view_loop<W>(handle: SimHandle<W>)
|
||||||
where
|
where
|
||||||
W: World,
|
W: World,
|
||||||
|
@ -69,52 +75,89 @@ where
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
let _input_handle = thread::spawn(|| input_loop(sender));
|
let _input_handle = thread::spawn(|| input_loop(sender));
|
||||||
|
|
||||||
let mut view_origin = pos!(0, 0);
|
let (x, y) = termion::terminal_size().unwrap();
|
||||||
|
let mut delay = 200u64;
|
||||||
|
let mut view_origin = pos!(-(x as i32) / 2, -(y as i32));
|
||||||
loop {
|
loop {
|
||||||
handle_inputs(&receiver, &mut view_origin);
|
handle_inputs(&receiver, &mut view_origin, &mut delay);
|
||||||
let world = handle.snapshot();
|
handle.set_delay(delay);
|
||||||
display_world(view_origin, world);
|
|
||||||
thread::sleep(VIEW_REFRESH_INTERVAL);
|
let mut canvas = Canvas::from_screen();
|
||||||
|
grid_layer(&mut canvas, view_origin);
|
||||||
|
world_layer(&mut canvas, handle.snapshot(), view_origin);
|
||||||
|
title_layer(&mut canvas, handle.delay());
|
||||||
|
canvas.display();
|
||||||
}
|
}
|
||||||
drop(handle);
|
drop(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_inputs(receiver: &mpsc::Receiver<InputCmd>, view_origin: &mut Pos) {
|
const MOVEMENT_STEP: i32 = 3;
|
||||||
if let Some(cmd) = receiver.try_recv().ok() {
|
|
||||||
|
fn handle_inputs(receiver: &mpsc::Receiver<InputCmd>, view_origin: &mut Pos, delay: &mut u64) {
|
||||||
|
if let Ok(cmd) = receiver.recv_timeout(VIEW_REFRESH_INTERVAL) {
|
||||||
match cmd {
|
match cmd {
|
||||||
InputCmd::Exit => exit(0),
|
InputCmd::Exit => {
|
||||||
|
println!("{}{}", termion::clear::All, termion::cursor::Goto(1, 1));
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
InputCmd::Move(direction) => {
|
InputCmd::Move(direction) => {
|
||||||
*view_origin = *view_origin
|
*view_origin = *view_origin
|
||||||
+ match direction {
|
+ match direction {
|
||||||
Dir::Up => pos!(0, -4),
|
Dir::Up => pos!(0, -MOVEMENT_STEP),
|
||||||
Dir::Down => pos!(0, 4),
|
Dir::Down => pos!(0, MOVEMENT_STEP),
|
||||||
Dir::Left => pos!(-4, 0),
|
Dir::Left => pos!(-2 * MOVEMENT_STEP, 0),
|
||||||
Dir::Right => pos!(4, 0),
|
Dir::Right => pos!(2 * MOVEMENT_STEP, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InputCmd::Accelerate => todo!(),
|
InputCmd::Accelerate => *delay -= 100,
|
||||||
InputCmd::Decelerate => todo!(),
|
InputCmd::Decelerate => *delay += 100,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_world<W>(view_origin: Pos, world: W)
|
fn grid_layer(canvas: &mut Canvas, view_origin: Pos) {
|
||||||
|
canvas.layer(|local_pos| {
|
||||||
|
let Pos { x, y } = local_pos + view_origin;
|
||||||
|
match (x % 16 == 0, y % 8 == 0) {
|
||||||
|
(true, true) => Some('┼'),
|
||||||
|
(true, _) => Some('│'),
|
||||||
|
(_, true) => Some('─'),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn world_layer<W>(canvas: &mut Canvas, world: W, view_origin: Pos)
|
||||||
where
|
where
|
||||||
W: World,
|
W: World,
|
||||||
{
|
{
|
||||||
let (width, height) = termion::terminal_size().unwrap();
|
canvas.layer(|mut local_pos| {
|
||||||
let mut result = String::new();
|
local_pos.y *= 2;
|
||||||
|
let pos_top = local_pos + view_origin;
|
||||||
|
let top = world.get(pos_top).is_active();
|
||||||
|
let pos_bottom = pos_top + pos!(0, 1);
|
||||||
|
let bottom = world.get(pos_bottom).is_active();
|
||||||
|
|
||||||
for ly in 0..(height) {
|
match (top, bottom) {
|
||||||
let next_line = termion::cursor::Goto(1, ly + 1);
|
(true, true) => Some('█'),
|
||||||
result += &format!("{next_line}");
|
(true, _) => Some('▀'),
|
||||||
for lx in 0..width {
|
(_, true) => Some('▄'),
|
||||||
let pos = view_origin + pos!(lx as i32, ly as i32);
|
_ => None,
|
||||||
let char = &if world.get(pos).is_active() { "#" } else { " " };
|
|
||||||
result += char
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
let clear = termion::clear::All;
|
}
|
||||||
print!("{clear}{result}");
|
|
||||||
stdout().flush().unwrap();
|
#[allow(clippy::useless_format)]
|
||||||
|
fn title_layer(canvas: &mut Canvas, de: usize) {
|
||||||
|
let table = [
|
||||||
|
format!("│ <golrs> | [+]: speed up │"),
|
||||||
|
format!("│ delay: | [-]: slow down │"),
|
||||||
|
format!("│ {de:>7} ms | [q]: quit │"),
|
||||||
|
format!("└──────────────────────────────┘"),
|
||||||
|
];
|
||||||
|
canvas.layer(|Pos { x, y }| {
|
||||||
|
table
|
||||||
|
.get(y as usize)
|
||||||
|
.and_then(|line| line.chars().nth(x as usize))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
54
src/view/canvas.rs
Normal file
54
src/view/canvas.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use std::io::{stdout, Write};
|
||||||
|
|
||||||
|
use crate::{pos, Pos};
|
||||||
|
|
||||||
|
pub struct Canvas {
|
||||||
|
lines: Vec<String>,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Canvas {
|
||||||
|
pub fn from_screen() -> Self {
|
||||||
|
let (width, height) = termion::terminal_size().unwrap();
|
||||||
|
Self::new(width as usize, (height - 1) as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
|
let lines = (0..height)
|
||||||
|
.map(|_| (0..width).map(|_| ' '.to_string()).collect::<String>())
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
height,
|
||||||
|
lines,
|
||||||
|
width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layer(&mut self, f: impl Fn(Pos) -> Option<char>) {
|
||||||
|
for y in 0..self.height {
|
||||||
|
for x in 0..self.width {
|
||||||
|
if let Some(char) = f(pos!(x as i32, y as i32)) {
|
||||||
|
let line = &mut self.lines[y];
|
||||||
|
line.replace_range(
|
||||||
|
line.char_indices()
|
||||||
|
.nth(x)
|
||||||
|
.map(|(pos, ch)| (pos..pos + ch.len_utf8()))
|
||||||
|
.unwrap(),
|
||||||
|
&format!("{char}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(&self) {
|
||||||
|
let clear = termion::clear::All;
|
||||||
|
print!("{clear}");
|
||||||
|
for (index, line) in self.lines.iter().enumerate() {
|
||||||
|
let goto = termion::cursor::Goto(1, index as u16 + 1);
|
||||||
|
println!("{goto}{line}");
|
||||||
|
}
|
||||||
|
stdout().flush().unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::Pos;
|
use crate::Pos;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub struct Cell {
|
pub struct Cell {
|
||||||
active: bool,
|
active: bool,
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,6 @@ impl Cell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Cell {
|
|
||||||
fn default() -> Self {
|
|
||||||
Cell { active: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait World: Default + Clone + Send + 'static {
|
pub trait World: Default + Clone + Send + 'static {
|
||||||
fn get(&self, pos: Pos) -> Cell;
|
fn get(&self, pos: Pos) -> Cell;
|
||||||
fn set(&mut self, pos: Pos, cell: Cell);
|
fn set(&mut self, pos: Pos, cell: Cell);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue