Files
RustySweep/src/main.rs

636 lines
16 KiB
Rust

use rand::{self, Rng};
use std::io;
use std::str::FromStr;
use std::sync::RwLock;
static GLOBAL_STATE: RwLock<GameState> = RwLock::new(GameState::new());
#[derive(Clone, Copy)]
struct GameState {
picked_tile: bool,
game_ended: bool,
game_won: bool,
game_ending_tile: Option<(usize, usize)>,
}
impl GameState {
const fn new() -> Self {
return Self {
picked_tile: false,
game_ended: false,
game_won: false,
game_ending_tile: None,
};
}
fn restart(&mut self) {
*self = Self::new();
}
}
#[derive(Clone, Debug)]
struct Board {
rows: usize,
cols: usize,
mines_count: usize,
tiles: Vec<Tile>,
}
impl Board {
fn new(rows: usize, cols: usize, mines: usize) -> Self {
assert!((rows * cols) >= mines);
let mut tiles = Vec::with_capacity(rows * cols);
unsafe {
tiles.set_len(tiles.capacity());
};
tiles.fill(Tile::new());
Self {
rows,
cols,
mines_count: mines,
tiles,
}
}
// reveals mines and adjacent mines if the revealed mine is not surrounded by any mines
pub fn reveal(&mut self, x: usize, y: usize) {
let tile_idx = (y * self.cols) + x;
if !GLOBAL_STATE.read().unwrap().picked_tile {
if self.tiles[tile_idx].has_mine {
println!(
"mine_count: {}",
self.tiles.iter().filter(|tile| tile.has_mine).count()
);
// Todo: move mine
let (mut new_x, mut new_y) = self.get_new_pos();
while new_x == x && new_y == y {
(new_x, new_y) = self.get_new_pos();
}
let tile_a = self.tiles[(new_y * self.cols) + new_x];
let tile_b = self.tiles[tile_idx];
self.tiles[tile_idx] = tile_a;
self.tiles[(new_y * self.cols) + new_x] = tile_b;
return self.reveal(x, y);
}
GLOBAL_STATE.write().unwrap().picked_tile = true;
}
let tile = &mut self.tiles[tile_idx];
if tile.revealed {
return;
}
tile.reveal();
if GLOBAL_STATE.read().unwrap().game_ended {
GLOBAL_STATE.write().unwrap().game_ending_tile = Some((x, y));
return;
}
let neighbors_pos = self.get_neighboring_tiles(x, y);
// if any neighboring tiles have a mine, stop the recursive reveal
if neighbors_pos
.iter()
.filter(|(tile_x, tile_y)| {
let tile = &self.tiles[(tile_y * self.cols) + tile_x];
tile.has_mine
})
.count()
> 0
{
return;
}
for (tile_x, tile_y) in neighbors_pos {
self.reveal(tile_x, tile_y);
}
}
pub fn flag(&mut self, x: usize, y: usize) {
let tile_idx = (y * self.cols) + x;
let tile = &mut self.tiles[tile_idx];
tile.flag();
}
pub fn remaining_tiles(&self) -> usize {
return self
.tiles
.iter()
.filter(|tile| !tile.revealed && !tile.has_flag)
.count();
}
pub fn flagged_tiles(&self) -> usize {
return self.tiles.iter().filter(|tile| tile.has_flag).count();
}
// TODO: gave save us all from these horrible names
fn get_neighboring_tiles(&self, x: usize, y: usize) -> Vec<(usize, usize)> {
let mut neighbors: Vec<(usize, usize)> = Vec::new();
// dumb solution, yes? Do I care, not really.
let tl_x = x as isize - 1;
let tl_y = y as isize - 1;
for tile_x in 0..3 {
for tile_y in 0..3 {
let check_x = tl_x + tile_x;
let check_y = tl_y + tile_y;
if (check_x < 0 || check_y < 0)
|| (check_x > self.cols as isize - 1 || check_y > self.rows as isize - 1)
{
continue;
}
if check_x as usize == x && check_y as usize == y {
// Don't include outselfs in the check obv
continue;
}
neighbors.push((check_x as usize, check_y as usize));
}
}
neighbors
}
pub fn flag_all_remaining_tiles(&mut self) {
if self.remaining_tiles() + self.flagged_tiles() != self.mines_count {
panic!("Auto win, but there are more tiles than mines");
}
let mine_tiles = self
.tiles
.iter_mut()
.filter(|tile| !tile.has_flag && !tile.revealed)
.collect::<Vec<_>>();
for tile in mine_tiles {
tile.flag();
}
}
pub fn draw(&self) {
let mine_count = self.mines_count - self.tiles.iter().filter(|tile| tile.has_flag).count();
// use the max function because if the number is 0, the digits are 0
let digits = f64::max(f64::floor(f64::log10(mine_count as f64) + 1.0), 1.0) as usize;
println!("{}", "".repeat(self.cols));
println!(
"{string:<width$}{mine_count}",
width = (self.cols - (digits + 1)),
string = " ".repeat(self.cols - (digits + 1)),
mine_count = mine_count
);
println!("{}", "".repeat(self.cols));
for row in 0..self.rows {
for col in 0..self.cols {
if col == 0 {
print!("{row:<02}");
}
let tile = self.tiles[(row * self.cols) + col];
if GLOBAL_STATE.read().unwrap().game_ended && tile.has_mine {
// Highlight the game ending mine in red, and the flagged mines in green
print!(
"{}*\x1B[0m",
if tile.has_flag {
"\x1B[92m"
} else if {
let game_ending_tile =
GLOBAL_STATE.read().unwrap().game_ending_tile.unwrap();
game_ending_tile.0 == col && game_ending_tile.1 == row
} {
"\x1B[91m"
} else {
""
}
);
if col == self.cols - 1 {
print!("");
}
continue;
}
if tile.revealed {
let nearby_mines = self
.get_neighboring_tiles(col, row)
.iter()
.filter(|(tile_x, tile_y)| {
let tile = &self.tiles[(tile_y * self.cols) + tile_x];
tile.has_mine
})
.count();
if nearby_mines == 0 {
print!(" ");
} else {
print!(
"{}{nearby_mines}\x1B[0m",
match nearby_mines {
1 => "\x1B[32m",
2 => "\x1B[37m",
3 => "\x1B[96m",
4 => "\x1B[33m",
5 => "\x1B[34m",
6 => "\x1B[35m",
7 => "\x1B[31m",
8 => "\x1B[97m",
_ => panic!("More than 8 nearby mines!"),
}
);
}
} else if tile.has_flag {
if GLOBAL_STATE.read().unwrap().game_ended {
print!("\x1B[91m^\x1B0m");
} else {
print!("^");
}
} else {
print!("#");
}
if col == self.cols - 1 {
print!("");
}
}
println!("");
}
println!("{}", "".repeat(self.cols));
}
pub fn generate_mines(&mut self) {
for _ in 0..self.mines_count {
let pos = self.get_new_pos();
let idx = (pos.1 * self.cols) + pos.0;
self.tiles[idx].has_mine = true;
}
}
fn get_new_pos(&self) -> (usize, usize) {
let mut y = rng(self.rows);
let mut x = rng(self.cols);
let mut idx = (y * self.cols) + x;
while self.tiles[idx].has_mine != false {
y = rng(self.rows);
x = rng(self.cols);
idx = (y * self.cols) + x;
}
return (x, y);
}
}
#[derive(Clone, Copy, Debug)]
struct Tile {
pub revealed: bool,
pub has_mine: bool,
pub has_flag: bool,
}
impl Tile {
fn new() -> Self {
Self {
revealed: false,
has_mine: false,
has_flag: false,
}
}
fn reveal(&mut self) {
if self.has_mine {
GLOBAL_STATE.write().unwrap().game_ended = true;
return;
}
self.has_flag = false;
self.revealed = true;
}
fn flag(&mut self) {
self.has_flag = !self.has_flag;
}
}
fn main() -> io::Result<()> {
print!("\x1B[2J\x1B[1;1H");
println!("Starting RustySweep... (bad name ik)");
loop {
GLOBAL_STATE.write().unwrap().restart();
game_loop()?;
}
}
fn game_loop() -> io::Result<()> {
let mut stdin = String::new();
println!("Please select board size");
let boards = [
Board::new(9, 9, 10),
Board::new(16, 16, 40),
Board::new(16, 30, 99),
];
for (i, board) in boards.iter().enumerate() {
println!(
"[{}]: {:<02}x{:<02} {:<02}",
i + 1,
board.rows,
board.cols,
board.mines_count
);
}
println!("[{}]: Custom board", boards.len() + 1);
io::stdin().read_line(&mut stdin)?;
// easier if we want to make custom sized boards a thing later
let mut board = match usize::from_str(&stdin.trim()) {
Err(_) => {
println!("Selected board is invalid!");
return Ok(());
}
Ok(idx) => {
if idx == boards.len() + 1 {
// custom board
let custom_board = make_custom_board();
let board = match custom_board {
Ok(board) => board,
Err(board_err) => match board_err {
CustomBoardError::Cancel => return Ok(()),
CustomBoardError::Error => {
println!("Invalid custom board!");
return Ok(());
}
},
};
board
} else {
if idx > boards.len() {
println!("Selected board is invalid!");
return Ok(());
}
boards[idx - 1].clone()
}
}
};
drop(stdin);
board.generate_mines();
loop {
print!("\x1B[2J\x1B[1;1H");
if board.remaining_tiles() == 0 {
GLOBAL_STATE.write().unwrap().game_won = true;
}
if (board.remaining_tiles() + board.flagged_tiles()) == board.mines_count {
board.flag_all_remaining_tiles();
GLOBAL_STATE.write().unwrap().game_won = true;
}
board.draw();
if GLOBAL_STATE.read().unwrap().game_won {
println!("You won!");
enter_to_continue();
break;
}
if GLOBAL_STATE.read().unwrap().game_ended {
println!("Game over!");
enter_to_continue();
break;
}
let x = get_input_num("Select an X position", None);
if x.is_cancel() || x.is_invalid() {
continue;
}
if x.get_num() >= board.cols {
println!("X position is invalid!");
enter_to_continue();
continue;
}
let y = get_input_num("Select a Y position", None);
if y.is_cancel() || x.is_invalid() {
continue;
}
if y.get_num() >= board.rows {
println!("Y position is invalid!");
enter_to_continue();
continue;
}
let action = get_input_num(
"What would you like to do",
Some(&["Reveal the tile", "Place or remove a flag"]),
);
if action.is_cancel() {
continue;
}
let action: Action = ((action.get_num() as u8) - 1).into();
println!("User action: {action:?}ing {} {}", x.get_num(), y.get_num());
match action {
Action::Reveal => board.reveal(x.get_num(), y.get_num()),
Action::Flag => board.flag(x.get_num(), y.get_num()),
}
}
Ok(())
}
fn enter_to_continue() {
println!("Press Return to continue!");
let _ = io::stdin().read_line(&mut String::new());
}
enum CustomBoardError {
Cancel,
Error,
}
fn make_custom_board() -> Result<Board, CustomBoardError> {
let mut cols = get_input_num("Board width", None);
if cols.is_cancel() {
return Err(CustomBoardError::Cancel);
}
if cols.is_invalid() {
return Err(CustomBoardError::Error);
}
let mut rows = get_input_num("Board height", None);
if rows.is_cancel() {
return Err(CustomBoardError::Cancel);
}
if rows.is_invalid() {
return Err(CustomBoardError::Error);
}
let mut mines = get_input_num("Number of mines", None);
if mines.is_cancel() {
return Err(CustomBoardError::Cancel);
}
if mines.is_invalid() {
return Err(CustomBoardError::Error);
}
// winmine minimums at least in the winmine from archive.org
if cols.get_num() < 8 {
cols = Input::Num(8)
}
if rows.get_num() < 8 {
rows = Input::Num(8);
}
if mines.get_num() < 10 {
mines = Input::Num(10);
}
if mines.get_num() > (rows.get_num() - 1) * (cols.get_num() - 1) {
mines = Input::Num((rows.get_num() - 1) * (cols.get_num() - 1));
}
return Ok(Board::new(rows.get_num(), cols.get_num(), mines.get_num()));
}
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
enum Action {
Reveal,
Flag,
}
impl From<u8> for Action {
fn from(other: u8) -> Self {
match other {
0 => Self::Reveal,
1 => Self::Flag,
_ => panic!("Invalid Action {other}!"),
}
}
}
enum Input {
Num(usize),
Cancel,
Invalid,
}
impl Input {
fn is_cancel(&self) -> bool {
match self {
Input::Cancel => return true,
_ => return false,
}
}
fn get_num(&self) -> usize {
match self {
Input::Num(num) => return num.clone(),
_ => panic!("tried to unwrap a non-Num value!"),
}
}
fn is_invalid(&self) -> bool {
match self {
Input::Invalid => return true,
_ => return false,
}
}
}
fn get_input_num(message: &str, options: Option<&[&str]>) -> Input {
println!("{message} (c to cancel):");
if options.is_some() {
for (i, option) in options.unwrap().iter().enumerate() {
println!("[{}]: {option}", i + 1);
}
}
let mut stdin = String::new();
io::stdin().read_line(&mut stdin).unwrap();
if stdin.as_bytes()[0] == b'c' {
return Input::Cancel;
}
let num = match usize::from_str(&stdin.trim()) {
Err(_) => {
return Input::Invalid;
}
Ok(idx) => {
if options.is_some() {
if idx > options.unwrap().len() {
return Input::Invalid;
}
}
idx
}
};
return Input::Num(num);
}
fn rng(max: usize) -> usize {
rand::thread_rng().gen_range(0..max)
}