[no description]
bendn 4 weeks ago
commit 48ee653
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml22
-rw-r--r--assets/tiles.pngbin0 -> 88859 bytes
-rw-r--r--src/main.rs239
-rw-r--r--src/picking.rs106
5 files changed, 369 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96ef6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..f2205dd
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "bevinator"
+version = "0.1.0"
+edition = "2024"
+
+
+[dependencies]
+aoc = { version = "0.1.0", path = "../aoc" }
+bevy = { version = "0.19.0", features = ["dynamic_linking"] }
+bevy-inspector-egui = "0.37.0"
+bevy_ecs_tilemap = { git = "https://github.com/taboky-dev/bevy_ecs_tilemap", branch = "bevy-0.19" }
+rand = "0.10.1"
+
+[profile.dev]
+opt-level = 1
+overflow-checks= false
+
+[profile.dev.package."*"]
+opt-level = 3
+
+[patch.crates-io]
+rustc-hash = { git = "https://github.com/rust-lang/rustc-hash.git", rev= "1a998d5b89b04ba730d4cd249f811e8b48aa7d8c" }
diff --git a/assets/tiles.png b/assets/tiles.png
new file mode 100644
index 0000000..49d88b5
--- /dev/null
+++ b/assets/tiles.png
Binary files differ
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..8644542
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,239 @@
+#![feature(try_blocks)]
+use std::time::Duration;
+
+use bevy::{
+ DefaultPlugins,
+ app::App,
+ camera::ScalingMode,
+ ecs::{event::Trigger, observer::ObservedBy, resource::IsResource},
+ input::{common_conditions::input_just_pressed, keyboard::KeyboardInput},
+ prelude::*,
+ window::{Window, WindowPlugin},
+};
+use bevy_ecs_tilemap::{helpers::square_grid::neighbors::Neighbors, prelude::*};
+use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
+use rand::{rng, seq::IteratorRandom};
+
+use crate::picking::TilemapPickingPlugin;
+
+const W: usize = 20;
+const H: usize = 10;
+const BOMB_COUNT: usize = 30;
+fn main() -> AppExit {
+ App::new()
+ .add_plugins(
+ DefaultPlugins
+ .set(WindowPlugin {
+ primary_window: Some(Window {
+ title: "Minesweeper!".into(),
+ ..default()
+ }),
+ ..default()
+ })
+ .set(ImagePlugin::default_linear()),
+ )
+ .insert_resource(Grid(ge()))
+ .add_plugins(TilemapPlugin)
+ // .add_plugins(EguiPlugin::default())
+ // .add_plugins(WorldInspectorPlugin::new())
+ .add_plugins(TilemapPickingPlugin)
+ .add_systems(Startup, startup)
+ // .add_systems(Update, movement)
+ .add_observer(|_: On<End>, q: Query<&mut TileTextureIndex, With<Bomb>>| {
+ for mut lem in q {
+ if lem.0 == 1 {
+ return;
+ }
+ if lem.0 != 12 {
+ lem.0 = 11;
+ }
+ }
+ })
+ .add_observer(
+ |_: On<End>, q: Query<&mut TileTextureIndex, (Without<Bomb>, With<Flag>)>| {
+ for mut lem in q {
+ lem.0 = 13;
+ }
+ },
+ )
+ .add_observer(
+ |_: On<End>, q: Query<Entity, (With<TilePos>, With<ObservedBy>)>, mut c: Commands| {
+ // println!("{q:?}");
+ for lem in q {
+ c.entity(lem).remove::<ObservedBy>();
+ }
+ },
+ )
+ .add_systems(Update, reload.run_if(input_just_pressed(KeyCode::KeyR)))
+ .run()
+}
+#[derive(Resource)]
+struct Grid([[u8; W]; W]);
+#[derive(EntityEvent, Debug, Clone, Reflect)]
+struct Zero(Entity);
+#[derive(Event, Debug, Clone, Reflect)]
+struct End;
+#[derive(Component)]
+struct Bomb;
+#[derive(Component)]
+struct Flag;
+#[derive(Component)]
+struct Murderable;
+
+macro_rules! all {
+ () => {
+ (0..W).flat_map(|x| (0..H).map(move |y| (x, y)))
+ };
+}
+
+fn reveal<E: EntityEvent>()
+-> impl Fn(On<E>, Query<(&TilePos, &mut TileTextureIndex)>, Res<Grid>, Query<&TileStorage>, Commands)
+{
+ move |ev, mut tile, ge, store, mut cmds| {
+ let Ok((&tp @ TilePos { x, y }, mut sp)) = tile.get_mut(ev.event_target()) else {
+ return;
+ };
+ *sp = TileTextureIndex(match ge.0[y as usize][x as usize] {
+ 0 if (sp.0 != 10 && sp.0 == 0) => {
+ let map_size = TilemapSize {
+ x: W as u32,
+ y: H as u32,
+ };
+ for lem in Neighbors::get_square_neighboring_positions(&tp, &map_size, false)
+ .entities(store.single().unwrap())
+ .iter()
+ {
+ cmds.trigger(Zero(*lem));
+ }
+ 10
+ }
+ n @ 1..=8 => 1 + n as u32,
+ _ => return,
+ });
+ }
+}
+fn clicked()
+-> impl Fn(On<Pointer<Press>>, Query<(&TilePos, &mut TileTextureIndex)>, Res<Grid>, Commands) {
+ move |ev, mut tile, ge, mut cmds| {
+ let Ok((&TilePos { x, y }, mut sp)) = tile.get_mut(ev.event_target()) else {
+ return;
+ };
+ let real = ge.0[y as usize][x as usize];
+ sp.0 = match ev.event().button {
+ PointerButton::Primary if sp.0 == 0 => match real {
+ 255 => {
+ cmds.trigger(End);
+ 12
+ }
+ 0 => {
+ cmds.trigger(Zero(ev.event_target()));
+ return;
+ }
+ n @ 1..=8 => 1 + n as u32,
+ _ => 10,
+ },
+ PointerButton::Secondary if sp.0 < 2 => {
+ if sp.0 == 0 {
+ cmds.entity(ev.event_target()).insert(Flag);
+ } else {
+ cmds.entity(ev.event_target()).remove::<Flag>();
+ }
+ (!(sp.0 != 0)) as u32
+ }
+ _ => return,
+ };
+ }
+}
+fn reload(
+ mut commands: Commands,
+ mut gr: ResMut<Grid>,
+ asset_server: Res<AssetServer>,
+ q: Query<Entity, With<Murderable>>,
+) {
+ gr.0 = ge();
+ for lem in q {
+ commands.entity(lem).despawn();
+ }
+ startup(commands, gr.into(), asset_server);
+}
+fn startup(mut commands: Commands, ge: Res<Grid>, asset_server: Res<AssetServer>) {
+ // commands.spawn(Camera2d);
+ commands.spawn((
+ Camera2d,
+ Projection::Orthographic(OrthographicProjection {
+ scaling_mode: ScalingMode::AutoMin {
+ min_width: (W as f32 + 4.) * 256.,
+ min_height: (H as f32 + 4.) * 256.,
+ },
+ ..OrthographicProjection::default_2d()
+ }),
+ Murderable,
+ ));
+ let texture_handle: Handle<Image> = asset_server.load("tiles.png");
+
+ let map_size = TilemapSize {
+ x: W as u32,
+ y: H as u32,
+ };
+
+ let mut tile_storage = TileStorage::empty(map_size);
+ let tilemap_entity = commands.spawn_empty().insert(Murderable).id();
+ let tilemap_id = TilemapId(tilemap_entity);
+
+ for (x, y) in all!() {
+ let position = TilePos {
+ x: x as u32,
+ y: y as u32,
+ };
+ let mut e = commands.spawn((
+ TileBundle {
+ texture_index: TileTextureIndex(0),
+ position,
+ tilemap_id,
+ ..default()
+ },
+ Murderable,
+ ));
+ if ge.0[y][x] == 255 {
+ e.insert(Bomb);
+ }
+ let tile_entity = e.observe(clicked()).observe(reveal::<Zero>()).id();
+ tile_storage.set(&position, tile_entity);
+ }
+
+ let tile_size = TilemapTileSize { x: 256.0, y: 256.0 };
+ let grid_size = tile_size.into();
+
+ commands.entity(tilemap_entity).insert((
+ TilemapBundle {
+ grid_size,
+ size: map_size,
+ storage: tile_storage,
+ texture: TilemapTexture::Single(texture_handle),
+ tile_size,
+ map_type: TilemapType::Square,
+ anchor: TilemapAnchor::Center,
+
+ ..Default::default()
+ },
+ Murderable,
+ ));
+}
+
+fn ge() -> [[u8; W]; W] {
+ let mut g = [[0u8; W]; W];
+
+ for (x, y) in all!().sample(&mut rng(), BOMB_COUNT) {
+ g[y][x] = 255;
+ let lems = aoc::util::nb((x, y));
+ lems.into_iter().for_each(|(x, y)| {
+ try {
+ let lem = g.get_mut(y)?.get_mut(x)?;
+ *lem = lem.saturating_add(1);
+ };
+ });
+ }
+
+ g
+}
+mod picking;
diff --git a/src/picking.rs b/src/picking.rs
new file mode 100644
index 0000000..282769c
--- /dev/null
+++ b/src/picking.rs
@@ -0,0 +1,106 @@
+use bevy::camera::RenderTarget;
+use bevy::picking::PickingSystems;
+use bevy::picking::backend::HitData;
+use bevy::picking::backend::PointerHits;
+use bevy::picking::pointer::PointerId;
+use bevy::picking::pointer::PointerLocation;
+use bevy::prelude::*;
+use bevy::window::PrimaryWindow;
+use bevy_ecs_tilemap::prelude::*;
+
+pub struct TilemapPickingPlugin;
+
+impl Plugin for TilemapPickingPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(PreUpdate, tile_picking.in_set(PickingSystems::Backend));
+ }
+}
+
+fn tile_picking(
+ pointers_q: Query<(&PointerId, &PointerLocation)>,
+ cameras_q: Query<(
+ Entity,
+ &Camera,
+ &RenderTarget,
+ &GlobalTransform,
+ &Projection,
+ )>,
+ primary_window_q: Query<Entity, With<PrimaryWindow>>,
+ tilemap_q: Query<(
+ &TilemapSize,
+ &TilemapGridSize,
+ &TilemapTileSize,
+ &TilemapAnchor,
+ &TilemapType,
+ &TileStorage,
+ &GlobalTransform,
+ &ViewVisibility,
+ )>,
+ mut output: MessageWriter<PointerHits>,
+) {
+ for (p_id, p_loc) in pointers_q
+ .iter()
+ .filter_map(|(p_id, p_loc)| p_loc.location().map(|l| (p_id, l)))
+ {
+ let Some((cam_entity, camera, _cam_target, cam_transform, _cam_ortho)) = cameras_q
+ .iter()
+ .filter(|(_, camera, ..)| camera.is_active)
+ .find(|(_, _, cam_target, ..)| {
+ cam_target
+ .normalize(match primary_window_q.single() {
+ Ok(w) => Some(w),
+ Err(_) => return false,
+ })
+ .as_ref()
+ == Some(&p_loc.target)
+ })
+ else {
+ continue;
+ };
+
+ let Ok(cursor_pos_world) = camera.viewport_to_world_2d(cam_transform, p_loc.position)
+ else {
+ continue;
+ };
+
+ let picks = tilemap_q
+ .iter()
+ .filter(|(.., vis)| vis.get())
+ .filter_map(
+ |(
+ map_s,
+ map_grid_size,
+ map_tile_size,
+ map_anchor,
+ map_type,
+ map_store,
+ map_transform,
+ ..,
+ )| {
+ let in_map_pos = {
+ let pos = Vec4::from((cursor_pos_world, 0., 1.0));
+ let in_map_pos = map_transform.to_matrix().inverse() * pos;
+ in_map_pos.xy()
+ };
+
+ let tile_entity = TilePos::from_world_pos(
+ &in_map_pos,
+ map_s,
+ map_grid_size,
+ map_tile_size,
+ map_type,
+ map_anchor,
+ )
+ .and_then(|tile_pos| map_store.get(&tile_pos))?;
+
+ Some((
+ tile_entity,
+ HitData::new(cam_entity, -map_transform.translation().z, None, None),
+ ))
+ },
+ )
+ .collect::<Vec<_>>();
+
+ output.write(PointerHits::new(*p_id, picks, camera.order as f32));
+ }
+}