initial commit

This commit is contained in:
Zoe
2024-02-07 21:18:17 -06:00
commit 181d61ede3
6 changed files with 730 additions and 0 deletions

75
Cargo.lock generated Normal file
View File

@@ -0,0 +1,75 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rusty-sweep"
version = "0.1.0"
dependencies = [
"rand",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "rusty-sweep"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8.5"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 juls0730
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19
README.md Normal file
View File

@@ -0,0 +1,19 @@
# RustySweep
A somewhat faithful to the original winmine minesweeper clone made entirely in rust designed to be run on the CLI. Might add a cursor interface later if I feel like it.
## Getting Started
1. **Clone the Repository:**
```bash
git clone https://github.com/juls0730/RustySweep.git
```
2. **Run the Game:**
```bash
cd RustySweep
cargo run --release
```
## Preview:
![9x9 example](resources/9x9%20example.png)

BIN
resources/9x9 example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

606
src/main.rs Normal file
View 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)
}