make some spaghetti

This commit is contained in:
Vinzenz Schroeter 2024-05-18 23:24:18 +02:00
parent 879ecde430
commit e2dd9e78ac
4 changed files with 257 additions and 175 deletions

View file

@ -1,3 +1,5 @@
# servicepoint-life # servicepoint-life
More fully featured game of life for the servicepoint display based on the example in the repo. More fully featured game of life for the servicepoint display based on the example in the repo.

View file

@ -1,167 +0,0 @@
use std::time::Duration;
use crossterm::event;
use crossterm::event::KeyCode::Modifier;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use log::{debug, info, warn};
use rand::{distributions, Rng};
use servicepoint2::Command::CharBrightness;
use servicepoint2::{
ByteGrid, Command, CompressionCode, Connection, Origin, PixelGrid, TILE_HEIGHT, TILE_WIDTH,
};
use crate::Cli;
pub(crate) struct App {
connection: Connection,
probability: f64,
field: PixelGrid,
}
impl App {
#[must_use]
pub fn new(connection: Connection, cli: &Cli) -> Self {
Self {
connection,
probability: cli.probability,
field: Self::make_random_field(cli.probability),
}
}
pub fn step(&mut self) -> bool {
self.send_image();
self.change_brightness();
self.field = self.game_iteration();
self.handle_events()
}
fn game_iteration(&self) -> PixelGrid {
let mut next = self.field.clone();
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 =
matches!((old_state, neighbors), (true, 2) | (true, 3) | (false, 3));
next.set(x, y, new_state);
}
}
next
}
fn send_image(&self) {
let command = Command::BitmapLinearWin(
Origin(0, 0),
self.field.clone(),
CompressionCode::Uncompressed,
);
self.connection
.send(command.into())
.expect("could not send");
}
fn count_neighbors(&self, x: usize, y: usize) -> u8 {
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
{
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;
}
}
count
}
fn make_random_field(probability: f64) -> PixelGrid {
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));
}
}
field
}
fn handle_events(&mut self) -> bool {
if !event::poll(Duration::from_secs(0)).expect("could not poll") {
return true;
}
match event::read().expect("could not read event") {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_press(key_event)
}
event => {
debug!("unhandled event {event:?}");
return true;
}
}
}
fn handle_key_press(&mut self, event: KeyEvent) -> bool {
match event.code {
KeyCode::Char('q') => {
warn!("q pressed, terminating");
return false;
}
KeyCode::Char(' ') => {
info!("generating new random field");
self.field = Self::make_random_field(self.probability);
}
key_code => {
debug!("unhandled KeyCode {key_code:?}");
}
}
true
}
fn change_brightness(&self) {
let mut rng = rand::thread_rng();
if !rng.gen_ratio(1, 10) {
return;
}
let min_size = 1;
let x = rng.gen_range(0..TILE_WIDTH - min_size);
let y = rng.gen_range(0..TILE_HEIGHT - min_size);
let w = rng.gen_range(min_size..=TILE_WIDTH - x);
let h = rng.gen_range(min_size..=TILE_HEIGHT - y);
let origin = Origin(x, y);
let mut luma = ByteGrid::new(w as usize, h as usize);
for y in 0..h as usize {
for x in 0..w as usize {
luma.set(x, y, rng.gen());
}
}
self.connection
.send(CharBrightness(origin, luma).into())
.expect("could not send brightness");
}
}

101
src/game.rs Normal file
View file

@ -0,0 +1,101 @@
use rand::Rng;
use servicepoint2::{ByteGrid, PixelGrid, TILE_HEIGHT, TILE_WIDTH};
pub(crate) struct Game {
pub field: PixelGrid,
pub luma: ByteGrid,
}
impl Game {
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();
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 =
matches!((old_state, neighbors), (true, 2) | (true, 3) | (false, 3));
next.set(x, y, new_state);
}
}
next
}
fn count_neighbors(&self, x: usize, y: usize) -> u8 {
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
{
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;
}
}
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),
}
}
}

View file

@ -1,24 +1,34 @@
use std::io::stdout;
use std::time::Duration; use std::time::Duration;
use std::{io, thread}; use std::{io, thread};
use clap::Parser; use clap::Parser;
use crossterm::execute; use crossterm::event::{Event, KeyCode, KeyEventKind};
use crossterm::style::{Print, PrintStyledContent, Stylize};
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{event, execute};
use log::LevelFilter; use log::LevelFilter;
use servicepoint2::Connection; use rand::{distributions, Rng};
use servicepoint2::Command::{BitmapLinearWin, CharBrightness};
use servicepoint2::{
ByteGrid, CompressionCode, Connection, Origin, PixelGrid, PIXEL_WIDTH, TILE_HEIGHT, TILE_WIDTH,
};
use crate::app::App; use crate::game::Game;
mod app; mod game;
#[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.5f64)] #[arg(short, long, default_value_t = 0.4f64)]
probability: f64, probability: f64,
#[arg(short, long, default_value_t = true)]
extended: bool,
} }
// TODO: itsa spaghetti! 👌
fn main() { fn main() {
env_logger::builder() env_logger::builder()
.filter_level(LevelFilter::Info) .filter_level(LevelFilter::Info)
@ -33,14 +43,150 @@ fn main() {
crossterm::terminal::enable_raw_mode().expect("could not enable raw terminal mode"); crossterm::terminal::enable_raw_mode().expect("could not enable raw terminal mode");
let mut app = App::new(connection, &cli); let mut left = Game::default();
while app.step() { 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 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') => {
execute!(
stdout(),
Print("h for help\r\n"),
Print("q to quit\r\n"),
Print("a to reset left field\r\n"),
Print("d to reset right field\r\n")
)
.unwrap();
}
KeyCode::Char('q') => {
execute!(stdout(), PrintStyledContent("terminating\r\n".red()))
.unwrap();
close_requested = true;
}
KeyCode::Char('a') => {
execute!(
stdout(),
PrintStyledContent(
"generating new random field for left\r\n".grey()
)
)
.unwrap();
left = make_random_field(cli.probability);
}
KeyCode::Char('d') => {
execute!(
stdout(),
PrintStyledContent(
"generating new random field for right\r\n".grey()
)
)
.unwrap();
right = make_random_field(cli.probability);
}
KeyCode::Right => {
split_pixel += 1;
}
KeyCode::Left => {
split_pixel -= 1;
}
key_code => {
execute!(
stdout(),
PrintStyledContent(
format!("unhandled KeyCode {key_code:?}\r\n").dark_grey()
)
)
.unwrap();
}
}
}
event => {
execute!(
stdout(),
PrintStyledContent(format!("unhandled event {event:?}\r\n").dark_grey()),
)
.unwrap();
}
}
}
thread::sleep(Duration::from_millis(30)); thread::sleep(Duration::from_millis(30));
} }
crossterm::terminal::disable_raw_mode().expect("could not disable raw terminal mode"); crossterm::terminal::disable_raw_mode().expect("could not disable raw terminal mode");
if entered_alternate { if entered_alternate {
execute!(io::stdout(), LeaveAlternateScreen).expect("could not leave alternate screen"); execute!(stdout(), LeaveAlternateScreen).expect("could not leave alternate screen");
} }
} }
fn make_random_field(probability: f64) -> 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 }
}