#![allow(clippy::type_complexity)] #![allow(clippy::too_many_arguments)] mod utils; use bevy::window::{ClosingWindow, WindowResolution}; use bevy::{prelude::*, window::WindowCloseRequested}; use helpers::grid::{Flag, GridPlugin, Revealed, Tile, TileClickEvent, TileOffset, TileType}; use serde::{Deserialize, Serialize}; mod helpers; #[derive(Deref, Resource)] pub struct PlayerAlive(bool); #[derive(Deref, Resource, Reflect, Serialize, Deserialize)] #[reflect(Resource)] pub struct Score(usize); impl FromWorld for PlayerAlive { fn from_world(world: &mut World) -> Self { let alive = world.resource::(); Self(**alive) } } #[derive(Deref, Resource)] pub struct FontHandle(Handle); impl FromWorld for FontHandle { fn from_world(world: &mut World) -> Self { let asset_server = world.resource::(); Self(asset_server.load("fonts/FiraSans-Bold.ttf")) } } fn setup_camera(mut commands: Commands) { let translation = Vec3 { x: -(1280.0 / 2.0), y: -(720.0 / 2.0), z: 1.0, }; let offset_x = (translation.x / 16.0) as isize; let offset_y = (translation.y / 16.0) as isize; commands.spawn(( Camera2dBundle { projection: OrthographicProjection { far: 1000.0, near: -1000.0, scaling_mode: bevy::render::camera::ScalingMode::Fixed { width: 1280.0, height: 720.0, }, scale: 0.75, ..Default::default() }, transform: Transform { translation: (translation % 16.0), ..default() }, ..default() }, TileOffset { x: offset_x, y: offset_y, translation, }, )); } fn tile_clicked( mut commands: Commands, mut tiles: Query<(Entity, &Tile, &mut TileType)>, mut tile_flag: Query<&mut Flag>, mut tile_revealed: Query<&mut Revealed>, mut ev_click: EventReader, mut score: ResMut, mut alive: ResMut, ) { if !**alive { return; } for ev in ev_click.read() { if reveal( &mut commands, ev.x, ev.y, ev.button, &mut tiles, &mut tile_flag, &mut tile_revealed, &mut score, 10, ) && ev.button == MouseButton::Left { *alive = PlayerAlive(false); println!("Score: {}", **score); } } } fn reveal( commands: &mut Commands, x: isize, y: isize, button: MouseButton, tiles: &mut Query<(Entity, &Tile, &mut TileType)>, tile_flag: &mut Query<&mut Flag>, tile_revealed: &mut Query<&mut Revealed>, score: &mut ResMut, n: isize, ) -> bool { use std::collections::{HashSet, VecDeque}; let mut visited = HashSet::new(); let mut queue = VecDeque::new(); queue.push_back((x, y)); while let Some((cx, cy)) = queue.pop_front() { if (cx - x).abs() > n || (cy - y).abs() > n { continue; } if visited.contains(&(cx, cy)) { continue; } visited.insert((cx, cy)); let result = get_tile(cx, cy); let mut found = false; for (ent, tile, mut kind) in tiles.iter_mut() { if tile.x == cx && tile.y == cy { // tile found *kind = result; if button == MouseButton::Right && tile_revealed.get(ent).is_err() { if tile_flag.get(ent).is_ok() { if *kind == TileType::Mine { **score = Score(***score - 1); } commands.entity(ent).remove::(); } else { if *kind == TileType::Mine { **score = Score(***score + 1); } commands.entity(ent).insert(Flag); } } else if tile_flag.get(ent).is_err() { commands.entity(ent).insert(Revealed); } found = true; break; } } if !found { let tile_pos = Tile { x: cx, y: cy }; let mut ent = commands.spawn(tile_pos); ent.insert(result); if button == MouseButton::Left { ent.insert(Revealed); } if button == MouseButton::Right { if result == TileType::Mine { **score = Score(***score + 1); } ent.insert(Flag); } if result == TileType::Empty && button == MouseButton::Left { for i in cx - 1..=cx + 1 { for j in cy - 1..=cy + 1 { if i != cx || j != cy { queue.push_back((i, j)); } } } } } if result == TileType::Mine { return true; } } false } fn is_mine(x: isize, y: isize) -> bool { let value = get_mine_value(x, y); let x = x as f64; let y = y as f64; let distance = (x.powf(2.0) + y.powf(2.0)).sqrt(); value > (1.0 - (distance / 100.0)).min(0.7) } const SEED: u32 = 65536; fn get_mine_value(x: isize, y: isize) -> f64 { use noise::{NoiseFn, Perlin}; let x = x as f64; let y = y as f64; const SCALE: f64 = 2.0; let result2 = Perlin::new(SEED).get([x / (SCALE * 2.0), y / (SCALE * 2.0), 3.8]); let result1 = Perlin::new(SEED + 1).get([x / (SCALE / 2.0), y / (SCALE / 2.0), 1.5]); let result1 = 1.0 - result1.powf(2.5); let p = 4.0; result1 * (1.0 - (((1.0 - result2) * 2.0).powf(p)) / 2.0) } fn get_tile(x: isize, y: isize) -> TileType { if is_mine(x, y) { return TileType::Mine; } let mut surrounding = 0; for i in x - 1..x + 2 { for j in y - 1..y + 2 { if is_mine(i, j) { surrounding += 1; } } } surrounding.into() } fn load_game(mut commands: Commands, asset_server: Res) { // "Spawning" a scene bundle creates a new entity and spawns new instances // of the given scene's entities as children of that entity. commands.spawn(DynamicSceneBundle { // Scenes are loaded just like any other asset. scene: asset_server.load("save.sav"), ..default() }); } fn save_on_exit( world: &World, type_registry: Res, mut close_request: EventReader, ) { for _ in close_request.read() { println!("Saving"); let type_registry = type_registry.read(); let dscene = DynamicSceneBuilder::from_world(world) .deny_all() .deny_all_resources() .allow::() .allow::() .allow::() .allow::() .allow::() .allow::() .allow::() .allow_resource::() .extract_entities(world.iter_entities().filter_map(|entity| { world.entity(entity.id()).get::()?; Some(entity.id()) })) // .extract_entities(world.iter_entities().filter_map(|entity| { // world.entity(entity.id()).get::()?; // Some(entity.id()) // })) .extract_resources() .build(); let serialized = match dscene.serialize(&type_registry) { Ok(s) => s, Err(e) => { eprintln!("Could not serialize scene: {:?}", e); return; } }; if let Err(e) = std::fs::write("./assets/save.sav", serialized) { eprintln!("Unable to write save file: {:?}", e); } } } fn main() { // use imageproc::drawing::Canvas; // // prepare an image // let width = 2048; // let height = 2048; // let mut canvas = image::DynamicImage::new(width, height, image::ColorType::Rgb8); // for x in 0..width { // for y in 0..height { // // let c = (get_mine_value( // // x as isize - (width as isize / 2_isize), // // y as isize - (height as isize / 2_isize), // // ) * 255.0) as u8; // // let color = [c, c, c, 1]; // let color = if is_mine( // x as isize - (width as isize / 2_isize), // y as isize - (height as isize / 2_isize), // ) { // [255, 255, 255, 1] // } else { // [0, 0, 0, 1] // }; // canvas.draw_pixel( // x, // (-(y as isize) + (height - 1) as isize) as u32, // color.into(), // ); // } // } // if let Err(e) = canvas.save("rawr.png") { // eprintln!("Error saving image: {:?}", e); // } App::new() .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: String::from("Infinite Minesweeper"), resolution: WindowResolution::new(1280.0, 720.0), ..Default::default() }), exit_condition: bevy::window::ExitCondition::OnAllClosed, close_when_requested: true, }), // bevy_inspector_egui::quick::WorldInspectorPlugin::new(), )) .insert_resource(PlayerAlive(true)) .register_type::() .register_type::() .insert_resource(Score(0)) .init_resource::() .add_plugins(GridPlugin) .add_systems(Startup, (load_game, setup_camera)) .add_systems(Update, (tile_clicked, save_on_exit)) .run(); }