Files
term-minesweeper/src/main.rs

442 lines
14 KiB
Rust

use crossterm::{
event::{
poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton,
MouseEventKind,
},
terminal::{disable_raw_mode, enable_raw_mode},
ExecutableCommand,
};
use rand::prelude::*;
use std::io;
const BEGINNER_DIFFICULTY: Difficulty = Difficulty {
width: 8,
height: 8,
mine_count: 10,
};
const INTERMEDIATE_DIFFICULTY: Difficulty = Difficulty {
width: 16,
height: 16,
mine_count: 40,
};
const EXPERT_DIFFICULTY: Difficulty = Difficulty {
width: 30,
height: 16,
mine_count: 99,
};
const TEST_POS: [(i8, i8); 8] = [
(-1, 1),
(0, 1),
(1, 1),
(-1, 0),
(1, 0),
(-1, -1),
(0, -1),
(1, -1),
];
const MINE: &str = "\x1b[48;2;0;0;0m \x1b[0m";
const FLAG: &str = "\x1b[48;2;255;0;0m \x1b[0m";
const EMPTY: &str = "\x1b[48;2;255;255;255m \x1b[0m";
const HIDDEN: &str = "\x1b[48;2;100;100;100m \x1b[0m";
fn main() {
let difficulty = match args() {
Some(v) => v,
None => {
println!("Invalid or missing argument.");
print_usage();
exit();
}
};
let mut board = Board::new(&difficulty);
let mut stdout = io::stdout();
let mut rng = rand::thread_rng();
print!("\x1b[?25l");
board.render();
board.calculate(&difficulty);
if difficulty.mine_count != difficulty.width * difficulty.height {
loop {
let input = input(&mut stdout, &difficulty);
if input.button != MouseButton::Left
|| !validate_pos(input.pos.x as i8, input.pos.y as i8, &difficulty)
{
continue;
}
let cell = &mut board.0[input.pos.y][input.pos.x];
if cell.kind != CellKind::Mine {
handle_input(&input, &mut board, &difficulty);
break;
}
// Since mine placement is updated, recalculation is needed.
board.spawn_mine(
&mut rng,
0..difficulty.width * difficulty.height - difficulty.mine_count,
);
board.0[input.pos.y][input.pos.x].kind = CellKind::MineCount(0);
board.calculate(&difficulty);
handle_input(&input, &mut board, &difficulty);
break;
}
}
board.calculate(&difficulty);
loop {
board.render();
let input = input(&mut stdout, &difficulty);
handle_input(&input, &mut board, &difficulty);
}
}
struct Difficulty {
width: usize,
height: usize,
mine_count: usize,
}
struct Board(Vec<Vec<Cell>>);
#[derive(Clone, Copy, Debug)]
struct Cell {
kind: CellKind,
visibility: Visibility,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum CellKind {
Mine,
MineCount(u8),
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Visibility {
Visible,
Hidden,
Flag,
}
struct Input {
button: MouseButton,
pos: Position,
}
#[derive(Debug)]
struct Position {
x: usize,
y: usize,
}
impl Board {
fn new(difficulty: &Difficulty) -> Self {
let mut rng = rand::thread_rng();
let mut board = Board(vec![
vec![
Cell {
kind: CellKind::MineCount(0),
visibility: Visibility::Hidden,
};
difficulty.width
];
difficulty.height
]);
for i in 0..difficulty.mine_count {
board.spawn_mine(&mut rng, 0..difficulty.width * difficulty.height - i);
}
board
}
fn spawn_mine(&mut self, rng: &mut ThreadRng, range: std::ops::Range<usize>) {
let c = rng.gen_range(range);
let mut i = 0;
for line in self.0.iter_mut() {
for cell in line.iter_mut() {
if cell.kind != CellKind::Mine {
if i == c {
cell.kind = CellKind::Mine;
}
i += 1;
}
}
}
}
fn calculate(&mut self, difficulty: &Difficulty) {
let mut changes = vec![];
for (y, line) in self.0.iter().enumerate() {
for (x, cell) in line.iter().enumerate() {
if let CellKind::MineCount(_) = cell.kind {
let mut n = 0;
for (test_x, test_y) in TEST_POS {
if let Some(pos) =
test_new_pos(&Position { x, y }, (test_x, test_y), difficulty)
{
if self.0[pos.y][pos.x].kind == CellKind::Mine {
n += 1;
}
}
}
changes.push((Position { x, y }, n));
}
}
}
for (pos, n) in changes {
if let CellKind::MineCount(c) = &mut self.0[pos.y][pos.x].kind {
*c = n;
}
}
}
fn inside(&self, x: i8, y: i8, difficulty: &Difficulty) -> bool {
if validate_pos(x, y, difficulty) {
let cell = self.0[y as usize][x as usize];
return cell.visibility == Visibility::Hidden && cell.kind == CellKind::MineCount(0);
}
false
}
fn pad_fill(&mut self, difficulty: &Difficulty) {
for y in 0..difficulty.height {
for x in 0..difficulty.width {
let cell = self.0[y][x];
if cell.visibility == Visibility::Visible && cell.kind == CellKind::MineCount(0) {
for (test_x, test_y) in TEST_POS {
if let Some(pos) =
test_new_pos(&Position { x, y }, (test_x, test_y), difficulty)
{
if let CellKind::MineCount(_) = self.0[pos.y][pos.x].kind {
self.0[pos.y][pos.x].visibility = Visibility::Visible;
}
}
}
}
}
}
}
fn fill(&mut self, x: i8, y: i8, difficulty: &Difficulty) {
if !self.inside(x, y, difficulty) {
return;
}
let mut s = vec![];
s.push((x, x, y, 1));
s.push((x, x, y - 1, -1));
while let Some((mut x1, x2, y, dy)) = s.pop() {
let mut x = x1;
if self.inside(x, y, difficulty) {
while self.inside(x - 1, y, difficulty) {
self.0[y as usize][x as usize - 1].visibility = Visibility::Visible;
x -= 1;
}
if x < x1 {
s.push((x, x1 - 1, y - dy, -dy));
}
}
while x1 <= x2 {
while self.inside(x1, y, difficulty) {
self.0[y as usize][x1 as usize].visibility = Visibility::Visible;
x1 += 1;
}
if x1 > x {
s.push((x, x1 - 1, y + dy, dy));
}
if x1 - 1 > x2 {
s.push((x2 + 1, x1 - 1, y - dy, -dy));
}
x1 += 1;
while x1 < x2 && !self.inside(x1, y, difficulty) {
x1 += 1;
}
x = x1;
}
}
}
fn game_over(&mut self) {
self.0
.iter_mut()
.flatten()
.for_each(|c| c.visibility = Visibility::Visible);
self.render();
disable_raw_mode().unwrap();
println!("Game Over!");
exit();
}
fn win_condition(&mut self) {
for c in self.0.iter().flatten() {
if c.kind != CellKind::Mine && c.visibility == Visibility::Hidden {
return;
}
}
self.0
.iter_mut()
.flatten()
.for_each(|c| c.visibility = Visibility::Visible);
self.render();
disable_raw_mode().unwrap();
println!("Game Won!");
exit();
}
fn render(&self) {
let mut s = String::new();
s.push_str("\x1b[2J\x1b[H");
for line in self.0.iter().rev() {
for cell in line {
match cell.visibility {
Visibility::Visible => match cell.kind {
CellKind::Mine => s.push_str(MINE),
CellKind::MineCount(0) => s.push_str(EMPTY),
CellKind::MineCount(n) => s.push_str(&format!(
"\x1b[48;2;255;255;255m\x1b[38;2;0;0;0m{n} \x1b[0m"
)),
},
Visibility::Hidden => s.push_str(HIDDEN),
Visibility::Flag => s.push_str(FLAG),
}
}
s.push('\n');
}
disable_raw_mode().unwrap();
print!("{s}");
enable_raw_mode().unwrap();
}
}
fn input(stdout: &mut io::Stdout, difficulty: &Difficulty) -> Input {
stdout.execute(EnableMouseCapture).unwrap();
loop {
if poll(std::time::Duration::from_secs(5)).unwrap() {
while let Ok(r) = read() {
if let Event::Mouse(event) = r {
if let MouseEventKind::Down(button) = event.kind {
if button == MouseButton::Left || button == MouseButton::Right {
if difficulty
.height
.checked_sub(event.row as usize + 1)
.is_none()
{
continue;
}
let (y, x) = (
difficulty.height - event.row as usize - 1,
(event.column / 2) as usize,
);
stdout.execute(DisableMouseCapture).unwrap();
return Input {
button,
pos: Position { x, y },
};
}
}
} else if let Event::Key(k) = r {
if k.code == KeyCode::Char('q') {
exit();
}
}
}
}
}
}
fn handle_input(input: &Input, board: &mut Board, difficulty: &Difficulty) {
if validate_pos(input.pos.x as i8, input.pos.y as i8, difficulty) {
if input.button == MouseButton::Left {
board.fill(input.pos.x as i8, input.pos.y as i8, difficulty);
board.pad_fill(difficulty);
let cell = &mut board.0[input.pos.y][input.pos.x];
if cell.visibility == Visibility::Hidden {
cell.visibility = Visibility::Visible;
if cell.kind == CellKind::Mine {
board.game_over();
}
board.win_condition();
}
} else if input.button == MouseButton::Right {
let cell = &mut board.0[input.pos.y][input.pos.x];
if cell.visibility == Visibility::Hidden {
cell.visibility = Visibility::Flag;
} else if cell.visibility == Visibility::Flag {
cell.visibility = Visibility::Hidden;
}
}
}
}
fn test_new_pos(pos: &Position, (x, y): (i8, i8), difficulty: &Difficulty) -> Option<Position> {
let (p_x, p_y) = (pos.x as i8, pos.y as i8);
if validate_pos(p_x + x, p_y + y, difficulty) {
Some(Position {
x: (p_x + x) as usize,
y: (p_y + y) as usize,
})
} else {
None
}
}
fn validate_pos(x: i8, y: i8, difficulty: &Difficulty) -> bool {
x >= 0 && x < difficulty.width as i8 && y >= 0 && y < difficulty.height as i8
}
fn args() -> Option<Difficulty> {
let mut args = std::env::args().skip(1);
match &args.next()?[..] {
"-d" | "--difficulty" => match &args.next()?[..] {
"1" | "b" | "beginner" => Some(BEGINNER_DIFFICULTY),
"2" | "i" | "intermediate" => Some(INTERMEDIATE_DIFFICULTY),
"3" | "e" | "expert" => Some(EXPERT_DIFFICULTY),
_ => None,
},
"-c" | "--custom" => {
let (width, height, mine_count) = (
args.next()?.parse().ok()?,
args.next()?.parse().ok()?,
args.next()?.parse().ok()?,
);
Some(Difficulty {
width,
height,
mine_count,
})
}
_ => None,
}
}
fn print_usage() {
println!(
r#"
USAGE:
minesweeper OPTIONS VALUES
ARGUMENTS:
-d, --difficulty set the difficulty, <DIFFICULTY>, accepted values: [["1", "b", "beginner"], ["2", "i", "intermediate"], ["3", "e", "expert"]]
-c, --custom set custom size and mine count, <WIDTH> <HEIGHT> <MINE_COUNT>
DIFFICULTY LEVELS:
WIDTH HEIGHT MINE_COUNT
Beginner: 8 8 10
Intermediate: 16 16 40
Expert: 30 16 99
EXAMPLES:
minesweeper -d 1 minesweeper with the beginner difficulty preset
minesweeper -c 16 30 80 minsweeper with a width of 16, a height of 30 and mine count of 80
minesweeper -d expert minesweeper with the expert difficulty preset
minesweeper -d i minesweeper with the intermediate difficulty preset
REFERENCES:
https://en.wikipedia.org/wiki/Minesweeper_game#Objective_and_strategy"#
);
}
fn exit() -> ! {
print!("\x1b[?25h");
disable_raw_mode().unwrap();
std::process::exit(0)
}