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.
|
||||
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",
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
113
src/game.rs
113
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<bool>,
|
||||
use crate::rules::Rules;
|
||||
|
||||
pub(crate) struct Game<TState, TGrid, TKernel, const KERNEL_SIZE: usize>
|
||||
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) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
385
src/main.rs
385
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<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()
|
||||
.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<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 }
|
||||
execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen");
|
||||
}
|
||||
|
|
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