more possible game variants
This commit is contained in:
parent
b3ba53f846
commit
2799341651
116
Cargo.lock
generated
116
Cargo.lock
generated
|
@ -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",
|
|
||||||
]
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
113
src/game.rs
113
src/game.rs
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
385
src/main.rs
385
src/main.rs
|
@ -1,203 +1,246 @@
|
||||||
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() {
|
||||||
|
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<TGrid, TValue>(field: &mut TGrid)
|
||||||
|
where TGrid: Grid<TValue>, Standard: Distribution<TValue>
|
||||||
|
{
|
||||||
|
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()
|
env_logger::builder()
|
||||||
.filter_level(LevelFilter::Info)
|
.filter_level(LevelFilter::Info)
|
||||||
.parse_default_env()
|
.parse_default_env()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
execute!(stdout(), EnterAlternateScreen, EnableLineWrap).expect("could not enter alternate screen");
|
||||||
let connection = Connection::open(&cli.destination)
|
|
||||||
.expect("Could not connect. Did you forget `--destination`?");
|
|
||||||
|
|
||||||
let entered_alternate = execute!(stdout(), EnterAlternateScreen, EnableLineWrap).is_ok();
|
|
||||||
|
|
||||||
enable_raw_mode().expect("could not enable raw terminal mode");
|
enable_raw_mode().expect("could not enable raw terminal mode");
|
||||||
|
|
||||||
let mut left = Game::default();
|
Connection::open(Cli::parse().destination)
|
||||||
let mut right = Game::default();
|
.expect("Could not connect. Did you forget `--destination`?")
|
||||||
|
}
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fn de_init() {
|
||||||
disable_raw_mode().expect("could not disable raw terminal mode");
|
disable_raw_mode().expect("could not disable raw terminal mode");
|
||||||
|
execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen");
|
||||||
if entered_alternate {
|
|
||||||
execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn println_info(text: impl Into<String>) {
|
|
||||||
println_command(PrintStyledContent(text.into().white()))
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 }
|
|
||||||
}
|
}
|
||||||
|
|
19
src/print.rs
Normal file
19
src/print.rs
Normal 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
119
src/rules.rs
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue