commit a2241e51239fca6c0cce736fd4a6773964161d20 Author: Rokas Puzonas Date: Fri Jun 3 21:02:24 2022 +0300 feat: initial commit? diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..1275ff5 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,3 @@ +hard_tabs = true +tab_spaces = 2 +max_width = 80 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0e12abe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "rust-game-of-life" +version = "0.1.0" +edition = "2021" + +[dependencies] +raylib = "3.7.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebb9d96 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Game Of Life using Rust diff --git a/src/game_of_life.rs b/src/game_of_life.rs new file mode 100644 index 0000000..d335861 --- /dev/null +++ b/src/game_of_life.rs @@ -0,0 +1,85 @@ +use std::collections::HashSet; + +pub type Point = (i32, i32); + +#[derive(Debug)] +pub struct GameOfLife { + pub alive_tiles: HashSet<(i32, i32)>, +} + +impl GameOfLife { + pub fn new() -> GameOfLife { + GameOfLife { + alive_tiles: HashSet::new(), + } + } + + pub fn get_tile(&self, point: Point) -> bool { + self.alive_tiles.contains(&point) + } + + pub fn toggle_tile(&mut self, point: Point) { + if self.alive_tiles.contains(&point) { + self.alive_tiles.remove(&point); + } else { + self.alive_tiles.insert(point); + } + } + + fn iter_neighbours(&self, (x, y): Point) -> Vec { + (x-1..=x+1) + .flat_map(move |x| (y-1..=y+1).map(move |y| (x, y))) + .filter(|point| !(point.0 == x && point.1 == y)) + .collect::>() + } + + fn count_neighbours(&self, point: Point) -> u8 { + self.iter_neighbours(point).iter() + .filter(|point| self.alive_tiles.contains(point)) + .count() as u8 + } + + fn get_dead_tiles(&self) -> HashSet { + self.alive_tiles.iter() + .flat_map(|point| self.iter_neighbours(*point)) + .filter(|point| !self.alive_tiles.contains(point)) + .collect::>() + } + + pub fn tick(&mut self) { + let mut new_alive_tiles = HashSet::new(); + + for alive_tile in &self.alive_tiles { + let neighours = self.count_neighbours(*alive_tile); + if neighours == 2 || neighours == 3 { + new_alive_tiles.insert(*alive_tile); + } + } + + for dead_tile in self.get_dead_tiles() { + let neighours = self.count_neighbours(dead_tile); + if neighours == 3 { + new_alive_tiles.insert(dead_tile); + } + } + + self.alive_tiles = new_alive_tiles; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let mut game = GameOfLife::new(); + + game.toggle_tile((3, 3)); + game.toggle_tile((4, 3)); + game.toggle_tile((4, 4)); + game.toggle_tile((3, 4)); + + assert_eq!(game.count_neighbours((3, 3)), 3); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a87a8b7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,141 @@ +mod game_of_life; + +use std::ffi::CString; + +use game_of_life::*; +use raylib::prelude::*; + +fn draw_label(d: &mut RaylibDrawHandle, x: f32, y: f32, text: &str) { + d.gui_label(rrect(x, y, 50., 0.), Some(&CString::new(text).unwrap())); +} + +fn to_tile_space(x: i32, y: i32, offset_x: i32, offset_y: i32, tile_size: i32) -> (i32, i32) { + ( + ((x - offset_x) as f32 / tile_size as f32).floor() as i32, + ((y - offset_y) as f32 / tile_size as f32).floor() as i32, + ) +} + +fn main() { + let (mut rl, thread) = raylib::init() + .size(640, 480) + .resizable() + .title("Game Of Life") + .build(); + + rl.set_target_fps(60); + + let mut game = GameOfLife::new(); + let tile_size = 16; + let edge_thickness = 1.0; + let mut dead_color = Color::BLACK; + let mut alive_color = Color::WHITE; + let mut edge_color = Color::GRAY; + let overlay_color = Color { r: 0, g: 0, b: 0, a: 180 }; + + let mut running = false; + let mut update_rate = 10; // in times per second + let mut timer = 0; + + let mut camera_x = 0; + let mut camera_y = 0; + let mut last_mouse = (0, 0); + + let mut show_options = true; + + while !rl.window_should_close() { + let screen_width = rl.get_screen_width(); + let screen_height = rl.get_screen_height(); + let offset_x = camera_x + screen_width/2; + let offset_y = camera_y + screen_height/2; + + if rl.is_key_pressed(KeyboardKey::KEY_TAB) { + show_options = !show_options + } + if !show_options { + if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { + running = !running; + } + if running { + if timer == 0 { + game.tick(); + timer = 60 / update_rate; + } + timer -= 1; + } + if rl.is_key_pressed(KeyboardKey::KEY_SPACE) && !running { + game.tick(); + } + + let mx = rl.get_mouse_x(); + let my = rl.get_mouse_y(); + let (tile_x, tile_y) = to_tile_space(mx, my, offset_x, offset_y, tile_size); + if rl.is_mouse_button_down(MouseButton::MOUSE_LEFT_BUTTON) { + let (last_tile_x, last_tile_y) = to_tile_space(last_mouse.0, last_mouse.1, offset_x, offset_y, tile_size); + if !(tile_x == last_tile_x && tile_y == last_tile_y) { + game.toggle_tile((tile_x, tile_y)); + running = false; + } + } + if rl.is_mouse_button_pressed(MouseButton::MOUSE_LEFT_BUTTON) { + game.toggle_tile((tile_x, tile_y)); + running = false; + } + if rl.is_mouse_button_down(MouseButton::MOUSE_MIDDLE_BUTTON) { + let dx = rl.get_mouse_x() - last_mouse.0; + let dy = rl.get_mouse_y() - last_mouse.1; + camera_x += dx; + camera_y += dy; + } + last_mouse = (mx, my); + } + + let mut d = rl.begin_drawing(&thread); + d.clear_background(dead_color); + for tile_dy in 0..screen_height/tile_size { + for tile_dx in 0..screen_width/tile_size { + let tile_x = tile_dx - offset_x / tile_size; + let tile_y = tile_dy - offset_y / tile_size; + if game.get_tile((tile_x, tile_y)) { + let x = tile_dx * tile_size + (offset_x % tile_size); + let y = tile_dy * tile_size + (offset_y % tile_size); + d.draw_rectangle(x, y, tile_size, tile_size, alive_color); + } + } + } + + for y in (0..screen_height+tile_size).step_by(tile_size as usize) { + let y = y + offset_y % tile_size; + d.draw_line_ex(rvec2(0.0, y), rvec2(screen_width, y), edge_thickness, edge_color); + } + for x in (0..screen_width+tile_size).step_by(tile_size as usize) { + let x = x + offset_x % tile_size; + d.draw_line_ex(rvec2(x, 0.0), rvec2(x, screen_height), edge_thickness, edge_color); + } + + if show_options { + d.draw_rectangle(0, 0, screen_width, screen_height, overlay_color); + if d.gui_window_box(rrect(10., 10., 300., 220.), Some(&CString::new("Options").unwrap())) { + show_options = false; + } + + draw_label(&mut d, 15., 45., "Tab - Toggle options"); + draw_label(&mut d, 15., 60., "Enter - Toggle simulation"); + draw_label(&mut d, 15., 75., "Space - Step simulation"); + draw_label(&mut d, 15., 90., "Left mouse click - Toggle cell"); + draw_label(&mut d, 15., 105., "Middle mouse drag - Move camera"); + + draw_label(&mut d, 15., 132.5, "Simulation speed"); + update_rate = d.gui_slider(rrect(150., 125., 80., 15.), Some(&CString::new("1 FPS").unwrap()), Some(&CString::new("30 FPS").unwrap()), update_rate as f32, 1., 30.) as i32; + + draw_label(&mut d, 15., 160., "\"Dead\" color"); + dead_color = d.gui_color_picker(rrect(20., 170., 40., 40.), dead_color); + + draw_label(&mut d, 110., 160., "\"Alive\" color"); + alive_color = d.gui_color_picker(rrect(115., 170., 40., 40.), alive_color); + + draw_label(&mut d, 205., 160., "Edge color"); + edge_color = d.gui_color_picker(rrect(210., 170., 40., 40.), edge_color); + } + } +}