From 0d2b7c655974380fc143c030372313e03494f655 Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 12:43:22 -0400 Subject: [PATCH 1/8] Initial commit of Life in Rust --- 55_Life/rust/Cargo.toml | 8 ++ 55_Life/rust/src/main.rs | 253 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 55_Life/rust/Cargo.toml create mode 100644 55_Life/rust/src/main.rs 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/src/main.rs b/55_Life/rust/src/main.rs new file mode 100644 index 00000000..d2901e6c --- /dev/null +++ b/55_Life/rust/src/main.rs @@ -0,0 +1,253 @@ +use std::{io, thread, time}; + +const HEIGHT:usize = 24; +const WIDTH:usize = 70; + +// The BASIC implementation uses a 24x70 array of integers to represent the board state. +// 1 is alive, 2 is about to die, 3 is about to be born, all other values are dead. +// (I'm not actually sure whether there are other values besides zero.) +// Here, we'll use an enum instead. +#[derive(Clone, Copy, PartialEq)] +enum CellState { + Empty, + Alive, + AboutToDie, + AboutToBeBorn +} + +// Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. +// Since that isn't too big (even in the 70's), we just store the whole board as an +// array of CellState. I'm experimenting with using an array-of-arrays to make references +// more convenient. +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: 1, + invalid: false, + } + } +} + +fn main() { + println!(); println!(); println!(); + println!("Enter your pattern: "); + let mut board = parse_pattern(get_pattern()); + loop { + finish_cell_transitions(&mut board); + print_board(&board); + update_bounds(&mut board); + update_board(&mut board); + if board.population == 0 { + break; // this isn't in the original implementation but I wanted it + } + delay(); + } +} + +fn get_pattern() -> Vec { + 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. + // I think 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; + } + lines.push(line.to_string()); + } +} + +fn parse_pattern(rows: Vec) -> Board { + // A robust program would check the bounds of the inputs here. I'm not doing that, + // because the BASIC implementation didn't, and for me, part of the joy of these + // books back in the day was learning how my inputs could break things. + + let mut board = Board::new(); + + // Strings are UTF-8 in Rust, so characters can take multiple bytes. We will convert + // each to a Vec up front so that we don't have to do that conversion multiple + // times (to find the length of the strings in chars, then to parse each char). + // The into_iter() method consumes rows() so it can no longer be used. + let char_vecs = Vec::from_iter(rows.into_iter().map(|s| Vec::from_iter(s.chars()))); + + // 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 = char_vecs.len(); + let ncols = char_vecs.iter() + .map(|l| l.len()) + .max() + .unwrap_or(0); // handles the case where rows is empty + + // Note that there's a subtlety here. The len() method returns a usize, i.e., an + // unsigned int, so the result type is the same. If nlines >= 24 or ncols >= 68, the + // result will wrap around to a giant value. These are stricter limits than you'd + // expect from just looking at the 24x70 bounds, but again, we're preserving the + // original behavior. + 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. The enumerate() method augments the iterator with an index. + for (row_index, pattern) in char_vecs.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) { + for row in board.cells[board.min_row-1..=board.max_row+1].iter_mut() { + for cell in row[board.min_col-1..=board.max_col+1].iter_mut() { + if *cell == CellState::AboutToBeBorn { + *cell = CellState::Alive; + board.population += 1; + } else if *cell == CellState::AboutToDie { + *cell = CellState::Empty; + board.population -= 1; + } + } + } +} + +fn print_board(board: &Board) { + println!(); println!(); println!(); + println!("Generation: {}", board.generation); + println!("Population: {}", board.population); + if board.invalid { + println!("Invalid!"); + } + for row_index in 0..HEIGHT { + for col_index in 0..WIDTH { + let rep = if board.cells[row_index][col_index] == CellState::Alive { "*" } else { " " }; + print!("{rep}"); + } + println!(); + } +} + +fn update_bounds(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. + // We could improve efficiency here by only searching one row outside the previous bounds. + board.min_row = HEIGHT; + board.max_row = 0; + board.min_col = WIDTH; + board.max_col = 0; + for (irow, row) in board.cells.iter().enumerate() { + let mut any_set = false; + for (icol, cell) in row.iter().enumerate() { + if *cell == CellState::Alive { + any_set = true; + if board.min_col > icol { + board.min_col = icol; + } + if board.max_col < icol { + board.max_col = icol; + } + } + } + if any_set { + if board.min_row > irow { + board.min_row = irow; + } + if board.max_row < irow { + board.max_row = irow; + } + } + } + // 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 board.min_row < 2 { + board.min_row = 2; + board.invalid = true; + } + if board.max_row > HEIGHT - 3 { + board.max_row = HEIGHT - 3; + board.invalid = true; + } + if board.min_col < 2 { + board.min_col = 2; + board.invalid = true; + } + if board.max_col > WIDTH - 3 { + board.max_col = WIDTH - 3; + board.invalid = true; + } +} + +fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 { + let mut count = 0; + assert!((1..=HEIGHT-2).contains(&row_index)); + assert!((1..=WIDTH-2).contains(&col_index)); + 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 update_board(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); + let this_cell_state = &mut board.cells[row_index][col_index]; // borrow a mutable reference to the array cell + *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)); +} From a068af4bc9670470e5f82f1d9d52c1d2c77ad80c Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 16:26:28 -0400 Subject: [PATCH 2/8] Do bounds update in finish_cell_transitions Merged the functionality of update_bounds into finish_cell_transitions, eliminating a loop. --- 55_Life/rust/src/main.rs | 107 +++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 55 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index d2901e6c..d7ccb4e3 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -52,7 +52,6 @@ fn main() { loop { finish_cell_transitions(&mut board); print_board(&board); - update_bounds(&mut board); update_board(&mut board); if board.population == 0 { break; // this isn't in the original implementation but I wanted it @@ -135,8 +134,16 @@ fn parse_pattern(rows: Vec) -> Board { } fn finish_cell_transitions(board: &mut Board) { - for row in board.cells[board.min_row-1..=board.max_row+1].iter_mut() { - for cell in row[board.min_col-1..=board.max_col+1].iter_mut() { + // 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; @@ -144,8 +151,50 @@ fn finish_cell_transitions(board: &mut Board) { *cell = CellState::Empty; board.population -= 1; } + if *cell == CellState::Alive { + any_alive_this_row = true; + if min_col > col_index { + min_col = col_index; + } + if max_col < col_index { + max_col = col_index; + } + } + } + if any_alive_this_row { + if min_row > row_index { + min_row = row_index; + } + if max_row < row_index { + 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) { @@ -164,58 +213,6 @@ fn print_board(board: &Board) { } } -fn update_bounds(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. - // We could improve efficiency here by only searching one row outside the previous bounds. - board.min_row = HEIGHT; - board.max_row = 0; - board.min_col = WIDTH; - board.max_col = 0; - for (irow, row) in board.cells.iter().enumerate() { - let mut any_set = false; - for (icol, cell) in row.iter().enumerate() { - if *cell == CellState::Alive { - any_set = true; - if board.min_col > icol { - board.min_col = icol; - } - if board.max_col < icol { - board.max_col = icol; - } - } - } - if any_set { - if board.min_row > irow { - board.min_row = irow; - } - if board.max_row < irow { - board.max_row = irow; - } - } - } - // 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 board.min_row < 2 { - board.min_row = 2; - board.invalid = true; - } - if board.max_row > HEIGHT - 3 { - board.max_row = HEIGHT - 3; - board.invalid = true; - } - if board.min_col < 2 { - board.min_col = 2; - board.invalid = true; - } - if board.max_col > WIDTH - 3 { - board.max_col = WIDTH - 3; - board.invalid = true; - } -} - fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 { let mut count = 0; assert!((1..=HEIGHT-2).contains(&row_index)); From 15724219b5500b903880d56907b7d8746bba298b Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 20:29:40 -0400 Subject: [PATCH 3/8] Input bounds checking, refactor and cleanup get_pattern now checks the size of the input to prevent out of bounds writes., and converts String to Vec immediately. Refactors: changed function names, ran rust-fmt, improved some comments --- 55_Life/rust/src/main.rs | 95 +++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index d7ccb4e3..37e0f6c0 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -1,7 +1,7 @@ use std::{io, thread, time}; -const HEIGHT:usize = 24; -const WIDTH:usize = 70; +const HEIGHT: usize = 24; +const WIDTH: usize = 70; // The BASIC implementation uses a 24x70 array of integers to represent the board state. // 1 is alive, 2 is about to die, 3 is about to be born, all other values are dead. @@ -12,7 +12,7 @@ enum CellState { Empty, Alive, AboutToDie, - AboutToBeBorn + AboutToBeBorn, } // Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. @@ -27,7 +27,7 @@ struct Board { max_col: usize, population: usize, generation: usize, - invalid: bool + invalid: bool, } impl Board { @@ -52,84 +52,86 @@ fn main() { loop { finish_cell_transitions(&mut board); print_board(&board); - update_board(&mut board); + mark_cell_transitions(&mut board); if board.population == 0 { - break; // this isn't in the original implementation but I wanted it + break; // this isn't in the original implementation but it seemed better than + // spewing blank screens } delay(); } } -fn get_pattern() -> Vec { +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. - // I think that's reasonable behavior in this case. + // 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; } - lines.push(line.to_string()); + // Handle Unicode by converting the string to a vector of characters up front. We + // do this here because we care about lengths and column alignment, 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 { - // A robust program would check the bounds of the inputs here. I'm not doing that, - // because the BASIC implementation didn't, and for me, part of the joy of these - // books back in the day was learning how my inputs could break things. +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(); - // Strings are UTF-8 in Rust, so characters can take multiple bytes. We will convert - // each to a Vec up front so that we don't have to do that conversion multiple - // times (to find the length of the strings in chars, then to parse each char). - // The into_iter() method consumes rows() so it can no longer be used. - let char_vecs = Vec::from_iter(rows.into_iter().map(|s| Vec::from_iter(s.chars()))); - // 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 = char_vecs.len(); - let ncols = char_vecs.iter() - .map(|l| l.len()) - .max() - .unwrap_or(0); // handles the case where rows is empty + let nrows = rows.len(); + let ncols = rows.iter().map(|l| l.len()).max().unwrap_or(0); // handles the case where rows is empty - // Note that there's a subtlety here. The len() method returns a usize, i.e., an - // unsigned int, so the result type is the same. If nlines >= 24 or ncols >= 68, the - // result will wrap around to a giant value. These are stricter limits than you'd - // expect from just looking at the 24x70 bounds, but again, we're preserving the - // original behavior. + // If nrows >= 24 or ncols >= 68, these assignments will wrap around to large values. + // The array accesses below will then be out of bounds. Rust will bounds-check them + // and panic rather than performing an invalid access. 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. The enumerate() method augments the iterator with an index. - for (row_index, pattern) in char_vecs.iter().enumerate() - { + 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) - }}) - { + if *chr == ' ' || (*chr == '.' && col_index == 0) { + None + } else { + Some(board.min_col + col_index) + } + }) { board.cells[row][col] = CellState::Alive; board.population += 1; } } - board } @@ -214,15 +216,16 @@ fn print_board(board: &Board) { } 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; - assert!((1..=HEIGHT-2).contains(&row_index)); - assert!((1..=WIDTH-2).contains(&col_index)); 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 { + if board.cells[i][j] == CellState::Alive || board.cells[i][j] == CellState::AboutToDie { count += 1; } } @@ -230,7 +233,7 @@ fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 { count } -fn update_board(board: &mut Board) { +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); @@ -238,7 +241,7 @@ fn update_board(board: &mut Board) { *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 + _ => *this_cell_state, } } } From 6e46aba249410a1b0e88ae09406b60e5c0433dae Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 21:34:40 -0400 Subject: [PATCH 4/8] README file for the Rust port --- 55_Life/rust/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 55_Life/rust/README.md diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md new file mode 100644 index 00000000..7bd5dd22 --- /dev/null +++ b/55_Life/rust/README.md @@ -0,0 +1,20 @@ +# 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. +* 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. From 14e59ac5fe3a86a2fde1466859865a255cd999ab Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 21:37:05 -0400 Subject: [PATCH 5/8] Improve printing, make output closer to original Implemented Display for CellState, and tweaked outputs to match the BASIC implementation. Also fixed some more comments. --- 55_Life/rust/src/main.rs | 59 ++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index 37e0f6c0..0cfe707f 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -1,12 +1,24 @@ -use std::{io, thread, time}; +// Rust implementation of David Ahl's implementation of Conway's Life +// +// Jon Fetter-Degges +// October 2022 + +// I am a Rust newbie. Corrections and suggestions are welcome. + +use std::{fmt, io, thread, time}; const HEIGHT: usize = 24; const WIDTH: usize = 70; // The BASIC implementation uses a 24x70 array of integers to represent the board state. -// 1 is alive, 2 is about to die, 3 is about to be born, all other values are dead. -// (I'm not actually sure whether there are other values besides zero.) -// Here, we'll use an enum instead. +// 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. +// Without that we would only be able to borrow it. That seems silly for a simple enum +// like this one - it is required because enums can have large amounts of associated +// data, so the programmer needs to decide whether to allow copying. Similarly, PartialEq +// allows use of the == comparison. Again, this seems silly for a simple enum, but if +// some enum cases have associated data, it may require some thought. #[derive(Clone, Copy, PartialEq)] enum CellState { Empty, @@ -15,6 +27,20 @@ enum CellState { 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. // Since that isn't too big (even in the 70's), we just store the whole board as an // array of CellState. I'm experimenting with using an array-of-arrays to make references @@ -39,7 +65,7 @@ impl Board { min_col: 0, max_col: 0, population: 0, - generation: 1, + generation: 0, invalid: false, } } @@ -47,6 +73,8 @@ impl Board { 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 { @@ -104,11 +132,14 @@ fn parse_pattern(rows: Vec>) -> Board { // 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(); - let ncols = rows.iter().map(|l| l.len()).max().unwrap_or(0); // handles the case where rows is empty + // 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); - // If nrows >= 24 or ncols >= 68, these assignments will wrap around to large values. - // The array accesses below will then be out of bounds. Rust will bounds-check them - // and panic rather than performing an invalid access. + // 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; @@ -201,15 +232,15 @@ fn finish_cell_transitions(board: &mut Board) { fn print_board(board: &Board) { println!(); println!(); println!(); - println!("Generation: {}", board.generation); - println!("Population: {}", board.population); + print!("Generation: {} Population: {}", board.generation, board.population); if board.invalid { - println!("Invalid!"); + print!("Invalid!"); } + println!(); for row_index in 0..HEIGHT { for col_index in 0..WIDTH { - let rep = if board.cells[row_index][col_index] == CellState::Alive { "*" } else { " " }; - print!("{rep}"); + // This print will use the Display implementation for cell_state, above. + print!("{}", board.cells[row_index][col_index]); } println!(); } From 5214f2a68117d3964ab79d9bdfe0382546aaa8ed Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 21:49:40 -0400 Subject: [PATCH 6/8] One more implementation note --- 55_Life/rust/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md index 7bd5dd22..3cce8366 100644 --- a/55_Life/rust/README.md +++ b/55_Life/rust/README.md @@ -15,6 +15,7 @@ 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. From 7b929ecbb1cd7ee43acc1e01423f5f37737e126e Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 22:03:57 -0400 Subject: [PATCH 7/8] Small fixes and use min/max --- 55_Life/rust/README.md | 1 + 55_Life/rust/src/main.rs | 43 +++++++++++++++++----------------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md index 3cce8366..19f2d09d 100644 --- a/55_Life/rust/README.md +++ b/55_Life/rust/README.md @@ -19,3 +19,4 @@ At a command or shell prompt in the `rust` subdirectory, enter `cargo run`. * 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 index 0cfe707f..0c1e83cc 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -5,7 +5,7 @@ // I am a Rust newbie. Corrections and suggestions are welcome. -use std::{fmt, io, thread, time}; +use std::{cmp, fmt, io, thread, time}; const HEIGHT: usize = 24; const WIDTH: usize = 70; @@ -42,9 +42,8 @@ impl fmt::Display for CellState { } // Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. -// Since that isn't too big (even in the 70's), we just store the whole board as an -// array of CellState. I'm experimenting with using an array-of-arrays to make references -// more convenient. +// The board is an array of CellState. Using an array of arrays gives us bounds checking +// in both dimensions. struct Board { cells: [[CellState; WIDTH]; HEIGHT], min_row: usize, @@ -145,12 +144,13 @@ fn parse_pattern(rows: Vec>) -> Board { board.max_row = board.min_row + nrows - 1; board.max_col = board.min_col + ncols - 1; - // Loop over the rows provided. The enumerate() method augments the iterator with an index. + // 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. + // 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 @@ -186,22 +186,14 @@ fn finish_cell_transitions(board: &mut Board) { } if *cell == CellState::Alive { any_alive_this_row = true; - if min_col > col_index { - min_col = col_index; - } - if max_col < col_index { - max_col = col_index; - } + min_col = cmp::min(min_col, col_index); + max_col = cmp::max(max_col, col_index); } } if any_alive_this_row { - if min_row > row_index { - min_row = row_index; - } - if max_row < row_index { - max_row = row_index; - } - } + 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 @@ -232,14 +224,14 @@ fn finish_cell_transitions(board: &mut Board) { fn print_board(board: &Board) { println!(); println!(); println!(); - print!("Generation: {} Population: {}", board.generation, board.population); + print!("Generation: {} Population: {}", board.generation, board.population); if board.invalid { - print!("Invalid!"); + print!(" Invalid!"); } println!(); for row_index in 0..HEIGHT { for col_index in 0..WIDTH { - // This print will use the Display implementation for cell_state, above. + // This print uses the Display implementation for cell_state, above. print!("{}", board.cells[row_index][col_index]); } println!(); @@ -268,7 +260,8 @@ 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); - let this_cell_state = &mut board.cells[row_index][col_index]; // borrow a mutable reference to the array cell + // 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, From 5e3e7d60aee112dcccd4822baa4ff8345be4f6fb Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 22:18:32 -0400 Subject: [PATCH 8/8] couple more comment changes --- 55_Life/rust/src/main.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index 0c1e83cc..50aa4927 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -1,4 +1,4 @@ -// Rust implementation of David Ahl's implementation of Conway's Life +// Rust implementation of the "Basic Computer Games" version of Conway's Life // // Jon Fetter-Degges // October 2022 @@ -7,19 +7,14 @@ use std::{cmp, fmt, io, thread, time}; -const HEIGHT: usize = 24; -const WIDTH: usize = 70; - -// The BASIC implementation uses a 24x70 array of integers to represent the board state. -// 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. -// Without that we would only be able to borrow it. That seems silly for a simple enum -// like this one - it is required because enums can have large amounts of associated -// data, so the programmer needs to decide whether to allow copying. Similarly, PartialEq -// allows use of the == comparison. Again, this seems silly for a simple enum, but if -// some enum cases have associated data, it may require some thought. -#[derive(Clone, Copy, PartialEq)] +// 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, @@ -44,6 +39,9 @@ impl fmt::Display for CellState { // 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, @@ -104,8 +102,8 @@ fn get_pattern() -> Vec> { return lines; } // Handle Unicode by converting the string to a vector of characters up front. We - // do this here because we care about lengths and column alignment, so we might - // as well just do the Unicode parsing once. + // 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.");