From 27993416515ccc51ac1bb76315655d2b61ac72e4 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 19 May 2024 19:32:02 +0200 Subject: [PATCH] more possible game variants --- Cargo.lock | 116 +--------------- Cargo.toml | 2 +- src/game.rs | 113 +++++---------- src/main.rs | 385 ++++++++++++++++++++++++++++----------------------- src/print.rs | 19 +++ src/rules.rs | 119 ++++++++++++++++ 6 files changed, 388 insertions(+), 366 deletions(-) create mode 100644 src/print.rs create mode 100644 src/rules.rs diff --git a/Cargo.lock b/Cargo.lock index 2b0cb59..2c1efbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "aho-corasick" version = "1.1.3" @@ -78,38 +72,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "cc" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" -dependencies = [ - "jobserver", - "libc", - "once_cell", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -162,15 +124,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" -[[package]] -name = "crc32fast" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossterm" version = "0.27.0" @@ -219,16 +172,6 @@ dependencies = [ "log", ] -[[package]] -name = "flate2" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -258,15 +201,6 @@ version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" -[[package]] -name = "jobserver" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" -dependencies = [ - "libc", -] - [[package]] name = "libc" version = "0.2.155" @@ -295,15 +229,6 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" -[[package]] -name = "miniz_oxide" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" -dependencies = [ - "adler", -] - [[package]] name = "mio" version = "0.8.11" @@ -316,12 +241,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - [[package]] name = "parking_lot" version = "0.12.2" @@ -474,15 +393,12 @@ dependencies = [ [[package]] name = "servicepoint2" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d49d501f34100406d70730d887775ed5961b6343ccb10d5ef0e105d7b295ae" +checksum = "94d9aecc4d31a71578481de6c6d64383d374126c38469c9689067579c1d910fd" dependencies = [ - "bzip2", - "flate2", "log", "rust-lzma", - "zstd", ] [[package]] @@ -742,31 +658,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zstd" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 072dd68..2311197 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" clap = { version = "4.5.4", features = ["derive"] } rand = "0.9.0-alpha.1" env_logger = "0.11.3" -servicepoint2 = "0.4.1" +servicepoint2 = "0.4.2" crossterm = "0.27.0" log = "0.4.21" diff --git a/src/game.rs b/src/game.rs index 2580af9..0cd6f17 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,111 +1,64 @@ -use rand::Rng; -use servicepoint2::{ByteGrid, PixelGrid, TILE_HEIGHT, TILE_WIDTH}; +use servicepoint2::Grid; -pub(crate) struct Game { - pub field: PixelGrid, - pub luma: ByteGrid, - pub high_life: Option, +use crate::rules::Rules; + +pub(crate) struct Game + where TGrid: Grid, TState: Copy + PartialEq, TKernel: Copy +{ + pub field: TGrid, + pub rules: Rules, } -impl Game { +impl Game + where TGrid: Grid, TState: Copy + PartialEq, TKernel: Copy +{ 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(); + fn field_iteration(&self) -> TGrid { + let mut next = TGrid::new(self.field.width(), self.field.height()); 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 = match (old_state, neighbors) { - (true, 2) | (true, 3) | (false, 3) => true, - (false, 6) => match self.high_life { - None => false, - Some(true) => true, - Some(false) => self.luma.get(x / 8, y / 8) > 128 - }, - _ => false, - }; - + let new_state = (self.rules.next_state)(old_state, neighbors); next.set(x, y, new_state); } } next } - fn count_neighbors(&self, x: usize, y: usize) -> u8 { + fn count_neighbors(&self, x: usize, y: usize) -> i32 { 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 + let kernel = &self.rules.kernel; + assert_eq!(KERNEL_SIZE % 2, 1); + let offset = KERNEL_SIZE as i32 / 2; + + for (kernel_y, kernel_row) in kernel.iter().enumerate() { + let offset_y = kernel_y as i32 - offset; + + for (kernel_x, kernel_value) in kernel_row.iter().enumerate() { + let offset_x = kernel_x as i32 - offset; + let neighbor_x = x + offset_x; + let neighbor_y = y + offset_y; + + if neighbor_x < 0 + || neighbor_y < 0 + || neighbor_x >= self.field.width() as i32 + || neighbor_y >= 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; + let neighbor_state = self.field.get(neighbor_x as usize, neighbor_y as usize); + count += (self.rules.count_neighbor)(neighbor_state, *kernel_value); } } 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), - high_life: None, - } - } } diff --git a/src/main.rs b/src/main.rs index a8f02ce..aac03b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,203 +1,246 @@ use std::io::stdout; +use std::num::Wrapping; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; use clap::Parser; -use crossterm::{event, execute, queue}; +use crossterm::{event, execute}; use crossterm::event::{Event, KeyCode, KeyEventKind}; -use crossterm::style::{Print, PrintStyledContent, Stylize}; use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen, }; use log::LevelFilter; -use rand::{distributions, Rng}; -use servicepoint2::{ - ByteGrid, CompressionCode, Connection, Origin, PIXEL_WIDTH, PixelGrid, TILE_HEIGHT, TILE_WIDTH, -}; +use rand::distributions::{Distribution, Standard}; +use rand::Rng; +use servicepoint2::{ByteGrid, CompressionCode, Connection, FRAME_PACING, Grid, Origin, PIXEL_WIDTH, PixelGrid, TILE_HEIGHT, TILE_WIDTH}; use servicepoint2::Command::{BitmapLinearWin, CharBrightness}; use crate::game::Game; +use crate::print::{println_debug, println_info, println_warning}; +use crate::rules::Rules; mod game; +mod rules; +mod print; #[derive(Parser, Debug)] struct Cli { #[arg(short, long, default_value = "localhost:2342")] destination: String, - #[arg(short, long, default_value_t = 0.4f64)] - probability: f64, } -// TODO: itsa spaghetti! 👌 fn main() { + let connection = init(); + + let mut left_pixels = Game { + rules: Rules::day_and_night(), + field: PixelGrid::max_sized(), + }; + let mut right_pixels = Game { + rules: Rules::seeds(), + field: PixelGrid::max_sized(), + }; + let mut left_luma = Game { + rules: Rules::continuous_game_of_life(), + field: ByteGrid::new(TILE_WIDTH, TILE_HEIGHT), + }; + let mut right_luma = Game { + rules: Rules::continuous_game_of_life(), + 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 = PixelGrid::max_sized(); + let mut luma = ByteGrid::new(TILE_WIDTH, TILE_HEIGHT); + + let mut split_pixel = PIXEL_WIDTH / 2; + let mut split_speed = 1; + + let mut iteration = Wrapping(0u8); + + loop { + let start = Instant::now(); + + left_pixels.step(); + right_pixels.step(); + + left_luma.step(); + right_luma.step(); + + iteration += Wrapping(1u8); + split_pixel = usize::clamp(split_pixel + split_speed, 0, pixels.width() - 1); + + 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 parse_event(event::read().expect("could not read event")) { + AppEvent::None => {} + AppEvent::RandomizeLeftPixels => { + randomize(&mut left_pixels.field); + } + AppEvent::RandomizeRightPixels => { + randomize(&mut right_pixels.field); + } + AppEvent::RandomizeLeftLuma => { + randomize(&mut left_luma.field); + } + AppEvent::RandomizeRightLuma => { + randomize(&mut right_luma.field); + } + AppEvent::Accelerate => { + split_speed += 1; + } + AppEvent::Decelerate => { + split_speed -= 1; + } + AppEvent::Close => { + de_init(); + return; + } + } + } + + + let tick_time = start.elapsed(); + if tick_time < FRAME_PACING { + thread::sleep(FRAME_PACING - tick_time); + } + } +} + +enum AppEvent { + None, + Close, + RandomizeLeftPixels, + RandomizeRightPixels, + RandomizeLeftLuma, + RandomizeRightLuma, + Accelerate, + Decelerate, +} + +fn parse_event(event: Event) -> AppEvent { + 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("[→] move divider right"); + println_info("[←] move divider left"); + AppEvent::None + } + KeyCode::Char('q') => { + println_warning("terminating"); + AppEvent::Close + } + KeyCode::Char('d') => { + println_debug("randomizing left pixels"); + AppEvent::RandomizeLeftPixels + } + KeyCode::Char('e') => { + println_info("randomizing left luma"); + AppEvent::RandomizeLeftLuma + } + KeyCode::Char('f') => { + println_info("randomizing right pixels"); + AppEvent::RandomizeRightPixels + } + KeyCode::Char('r') => { + println_info("randomizing right luma"); + AppEvent::RandomizeRightLuma + } + KeyCode::Right => { + AppEvent::Accelerate + } + KeyCode::Left => { + AppEvent::Decelerate + } + key_code => { + println_debug(format!("unhandled KeyCode {key_code:?}")); + AppEvent::None + } + } + } + event => { + println_debug(format!("unhandled event {event:?}")); + AppEvent::None + } + } +} + +fn draw_pixels(pixels: &mut PixelGrid, left: &PixelGrid, right: &PixelGrid, 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 ByteGrid, 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 = if x == split_tile { + 255 + } else { + left_or_right.get(x, y) + }; + luma.set(x, y, set); + } + } +} + +fn send_to_screen(connection: &Connection, pixels: &PixelGrid, luma: &ByteGrid) { + 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"); +} + +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() -> Connection { env_logger::builder() .filter_level(LevelFilter::Info) .parse_default_env() .init(); - let cli = Cli::parse(); - let connection = Connection::open(&cli.destination) - .expect("Could not connect. Did you forget `--destination`?"); - - let entered_alternate = execute!(stdout(), EnterAlternateScreen, EnableLineWrap).is_ok(); - + execute!(stdout(), EnterAlternateScreen, EnableLineWrap).expect("could not enter alternate screen"); enable_raw_mode().expect("could not enable raw terminal mode"); - 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 high_life = Option::None; - - 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') => { - println_info("[h] help"); - println_info("[q] quit"); - println_info("[a] reset left field"); - println_info("[d] reset right field"); - println_info("[l] toggle high life rules for bright tiles"); - println_info("[→] move divider right"); - println_info("[←] move divider left"); - } - KeyCode::Char('q') => { - println_warning("terminating"); - close_requested = true; - } - KeyCode::Char('a') => { - println_debug("generating new random field for left"); - left = make_random_field(cli.probability, high_life); - } - KeyCode::Char('d') => { - println_info("generating new random field for right"); - right = make_random_field(cli.probability, high_life); - } - KeyCode::Char('l') => { - high_life = match high_life { - None => Some(false), - Some(false) => Some(true), - Some(true) => None, - }; - let state_formatted = match high_life { - None => "disabled".red(), - Some(false) => "enabled where bright".yellow(), - Some(true) => "enabled everywhere".green() - }; - left.high_life = high_life; - right.high_life = high_life; - queue!(stdout(), - Print("new high life state: "), - PrintStyledContent(state_formatted), - Print("\r\n")); - } - KeyCode::Right => { - split_pixel += 1; - } - KeyCode::Left => { - split_pixel -= 1; - } - key_code => { - println_debug(format!("unhandled KeyCode {key_code:?}")); - } - } - } - event => { - println_debug(format!("unhandled event {event:?}")); - } - } - } - - thread::sleep(Duration::from_millis(30)); - } + Connection::open(Cli::parse().destination) + .expect("Could not connect. Did you forget `--destination`?") +} +fn de_init() { disable_raw_mode().expect("could not disable raw terminal mode"); - - if entered_alternate { - execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen"); - } -} - -fn println_info(text: impl Into) { - println_command(PrintStyledContent(text.into().white())) -} - -fn println_debug(text: impl Into) { - println_command(PrintStyledContent(text.into().grey())) -} - -fn println_warning(text: impl Into) { - println_command(PrintStyledContent(text.into().red())) -} - -fn println_command(command: impl crossterm::Command) { - queue!(stdout(), command, Print("\r\n")); -} - -fn make_random_field(probability: f64, high_life: Option) -> 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, high_life } + execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen"); } diff --git a/src/print.rs b/src/print.rs new file mode 100644 index 0000000..a09a0ce --- /dev/null +++ b/src/print.rs @@ -0,0 +1,19 @@ +use std::io::stdout; +use crossterm::queue; +use crossterm::style::{Print, PrintStyledContent, Stylize}; + +pub fn println_info(text: impl Into) { + println_command(PrintStyledContent(text.into().white())) +} + +pub fn println_debug(text: impl Into) { + println_command(PrintStyledContent(text.into().grey())) +} + +pub fn println_warning(text: impl Into) { + println_command(PrintStyledContent(text.into().red())) +} + +pub fn println_command(command: impl crossterm::Command) { + queue!(stdout(), command, Print("\r\n")).expect("could not print"); +} \ No newline at end of file diff --git a/src/rules.rs b/src/rules.rs new file mode 100644 index 0000000..008fc7a --- /dev/null +++ b/src/rules.rs @@ -0,0 +1,119 @@ +pub struct Rules + where TState: Copy + PartialEq, TKernel: Copy +{ + pub kernel: [[TKernel; KERNEL_SIZE]; KERNEL_SIZE], + pub count_neighbor: fn(neighbor_state: TState, kernel_value: TKernel) -> i32, + pub next_state: fn(state: TState, kernel_result: i32) -> TState, +} + +pub const MOORE_NEIGHBORHOOD: [[bool; 3]; 3] = [ + [true, true, true], + [true, false, true], + [true, true, true] +]; + +pub fn count_true_neighbor(neighbor_state: bool, kernel_value: bool) -> i32 +{ + if neighbor_state && kernel_value { 1 } else { 0 } +} + +impl Rules { + #[must_use] + pub fn game_of_life() -> Self { + Self { + kernel: MOORE_NEIGHBORHOOD, + count_neighbor: count_true_neighbor, + next_state: |old_state, neighbors| + matches!((old_state, neighbors), (true, 2) | (true, 3) | (false, 3)), + } + } + + #[must_use] + pub fn high_life() -> Self { + Self { + kernel: MOORE_NEIGHBORHOOD, + count_neighbor: count_true_neighbor, + next_state: |old_state, neighbors| + matches!((old_state, neighbors), (true, 2) | (true, 3) | (false, 3)| (false, 6)), + } + } + + #[must_use] + pub fn seeds() -> Self { + Self { + kernel: MOORE_NEIGHBORHOOD, + count_neighbor: count_true_neighbor, + next_state: |state, neighbors| + matches!((state, neighbors), (false, 2)), + } + } + + #[must_use] + pub fn day_and_night() -> Self { + Self { + kernel: MOORE_NEIGHBORHOOD, + count_neighbor: count_true_neighbor, + next_state: |state, neighbors| { + match (state, neighbors) { + (false, 3) => true, + (false, 6) => true, + (false, 7) => true, + (false, 8) => true, + (true, 3) => true, + (true, 4) => true, + (true, 6) => true, + (true, 7) => true, + (true, 8) => true, + _ => false, + } + }, + } + } +} + +impl Rules { + #[must_use] + pub fn brians_brain() -> Self { + const ALIVE: u8 = u8::MAX; + const DYING: u8 = ALIVE / 2; + const DEAD: u8 = 0; + + Self { + kernel: MOORE_NEIGHBORHOOD, + count_neighbor: |state, kernel| { + if kernel && state == u8::MAX { 1 } else { 0 } + }, + next_state: |state, neighbors| { + match (state, neighbors) { + (ALIVE, _) => DYING, + (DYING, _) => DEAD, + (DEAD, 2) => ALIVE, + (random_state, _) => if random_state > DYING { + ALIVE + } else { + DEAD + } + } + }, + } + } + + pub fn continuous_game_of_life() -> Self { + Self { + kernel: MOORE_NEIGHBORHOOD, + count_neighbor: |state, kernel| { + if kernel && state >= u8::MAX / 2 { 1 } else { 0 } + }, + next_state: |old_state, neighbors| { + let is_alive = old_state >= u8::MAX / 2; + let delta = match (is_alive, neighbors) { + (true, 2) | (true, 3) | (false, 3) => 10, + _ => -10, + }; + + i32::clamp(old_state as i32 + delta, u8::MIN as i32, u8::MAX as i32) as u8 + }, + } + } +} +