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

View file

@ -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,
}
}
}

View file

@ -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
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
},
}
}
}