Merge pull request #820 from jonfetterdegges/main

Add Rust implementation of 55_Life
This commit is contained in:
Jeff Atwood
2022-10-12 16:46:39 -07:00
committed by GitHub
3 changed files with 305 additions and 0 deletions

8
55_Life/rust/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "rust"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

22
55_Life/rust/README.md Normal file
View File

@@ -0,0 +1,22 @@
# Conway's Life
Original from David Ahl's _Basic Computer Games_, downloaded from http://www.vintage-basic.net/games.html.
Ported to Rust by Jon Fetter-Degges
Developed and tested on Rust 1.64.0
## How to Run
Install Rust using the instructions at [rust-lang.org](https://www.rust-lang.org/tools/install).
At a command or shell prompt in the `rust` subdirectory, enter `cargo run`.
## Differences from Original Behavior
* The simulation stops if all cells die.
* `.` at the beginning of an input line is supported but optional.
* Input of more than 66 columns is rejected. Input will automatically terminate after 20 rows. Beyond these bounds, the original
implementation would have marked the board as invalid, and beyond 68 cols/24 rows it would have had an out of bounds array access.
* The check for the string "DONE" at the end of input is case-independent.
* The program pauses for half a second between each generation.

275
55_Life/rust/src/main.rs Normal file
View File

@@ -0,0 +1,275 @@
// Rust implementation of the "Basic Computer Games" version of Conway's Life
//
// Jon Fetter-Degges
// October 2022
// I am a Rust newbie. Corrections and suggestions are welcome.
use std::{cmp, fmt, io, thread, time};
// The BASIC implementation uses integers to represent the state of each cell: 1 is
// alive, 2 is about to die, 3 is about to be born, 0 is dead. Here, we'll use an enum
// instead.
// Deriving Copy (which requires Clone) allows us to use this enum value in assignments,
// and deriving Eq (or PartialEq) allows us to use the == operator. These need to be
// explicitly specified because some enums may have associated data that makes copies and
// comparisons more complicated or expensive.
#[derive(Clone, Copy, PartialEq, Eq)]
enum CellState {
Empty,
Alive,
AboutToDie,
AboutToBeBorn,
}
// Support direct printing of the cell. In this program cells will only be Alive or Empty
// when they are printed.
impl fmt::Display for CellState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let rep = match *self {
CellState::Empty => ' ',
CellState::Alive => '*',
CellState::AboutToDie => 'o',
CellState::AboutToBeBorn => '.',
};
write!(f, "{}", rep)
}
}
// Following the BASIC implementation, we will bound the board at 24 rows x 70 columns.
// The board is an array of CellState. Using an array of arrays gives us bounds checking
// in both dimensions.
const HEIGHT: usize = 24;
const WIDTH: usize = 70;
struct Board {
cells: [[CellState; WIDTH]; HEIGHT],
min_row: usize,
max_row: usize,
min_col: usize,
max_col: usize,
population: usize,
generation: usize,
invalid: bool,
}
impl Board {
fn new() -> Board {
Board {
cells: [[CellState::Empty; WIDTH]; HEIGHT],
min_row: 0,
max_row: 0,
min_col: 0,
max_col: 0,
population: 0,
generation: 0,
invalid: false,
}
}
}
fn main() {
println!(); println!(); println!();
println!("{:33}{}", " ", "Life");
println!("{:14}{}", " ", "Creative Computing Morristown, New Jersey");
println!("Enter your pattern: ");
let mut board = parse_pattern(get_pattern());
loop {
finish_cell_transitions(&mut board);
print_board(&board);
mark_cell_transitions(&mut board);
if board.population == 0 {
break; // this isn't in the original implementation but it seemed better than
// spewing blank screens
}
delay();
}
}
fn get_pattern() -> Vec<Vec<char>> {
let max_line_len = WIDTH - 4;
let max_line_count = HEIGHT - 4;
let mut lines = Vec::new();
loop {
let mut line = String::new();
// read_line reads into the buffer (appending if it's not empty). It returns the
// number of characters read, including the newline. This will be 0 on EOF.
// unwrap() will panic and terminate the program if there is an error reading
// from stdin. That's reasonable behavior in this case.
let nread = io::stdin().read_line(&mut line).unwrap();
let line = line.trim_end();
if nread == 0 || line.eq_ignore_ascii_case("DONE") {
return lines;
}
// Handle Unicode by converting the string to a vector of characters up front. We
// do this here because we check the number of characters several times, so we
// might as well just do the Unicode parsing once.
let line = Vec::from_iter(line.chars());
if line.len() > max_line_len {
println!("Line too long - the maximum is {max_line_len} characters.");
continue;
}
lines.push(line);
if lines.len() == max_line_count {
println!("Maximum line count reached. Starting simulation.");
return lines;
}
}
}
fn parse_pattern(rows: Vec<Vec<char>>) -> Board {
// This function assumes that the input pattern in rows is in-bounds. If the pattern
// is too large, this function will panic. get_pattern checks the size of the input,
// so it is safe to call this function with its results.
let mut board = Board::new();
// The BASIC implementation puts the pattern roughly in the center of the board,
// assuming that there are no blank rows at the beginning or end, or blanks entered
// at the beginning or end of every row. It wouldn't be hard to check for that, but
// for now we'll preserve the original behavior.
let nrows = rows.len();
// If rows is empty, the call to max will return None. The unwrap_or then provides a
// default value
let ncols = rows.iter().map(|l| l.len()).max().unwrap_or(0);
// The min and max values here are unsigned. If nrows >= 24 or ncols >= 68, these
// assignments will panic - they do not wrap around unless we use a function with
// that specific behavior. Again, we expect bounds checking on the input before this
// function is called.
board.min_row = 11 - nrows / 2;
board.min_col = 33 - ncols / 2;
board.max_row = board.min_row + nrows - 1;
board.max_col = board.min_col + ncols - 1;
// Loop over the rows provided. enumerate() augments the iterator with an index.
for (row_index, pattern) in rows.iter().enumerate() {
let row = board.min_row + row_index;
// Now loop over the non-empty cells in the current row. filter_map takes a
// closure that returns an Option. If the Option is None, filter_map filters out
// that entry from the for loop. If it's Some(x), filter_map executes the loop
// body with the value x.
for col in pattern.iter().enumerate().filter_map(|(col_index, chr)| {
if *chr == ' ' || (*chr == '.' && col_index == 0) {
None
} else {
Some(board.min_col + col_index)
}
}) {
board.cells[row][col] = CellState::Alive;
board.population += 1;
}
}
board
}
fn finish_cell_transitions(board: &mut Board) {
// In the BASIC implementation, this happens in the same loop that prints the board.
// We're breaking it out to improve separation of concerns.
let mut min_row = HEIGHT - 1;
let mut max_row = 0usize;
let mut min_col = WIDTH - 1;
let mut max_col = 0usize;
for row_index in board.min_row-1..=board.max_row+1 {
let mut any_alive_this_row = false;
for col_index in board.min_col-1..=board.max_col+1 {
let cell = &mut board.cells[row_index][col_index];
if *cell == CellState::AboutToBeBorn {
*cell = CellState::Alive;
board.population += 1;
} else if *cell == CellState::AboutToDie {
*cell = CellState::Empty;
board.population -= 1;
}
if *cell == CellState::Alive {
any_alive_this_row = true;
min_col = cmp::min(min_col, col_index);
max_col = cmp::max(max_col, col_index);
}
}
if any_alive_this_row {
min_row = cmp::min(min_row, row_index);
max_row = cmp::max(max_row, row_index);
}
}
// If anything is alive within two cells of the boundary, mark the board invalid and
// clamp the bounds. We need a two-cell margin because we'll count neighbors on cells
// one space outside the min/max, and when we count neighbors we go out by an
// additional space.
if min_row < 2 {
min_row = 2;
board.invalid = true;
}
if max_row > HEIGHT - 3 {
max_row = HEIGHT - 3;
board.invalid = true;
}
if min_col < 2 {
min_col = 2;
board.invalid = true;
}
if max_col > WIDTH - 3 {
max_col = WIDTH - 3;
board.invalid = true;
}
board.min_row = min_row;
board.max_row = max_row;
board.min_col = min_col;
board.max_col = max_col;
}
fn print_board(board: &Board) {
println!(); println!(); println!();
print!("Generation: {} Population: {}", board.generation, board.population);
if board.invalid {
print!(" Invalid!");
}
println!();
for row_index in 0..HEIGHT {
for col_index in 0..WIDTH {
// This print uses the Display implementation for cell_state, above.
print!("{}", board.cells[row_index][col_index]);
}
println!();
}
}
fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 {
// Simply loop over all the immediate neighbors of a cell. We assume that the row and
// column indices are not on (or outside) the boundary of the arrays; if they are,
// the function will panic instead of going out of bounds.
let mut count = 0;
for i in row_index-1..=row_index+1 {
for j in col_index-1..=col_index+1 {
if i == row_index && j == col_index {
continue;
}
if board.cells[i][j] == CellState::Alive || board.cells[i][j] == CellState::AboutToDie {
count += 1;
}
}
}
count
}
fn mark_cell_transitions(board: &mut Board) {
for row_index in board.min_row-1..=board.max_row+1 {
for col_index in board.min_col-1..=board.max_col+1 {
let neighbors = count_neighbors(board, row_index, col_index);
// Borrow a mutable reference to the array cell
let this_cell_state = &mut board.cells[row_index][col_index];
*this_cell_state = match *this_cell_state {
CellState::Empty if neighbors == 3 => CellState::AboutToBeBorn,
CellState::Alive if !(2..=3).contains(&neighbors) => CellState::AboutToDie,
_ => *this_cell_state,
}
}
}
board.generation += 1;
}
fn delay() {
thread::sleep(time::Duration::from_millis(500));
}