diff --git a/55_Life/rust/Cargo.toml b/55_Life/rust/Cargo.toml new file mode 100644 index 00000000..1ec69633 --- /dev/null +++ b/55_Life/rust/Cargo.toml @@ -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] diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md new file mode 100644 index 00000000..19f2d09d --- /dev/null +++ b/55_Life/rust/README.md @@ -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. diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs new file mode 100644 index 00000000..50aa4927 --- /dev/null +++ b/55_Life/rust/src/main.rs @@ -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> { + 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>) -> 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)); +}