improved canvas display and added controls

This commit is contained in:
JOLIMAITRE Matthieu 2022-08-31 18:47:20 +02:00
parent 36a93ab177
commit 9cf5a5b56d
6 changed files with 157 additions and 52 deletions

View file

@ -17,12 +17,14 @@ fn deserialize(str: &str) -> Vec<Pos> {
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

View file

@ -33,24 +33,22 @@ where
fn possible_change_pos(&self) -> impl Iterator<Item = Pos> + '_ {
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<Item = Pos> + '_ {
(-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<W>),
SetDelay(u64),
Delay(mpsc::Sender<usize>),
}
pub struct SimHandle<W>
@ -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<W>(receiver: mpsc::Receiver<SimCmd<W>>, state: State<W>)
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<W> = State::default();
@ -148,7 +160,7 @@ 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, 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

View file

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

View file

@ -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<InputCmd>) {
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<InputCmd>) {
const VIEW_REFRESH_INTERVAL: Duration = Duration::from_millis(100);
#[allow(unreachable_code)]
fn view_loop<W>(handle: SimHandle<W>)
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<InputCmd>, view_origin: &mut Pos) {
if let Some(cmd) = receiver.try_recv().ok() {
const MOVEMENT_STEP: i32 = 3;
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 {
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<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
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!("│ <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
View 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();
}
}

View file

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