diff --git a/README.md b/README.md index b669a8c..8fada83 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # servicepoint-life More fully featured game of life for the servicepoint display based on the example in the repo. + + diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 7a3a395..0000000 --- a/src/app.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::time::Duration; - -use crossterm::event; -use crossterm::event::KeyCode::Modifier; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; -use log::{debug, info, warn}; -use rand::{distributions, Rng}; -use servicepoint2::Command::CharBrightness; -use servicepoint2::{ - ByteGrid, Command, CompressionCode, Connection, Origin, PixelGrid, TILE_HEIGHT, TILE_WIDTH, -}; - -use crate::Cli; - -pub(crate) struct App { - connection: Connection, - probability: f64, - field: PixelGrid, -} - -impl App { - #[must_use] - pub fn new(connection: Connection, cli: &Cli) -> Self { - Self { - connection, - probability: cli.probability, - field: Self::make_random_field(cli.probability), - } - } - - pub fn step(&mut self) -> bool { - self.send_image(); - self.change_brightness(); - self.field = self.game_iteration(); - - self.handle_events() - } - - fn game_iteration(&self) -> PixelGrid { - let mut next = self.field.clone(); - for x in 0..self.field.width() { - for y in 0..self.field.height() { - let old_state = self.field.get(x, y); - let neighbors = self.count_neighbors(x, y); - - let new_state = - matches!((old_state, neighbors), (true, 2) | (true, 3) | (false, 3)); - next.set(x, y, new_state); - } - } - next - } - - fn send_image(&self) { - let command = Command::BitmapLinearWin( - Origin(0, 0), - self.field.clone(), - CompressionCode::Uncompressed, - ); - - self.connection - .send(command.into()) - .expect("could not send"); - } - - fn count_neighbors(&self, x: usize, y: usize) -> u8 { - let x = x as i32; - let y = y as i32; - let mut count = 0; - for nx in x - 1..=x + 1 { - for ny in y - 1..=y + 1 { - if nx == x && ny == y { - continue; // the cell itself does not count - } - - if nx < 0 - || ny < 0 - || nx >= self.field.width() as i32 - || ny >= self.field.height() as i32 - { - continue; // pixels outside the grid do not count - } - - if !self.field.get(nx as usize, ny as usize) { - continue; // dead cells do not count - } - - count += 1; - } - } - - count - } - - fn make_random_field(probability: f64) -> PixelGrid { - let mut field = PixelGrid::max_sized(); - let mut rng = rand::thread_rng(); - let d = distributions::Bernoulli::new(probability).unwrap(); - for x in 0..field.width() { - for y in 0..field.height() { - field.set(x, y, rng.sample(d)); - } - } - field - } - - fn handle_events(&mut self) -> bool { - if !event::poll(Duration::from_secs(0)).expect("could not poll") { - return true; - } - - match event::read().expect("could not read event") { - Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { - self.handle_key_press(key_event) - } - event => { - debug!("unhandled event {event:?}"); - return true; - } - } - } - - fn handle_key_press(&mut self, event: KeyEvent) -> bool { - match event.code { - KeyCode::Char('q') => { - warn!("q pressed, terminating"); - return false; - } - KeyCode::Char(' ') => { - info!("generating new random field"); - self.field = Self::make_random_field(self.probability); - } - key_code => { - debug!("unhandled KeyCode {key_code:?}"); - } - } - true - } - - fn change_brightness(&self) { - let mut rng = rand::thread_rng(); - - if !rng.gen_ratio(1, 10) { - return; - } - - let min_size = 1; - let x = rng.gen_range(0..TILE_WIDTH - min_size); - let y = rng.gen_range(0..TILE_HEIGHT - min_size); - - let w = rng.gen_range(min_size..=TILE_WIDTH - x); - let h = rng.gen_range(min_size..=TILE_HEIGHT - y); - - let origin = Origin(x, y); - let mut luma = ByteGrid::new(w as usize, h as usize); - - for y in 0..h as usize { - for x in 0..w as usize { - luma.set(x, y, rng.gen()); - } - } - - self.connection - .send(CharBrightness(origin, luma).into()) - .expect("could not send brightness"); - } -} diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..49d7a36 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,101 @@ +use rand::Rng; +use servicepoint2::{ByteGrid, PixelGrid, TILE_HEIGHT, TILE_WIDTH}; + +pub(crate) struct Game { + pub field: PixelGrid, + pub luma: ByteGrid, +} + +impl Game { + pub fn step(&mut self) { + let mut rng = rand::thread_rng(); + + self.field = self.field_iteration(); + + if rng.gen_ratio(1, 10) { + self.luma = self.luma_iteration(); + } + } + + fn field_iteration(&self) -> PixelGrid { + let mut next = self.field.clone(); + for x in 0..self.field.width() { + for y in 0..self.field.height() { + let old_state = self.field.get(x, y); + let neighbors = self.count_neighbors(x, y); + + let new_state = + matches!((old_state, neighbors), (true, 2) | (true, 3) | (false, 3)); + next.set(x, y, new_state); + } + } + next + } + + fn count_neighbors(&self, x: usize, y: usize) -> u8 { + let x = x as i32; + let y = y as i32; + let mut count = 0; + for nx in x - 1..=x + 1 { + for ny in y - 1..=y + 1 { + if nx == x && ny == y { + continue; // the cell itself does not count + } + + if nx < 0 + || ny < 0 + || nx >= self.field.width() as i32 + || ny >= self.field.height() as i32 + { + continue; // pixels outside the grid do not count + } + + if !self.field.get(nx as usize, ny as usize) { + continue; // dead cells do not count + } + + count += 1; + } + } + + count + } + + fn luma_iteration(&self) -> ByteGrid { + let mut rng = rand::thread_rng(); + + let min_size = 1; + let window_x = rng.gen_range(0..TILE_WIDTH as usize - min_size); + let window_y = rng.gen_range(0..TILE_HEIGHT as usize - min_size); + + let w = rng.gen_range(min_size..=TILE_WIDTH as usize - window_x); + let h = rng.gen_range(min_size..=TILE_HEIGHT as usize - window_y); + + let mut new_luma = self.luma.clone(); + for inner_y in 0..h { + for inner_x in 0..w { + let x = window_x + inner_x; + let y = window_y + inner_y; + let old_value = self.luma.get(x, y); + let new_value = i32::clamp( + old_value as i32 + rng.gen_range(-64..=64), + u8::MIN as i32, + u8::MAX as i32, + ) as u8; + + new_luma.set(x, y, new_value); + } + } + + new_luma + } +} + +impl Default for Game { + fn default() -> Self { + Self { + field: PixelGrid::max_sized(), + luma: ByteGrid::new(TILE_WIDTH as usize, TILE_HEIGHT as usize), + } + } +} diff --git a/src/main.rs b/src/main.rs index 038126c..716dfd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,34 @@ +use std::io::stdout; use std::time::Duration; use std::{io, thread}; use clap::Parser; -use crossterm::execute; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use crossterm::style::{Print, PrintStyledContent, Stylize}; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::{event, execute}; use log::LevelFilter; -use servicepoint2::Connection; +use rand::{distributions, Rng}; +use servicepoint2::Command::{BitmapLinearWin, CharBrightness}; +use servicepoint2::{ + ByteGrid, CompressionCode, Connection, Origin, PixelGrid, PIXEL_WIDTH, TILE_HEIGHT, TILE_WIDTH, +}; -use crate::app::App; +use crate::game::Game; -mod app; +mod game; #[derive(Parser, Debug)] struct Cli { #[arg(short, long, default_value = "localhost:2342")] destination: String, - #[arg(short, long, default_value_t = 0.5f64)] + #[arg(short, long, default_value_t = 0.4f64)] probability: f64, + #[arg(short, long, default_value_t = true)] + extended: bool, } +// TODO: itsa spaghetti! 👌 fn main() { env_logger::builder() .filter_level(LevelFilter::Info) @@ -33,14 +43,150 @@ fn main() { crossterm::terminal::enable_raw_mode().expect("could not enable raw terminal mode"); - let mut app = App::new(connection, &cli); - while app.step() { + let mut left = Game::default(); + let mut right = Game::default(); + + let mut pixels = PixelGrid::max_sized(); + let mut luma = ByteGrid::new(TILE_WIDTH as usize, TILE_HEIGHT as usize); + + let mut split_pixel = PIXEL_WIDTH as usize / 2; + + let mut close_requested = false; + while !close_requested { + left.step(); + right.step(); + + for x in 0..pixels.width() { + let left_or_right = if x < split_pixel { + &left.field + } else { + &right.field + }; + for y in 0..pixels.height() { + let set = left_or_right.get(x, y) || x == split_pixel; + pixels.set(x, y, set); + } + } + + let split_tile = split_pixel / 8; + for x in 0..luma.width() { + let left_or_right = if x < split_tile { + &left.luma + } else { + &right.luma + }; + for y in 0..luma.height() { + let set = if x == split_tile { + 255 + } else { + left_or_right.get(x, y) + }; + luma.set(x, y, set); + } + } + + let pixel_cmd = + BitmapLinearWin(Origin(0, 0), pixels.clone(), CompressionCode::Uncompressed); + connection + .send(pixel_cmd.into()) + .expect("could not send pixels"); + + connection + .send(CharBrightness(Origin(0, 0), luma.clone()).into()) + .expect("could not send brightness"); + + while event::poll(Duration::from_secs(0)).expect("could not poll") { + match event::read().expect("could not read event") { + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + match key_event.code { + KeyCode::Char('h') => { + execute!( + stdout(), + Print("h for help\r\n"), + Print("q to quit\r\n"), + Print("a to reset left field\r\n"), + Print("d to reset right field\r\n") + ) + .unwrap(); + } + KeyCode::Char('q') => { + execute!(stdout(), PrintStyledContent("terminating\r\n".red())) + .unwrap(); + close_requested = true; + } + KeyCode::Char('a') => { + execute!( + stdout(), + PrintStyledContent( + "generating new random field for left\r\n".grey() + ) + ) + .unwrap(); + left = make_random_field(cli.probability); + } + KeyCode::Char('d') => { + execute!( + stdout(), + PrintStyledContent( + "generating new random field for right\r\n".grey() + ) + ) + .unwrap(); + right = make_random_field(cli.probability); + } + KeyCode::Right => { + split_pixel += 1; + } + KeyCode::Left => { + split_pixel -= 1; + } + key_code => { + execute!( + stdout(), + PrintStyledContent( + format!("unhandled KeyCode {key_code:?}\r\n").dark_grey() + ) + ) + .unwrap(); + } + } + } + event => { + execute!( + stdout(), + PrintStyledContent(format!("unhandled event {event:?}\r\n").dark_grey()), + ) + .unwrap(); + } + } + } + thread::sleep(Duration::from_millis(30)); } crossterm::terminal::disable_raw_mode().expect("could not disable raw terminal mode"); if entered_alternate { - execute!(io::stdout(), LeaveAlternateScreen).expect("could not leave alternate screen"); + execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen"); } } + +fn make_random_field(probability: f64) -> Game { + let mut field = PixelGrid::max_sized(); + let mut rng = rand::thread_rng(); + let d = distributions::Bernoulli::new(probability).unwrap(); + for x in 0..field.width() { + for y in 0..field.height() { + field.set(x, y, rng.sample(d)); + } + } + + let mut luma = ByteGrid::new(TILE_WIDTH as usize, TILE_HEIGHT as usize); + for x in 0..luma.width() { + for y in 0..luma.height() { + luma.set(x, y, rng.gen()); + } + } + + Game { field, luma } +}