more possible game variants

This commit is contained in:
Vinzenz Schroeter 2024-05-19 19:32:02 +02:00
parent b3ba53f846
commit 2799341651
6 changed files with 388 additions and 366 deletions

116
Cargo.lock generated
View file

@ -2,12 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -78,38 +72,6 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -162,15 +124,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 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]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.27.0" version = "0.27.0"
@ -219,16 +172,6 @@ dependencies = [
"log", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@ -258,15 +201,6 @@ version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "jobserver"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.155" version = "0.2.155"
@ -295,15 +229,6 @@ version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 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]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.11"
@ -316,12 +241,6 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.2" version = "0.12.2"
@ -474,15 +393,12 @@ dependencies = [
[[package]] [[package]]
name = "servicepoint2" name = "servicepoint2"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54d49d501f34100406d70730d887775ed5961b6343ccb10d5ef0e105d7b295ae" checksum = "94d9aecc4d31a71578481de6c6d64383d374126c38469c9689067579c1d910fd"
dependencies = [ dependencies = [
"bzip2",
"flate2",
"log", "log",
"rust-lzma", "rust-lzma",
"zstd",
] ]
[[package]] [[package]]
@ -742,31 +658,3 @@ dependencies = [
"quote", "quote",
"syn", "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",
]

View file

@ -7,6 +7,6 @@ edition = "2021"
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
rand = "0.9.0-alpha.1" rand = "0.9.0-alpha.1"
env_logger = "0.11.3" env_logger = "0.11.3"
servicepoint2 = "0.4.1" servicepoint2 = "0.4.2"
crossterm = "0.27.0" crossterm = "0.27.0"
log = "0.4.21" log = "0.4.21"

View file

@ -1,111 +1,64 @@
use rand::Rng; use servicepoint2::Grid;
use servicepoint2::{ByteGrid, PixelGrid, TILE_HEIGHT, TILE_WIDTH};
pub(crate) struct Game { use crate::rules::Rules;
pub field: PixelGrid,
pub luma: ByteGrid, pub(crate) struct Game<TState, TGrid, TKernel, const KERNEL_SIZE: usize>
pub high_life: Option<bool>, where TGrid: Grid<TState>, TState: Copy + PartialEq, TKernel: Copy
{
pub field: TGrid,
pub rules: Rules<TState, TKernel, KERNEL_SIZE>,
} }
impl Game { impl<TState, TGrid, TKernel, const KERNEL_SIZE: usize> Game<TState, TGrid, TKernel, KERNEL_SIZE>
where TGrid: Grid<TState>, TState: Copy + PartialEq, TKernel: Copy
{
pub fn step(&mut self) { pub fn step(&mut self) {
let mut rng = rand::thread_rng();
self.field = self.field_iteration(); self.field = self.field_iteration();
if rng.gen_ratio(1, 10) {
self.luma = self.luma_iteration();
}
} }
fn field_iteration(&self) -> PixelGrid { fn field_iteration(&self) -> TGrid {
let mut next = self.field.clone(); let mut next = TGrid::new(self.field.width(), self.field.height());
for x in 0..self.field.width() { for x in 0..self.field.width() {
for y in 0..self.field.height() { for y in 0..self.field.height() {
let old_state = self.field.get(x, y); let old_state = self.field.get(x, y);
let neighbors = self.count_neighbors(x, y); let neighbors = self.count_neighbors(x, y);
let new_state = (self.rules.next_state)(old_state, neighbors);
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,
};
next.set(x, y, new_state); next.set(x, y, new_state);
} }
} }
next 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 x = x as i32;
let y = y as i32; let y = y as i32;
let mut count = 0; 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 let kernel = &self.rules.kernel;
|| ny < 0 assert_eq!(KERNEL_SIZE % 2, 1);
|| nx >= self.field.width() as i32 let offset = KERNEL_SIZE as i32 / 2;
|| ny >= self.field.height() as i32
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 continue; // pixels outside the grid do not count
} }
if !self.field.get(nx as usize, ny as usize) { let neighbor_state = self.field.get(neighbor_x as usize, neighbor_y as usize);
continue; // dead cells do not count count += (self.rules.count_neighbor)(neighbor_state, *kernel_value);
}
count += 1;
} }
} }
count 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,
}
}
} }

View file

@ -1,81 +1,197 @@
use std::io::stdout; use std::io::stdout;
use std::num::Wrapping;
use std::thread; use std::thread;
use std::time::Duration; use std::time::{Duration, Instant};
use clap::Parser; use clap::Parser;
use crossterm::{event, execute, queue}; use crossterm::{event, execute};
use crossterm::event::{Event, KeyCode, KeyEventKind}; use crossterm::event::{Event, KeyCode, KeyEventKind};
use crossterm::style::{Print, PrintStyledContent, Stylize};
use crossterm::terminal::{ use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen,
}; };
use log::LevelFilter; use log::LevelFilter;
use rand::{distributions, Rng}; use rand::distributions::{Distribution, Standard};
use servicepoint2::{ use rand::Rng;
ByteGrid, CompressionCode, Connection, Origin, PIXEL_WIDTH, PixelGrid, TILE_HEIGHT, TILE_WIDTH, use servicepoint2::{ByteGrid, CompressionCode, Connection, FRAME_PACING, Grid, Origin, PIXEL_WIDTH, PixelGrid, TILE_HEIGHT, TILE_WIDTH};
};
use servicepoint2::Command::{BitmapLinearWin, CharBrightness}; use servicepoint2::Command::{BitmapLinearWin, CharBrightness};
use crate::game::Game; use crate::game::Game;
use crate::print::{println_debug, println_info, println_warning};
use crate::rules::Rules;
mod game; mod game;
mod rules;
mod print;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
#[arg(short, long, default_value = "localhost:2342")] #[arg(short, long, default_value = "localhost:2342")]
destination: String, destination: String,
#[arg(short, long, default_value_t = 0.4f64)]
probability: f64,
} }
// TODO: itsa spaghetti! 👌
fn main() { fn main() {
env_logger::builder() let connection = init();
.filter_level(LevelFilter::Info)
.parse_default_env()
.init();
let cli = Cli::parse(); let mut left_pixels = Game {
let connection = Connection::open(&cli.destination) rules: Rules::day_and_night(),
.expect("Could not connect. Did you forget `--destination`?"); 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),
};
let entered_alternate = execute!(stdout(), EnterAlternateScreen, EnableLineWrap).is_ok(); randomize(&mut left_luma.field);
randomize(&mut left_pixels.field);
enable_raw_mode().expect("could not enable raw terminal mode"); randomize(&mut right_luma.field);
randomize(&mut right_pixels.field);
let mut left = Game::default();
let mut right = Game::default();
let mut pixels = PixelGrid::max_sized(); let mut pixels = PixelGrid::max_sized();
let mut luma = ByteGrid::new(TILE_WIDTH as usize, TILE_HEIGHT as usize); let mut luma = ByteGrid::new(TILE_WIDTH, TILE_HEIGHT);
let mut split_pixel = PIXEL_WIDTH as usize / 2; let mut split_pixel = PIXEL_WIDTH / 2;
let mut high_life = Option::None; let mut split_speed = 1;
let mut close_requested = false; let mut iteration = Wrapping(0u8);
while !close_requested {
left.step();
right.step();
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() { for x in 0..pixels.width() {
let left_or_right = if x < split_pixel { let left_or_right = if x < split_index { left } else { right };
&left.field
} else {
&right.field
};
for y in 0..pixels.height() { for y in 0..pixels.height() {
let set = left_or_right.get(x, y) || x == split_pixel; let set = x == split_index || left_or_right.get(x, y);
pixels.set(x, y, set); pixels.set(x, y, set);
} }
} }
}
let split_tile = split_pixel / 8; fn draw_luma(luma: &mut ByteGrid, left: &ByteGrid, right: &ByteGrid, split_tile: usize) {
for x in 0..luma.width() { for x in 0..luma.width() {
let left_or_right = if x < split_tile { let left_or_right = if x < split_tile { left } else { right };
&left.luma
} else {
&right.luma
};
for y in 0..luma.height() { for y in 0..luma.height() {
let set = if x == split_tile { let set = if x == split_tile {
255 255
@ -85,7 +201,9 @@ fn main() {
luma.set(x, y, set); luma.set(x, y, set);
} }
} }
}
fn send_to_screen(connection: &Connection, pixels: &PixelGrid, luma: &ByteGrid) {
let pixel_cmd = let pixel_cmd =
BitmapLinearWin(Origin(0, 0), pixels.clone(), CompressionCode::Uncompressed); BitmapLinearWin(Origin(0, 0), pixels.clone(), CompressionCode::Uncompressed);
connection connection
@ -95,109 +213,34 @@ fn main() {
connection connection
.send(CharBrightness(Origin(0, 0), luma.clone()).into()) .send(CharBrightness(Origin(0, 0), luma.clone()).into())
.expect("could not send brightness"); .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));
}
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<String>) { fn randomize<TGrid, TValue>(field: &mut TGrid)
println_command(PrintStyledContent(text.into().white())) where TGrid: Grid<TValue>, Standard: Distribution<TValue>
} {
fn println_debug(text: impl Into<String>) {
println_command(PrintStyledContent(text.into().grey()))
}
fn println_warning(text: impl Into<String>) {
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<bool>) -> Game {
let mut field = PixelGrid::max_sized();
let mut rng = rand::thread_rng(); 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() { for y in 0..field.height() {
field.set(x, y, rng.sample(d)); for x in 0..field.width() {
field.set(x, y, rng.gen());
} }
} }
}
let mut luma = ByteGrid::new(TILE_WIDTH as usize, TILE_HEIGHT as usize);
for x in 0..luma.width() { fn init() -> Connection {
for y in 0..luma.height() { env_logger::builder()
luma.set(x, y, rng.gen()); .filter_level(LevelFilter::Info)
} .parse_default_env()
} .init();
Game { field, luma, high_life } execute!(stdout(), EnterAlternateScreen, EnableLineWrap).expect("could not enter alternate screen");
enable_raw_mode().expect("could not enable raw terminal mode");
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");
execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen");
} }

19
src/print.rs Normal file
View file

@ -0,0 +1,19 @@
use std::io::stdout;
use crossterm::queue;
use crossterm::style::{Print, PrintStyledContent, Stylize};
pub fn println_info(text: impl Into<String>) {
println_command(PrintStyledContent(text.into().white()))
}
pub fn println_debug(text: impl Into<String>) {
println_command(PrintStyledContent(text.into().grey()))
}
pub fn println_warning(text: impl Into<String>) {
println_command(PrintStyledContent(text.into().red()))
}
pub fn println_command(command: impl crossterm::Command) {
queue!(stdout(), command, Print("\r\n")).expect("could not print");
}

119
src/rules.rs Normal file
View file

@ -0,0 +1,119 @@
pub struct Rules<TState, TKernel, const KERNEL_SIZE: usize>
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<bool, bool, 3> {
#[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<u8, bool, 3> {
#[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
},
}
}
}