From 87998aed6bbcb0bec46cc560268f8d05e114fbbf Mon Sep 17 00:00:00 2001 From: Palash Tyagi <23239946+Magnus167@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:28:41 +0100 Subject: [PATCH] Add Game of Life implementation with neighbor layer shifting and rules application --- src/gol.rs | 468 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 src/gol.rs diff --git a/src/gol.rs b/src/gol.rs new file mode 100644 index 0000000..8533046 --- /dev/null +++ b/src/gol.rs @@ -0,0 +1,468 @@ +// src/game.rs + +use rustframe::matrix::{BoolMatrix, BoolOps, IntMatrix, Matrix}; // Matrix for ::from_vec + +/// Helper function to create a shifted version of the game board. +/// (Using the version provided by the user) +/// +/// - `game`: The current state of the Game of Life as a `BoolMatrix`. +/// - `dr`: The row shift (delta row). Positive shifts down, negative shifts up. +/// - `dc`: The column shift (delta column). Positive shifts right, negative shifts left. +/// +/// Returns an `IntMatrix` of the same dimensions as `game`. +/// - Cells in the shifted matrix get value `1` if the corresponding source cell in `game` was `true` (alive). +/// - Cells that would source from outside `game`'s bounds (due to the shift) get value `0`. +fn get_shifted_neighbor_layer(game: &BoolMatrix, dr: isize, dc: isize) -> IntMatrix { + let rows = game.rows(); + let cols = game.cols(); + + if rows == 0 || cols == 0 { + // Handle 0x0 case, other 0-dim cases panic in Matrix::from_vec + return IntMatrix::from_vec(vec![], 0, 0); + } + + // Initialize with a matrix of 0s using from_vec. + // This demonstrates creating an IntMatrix and then populating it. + let mut shifted_layer = IntMatrix::from_vec(vec![0i32; rows * cols], rows, cols); + + for r_target in 0..rows { + // Iterate over cells in the *new* (target) shifted matrix + for c_target in 0..cols { + // Calculate where this target cell would have come from in the *original* game matrix + let r_source = r_target as isize - dr; + let c_source = c_target as isize - dc; + + // Check if the source coordinates are within the bounds of the original game matrix + if r_source >= 0 + && r_source < rows as isize + && c_source >= 0 + && c_source < cols as isize + { + // If the source cell in the original game was alive... + if game[(r_source as usize, c_source as usize)] { + // Demonstrates Index access on BoolMatrix + // ...then this cell in the shifted layer is 1. + shifted_layer[(r_target, c_target)] = 1; // Demonstrates IndexMut access on IntMatrix + } + } + // Else (source is out of bounds): it remains 0, as initialized. + } + } + shifted_layer // Return the constructed IntMatrix +} + +/// Calculates the next generation of Conway's Game of Life. +/// +/// This implementation uses a broadcast-like approach by creating shifted layers +/// for each neighbor and summing them up, then applying rules element-wise. +/// +/// - `current_game`: A `&BoolMatrix` representing the current state (true=alive). +/// +/// Returns: A new `BoolMatrix` for the next generation. +pub fn game_of_life_next_frame(current_game: &BoolMatrix) -> BoolMatrix { + let rows = current_game.rows(); + let cols = current_game.cols(); + + if rows == 0 && cols == 0 { + return BoolMatrix::from_vec(vec![], 0, 0); // Return an empty BoolMatrix + } + // Assuming valid non-empty dimensions (e.g., 25x25) as per typical GOL. + // Your Matrix::from_vec would panic for other invalid 0-dim cases. + + // Define the 8 neighbor offsets (row_delta, col_delta) + let neighbor_offsets: [(isize, isize); 8] = [ + (-1, -1), + (-1, 0), + (-1, 1), // Top row (NW, N, NE) + (0, -1), + (0, 1), // Middle row (W, E) + (1, -1), + (1, 0), + (1, 1), // Bottom row (SW, S, SE) + ]; + + // 1. Initialize `neighbor_counts` with the first shifted layer. + // This demonstrates creating an IntMatrix from a function and using it as a base. + let (first_dr, first_dc) = neighbor_offsets[0]; + let mut neighbor_counts = get_shifted_neighbor_layer(current_game, first_dr, first_dc); + + // 2. Add the remaining 7 neighbor layers. + // This demonstrates element-wise addition of matrices (`Matrix + Matrix`). + for i in 1..neighbor_offsets.len() { + let (dr, dc) = neighbor_offsets[i]; + let next_neighbor_layer = get_shifted_neighbor_layer(current_game, dr, dc); + // `neighbor_counts` (owned IntMatrix) + `next_neighbor_layer` (owned IntMatrix) + // uses `impl Add for Matrix`, consumes both, returns new owned `IntMatrix`. + neighbor_counts = neighbor_counts + next_neighbor_layer; + } + + // 3. Apply Game of Life rules using element-wise operations. + + // Rule: Survival or Birth based on neighbor counts. + // A cell is alive in the next generation if: + // (it's currently alive AND has 2 or 3 neighbors) OR + // (it's currently dead AND has exactly 3 neighbors) + + // `neighbor_counts.eq_elem(scalar)`: + // Demonstrates element-wise comparison of a Matrix with a scalar (broadcast). + // Returns an owned `BoolMatrix`. + let has_2_neighbors = neighbor_counts.eq_elem(2); + let has_3_neighbors = neighbor_counts.eq_elem(3); // This will be reused + + // `has_2_neighbors | has_3_neighbors`: + // Demonstrates element-wise OR (`Matrix | Matrix`). + // Consumes both operands, returns an owned `BoolMatrix`. + let has_2_or_3_neighbors = has_2_neighbors | has_3_neighbors.clone(); // Clone has_3_neighbors as it's used again + + // `current_game & &has_2_or_3_neighbors`: + // `current_game` is `&BoolMatrix`. `has_2_or_3_neighbors` is owned. + // Demonstrates element-wise AND (`&Matrix & &Matrix`). + // Borrows both operands, returns an owned `BoolMatrix`. + let survives = current_game & &has_2_or_3_neighbors; + + // `!current_game`: + // Demonstrates element-wise NOT (`!&Matrix`). + // Borrows operand, returns an owned `BoolMatrix`. + let is_dead = !current_game; + + // `is_dead & &has_3_neighbors`: + // `is_dead` is owned. `has_3_neighbors` is owned. + // Demonstrates element-wise AND (`Matrix & &Matrix`). + // Consumes `is_dead`, borrows `has_3_neighbors`, returns an owned `BoolMatrix`. + let births = is_dead & &has_3_neighbors; + + // `survives | births`: + // Demonstrates element-wise OR (`Matrix | Matrix`). + // Consumes both operands, returns an owned `BoolMatrix`. + let next_frame_game = survives | births; + + next_frame_game +} + +// --- Tests from previous example (can be kept or adapted) --- +#[cfg(test)] +mod tests { + use super::*; + use rustframe::matrix::{BoolMatrix, BoolOps}; // Assuming BoolOps is available for .count() + + #[test] + fn test_blinker_oscillator() { + let initial_data = vec![false, true, false, false, true, false, false, true, false]; + let game1 = BoolMatrix::from_vec(initial_data.clone(), 3, 3); + let expected_frame2_data = vec![false, false, false, true, true, true, false, false, false]; + let expected_game2 = BoolMatrix::from_vec(expected_frame2_data, 3, 3); + let game2 = game_of_life_next_frame(&game1); + assert_eq!( + game2.data(), + expected_game2.data(), + "Frame 1 to Frame 2 failed for blinker" + ); + let expected_game3 = BoolMatrix::from_vec(initial_data, 3, 3); + let game3 = game_of_life_next_frame(&game2); + assert_eq!( + game3.data(), + expected_game3.data(), + "Frame 2 to Frame 3 failed for blinker" + ); + } + + #[test] + fn test_empty_board_remains_empty() { + let board_3x3_all_false = BoolMatrix::from_vec(vec![false; 9], 3, 3); + let next_frame = game_of_life_next_frame(&board_3x3_all_false); + assert_eq!( + next_frame.count(), + 0, + "All-false board should result in all-false" + ); + } + + #[test] + fn test_zero_size_board() { + let board_0x0 = BoolMatrix::from_vec(vec![], 0, 0); + let next_frame = game_of_life_next_frame(&board_0x0); + assert_eq!(next_frame.rows(), 0); + assert_eq!(next_frame.cols(), 0); + assert!( + next_frame.data().is_empty(), + "0x0 board should result in 0x0 board" + ); + } + + #[test] + fn test_still_life_block() { + let block_data = vec![ + true, true, false, false, true, true, false, false, false, false, false, false, false, + false, false, false, + ]; + let game_block = BoolMatrix::from_vec(block_data.clone(), 4, 4); + let next_frame_block = game_of_life_next_frame(&game_block); + assert_eq!( + next_frame_block.data(), + game_block.data(), + "Block still life should remain unchanged" + ); + } +} + +// src/main.rs + +use rand::{self, Rng}; +use std::{str::MatchIndices, thread, time}; + +const BOARD_ROWS: usize = 100; +const BOARD_COLS: usize = 100; // Made wider for better viewing of patterns like gliders +const TICK_DURATION_MS: u64 = 10; // Milliseconds per frame + +/// Prints the Game of Life board to the console. +/// +/// - `board`: A reference to the `BoolMatrix` representing the current game state. +/// This function demonstrates `board.rows()`, `board.cols()`, and `board[(r, c)]` (Index trait). +fn print_board(board: &BoolMatrix) { + let mut print_str = String::new(); + print_str.push_str("+"); + for _ in 0..board.cols() { + print_str.push_str("--"); + } + print_str.push_str("+\n"); + for r in 0..board.rows() { + print_str.push_str("| "); + for c in 0..board.cols() { + if board[(r, c)] { + // Using Index trait for Matrix + print_str.push_str("██"); + } else { + print_str.push_str(" "); + } + } + print_str.push_str(" |\n"); + } + print_str.push_str("+"); + for _ in 0..board.cols() { + print_str.push_str("--"); + } + print_str.push_str("+\n\n"); + print!("{}", print_str); +} + +fn generate_glider(board: &mut BoolMatrix) { + // Initialize with a Glider pattern. + // It demonstrates how to set specific cells in the matrix. + // This demonstrates `IndexMut` for `current_board[(r, c)] = true;`. + let mut rng = rand::rng(); + let r_offset = rng.random_range(0..(BOARD_ROWS - 3)); + let c_offset = rng.random_range(0..(BOARD_COLS - 3)); + if board.rows() >= r_offset + 3 && board.cols() >= c_offset + 3 { + board[(r_offset + 0, c_offset + 1)] = true; + board[(r_offset + 1, c_offset + 2)] = true; + board[(r_offset + 2, c_offset + 0)] = true; + board[(r_offset + 2, c_offset + 1)] = true; + board[(r_offset + 2, c_offset + 2)] = true; + } +} + +fn generate_pulsar(board: &mut BoolMatrix) { + // Initialize with a Pulsar pattern. + // This demonstrates how to set specific cells in the matrix. + // This demonstrates `IndexMut` for `current_board[(r, c)] = true;`. + let mut rng = rand::rng(); + let r_offset = rng.random_range(0..(BOARD_ROWS - 17)); + let c_offset = rng.random_range(0..(BOARD_COLS - 17)); + if board.rows() >= r_offset + 17 && board.cols() >= c_offset + 17 { + let pulsar_coords = [ + (2, 4), + (2, 5), + (2, 6), + (2, 10), + (2, 11), + (2, 12), + (4, 2), + (4, 7), + (4, 9), + (4, 14), + (5, 2), + (5, 7), + (5, 9), + (5, 14), + (6, 2), + (6, 7), + (6, 9), + (6, 14), + (7, 4), + (7, 5), + (7, 6), + (7, 10), + (7, 11), + (7, 12), + ]; + for &(dr, dc) in pulsar_coords.iter() { + board[(r_offset + dr, c_offset + dc)] = true; + } + } + // (2,4),(2,5),(2,6), (2,10),(2,11),(2,12), // row 1 + // (4,2),(4,7),(4,9),(4,14), // row 2 + // (5,2),(5,7),(5,9),(5,14), // row 3 + // (6,2),(6,7),(6,9),(6,14), // row 4 + // (7,4),(7,5),(7,6), (7,10),(7,11),(7,12), // row 5 +} + +fn detect_stable_state( + current_board: &BoolMatrix, + previous_board_state: &Option, +) -> bool { + if let Some(ref prev_board) = previous_board_state { + // `*prev_board == current_board` demonstrates `PartialEq` for `Matrix`. + return *prev_board == *current_board; + } + false +} + +fn hash_board(board: &BoolMatrix, primes: Vec) -> usize { + let board_ints_vec = board + .data() + .iter() + .map(|&cell| if cell { 1 } else { 0 }) + .collect::>(); + + let ints_board = Matrix::from_vec(board_ints_vec, board.rows(), board.cols()); + + let primes_board = Matrix::from_vec(primes, ints_board.rows(), ints_board.cols()); + + let result = ints_board * primes_board; + let result: i32 = result.data().iter().sum(); + result as usize +} + +fn detect_repeating_state(board_hashes: &mut Vec) -> bool { + // so - detect alternating states. if 0==2, 1==3, 2==4, 3==5, 4==6, 5==7 + if board_hashes.len() < 4 { + return false; + } + let mut result = false; + if (board_hashes[0] == board_hashes[2]) && (board_hashes[1] == board_hashes[3]) { + result = true; + } + // remove the 0th item + board_hashes.remove(0); + result +} + +fn main() { + // Initialize the game board. + // This demonstrates `BoolMatrix::from_vec`. + let mut current_board = + BoolMatrix::from_vec(vec![false; BOARD_ROWS * BOARD_COLS], BOARD_ROWS, BOARD_COLS); + + let primes = generate_primes((BOARD_ROWS * BOARD_COLS) as i32); + + // let r_offset = 3; + // let c_offset = 3; + // if BOARD_ROWS >= r_offset + 3 && BOARD_COLS >= c_offset + 3 { + // current_board[(r_offset + 0, c_offset + 1)] = true; + // current_board[(r_offset + 1, c_offset + 2)] = true; + // current_board[(r_offset + 2, c_offset + 0)] = true; + // current_board[(r_offset + 2, c_offset + 1)] = true; + // current_board[(r_offset + 2, c_offset + 2)] = true; + // } else { + // println!("Board is too small for the default Glider pattern. Starting empty."); + // } + // gerate a random glider + + add_simulated_activity(&mut current_board); + + let mut generation_count: u32 = 0; + // `previous_board_state` will store a clone of the board. + // This demonstrates `Matrix::clone()` and later `PartialEq` for `Matrix`. + let mut previous_board_state: Option = None; + let mut board_hashes = Vec::new(); + let mut print_board_bool = true; + let mut print_bool_int = 0; + + loop { + // print!("{}[2J", 27 as char); // Clear screen and move cursor to top-left + + // if print_board_bool { + if print_bool_int % 10 == 0 { + print!("{}[2J", 27 as char); + println!("Conway's Game of Life - Generation: {}", generation_count); + + print_board(¤t_board); + println!("Alive cells: {}", ¤t_board.count()); + + // print_board_bool = false; + print_bool_int = 0; + } else { + // print_board_bool = true; + print_bool_int += 1; + } + // `current_board.count()` demonstrates a method from `BoolOps`. + board_hashes.push(hash_board(¤t_board, primes.clone())); + if detect_stable_state(¤t_board, &previous_board_state) { + println!( + "\nStable state detected at generation {}.", + generation_count + ); + add_simulated_activity(&mut current_board); + } + if detect_repeating_state(&mut board_hashes) { + println!( + "\nRepeating state detected at generation {}.", + generation_count + ); + add_simulated_activity(&mut current_board); + } + if !¤t_board.any() { + println!("\nExtinction at generation {}.", generation_count); + add_simulated_activity(&mut current_board); + } + + // `current_board.clone()` demonstrates `Clone` for `Matrix`. + previous_board_state = Some(current_board.clone()); + + // This is the core call to your game logic. + let next_board = game_of_life_next_frame(¤t_board); + current_board = next_board; + + generation_count += 1; + thread::sleep(time::Duration::from_millis(TICK_DURATION_MS)); + + // if generation_count > 500 { // Optional limit + // println!("\nReached generation limit."); + // break; + // } + } +} + +fn add_simulated_activity(current_board: &mut Matrix) { + for _ in 0..20 { + generate_glider(current_board); + } + + // Generate a Pulsar pattern + for _ in 0..10 { + generate_pulsar(current_board); + } +} + +// generate prime numbers +fn generate_primes(n: i32) -> Vec { + // I want to generate the first n primes + let mut primes = Vec::new(); + let mut count = 0; + let mut num = 2; // Start checking for primes from 2 + while count < n { + let mut is_prime = true; + for i in 2..=((num as f64).sqrt() as i32) { + if num % i == 0 { + is_prime = false; + break; + } + } + if is_prime { + primes.push(num); + count += 1; + } + num += 1; + } + primes +}