diff --git a/prototypes/assets/life/forward.png b/prototypes/assets/life/forward.png new file mode 100644 index 0000000..053cff6 Binary files /dev/null and b/prototypes/assets/life/forward.png differ diff --git a/prototypes/assets/life/next.png b/prototypes/assets/life/next.png new file mode 100644 index 0000000..7e5c775 Binary files /dev/null and b/prototypes/assets/life/next.png differ diff --git a/prototypes/assets/life/pause.png b/prototypes/assets/life/pause.png new file mode 100644 index 0000000..fec7b5b Binary files /dev/null and b/prototypes/assets/life/pause.png differ diff --git a/prototypes/assets/life/power.png b/prototypes/assets/life/power.png new file mode 100644 index 0000000..01e3148 Binary files /dev/null and b/prototypes/assets/life/power.png differ diff --git a/prototypes/assets/life/texture_08.png b/prototypes/assets/life/texture_08.png new file mode 100644 index 0000000..72e1a07 Binary files /dev/null and b/prototypes/assets/life/texture_08.png differ diff --git a/prototypes/assets/life/tileBlack_01.png b/prototypes/assets/life/tileBlack_01.png new file mode 100644 index 0000000..a022b62 Binary files /dev/null and b/prototypes/assets/life/tileBlack_01.png differ diff --git a/prototypes/assets/life/tileGrey_01.png b/prototypes/assets/life/tileGrey_01.png new file mode 100644 index 0000000..79eeb14 Binary files /dev/null and b/prototypes/assets/life/tileGrey_01.png differ diff --git a/prototypes/assets/life/zoomIn.png b/prototypes/assets/life/zoomIn.png new file mode 100644 index 0000000..3e30bbd Binary files /dev/null and b/prototypes/assets/life/zoomIn.png differ diff --git a/prototypes/assets/life/zoomOut.png b/prototypes/assets/life/zoomOut.png new file mode 100644 index 0000000..bf55c7e Binary files /dev/null and b/prototypes/assets/life/zoomOut.png differ diff --git a/prototypes/src/bin/life.rs b/prototypes/src/bin/life.rs new file mode 100644 index 0000000..1c19993 --- /dev/null +++ b/prototypes/src/bin/life.rs @@ -0,0 +1,384 @@ +// TODO: Rearchitected so it's Cell and Coordiantes not Cell { position { coordiantes } } + +use engine::*; + +const GREY_TILE: &str = "tileGrey_01.png"; +const BLACK_TILE: &str = "tileBlack_01.png"; +const BACKGROUND: &str = "texture_08.png"; + +fn main() { + let file_path = { + #[cfg(not(target_arch = "wasm32"))] + { + "assets/life".into() + } + #[cfg(target_arch = "wasm32")] + { + "assets".into() + } + }; + App::new() + .add_plugins(DefaultPlugins.set(AssetPlugin { + file_path, + ..default() + })) + .init_resource::() + .init_state::() + .add_systems(Startup, setup) + .add_systems( + Update, + update_grid_position.run_if(on_message::), + ) + .add_systems(Update, grid_gizmo) + .add_systems( + Update, + de_spawn_cells.run_if(input_just_pressed(MouseButton::Left)), + ) + .add_systems( + Update, + assert_cell_uniqueness.run_if(any_with_component::), + ) + .add_systems( + Update, + simulation_step.run_if(input_just_pressed(KeyCode::Space)), + ) + .add_observer(on_add_ui_button) + .add_observer(on_add_coordinates) + .run(); +} + +const SCALE: f32 = 100.0; + +#[derive(Debug, Component)] +#[require(Visibility, Transform)] +enum UiButton { + Power, + Pause, + Step, + Play, + ZoomOut, + ZoomIn, +} + +impl UiButton { + fn icon(&self) -> &'static str { + match self { + Self::Power => "power.png", + Self::Pause => "pause.png", + Self::Step => "next.png", + Self::Play => "forward.png", + Self::ZoomOut => "zoomOut.png", + Self::ZoomIn => "zoomIn.png", + } + } + +} + +fn on_add_ui_button( + trigger: On, + mut commands: Commands, + query: Query<&UiButton>, + server: Res, +) { + let button = query.get(trigger.entity).unwrap(); + commands + .entity(trigger.entity) + .insert(( + Button, + BackgroundColor(GRAY.into()), + ImageNode { + image: server.load(GREY_TILE), + color: WHITE.with_alpha(0.5).into(), + ..default() + }, + children![( + Node { + top: Val::Px(0.0), + left: Val::Px(0.0), + height: Val::Px(40.0), + ..default() + }, + ImageNode { + image: server.load(button.icon()), + ..default() + }, + )], + )) + .observe(responsive_button_over) + .observe(responsive_button_out); +} + +fn setup(mut commands: Commands) { + commands.spawn(( + Camera2d, + Camera { + clear_color: ClearColorConfig::Custom(WHITE.into()), + ..default() + }, + )); + + commands + .spawn((Node { + top: Val::Px(0.0), + left: Val::Px(0.0), + height: Val::Px(40.0), + width: Val::Percent(100.0), + ..default() + }, Transform::default())) + .with_children(|parent| { + parent.spawn(UiButton::Power); + parent.spawn(UiButton::Pause); + parent.spawn(UiButton::Step); + parent.spawn(UiButton::Play); + parent.spawn(UiButton::ZoomOut); + parent.spawn(UiButton::ZoomIn); + }); + + // Spawn Coordiantes + commands + .spawn((Transform::default(), Visibility::Visible)) + .with_children(|parent| { + for x in -10..10 { + for y in -10..10 { + parent.spawn(Coordinates { x, y }); + } + } + }); +} + +#[derive(Debug, Default, Clone, PartialEq, Hash, Eq, Component, Resource)] +#[require(Visibility, Transform)] +struct Coordinates { + x: isize, + y: isize, +} + +impl Coordinates { + fn neighbors(&self) -> impl Iterator { + [ + Coordinates { + x: self.x, + y: self.y + 1, + }, + Coordinates { + x: self.x + 1, + y: self.y + 1, + }, + Coordinates { + x: self.x + 1, + y: self.y, + }, + Coordinates { + x: self.x + 1, + y: self.y - 1, + }, + Coordinates { + x: self.x, + y: self.y - 1, + }, + Coordinates { + x: self.x - 1, + y: self.y - 1, + }, + Coordinates { + x: self.x - 1, + y: self.y, + }, + Coordinates { + x: self.x - 1, + y: self.y + 1, + }, + ] + .into_iter() + } + + fn is_neighbor_of(&self, other: &Coordinates) -> bool { + let (a1, b1) = (self.x, self.y); + let (a2, b2) = (other.x, other.y); + (a1 == a2 + 1 || a1 == a2 - 1) && (b1 == b2 + 1 || b1 == b2 - 1) + } + + fn as_translation(&self) -> Vec3 { + (Vec2::new(self.x as f32, self.y as f32) * SCALE).extend(0.0) + } +} + +fn on_add_coordinates(trigger: On, coordinates: Query<&Coordinates>, + cells: Query<&Cell>, + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + server: ResMut, +) { + // TODO: Reuse the same mesh + let mesh = { + let mesh_h = meshes.add(Rectangle::new(SCALE, SCALE)); + Mesh2d(mesh_h) + }; + + // TODO: Resuse the same material + let mat = { + let image: Handle = if cells.contains(trigger.entity) { + server.load(BLACK_TILE) + } else { + server.load(BACKGROUND) + }; + let mat_h = materials.add(image); + MeshMaterial2d(mat_h) + }; + + let t = { + let c = coordinates.get(trigger.entity).unwrap(); + if cells.contains(trigger.entity) { + Transform::from_translation(c.as_translation() + Vec3::new(0.0, 0.0, 1.0)) + } else { + Transform::from_translation(c.as_translation()) + } + }; + + commands.entity(trigger.entity).insert((mesh, mat, t)); +} + +/// When the cursor moves, update the Coordiantes +fn update_grid_position( + mut coordinates: ResMut, + q_window: Single<&Window>, + q_camera: Single<(&Camera, &GlobalTransform)>, +) { + if let Some(cursor_pos) = q_window.cursor_position() { + let (cam, cam_gt) = *q_camera; + if let Ok(pos) = cam.viewport_to_world_2d(cam_gt, cursor_pos) { + coordinates.x = (pos / SCALE).round().x as isize; + coordinates.y = (pos / SCALE).round().y as isize; + } + } +} + +/// Draw a gizmo at the current coordiantes +fn grid_gizmo(mut gizmos: Gizmos, coordinates: Res) { + let isometry = Isometry2d { + translation: coordinates.as_translation().truncate(), + ..default() + }; + gizmos.rect_2d(isometry, Vec2::ONE * SCALE, PURPLE) +} + +#[derive(Component, Debug, PartialEq, Hash, Eq)] +struct Cell; + +// When left mouse clicked, spawn a cell at GridPosition +fn de_spawn_cells( + mut commands: Commands, + coordinates: Res, + cells: Query<(Entity, &Coordinates), With>, +) { + if let Some(e) = cells + .iter() + .find_map(|(e, c)| (*coordinates == *c).then_some(e)) + { + commands.entity(e).despawn(); + } else { + commands.spawn((coordinates.clone(), Cell)); + } +} + +fn assert_cell_uniqueness(q: Query<(Entity, &Coordinates), With>) { + q.iter().for_each(|(e1, c1)| { + q.iter().for_each(|(e2, c2)| { + if e1 != e2 && c1 == c2 { + panic!("Duplicate cells at {:?}", c1); + } + }) + }) +} + +#[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone)] +enum SimulationState { + #[default] + Off, + Step, + Run, +} + +/// Simulate the Game of Life Rules: +/// 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation. +/// 2. Any live cell with two or three live neighbours lives on to the next generation. +/// 3. Any live cell with more than three live neighbours dies, as if by overpopulation. +/// 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. +/// +/// TODO: There is a lot of optimizations to be had here +fn simulation_step(q: Query<(Entity, &Coordinates), With>, mut commands: Commands) { + fn is_cell_alive(coordinates: &Coordinates, query: &Query<(Entity, &Coordinates), With>) -> bool { + query + .iter() + .any(|(_e, c)| *c == *coordinates) + } + + fn living_neighbors(c: &Coordinates, query: &Query<(Entity, &Coordinates), With>) -> usize { + c.neighbors() + .filter(|neighbor| { + query.iter().any( + |(_e, coordinates)| { *neighbor == *coordinates }, + ) + }) + .count() + } + + fn cell_entity(coordinates: &Coordinates, query: Query<(Entity, &Coordinates), With>) -> Option { + query + .iter() + .find_map(|(e, c)| (*c == *coordinates).then_some(e)) + } + + // Collect list of candidate positions... + let mut coordinates: HashSet = HashSet::new(); + + // Put all dead cells neighboring living cells into the coordinates hashset + // Each living cell + q.iter().for_each(|(_e, c)| { + coordinates.insert(c.clone()); + // Iterator over neighbor positions + // Filter to just dead neighbors + c.neighbors() + .filter(|c| !is_cell_alive(c, &q)) + .for_each(|c| { + coordinates.insert(c); + }); + }); + + // Get Neighbor Count of living cells + coordinates.iter().for_each(|c| { + let num_living_neighbors = living_neighbors(c, &q); + let is_living = is_cell_alive(c, &q); + + match num_living_neighbors { + ..2 | 4.. => { + if let Some(e) = cell_entity(c, q) { + debug!("killing {:?}", c); + commands.entity(e).despawn(); + } else { + debug!("failed to kill {:?}", c); + } + } + 2 => debug!("continue"), + 3 => { + if !is_living { + debug!("creating {:?}", c); + commands.spawn((c.clone(), Cell)); + } else { + debug!("continue") + } + } + }; + }); +} + +fn responsive_button_over(trigger: On>, mut bg: Query<&mut BackgroundColor>) { + let mut background_color = bg.get_mut(trigger.entity).unwrap(); + background_color.0 = WHITE.into(); +} + +fn responsive_button_out(trigger: On>, mut bg: Query<&mut BackgroundColor>) { + let mut background_color = bg.get_mut(trigger.entity).unwrap(); + background_color.0 = GRAY.into(); +}