Skip to main content

game_of_life/
game_of_life.rs

1//! Conway's Game of Life Example
2//! This example implements Conway's Game of Life using a `BoolMatrix` to represent the game board.
3//! It demonstrates matrix operations like shifting, counting neighbors, and applying game rules.
4//! The game runs in a loop, updating the board state and printing it to the console.
5//! To modify the behaviour of the example, please change the constants at the top of this file.
6
7
8use rustframe::matrix::{BoolMatrix, BoolOps, IntMatrix, Matrix};
9use rustframe::random::{rng, Rng};
10use std::{thread, time};
11
12const BOARD_SIZE: usize = 20; // Size of the board (50x50)
13const MAX_FRAMES: u32 = 1000;
14
15const TICK_DURATION_MS: u64 = 0; // Milliseconds per frame
16const SKIP_FRAMES: u32 = 1;
17const PRINT_BOARD: bool = true; // Set to false to disable printing the board
18
19fn main() {
20    let args = std::env::args().collect::<Vec<String>>();
21    let debug_mode = args.contains(&"--debug".to_string());
22    let print_mode = if debug_mode { false } else { PRINT_BOARD };
23
24    let mut current_board =
25        BoolMatrix::from_vec(vec![false; BOARD_SIZE * BOARD_SIZE], BOARD_SIZE, BOARD_SIZE);
26
27    let primes = generate_primes((BOARD_SIZE * BOARD_SIZE) as i32);
28
29    add_simulated_activity(&mut current_board, BOARD_SIZE);
30
31    let mut generation_count: u32 = 0;
32    let mut previous_board_state: Option<BoolMatrix> = None;
33    let mut board_hashes = Vec::new();
34    let mut print_bool_int = 0;
35
36    loop {
37        if print_bool_int % SKIP_FRAMES == 0 {
38            print_board(&current_board, generation_count, print_mode);
39
40            print_bool_int = 0;
41        } else {
42            print_bool_int += 1;
43        }
44        board_hashes.push(hash_board(&current_board, primes.clone()));
45        if detect_stable_state(&current_board, &previous_board_state) {
46            println!(
47                "\nStable state detected at generation {}.",
48                generation_count
49            );
50            add_simulated_activity(&mut current_board, BOARD_SIZE);
51        }
52        if detect_repeating_state(&mut board_hashes) {
53            println!(
54                "\nRepeating state detected at generation {}.",
55                generation_count
56            );
57            add_simulated_activity(&mut current_board, BOARD_SIZE);
58        }
59        if !&current_board.any() {
60            println!("\nExtinction at generation {}.", generation_count);
61            add_simulated_activity(&mut current_board, BOARD_SIZE);
62        }
63
64        previous_board_state = Some(current_board.clone());
65
66        let next_board = game_of_life_next_frame(&current_board);
67        current_board = next_board;
68
69        generation_count += 1;
70        thread::sleep(time::Duration::from_millis(TICK_DURATION_MS));
71
72        if (MAX_FRAMES > 0) && (generation_count > MAX_FRAMES) {
73            println!("\nReached generation limit.");
74            break;
75        }
76    }
77}
78
79/// Prints the Game of Life board to the console.
80///
81/// - `board`: A reference to the `BoolMatrix` representing the current game state.
82/// This function demonstrates `board.rows()`, `board.cols()`, and `board[(r, c)]` (Index trait).
83fn print_board(board: &BoolMatrix, generation_count: u32, print_mode: bool) {
84    if !print_mode {
85        return;
86    }
87
88    print!("{}[2J", 27 as char);
89    println!("Conway's Game of Life - Generation: {}", generation_count);
90    let mut print_str = String::new();
91    print_str.push_str("+");
92    for _ in 0..board.cols() {
93        print_str.push_str("--");
94    }
95    print_str.push_str("+\n");
96    for r in 0..board.rows() {
97        print_str.push_str("| ");
98        for c in 0..board.cols() {
99            if board[(r, c)] {
100                print_str.push_str("██");
101            } else {
102                print_str.push_str("  ");
103            }
104        }
105        print_str.push_str(" |\n");
106    }
107    print_str.push_str("+");
108    for _ in 0..board.cols() {
109        print_str.push_str("--");
110    }
111    print_str.push_str("+\n\n");
112    print!("{}", print_str);
113
114    println!("Alive cells: {}", board.count());
115}
116
117/// Helper function to create a shifted version of the game board.
118/// (Using the version provided by the user)
119///
120/// - `game`: The current state of the Game of Life as a `BoolMatrix`.
121/// - `dr`: The row shift (delta row). Positive shifts down, negative shifts up.
122/// - `dc`: The column shift (delta column). Positive shifts right, negative shifts left.
123///
124/// Returns an `IntMatrix` of the same dimensions as `game`.
125/// - Cells in the shifted matrix get value `1` if the corresponding source cell in `game` was `true` (alive).
126/// - Cells that would source from outside `game`'s bounds (due to the shift) get value `0`.
127fn get_shifted_neighbor_layer(game: &BoolMatrix, dr: isize, dc: isize) -> IntMatrix {
128    let rows = game.rows();
129    let cols = game.cols();
130
131    if rows == 0 || cols == 0 {
132        // Handle 0x0 case, other 0-dim cases panic in Matrix::from_vec
133        return IntMatrix::from_vec(vec![], 0, 0);
134    }
135
136    // Initialize with a matrix of 0s using from_vec.
137    // This demonstrates creating an IntMatrix and then populating it.
138    let mut shifted_layer = IntMatrix::from_vec(vec![0i32; rows * cols], rows, cols);
139
140    for r_target in 0..rows {
141        // Iterate over cells in the *new* (target) shifted matrix
142        for c_target in 0..cols {
143            // Calculate where this target cell would have come from in the *original* game matrix
144            let r_source = r_target as isize - dr;
145            let c_source = c_target as isize - dc;
146
147            // Check if the source coordinates are within the bounds of the original game matrix
148            if r_source >= 0
149                && r_source < rows as isize
150                && c_source >= 0
151                && c_source < cols as isize
152            {
153                // If the source cell in the original game was alive...
154                if game[(r_source as usize, c_source as usize)] {
155                    // Demonstrates Index access on BoolMatrix
156                    // ...then this cell in the shifted layer is 1.
157                    shifted_layer[(r_target, c_target)] = 1; // Demonstrates IndexMut access on IntMatrix
158                }
159            }
160            // Else (source is out of bounds): it remains 0, as initialized.
161        }
162    }
163    shifted_layer // Return the constructed IntMatrix
164}
165
166/// Calculates the next generation of Conway's Game of Life.
167///
168/// This implementation uses a broadcast-like approach by creating shifted layers
169/// for each neighbor and summing them up, then applying rules element-wise.
170///
171/// - `current_game`: A `&BoolMatrix` representing the current state (true=alive).
172///
173/// Returns: A new `BoolMatrix` for the next generation.
174pub fn game_of_life_next_frame(current_game: &BoolMatrix) -> BoolMatrix {
175    let rows = current_game.rows();
176    let cols = current_game.cols();
177
178    if rows == 0 && cols == 0 {
179        return BoolMatrix::from_vec(vec![], 0, 0); // Return an empty BoolMatrix
180    }
181    // Define the 8 neighbor offsets (row_delta, col_delta)
182    let neighbor_offsets: [(isize, isize); 8] = [
183        (-1, -1),
184        (-1, 0),
185        (-1, 1),
186        (0, -1),
187        (0, 1),
188        (1, -1),
189        (1, 0),
190        (1, 1),
191    ];
192
193    let (first_dr, first_dc) = neighbor_offsets[0];
194    let mut neighbor_counts = get_shifted_neighbor_layer(current_game, first_dr, first_dc);
195
196    for i in 1..neighbor_offsets.len() {
197        let (dr, dc) = neighbor_offsets[i];
198        let next_neighbor_layer = get_shifted_neighbor_layer(current_game, dr, dc);
199        neighbor_counts = neighbor_counts + next_neighbor_layer;
200    }
201
202    let has_2_neighbors = neighbor_counts.eq_elem(2);
203    let has_3_neighbors = neighbor_counts.eq_elem(3);
204
205    let has_2_or_3_neighbors = has_2_neighbors | has_3_neighbors.clone();
206
207    let survives = current_game & &has_2_or_3_neighbors;
208
209    let is_dead = !current_game;
210
211    let births = is_dead & &has_3_neighbors;
212
213    let next_frame_game = survives | births;
214
215    next_frame_game
216}
217
218pub fn generate_glider(board: &mut BoolMatrix, board_size: usize) {
219    // Initialize with a Glider pattern.
220    // It demonstrates how to set specific cells in the matrix.
221    // This demonstrates `IndexMut` for `current_board[(r, c)] = true;`.
222    let mut rng = rng();
223    let r_offset = rng.random_range(0..(board_size - 3));
224    let c_offset = rng.random_range(0..(board_size - 3));
225    if board.rows() >= r_offset + 3 && board.cols() >= c_offset + 3 {
226        board[(r_offset + 0, c_offset + 1)] = true;
227        board[(r_offset + 1, c_offset + 2)] = true;
228        board[(r_offset + 2, c_offset + 0)] = true;
229        board[(r_offset + 2, c_offset + 1)] = true;
230        board[(r_offset + 2, c_offset + 2)] = true;
231    }
232}
233
234pub fn generate_pulsar(board: &mut BoolMatrix, board_size: usize) {
235    // Initialize with a Pulsar pattern.
236    // This demonstrates how to set specific cells in the matrix.
237    // This demonstrates `IndexMut` for `current_board[(r, c)] = true;`.
238    let mut rng = rng();
239    let r_offset = rng.random_range(0..(board_size - 17));
240    let c_offset = rng.random_range(0..(board_size - 17));
241    if board.rows() >= r_offset + 17 && board.cols() >= c_offset + 17 {
242        let pulsar_coords = [
243            (2, 4),
244            (2, 5),
245            (2, 6),
246            (2, 10),
247            (2, 11),
248            (2, 12),
249            (4, 2),
250            (4, 7),
251            (4, 9),
252            (4, 14),
253            (5, 2),
254            (5, 7),
255            (5, 9),
256            (5, 14),
257            (6, 2),
258            (6, 7),
259            (6, 9),
260            (6, 14),
261            (7, 4),
262            (7, 5),
263            (7, 6),
264            (7, 10),
265            (7, 11),
266            (7, 12),
267        ];
268        for &(dr, dc) in pulsar_coords.iter() {
269            board[(r_offset + dr, c_offset + dc)] = true;
270        }
271    }
272}
273
274pub fn detect_stable_state(
275    current_board: &BoolMatrix,
276    previous_board_state: &Option<BoolMatrix>,
277) -> bool {
278    if let Some(ref prev_board) = previous_board_state {
279        // `*prev_board == current_board` demonstrates `PartialEq` for `Matrix`.
280        return *prev_board == *current_board;
281    }
282    false
283}
284
285pub fn hash_board(board: &BoolMatrix, primes: Vec<i32>) -> usize {
286    let board_ints_vec = board
287        .data()
288        .iter()
289        .map(|&cell| if cell { 1 } else { 0 })
290        .collect::<Vec<i32>>();
291
292    let ints_board = Matrix::from_vec(board_ints_vec, board.rows(), board.cols());
293
294    let primes_board = Matrix::from_vec(primes, ints_board.rows(), ints_board.cols());
295
296    let result = ints_board * primes_board;
297    let result: i32 = result.data().iter().sum();
298    result as usize
299}
300
301pub fn detect_repeating_state(board_hashes: &mut Vec<usize>) -> bool {
302    // so - detect alternating states. if 0==2, 1==3, 2==4, 3==5, 4==6, 5==7
303    if board_hashes.len() < 4 {
304        return false;
305    }
306    let mut result = false;
307    if (board_hashes[0] == board_hashes[2]) && (board_hashes[0] == board_hashes[2]) {
308        result = true;
309    }
310    // remove the 0th item
311    board_hashes.remove(0);
312    result
313}
314
315pub fn add_simulated_activity(current_board: &mut BoolMatrix, board_size: usize) {
316    for _ in 0..20 {
317        generate_glider(current_board, board_size);
318    }
319
320    // Generate a Pulsar pattern
321    for _ in 0..10 {
322        generate_pulsar(current_board, board_size);
323    }
324}
325
326// generate prime numbers
327pub fn generate_primes(n: i32) -> Vec<i32> {
328    // I want to generate the first n primes
329    let mut primes = Vec::new();
330    let mut count = 0;
331    let mut num = 2; // Start checking for primes from 2
332    while count < n {
333        let mut is_prime = true;
334        for i in 2..=((num as f64).sqrt() as i32) {
335            if num % i == 0 {
336                is_prime = false;
337                break;
338            }
339        }
340        if is_prime {
341            primes.push(num);
342            count += 1;
343        }
344        num += 1;
345    }
346    primes
347}
348
349// --- Tests from previous example (can be kept or adapted) ---
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use rustframe::matrix::{BoolMatrix, BoolOps}; // Assuming BoolOps is available for .count()
354
355    #[test]
356    fn test_blinker_oscillator() {
357        let initial_data = vec![false, true, false, false, true, false, false, true, false];
358        let game1 = BoolMatrix::from_vec(initial_data.clone(), 3, 3);
359        let expected_frame2_data = vec![false, false, false, true, true, true, false, false, false];
360        let expected_game2 = BoolMatrix::from_vec(expected_frame2_data, 3, 3);
361        let game2 = game_of_life_next_frame(&game1);
362        assert_eq!(
363            game2.data(),
364            expected_game2.data(),
365            "Frame 1 to Frame 2 failed for blinker"
366        );
367        let expected_game3 = BoolMatrix::from_vec(initial_data, 3, 3);
368        let game3 = game_of_life_next_frame(&game2);
369        assert_eq!(
370            game3.data(),
371            expected_game3.data(),
372            "Frame 2 to Frame 3 failed for blinker"
373        );
374    }
375
376    #[test]
377    fn test_empty_board_remains_empty() {
378        let board_3x3_all_false = BoolMatrix::from_vec(vec![false; 9], 3, 3);
379        let next_frame = game_of_life_next_frame(&board_3x3_all_false);
380        assert_eq!(
381            next_frame.count(),
382            0,
383            "All-false board should result in all-false"
384        );
385    }
386
387    #[test]
388    fn test_zero_size_board() {
389        let board_0x0 = BoolMatrix::from_vec(vec![], 0, 0);
390        let next_frame = game_of_life_next_frame(&board_0x0);
391        assert_eq!(next_frame.rows(), 0);
392        assert_eq!(next_frame.cols(), 0);
393        assert!(
394            next_frame.data().is_empty(),
395            "0x0 board should result in 0x0 board"
396        );
397    }
398
399    #[test]
400    fn test_still_life_block() {
401        let block_data = vec![
402            true, true, false, false, true, true, false, false, false, false, false, false, false,
403            false, false, false,
404        ];
405        let game_block = BoolMatrix::from_vec(block_data.clone(), 4, 4);
406        let next_frame_block = game_of_life_next_frame(&game_block);
407        assert_eq!(
408            next_frame_block.data(),
409            game_block.data(),
410            "Block still life should remain unchanged"
411        );
412    }
413}