diff --git a/Cargo.lock b/Cargo.lock index b68b5aa..2cb5d30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "anstream" version = "0.6.14" @@ -202,29 +193,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "env_filter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] - [[package]] name = "errno" version = "0.3.11" @@ -258,12 +226,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -304,12 +266,6 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -[[package]] -name = "memchr" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" - [[package]] name = "mio" version = "1.0.3" @@ -426,35 +382,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" - [[package]] name = "rust-lzma" version = "0.6.0" @@ -503,8 +430,6 @@ version = "0.2.0" dependencies = [ "clap", "crossterm", - "env_logger", - "log", "rand", "servicepoint", ] diff --git a/Cargo.toml b/Cargo.toml index 9b1cb68..feec40c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,7 @@ rust-version = "1.70.0" [dependencies] clap = { version = "4.5", features = ["derive"] } rand = "0.8" -env_logger = "0.11" crossterm = "0.29" -log = "0.4" [dependencies.servicepoint] package = "servicepoint" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..1b5ee67 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,153 @@ +use crate::{ + print::{println_debug, println_info, println_warning}, + simulation::{Simulation, SimulationEvent}, + Cli, +}; +use crossterm::{ + event, + event::{Event, KeyCode, KeyEvent, KeyEventKind}, + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, EnableLineWrap, EnterAlternateScreen, + LeaveAlternateScreen, + }, +}; +use servicepoint::{ + Bitmap, BitmapCommand, BrightnessGrid, BrightnessGridCommand, SendCommandExt, UdpSocketExt, + FRAME_PACING, TILE_HEIGHT, TILE_WIDTH, +}; +use std::{ + io::stdout, + net::UdpSocket, + thread, + time::{Duration, Instant}, +}; + +pub(crate) struct App { + connection: UdpSocket, + sim: Simulation, + target_duration: Duration, + pixels: Bitmap, + luma: BrightnessGrid, + terminated: bool, +} + +impl App { + pub fn new(cli: Cli) -> Self { + let connection = UdpSocket::bind_connect(cli.destination) + .expect("Could not connect. Did you forget `--destination`?"); + + execute!(stdout(), EnterAlternateScreen, EnableLineWrap) + .expect("could not enter alternate screen"); + enable_raw_mode().expect("could not enable raw terminal mode"); + + Self { + connection, + sim: Simulation::new(), + terminated: false, + target_duration: FRAME_PACING, + pixels: Bitmap::max_sized(), + luma: BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT), + } + } + + pub(crate) fn run_iteration(&mut self) { + let start = Instant::now(); + self.sim.run_iteration(); + + self.sim.draw_state(&mut self.pixels, &mut self.luma); + let cmd: BitmapCommand = self.pixels.clone().into(); + self.connection.send_command(cmd).unwrap(); + let cmd: BrightnessGridCommand = self.luma.clone().into(); + self.connection.send_command(cmd).unwrap(); + + self.poll_events(); + + let tick_time = start.elapsed(); + if tick_time < self.target_duration { + thread::sleep(self.target_duration - tick_time); + } + } + + pub(crate) fn terminated(&self) -> bool { + self.terminated + } + + fn poll_events(&mut self) -> bool { + while event::poll(Duration::from_secs(0)).expect("could not poll") { + let event = event::read().expect("could not read event"); + + if let Event::Key(KeyEvent { + kind: KeyEventKind::Press, + code, + .. + }) = event + { + if let Some(sim_event) = self.handle_key(code) { + self.sim.handle_event(sim_event); + }; + } else { + println_debug(format!("unhandled event {event:?}")); + } + } + false + } + + fn handle_key(&mut self, code: KeyCode) -> Option { + match code { + KeyCode::Char('d') => Some(SimulationEvent::RandomizeLeftPixels), + KeyCode::Char('e') => Some(SimulationEvent::RandomizeLeftLuma), + KeyCode::Char('f') => Some(SimulationEvent::RandomizeRightPixels), + KeyCode::Char('r') => Some(SimulationEvent::RandomizeRightLuma), + KeyCode::Right => Some(SimulationEvent::SeparatorAccelerate), + KeyCode::Left => Some(SimulationEvent::SeparatorDecelerate), + KeyCode::Char('h') => { + println_info("[h] help"); + println_info("[q] quit"); + println_info("[d] randomize left pixels"); + println_info("[e] randomize left luma"); + println_info("[r] randomize right pixels"); + println_info("[f] randomize right luma"); + println_info("[→] accelerate divider right"); + println_info("[←] accelerate divider left"); + None + } + KeyCode::Char('q') => { + println_warning("terminating"); + self.terminated = true; + None + } + KeyCode::Up => { + self.target_duration = self + .target_duration + .saturating_sub(Duration::from_millis(1)); + println_info(format!( + "increased simulation speed to {} ups", + 1f64 / self.target_duration.as_secs_f64() + )); + None + } + KeyCode::Down => { + self.target_duration = self + .target_duration + .saturating_add(Duration::from_millis(1)); + println_info(format!( + "decreased simulation speed to {} ups", + 1f64 / self.target_duration.as_secs_f64() + )); + None + } + key_code => { + println_debug(format!("unhandled KeyCode {key_code:?}")); + None + } + } + } +} + +impl Drop for App { + fn drop(&mut self) { + disable_raw_mode().expect("could not disable raw terminal mode"); + execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen"); + } +} diff --git a/src/main.rs b/src/main.rs index 0679ef0..324ed38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,39 +1,11 @@ -use std::{ - io::stdout, - net::UdpSocket, - num::Wrapping, - thread, - time::{Duration, Instant}, -}; -use crate::{ - game::Game, - print::{println_debug, println_info, println_warning}, - rules::{generate_bb3, generate_u8b3}, -}; +use crate::app::App; use clap::Parser; -use crossterm::{ - event, - event::{Event, KeyCode, KeyEventKind}, - execute, - terminal::{ - disable_raw_mode, enable_raw_mode, EnableLineWrap, EnterAlternateScreen, - LeaveAlternateScreen, - }, -}; -use log::LevelFilter; -use rand::{ - distributions::{Distribution, Standard}, - Rng, -}; -use servicepoint::{ - Bitmap, BitmapCommand, Brightness, BrightnessGrid, BrightnessGridCommand, ByteGrid, Grid, - SendCommandExt, UdpSocketExt, ValueGrid, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, TILE_HEIGHT, - TILE_WIDTH, -}; +mod app; mod game; mod print; mod rules; +mod simulation; #[derive(Parser, Debug)] struct Cli { @@ -42,263 +14,8 @@ struct Cli { } fn main() { - let connection = init(); - - let mut left_pixels = Game { - rules: generate_bb3(), - field: ValueGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT), - }; - let mut right_pixels = Game { - rules: generate_bb3(), - field: ValueGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT), - }; - let mut left_luma = Game { - rules: generate_u8b3(), - field: ByteGrid::new(TILE_WIDTH, TILE_HEIGHT), - }; - let mut right_luma = Game { - rules: generate_u8b3(), - field: ByteGrid::new(TILE_WIDTH, TILE_HEIGHT), - }; - - randomize(&mut left_luma.field); - randomize(&mut left_pixels.field); - randomize(&mut right_luma.field); - randomize(&mut right_pixels.field); - - let mut pixels = Bitmap::max_sized(); - let mut luma = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT); - - let mut split_pixel = 0; - let mut split_speed: i32 = 1; - - let mut iteration = Wrapping(0u8); - - let mut target_duration = FRAME_PACING; - - loop { - let start = Instant::now(); - - left_pixels.step(); - right_pixels.step(); - - if iteration % Wrapping(10) == Wrapping(0) { - left_luma.step(); - right_luma.step(); - } - - iteration += Wrapping(1u8); - - if split_speed > 0 && split_pixel == pixels.width() { - split_pixel = 0; - - (left_luma, right_luma) = (right_luma, left_luma); - (left_pixels, right_pixels) = (right_pixels, left_pixels); - - randomize(&mut left_pixels.field); - randomize(&mut left_luma.field); - left_pixels.rules = generate_bb3(); - left_luma.rules = generate_u8b3(); - } else if split_speed < 0 && split_pixel == 0 { - split_pixel = pixels.width(); - - (left_luma, right_luma) = (right_luma, left_luma); - (left_pixels, right_pixels) = (right_pixels, left_pixels); - - randomize(&mut right_pixels.field); - randomize(&mut right_luma.field); - right_pixels.rules = generate_bb3(); - right_luma.rules = generate_u8b3(); - } - - split_pixel = - i32::clamp(split_pixel as i32 + split_speed, 0, pixels.width() as i32) as usize; - - draw_pixels( - &mut pixels, - &left_pixels.field, - &right_pixels.field, - split_pixel, - ); - draw_luma( - &mut luma, - &left_luma.field, - &right_luma.field, - split_pixel / 8, - ); - send_to_screen(&connection, &pixels, &luma); - - while event::poll(Duration::from_secs(0)).expect("could not poll") { - match event::read().expect("could not read event").try_into() { - Err(_) => {} - Ok(AppEvent::RandomizeLeftPixels) => { - randomize(&mut left_pixels.field); - println_debug("randomized left pixels"); - } - Ok(AppEvent::RandomizeRightPixels) => { - randomize(&mut right_pixels.field); - println_info("randomized right pixels"); - } - Ok(AppEvent::RandomizeLeftLuma) => { - randomize(&mut left_luma.field); - println_info("randomized left luma"); - } - Ok(AppEvent::RandomizeRightLuma) => { - randomize(&mut right_luma.field); - println_info("randomized right luma"); - } - Ok(AppEvent::SeparatorAccelerate) => { - split_speed += 1; - println_info(format!("increased separator speed to {split_speed}")); - } - Ok(AppEvent::SeparatorDecelerate) => { - split_speed -= 1; - println_info(format!("decreased separator speed to {split_speed}")); - } - Ok(AppEvent::Close) => { - println_warning("terminating"); - de_init(); - return; - } - Ok(AppEvent::SimulationSpeedUp) => { - target_duration = target_duration.saturating_sub(Duration::from_millis(1)); - println_info(format!( - "increased simulation speed to {} ups", - 1f64 / target_duration.as_secs_f64() - )); - } - Ok(AppEvent::SimulationSpeedDown) => { - target_duration = target_duration.saturating_add(Duration::from_millis(1)); - println_info(format!( - "decreased simulation speed to {} ups", - 1f64 / target_duration.as_secs_f64() - )); - } - } - } - - let tick_time = start.elapsed(); - if tick_time < target_duration { - thread::sleep(target_duration - tick_time); - } + let mut app = App::new(Cli::parse()); + while !app.terminated() { + app.run_iteration(); } } - -enum AppEvent { - Close, - RandomizeLeftPixels, - RandomizeRightPixels, - RandomizeLeftLuma, - RandomizeRightLuma, - SeparatorAccelerate, - SeparatorDecelerate, - SimulationSpeedUp, - SimulationSpeedDown, -} - -impl TryFrom for AppEvent { - type Error = (); - - fn try_from(event: Event) -> Result { - match event { - Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { - match key_event.code { - KeyCode::Char('h') => { - println_info("[h] help"); - println_info("[q] quit"); - println_info("[d] randomize left pixels"); - println_info("[e] randomize left luma"); - println_info("[r] randomize right pixels"); - println_info("[f] randomize right luma"); - println_info("[→] accelerate divider right"); - println_info("[←] accelerate divider left"); - Err(()) - } - KeyCode::Char('q') => Ok(AppEvent::Close), - KeyCode::Char('d') => Ok(AppEvent::RandomizeLeftPixels), - KeyCode::Char('e') => Ok(AppEvent::RandomizeLeftLuma), - KeyCode::Char('f') => Ok(AppEvent::RandomizeRightPixels), - KeyCode::Char('r') => Ok(AppEvent::RandomizeRightLuma), - KeyCode::Right => Ok(AppEvent::SeparatorAccelerate), - KeyCode::Left => Ok(AppEvent::SeparatorDecelerate), - KeyCode::Up => Ok(AppEvent::SimulationSpeedUp), - KeyCode::Down => Ok(AppEvent::SimulationSpeedDown), - key_code => { - println_debug(format!("unhandled KeyCode {key_code:?}")); - Err(()) - } - } - } - event => { - println_debug(format!("unhandled event {event:?}")); - Err(()) - } - } - } -} - -fn draw_pixels( - pixels: &mut Bitmap, - left: &ValueGrid, - right: &ValueGrid, - split_index: usize, -) { - for x in 0..pixels.width() { - let left_or_right = if x < split_index { left } else { right }; - for y in 0..pixels.height() { - let set = x == split_index || left_or_right.get(x, y); - pixels.set(x, y, set); - } - } -} - -fn draw_luma(luma: &mut BrightnessGrid, left: &ByteGrid, right: &ByteGrid, split_tile: usize) { - for x in 0..luma.width() { - let left_or_right = if x < split_tile { left } else { right }; - for y in 0..luma.height() { - let set: u8 = left_or_right.get(x, y) / u8::MAX * u8::from(Brightness::MAX); - let set = Brightness::try_from(set).unwrap(); - luma.set(x, y, set); - } - } -} - -fn send_to_screen(connection: &UdpSocket, pixels: &Bitmap, luma: &BrightnessGrid) { - let cmd: BitmapCommand = pixels.clone().into(); - connection.send_command(cmd).unwrap(); - let cmd: BrightnessGridCommand = luma.clone().into(); - connection.send_command(cmd).unwrap(); -} - -fn randomize(field: &mut TGrid) -where - TGrid: Grid, - Standard: Distribution, -{ - let mut rng = rand::thread_rng(); - - for y in 0..field.height() { - for x in 0..field.width() { - field.set(x, y, rng.gen()); - } - } -} - -fn init() -> UdpSocket { - env_logger::builder() - .filter_level(LevelFilter::Info) - .parse_default_env() - .init(); - - execute!(stdout(), EnterAlternateScreen, EnableLineWrap) - .expect("could not enter alternate screen"); - enable_raw_mode().expect("could not enable raw terminal mode"); - - UdpSocket::bind_connect(Cli::parse().destination) - .expect("Could not connect. Did you forget `--destination`?") -} - -fn de_init() { - disable_raw_mode().expect("could not disable raw terminal mode"); - execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen"); -} diff --git a/src/simulation.rs b/src/simulation.rs new file mode 100644 index 0000000..579f645 --- /dev/null +++ b/src/simulation.rs @@ -0,0 +1,180 @@ +use crate::{ + game::Game, + print::{println_debug, println_info}, + rules::{generate_bb3, generate_u8b3}, +}; +use rand::{distributions::Standard, prelude::Distribution, Rng}; +use servicepoint::{ + Bitmap, Brightness, BrightnessGrid, Grid, Value, ValueGrid, PIXEL_HEIGHT, PIXEL_WIDTH, + TILE_HEIGHT, TILE_SIZE, TILE_WIDTH, +}; +use std::num::Wrapping; + +pub(crate) struct Simulation { + pub(crate) left_pixels: Game, + pub(crate) right_pixels: Game, + pub(crate) left_luma: Game, + pub(crate) right_luma: Game, + split_pixel: usize, + pub(crate) split_speed: i32, + iteration: Wrapping, +} + +pub enum SimulationEvent { + RandomizeLeftPixels, + RandomizeRightPixels, + RandomizeLeftLuma, + RandomizeRightLuma, + SeparatorAccelerate, + SeparatorDecelerate, +} + +impl Simulation { + pub fn new() -> Simulation { + let left_pixels = Game { + rules: generate_bb3(), + field: make_randomized(PIXEL_WIDTH, PIXEL_HEIGHT), + }; + let right_pixels = Game { + rules: generate_bb3(), + field: make_randomized(PIXEL_WIDTH, PIXEL_HEIGHT), + }; + let left_luma = Game { + rules: generate_u8b3(), + field: make_randomized(TILE_WIDTH, TILE_HEIGHT), + }; + let right_luma = Game { + rules: generate_u8b3(), + field: make_randomized(TILE_WIDTH, TILE_HEIGHT), + }; + + Self { + left_pixels, + right_pixels, + left_luma, + right_luma, + split_pixel: 0, + split_speed: 1, + iteration: Wrapping(0u8), + } + } + + fn swap_left_right(&mut self) { + std::mem::swap(&mut self.left_luma, &mut self.right_luma); + std::mem::swap(&mut self.left_pixels, &mut self.right_pixels); + } + + fn regenerate(pixels: &mut Game, luma: &mut Game) { + randomize(&mut pixels.field); + randomize(&mut luma.field); + pixels.rules = generate_bb3(); + luma.rules = generate_u8b3(); + } + + pub(crate) fn run_iteration(&mut self) { + self.left_pixels.step(); + self.right_pixels.step(); + + if self.iteration % Wrapping(10) == Wrapping(0) { + self.left_luma.step(); + self.right_luma.step(); + } + + self.iteration += Wrapping(1u8); + + if self.split_speed > 0 && self.split_pixel == self.left_pixels.field.width() { + self.split_pixel = 0; + self.swap_left_right(); + Self::regenerate(&mut self.left_pixels, &mut self.left_luma); + } else if self.split_speed < 0 && self.split_pixel == 0 { + self.split_pixel = self.left_pixels.field.width(); + self.swap_left_right(); + Self::regenerate(&mut self.right_pixels, &mut self.right_luma); + } + + self.split_pixel = i32::clamp( + self.split_pixel as i32 + self.split_speed, + 0, + self.left_pixels.field.width() as i32, + ) as usize; + } + + pub(crate) fn draw_state(&self, pixels: &mut Bitmap, luma: &mut BrightnessGrid) { + for x in 0..pixels.width() { + let left_or_right = if x < self.split_pixel { + &self.left_pixels.field + } else { + &self.right_pixels.field + }; + for y in 0..pixels.height() { + let set = x == self.split_pixel || left_or_right.get(x, y); + pixels.set(x, y, set); + } + } + + let split_tile = self.split_pixel / TILE_SIZE; + for x in 0..luma.width() { + let left_or_right = if x < split_tile { + &self.left_luma.field + } else { + &self.right_luma.field + }; + for y in 0..luma.height() { + let set: u8 = left_or_right.get(x, y) / u8::MAX * u8::from(Brightness::MAX); + let set = Brightness::try_from(set).unwrap(); + luma.set(x, y, set); + } + } + } + + pub(crate) fn handle_event(&mut self, event: SimulationEvent) { + match event { + SimulationEvent::RandomizeLeftPixels => { + randomize(&mut self.left_pixels.field); + println_debug("randomized left pixels"); + } + SimulationEvent::RandomizeRightPixels => { + randomize(&mut self.right_pixels.field); + println_info("randomized right pixels"); + } + SimulationEvent::RandomizeLeftLuma => { + randomize(&mut self.left_luma.field); + println_info("randomized left luma"); + } + SimulationEvent::RandomizeRightLuma => { + randomize(&mut self.right_luma.field); + println_info("randomized right luma"); + } + SimulationEvent::SeparatorAccelerate => { + self.split_speed += 1; + println_info(format!("increased separator speed to {}", self.split_speed)); + } + SimulationEvent::SeparatorDecelerate => { + self.split_speed -= 1; + println_info(format!("decreased separator speed to {}", self.split_speed)); + } + } + } +} + +fn make_randomized(width: usize, height: usize) -> ValueGrid +where + Standard: Distribution, +{ + let mut pixels = ValueGrid::new(width, height); + randomize(&mut pixels); + pixels +} + +fn randomize(field: &mut ValueGrid) +where + Standard: Distribution, +{ + let mut rng = rand::thread_rng(); + + for y in 0..field.height() { + for x in 0..field.width() { + field.set(x, y, rng.gen()); + } + } +}