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);
|
||||
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
|
||||
|
|
32
src/sim.rs
32
src/sim.rs
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
99
src/view.rs
99
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<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
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;
|
||||
|
||||
#[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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue