make some spaghetti
This commit is contained in:
parent
879ecde430
commit
e2dd9e78ac
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
167
src/app.rs
167
src/app.rs
|
@ -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
101
src/game.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
162
src/main.rs
162
src/main.rs
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue