356 lines
10 KiB
Rust
356 lines
10 KiB
Rust
#![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::<PlayerAlive>();
|
|
Self(**alive)
|
|
}
|
|
}
|
|
|
|
#[derive(Deref, Resource)]
|
|
pub struct FontHandle(Handle<Font>);
|
|
|
|
impl FromWorld for FontHandle {
|
|
fn from_world(world: &mut World) -> Self {
|
|
let asset_server = world.resource::<AssetServer>();
|
|
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<TileClickEvent>,
|
|
mut score: ResMut<Score>,
|
|
mut alive: ResMut<PlayerAlive>,
|
|
) {
|
|
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<Score>,
|
|
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::<Flag>();
|
|
} 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<AssetServer>) {
|
|
// "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<AppTypeRegistry>,
|
|
mut close_request: EventReader<WindowCloseRequested>,
|
|
) {
|
|
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::<Tile>()
|
|
.allow::<Flag>()
|
|
.allow::<Revealed>()
|
|
.allow::<TileType>()
|
|
.allow::<Transform>()
|
|
.allow::<TileOffset>()
|
|
.allow::<OrthographicProjection>()
|
|
.allow_resource::<Score>()
|
|
.extract_entities(world.iter_entities().filter_map(|entity| {
|
|
world.entity(entity.id()).get::<Tile>()?;
|
|
Some(entity.id())
|
|
}))
|
|
// .extract_entities(world.iter_entities().filter_map(|entity| {
|
|
// world.entity(entity.id()).get::<TileOffset>()?;
|
|
// 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::<Score>()
|
|
.register_type::<TileOffset>()
|
|
.insert_resource(Score(0))
|
|
.init_resource::<FontHandle>()
|
|
.add_plugins(GridPlugin)
|
|
.add_systems(Startup, (load_game, setup_camera))
|
|
.add_systems(Update, (tile_clicked, save_on_exit))
|
|
.run();
|
|
}
|