commit 5f1f0e1f7b786e5c3c9d8a53df3e74ce44b0dd98 Author: Val Date: Fri May 31 16:19:22 2024 -0500 game-life: Implement Conway's Game of Life diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9180d4a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "game-life" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..77a9e17 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "game-life" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f2a639d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,98 @@ +//! Conway's Game of Life on a torus + +pub fn rem(lhs: isize, rhs: usize) -> usize { + lhs.rem_euclid(rhs as _) as usize +} + +pub struct Board { + pub front: Box<[[u8; X]; Y]>, // The display buffer + pub back: Box<[[u8; X]; Y]>, // These are heap allocated to make buffer swaps a pointer operation +} + +impl Default for Board { + fn default() -> Self { + Self { + back: Box::new([[0; X]; Y]), + front: Box::new([[0; X]; Y]), + } + } +} + +impl Board { + /// Copies a template into the top left corner of the front buffer + pub fn copy_from(&mut self, other: [[u8; AY]; AX]) { + for (y, other) in other.iter().enumerate().take(Y) { + for (x, other) in other.iter().enumerate().take(X) { + self.set(x, y, *other); + } + } + self.swap() + } + /// Sets `(x, y)` to `value` in the back buffer + pub fn set(&mut self, x: usize, y: usize, value: u8) { + self.back[y][x] = value; + } + /// Gets the value of `(x, y)` in the front buffer + pub fn get(&self, x: usize, y: usize) -> u8 { + self.front[y][x] + } + /// Swaps the front and back buffers + pub fn swap(&mut self) { + let Self { back, front, .. } = self; + std::mem::swap(front, back) + } + /// sums the immediate neighbors of (cx, dx) + pub fn sum_neighbors(&self, cx: usize, cy: usize) -> u8 { + let (cx, cy, mut neighbors) = (cx as isize, cy as isize, 0); + for dx in -1..=1 { + for dy in -1..=1 { + if (dx, dy) == (0, 0) { + continue; + } + let (x, y) = (rem(cx + dx, X), rem(cy + dy, Y)); + neighbors += self.get(x, y) + } + } + neighbors + } + /// Evaluates the Game of Life ruleset + pub fn life(&mut self, x: usize, y: usize) { + let state = match (self.get(x, y), self.sum_neighbors(x, y)) { + (0, 2) => 0, // A dead cell with 2 neighbors dies + (_, 2..=3) => 1, // A cell with 2 or 3 neigbors comes alive + _ => 0, // Any other cell dies + }; + self.set(x, y, state) + } + /// Produces a new generation + pub fn update(&mut self) { + for y in 0..Y { + for x in 0..X { + self.life(x, y); + } + } + self.swap() + } +} + +impl std::fmt::Display for Board { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "┌{:─<1$}┐", "", X * 2)?; + for y in self.front.iter() { + "│".fmt(f)?; + for x in y { + f.write_str(to_str(*x))?; + } + "│\n".fmt(f)?; + } + write!(f, "└{:─<1$}┘", "", X * 2) + } +} + +/// Gets a little guy if the cell is alive +fn to_str(cell: u8) -> &'static str { + match cell { + 0 => " ", + _ => "🮲🮳", + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c9c8638 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +use std::time::{Duration, Instant}; + +use game_life::Board; + +const W: usize = 22; +const H: usize = 12; +// Framerate +const RATE: f32 = 60.0; + +#[allow(unused_mut)] +fn main() { + let mut board: Board = Default::default(); + // Stick a glider in the top left corner + board.copy_from([ + [0, 1, 0], + [0, 0, 1], + [1, 1, 1], + ]); + println!("\x1b[H\x1b[J"); + loop { + let time = Instant::now(); + println!("\x1b[H{board}"); + board.update(); + std::thread::sleep(Duration::from_secs_f32(RATE.recip()).saturating_sub(time.elapsed())); + } +}