(This post was generated by an LLM with direction from a human.)
Introduction
Welcome back to my blog! In my last post, I showed you a simple implementation of Conway’s Game of Life I wrote in Rust. Today, I’m excited to walk you through an updated version of the project. This classic cellular automaton was devised by mathematician John Conway in 1970, and it’s a perfect project to explore Rust’s strengths in systems programming and concurrency.
Below, I’ll detail the design and implementation of the Game of Life, explaining each part of the code to help you understand the intricacies involved. Let’s dive in!
Setting Up the Environment
First, let’s look at the dependencies required for our implementation. We’ll use the crossterm
crate for terminal handling and rand
for generating random initial states.
Add these dependencies to your Cargo.toml
:
[dependencies]
crossterm = "0.23"
rand = "0.8"
termsize = "0.3"
Defining the Console Size
We start by defining a ConsoleSize
struct to represent the dimensions of our terminal window:
struct ConsoleSize {
rows: usize,
cols: usize,
}
This struct will store the number of rows and columns in the terminal, which we will use to size our grid appropriately.
Initializing the Grid
The next step is to initialize the grid with a random pattern of live and dead cells. Here’s how we do it:
fn initialize_grid() -> Result<(Vec<Vec<bool>>, ConsoleSize), Box<dyn Error>> {
let size = termsize::get().ok_or("Failed to get terminal size")?;
let mut rng = rand::thread_rng();
let mut grid = vec![vec![false; size.cols as usize]; size.rows as usize];
for i in 0..size.cols as usize {
for j in 0..size.rows as usize {
grid[j][i] = rng.gen_bool(0.2); // Reduced the probability to make the grid less crowded.
}
}
Ok((
grid,
ConsoleSize {
rows: size.rows as usize,
cols: size.cols as usize,
},
))
}
Here, we use the termsize
crate to get the current terminal size. We then create a 2D vector to represent our grid, initializing each cell randomly as either live or dead.
Displaying the Grid
To visualize our grid, we need a function to print it to the console. We use the crossterm
crate to handle terminal output:
fn display_grid(grid: &[Vec<bool>], prev_grid: &[Vec<bool>]) -> Result<(), Box<dyn Error>> {
let mut stdout = stdout();
for (y, row) in grid.iter().enumerate() {
for (x, &cell) in row.iter().enumerate() {
if cell != prev_grid[y][x] {
stdout.execute(cursor::MoveTo(x as u16, y as u16))?;
if cell {
stdout
.execute(SetForegroundColor(Color::Green))?
.execute(Print("#"))?;
} else {
stdout
.execute(SetForegroundColor(Color::Black))?
.execute(Print(" "))?;
}
}
}
}
stdout.flush()?;
Ok(())
}
This function iterates over the grid and prints a character for each cell, using different colors to represent live and dead cells.
Counting the Neighbors
To apply the rules of the Game of Life, we need a function to count the live neighbors of a given cell:
fn live_neighbors(grid: &[Vec<bool>], x: usize, y: usize) -> usize {
let mut count = 0;
for i in -1..=1 {
for j in -1..=1 {
if i == 0 && j == 0 {
continue;
}
if let Some(&cell) = grid
.get((y as isize + i) as usize)
.and_then(|row| row.get((x as isize + j) as usize))
{
count += cell as usize;
}
}
}
count
}
This function checks the eight neighbors of a cell, counting how many of them are live.
Updating the Grid
With the ability to count live neighbors, we can now implement the function to update the grid according to the Game of Life rules:
fn update_grid(grid: &mut [Vec<bool>], size: &ConsoleSize) -> Vec<Vec<bool>> {
let mut new_grid = vec![vec![false; size.cols]; size.rows];
for i in 0..size.rows {
for j in 0..size.cols {
let live_neighbors = live_neighbors(grid, j, i);
if grid[i][j] {
new_grid[i][j] = live_neighbors == 2 || live_neighbors == 3;
} else {
new_grid[i][j] = live_neighbors == 3;
}
}
}
new_grid
}
This function creates a new grid and applies the rules to determine the next state of each cell based on its current state and the number of live neighbors.
Main Function
Finally, we bring everything together in the main function:
fn main() -> Result<(), Box<dyn Error>> {
let (mut grid, console_size) = initialize_grid()?;
let mut prev_grid = grid.clone();
execute!(stdout(), Clear(ClearType::All))?;
loop {
display_grid(&grid, &prev_grid)?;
prev_grid = grid.clone();
grid = update_grid(&mut grid, &console_size);
thread::sleep(Duration::from_millis(100));
}
}
This function initializes the grid, clears the screen, and enters an infinite loop where it continuously updates and displays the grid, pausing briefly between iterations to control the speed of the simulation.
Conclusion
This Rust implementation of Conway’s Game of Life showcases Rust’s capabilities in handling low-level system tasks efficiently. The crossterm
crate makes terminal manipulation straightforward, and Rust’s concurrency features ensure our simulation runs smoothly.
Feel free to check out the full code and experiment with different parameters or enhancements. Happy coding!
Check out the full implementation on my GitHub repository.
Pingback: My Rust Implementation of Conway's Game of Life - [email protected]