initial commit
This commit is contained in:
606
src/main.rs
Normal file
606
src/main.rs
Normal file
@@ -0,0 +1,606 @@
|
||||
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();
|
||||
}
|
||||
|
||||
// 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 signed_x = x as isize;
|
||||
let signed_y = y as isize;
|
||||
let tl_x = signed_x - 1;
|
||||
let tl_y = signed_y - 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 draw(&self) {
|
||||
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 {
|
||||
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) {
|
||||
println!("{}", self.has_flag);
|
||||
self.has_flag = !self.has_flag;
|
||||
println!("{}", 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");
|
||||
gith
|
||||
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
|
||||
.tiles
|
||||
.iter()
|
||||
.filter(|tile| !tile.revealed && !tile.has_flag)
|
||||
.count()
|
||||
== 0
|
||||
{
|
||||
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()),
|
||||
}
|
||||
|
||||
println!(
|
||||
"Tile at {} {} is now: {:?}",
|
||||
x.get_num(),
|
||||
y.get_num(),
|
||||
board.tiles[(y.get_num() * board.cols) + x.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);
|
||||
}
|
||||
|
||||
if mines.get_num() == (cols.get_num() * rows.get_num())
|
||||
|| (cols.get_num() * rows.get_num()) < mines.get_num()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user