use launchy::mk2;
use launchy::{InputDevice as _, OutputDevice as _};
use rodio::Source as _;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
type SoundEffect = rodio::source::Buffered<Box<dyn rodio::Source<Item = f32> + Send>>;
struct Mutex<T>(std::sync::Mutex<T>);
impl<T> Mutex<T> {
pub fn lock(&self) -> impl std::ops::DerefMut<Target = T> + '_ {
self.0.lock().unwrap()
}
pub fn new(value: T) -> Self {
Self(std::sync::Mutex::new(value))
}
}
struct Samples {
start: SoundEffect,
click: SoundEffect,
lose: SoundEffect,
win: SoundEffect,
}
struct State {
colors: [mk2::RgbColor; 9],
mines: Vec<(u8, u8)>,
uncovered: Vec<(u8, u8)>,
flagged: HashSet<(u8, u8)>,
currently_pressed: HashMap<(u8, u8), std::time::Instant>,
output: mk2::Output,
audio: rodio::OutputStreamHandle,
samples: Samples,
game_won: bool,
}
fn uncover(state: &mut State, x: u8, y: u8) -> Result<(), launchy::MidiError> {
if state.uncovered.contains(&(x, y)) {
return Ok(());
}
let mut num_neighbor_mines = 0;
for neighbor_x in x.saturating_sub(1)..=(x + 1).min(7) {
for neighbor_y in y.saturating_sub(1)..=(y + 1).min(7) {
if (x, y) == (neighbor_x, neighbor_y) {
continue;
}
if state.mines.contains(&(neighbor_x, neighbor_y)) {
num_neighbor_mines += 1;
}
}
}
state.output.light_rgb(
mk2::Button::GridButton { x, y },
state.colors[num_neighbor_mines],
)?;
state.uncovered.push((x, y));
std::thread::sleep(std::time::Duration::from_millis(50));
if num_neighbor_mines == 0 {
for neighbor_x in x.saturating_sub(1)..=(x + 1).min(7) {
for neighbor_y in y.saturating_sub(1)..=(y + 1).min(7) {
if (x, y) == (neighbor_x, neighbor_y) {
continue;
}
uncover(state, neighbor_x, neighbor_y)?;
}
}
}
Ok(())
}
fn generate_mines(n: usize) -> Vec<(u8, u8)> {
use nanorand::Rng as _;
let mut rng = nanorand::tls_rng();
let mut mines = vec![];
while mines.len() < n {
let (x, y) = (rng.generate_range(0..=7), rng.generate_range(0..=7));
if !mines.contains(&(x, y)) {
mines.push((x, y));
}
}
mines
}
fn handle(state: &Arc<Mutex<State>>, msg: &mk2::Message) -> Result<(), Box<dyn std::error::Error>> {
if state.lock().game_won {
return Ok(());
}
match *msg {
mk2::Message::Press {
button: button @ mk2::Button::GridButton { x, y },
} => {
let press_time = std::time::Instant::now();
state.lock().currently_pressed.insert((x, y), press_time);
let state = state.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(400));
let mut state = state.lock();
if state.currently_pressed.remove(&(x, y)) == Some(press_time) {
if state.flagged.insert((x, y)) {
state
.output
.light_rgb(button, mk2::RgbColor::new(10, 0, 0))?;
} else {
state.flagged.remove(&(x, y));
state.output.light(button, mk2::PaletteColor::BLACK)?;
}
}
Ok::<(), launchy::MidiError>(())
});
}
mk2::Message::Release {
button: mk2::Button::GridButton { x, y },
} => {
let mut state = state.lock();
let state = &mut *state;
if state.currently_pressed.remove(&(x, y)).is_none() {
return Ok(());
}
if state.mines.contains(&(x, y)) {
state.audio.play_raw(state.samples.lose.clone())?;
for &(mine_x, mine_y) in &state.mines {
state.output.pulse(
mk2::Button::GridButton {
x: mine_x,
y: mine_y,
},
mk2::PaletteColor::RED,
)?;
}
return Ok(());
}
state.audio.play_raw(state.samples.click.clone())?;
uncover(state, x, y)?;
if state.uncovered.len() == 64 - state.mines.len() {
state.game_won = true;
state.audio.play_raw(state.samples.win.clone())?;
}
}
_ => {}
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn load_sound(path: &str) -> Result<SoundEffect, Box<dyn std::error::Error>> {
Ok((Box::new(
rodio::Decoder::new(std::io::BufReader::new(std::fs::File::open(path)?))?
.convert_samples(),
) as Box<dyn rodio::Source<Item = f32> + Send>)
.buffered())
}
let samples = Samples {
start: load_sound("examples/minesweeper/sound/start.wav")?,
click: load_sound("examples/minesweeper/sound/click.wav")?,
lose: load_sound("examples/minesweeper/sound/lose.wav")?,
win: load_sound("examples/minesweeper/sound/win.wav")?,
};
let output = mk2::Output::guess()?;
let (_stream, stream_handle) = rodio::OutputStream::try_default()?;
let mut state = State {
colors: [
(0, 3, 0),
(4, 15, 0),
(25, 50, 10),
(60, 50, 5),
(60, 30, 18),
(60, 10, 30),
(50, 0, 50),
(20, 0, 60),
(0, 0, 60),
]
.map(|(r, g, b)| mk2::RgbColor::new(r, g, b)),
mines: generate_mines(10),
uncovered: vec![],
flagged: HashSet::new(),
currently_pressed: HashMap::new(),
output,
audio: stream_handle,
samples,
game_won: false,
};
for (i, &color) in state.colors[1..].iter().enumerate() {
state
.output
.light_rgb(mk2::Button::GridButton { x: 8, y: i as _ }, color)?;
}
state.audio.play_raw(state.samples.start.clone())?;
let state = Arc::new(Mutex::new(state));
let _input = mk2::Input::guess(move |msg| {
if let Err(e) = handle(&state, &msg) {
println!("Error while handling event {:?}: {}", msg, e);
}
})?;
let _ = std::io::stdin().read_line(&mut String::new());
Ok(())
}