From 9cf5a5b56dd007dea2adc20b9ac15a78aa7bdf7c Mon Sep 17 00:00:00 2001 From: JOLIMAITRE Matthieu Date: Wed, 31 Aug 2022 18:47:20 +0200 Subject: [PATCH] improved canvas display and added controls --- src/main.rs | 8 ++-- src/sim.rs | 36 ++++++++++------ src/utils.rs | 2 +- src/view.rs | 101 ++++++++++++++++++++++++++++++++------------- src/view/canvas.rs | 54 ++++++++++++++++++++++++ src/world.rs | 8 +--- 6 files changed, 157 insertions(+), 52 deletions(-) create mode 100644 src/view/canvas.rs diff --git a/src/main.rs b/src/main.rs index 84fc242..38c147c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,12 +17,14 @@ fn deserialize(str: &str) -> Vec { let mut pos = pos!(0, 0); for c in str.chars() { match c { - '#' => { + ' ' => { + pos.x += 1; + } + '\n' => pos = pos!(0, pos.y + 1), + _ => { result.push(pos); pos.x += 1 } - '\n' => pos = pos!(0, pos.y + 1), - _ => pos.x += 1, } } result diff --git a/src/sim.rs b/src/sim.rs index 389f776..20ab616 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -33,24 +33,22 @@ where fn possible_change_pos(&self) -> impl Iterator + '_ { self.actives() .into_iter() - .map(|p| self.get_neighbors(p)) - .flatten() + .flat_map(|p| self.get_neighbors(p)) } pub fn get_neighbors(&self, pos: Pos) -> impl Iterator + '_ { (-1..=1) - .map(|x| (-1..=1).map(move |y| pos!(x, y))) - .flatten() + .flat_map(|x| (-1..=1).map(move |y| pos!(x, y))) .map(move |p| pos + p) } 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 { self.get_neighbors(pos) - .filter(|pos| self.is_cell_alive(pos.clone())) + .filter(|pos| self.is_cell_alive(*pos)) .count() } @@ -64,6 +62,8 @@ where W: World, { Snapshot(mpsc::Sender), + SetDelay(u64), + Delay(mpsc::Sender), } pub struct SimHandle @@ -86,6 +86,16 @@ where self.sender.send(SimCmd::Snapshot(sender)).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)] @@ -124,23 +134,25 @@ where } 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 tick_interval = Duration::from_millis(200); let mut current_state = state; let mut last_update = SystemTime::now(); loop { - if let Some(cmd) = receiver.try_recv().ok() { + if let Ok(cmd) = receiver.try_recv() { match cmd { 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 mut new_state: State = State::default(); @@ -148,10 +160,10 @@ where 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 + (true, count) if !(3..=4).contains(&count) => (), // die + (true, _) => new_state.set(pos, Cell::active()), // stay (false, 3) => new_state.set(pos, Cell::active()), // becomes alive - _ => (), // stays dead + _ => (), // stays dead } } current_state = new_state; diff --git a/src/utils.rs b/src/utils.rs index 0c5995e..af45b9f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ pub struct Pos { #[macro_export] macro_rules! pos { ($x:expr, $y:expr) => { - crate::Pos { x: $x, y: $y } + Pos { x: $x, y: $y } }; } diff --git a/src/view.rs b/src/view.rs index eaf1ef0..3fef05a 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,11 +1,14 @@ use std::{ - io::{stdin, stdout, Write}, + io::{stdin, stdout}, process::exit, sync::mpsc, thread::{self, JoinHandle}, time::Duration, }; +pub use canvas::Canvas; +mod canvas; + use termion::{event::Key, input::TermRead, raw::IntoRawMode}; use crate::{pos, Pos, SimHandle, World}; @@ -52,6 +55,8 @@ fn input_loop(sender: mpsc::Sender) { Key::Down => InputCmd::Move(Dir::Down), Key::Left => InputCmd::Move(Dir::Left), Key::Right => InputCmd::Move(Dir::Right), + Key::Char('+') => InputCmd::Accelerate, + Key::Char('-') => InputCmd::Decelerate, _ => continue, }; @@ -62,6 +67,7 @@ fn input_loop(sender: mpsc::Sender) { const VIEW_REFRESH_INTERVAL: Duration = Duration::from_millis(100); +#[allow(unreachable_code)] fn view_loop(handle: SimHandle) where W: World, @@ -69,52 +75,89 @@ where let (sender, receiver) = mpsc::channel(); 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 { - handle_inputs(&receiver, &mut view_origin); - let world = handle.snapshot(); - display_world(view_origin, world); - thread::sleep(VIEW_REFRESH_INTERVAL); + handle_inputs(&receiver, &mut view_origin, &mut delay); + handle.set_delay(delay); + + 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); } -fn handle_inputs(receiver: &mpsc::Receiver, view_origin: &mut Pos) { - if let Some(cmd) = receiver.try_recv().ok() { +const MOVEMENT_STEP: i32 = 3; + +fn handle_inputs(receiver: &mpsc::Receiver, view_origin: &mut Pos, delay: &mut u64) { + if let Ok(cmd) = receiver.recv_timeout(VIEW_REFRESH_INTERVAL) { match cmd { - InputCmd::Exit => exit(0), + InputCmd::Exit => { + println!("{}{}", termion::clear::All, termion::cursor::Goto(1, 1)); + 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), + Dir::Up => pos!(0, -MOVEMENT_STEP), + Dir::Down => pos!(0, MOVEMENT_STEP), + Dir::Left => pos!(-2 * MOVEMENT_STEP, 0), + Dir::Right => pos!(2 * MOVEMENT_STEP, 0), } } - InputCmd::Accelerate => todo!(), - InputCmd::Decelerate => todo!(), + InputCmd::Accelerate => *delay -= 100, + InputCmd::Decelerate => *delay += 100, } } } -fn display_world(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(canvas: &mut Canvas, world: W, view_origin: Pos) where W: World, { - let (width, height) = termion::terminal_size().unwrap(); - let mut result = String::new(); + canvas.layer(|mut local_pos| { + 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) { - 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 + match (top, bottom) { + (true, true) => Some('█'), + (true, _) => Some('▀'), + (_, true) => Some('▄'), + _ => None, } - } - 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!("│ | [+]: 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)) + }) } diff --git a/src/view/canvas.rs b/src/view/canvas.rs new file mode 100644 index 0000000..f8021d2 --- /dev/null +++ b/src/view/canvas.rs @@ -0,0 +1,54 @@ +use std::io::{stdout, Write}; + +use crate::{pos, Pos}; + +pub struct Canvas { + lines: Vec, + 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::()) + .collect(); + Self { + height, + lines, + width, + } + } + + pub fn layer(&mut self, f: impl Fn(Pos) -> Option) { + 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(); + } +} diff --git a/src/world.rs b/src/world.rs index 7c614ae..245606c 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,6 +1,6 @@ use crate::Pos; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Cell { 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 { fn get(&self, pos: Pos) -> Cell; fn set(&mut self, pos: Pos, cell: Cell);