game-life: Implement Conway's Game of Life
This commit is contained in:
commit
5f1f0e1f7b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
@ -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"
|
6
Cargo.toml
Normal file
6
Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "game-life"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
98
src/lib.rs
Normal file
98
src/lib.rs
Normal file
@ -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<const X: usize, const Y: usize> {
|
||||||
|
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<const X: usize, const Y: usize> Default for Board<X, Y> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
back: Box::new([[0; X]; Y]),
|
||||||
|
front: Box::new([[0; X]; Y]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const X: usize, const Y: usize> Board<X, Y> {
|
||||||
|
/// Copies a template into the top left corner of the front buffer
|
||||||
|
pub fn copy_from<const AX: usize, const AY: usize>(&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<const X: usize, const Y: usize> std::fmt::Display for Board<X, Y> {
|
||||||
|
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 => " ",
|
||||||
|
_ => "🮲🮳",
|
||||||
|
}
|
||||||
|
}
|
26
src/main.rs
Normal file
26
src/main.rs
Normal file
@ -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<W, H> = 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()));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user