From 003757b36c5150aa2afabb0b0da01499e4987f73 Mon Sep 17 00:00:00 2001 From: ElementG9 Date: Tue, 14 Apr 2020 00:50:17 -0600 Subject: [PATCH] Make the game --- .gitignore | 1 + Cargo.lock | 84 +++++++++++ Cargo.toml | 10 ++ src/lib.rs | 397 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 + 5 files changed, 495 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..29756e2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,84 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rustsweeper" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..036d2f1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rustsweeper" +version = "0.1.0" +authors = ["ElementG9 "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.7.3" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5a91bd4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,397 @@ +extern crate rand; +use std::io::Write; + +pub fn run() { + println!("Welcome to Rustsweeper!"); + let mut width = 10; + let mut height = 10; + match prompt("\nChoose your size:\n1. 10 x 10\n2. 20 x 10\n3. 20 x 20\n4. 40 x 20\n> ", 1, 4) { + 1 => { + width = 10; + height = 10; + }, + 2 => { + width = 20; + height = 10; + }, + 3 => { + width = 20; + height = 20; + }, + 4 => { + width = 40; + height = 20; + }, + _ => {} + }; + let mut difficulty = Difficulty::Easy; + match prompt("\nChoose your difficulty:\n1. Easy - 10% bombs\n2. Medium - 20% bombs\n3. Hard - 40% bombs\n> ", 1, 3) { + 1 => { + difficulty = Difficulty::Easy; + }, + 2 => { + difficulty = Difficulty::Medium; + }, + 3 => { + difficulty = Difficulty::Hard; + }, + _ => {} + }; + + print!("\n"); + let mut game = Game::new(width, height, difficulty); + while game.running { + println!("{}", game.get_board()); + match prompt("\nWhat would you like to do?\n1. Uncover a tile\n2. Flag a tile\n> ", 1, 2) { + 1 => { + let pos = prompt_coords(0, game.board.width - 1, 0, game.board.height - 1); + println!("Uncovering ({}, {})", pos.x, pos.y); + game.uncover(pos.x as usize, pos.y as usize); + }, + 2 => { + let pos = prompt_coords(0, game.board.width - 1, 0, game.board.height - 1); + println!("Flagging ({}, {})", pos.x, pos.y); + game.flag(pos.x as usize, pos.y as usize); + }, + _ => {} + }; + } +} + +#[allow(dead_code)] +#[derive(Debug, PartialEq, Clone)] +pub enum Cell { + Bomb, + Empty, +} +#[allow(dead_code)] +#[derive(Debug, PartialEq, Clone)] +pub enum Difficulty { + Easy, + Medium, + Hard, +} + +#[derive(Debug, Clone, Copy)] +pub struct Coord { + pub x: isize, + pub y: isize, + pub valid: bool +} +impl Coord { + pub fn new(x: isize, y: isize) -> Coord { + Coord { + x, + y, + valid: true + } + } +} + +#[allow(dead_code)] +pub struct Board { + pub width: usize, + pub height: usize, + pub values: Vec>, +} +impl Board { + pub fn new(width: usize, height: usize) -> Board { + let mut values = Vec::new(); + for _y in 0..height { + let mut row = Vec::new(); + for _x in 0..width { + row.push(Cell::Empty); + } + values.push(row); + } + let values = values; + Board { + width, + height, + values, + } + } + pub fn get<'a>(&'a self, x: usize, y: usize) -> &'a Cell { + &self.values[y][x] + } + pub fn set(&mut self, x: usize, y: usize, value: Cell) { + self.values[y][x] = value; + } +} + +#[allow(dead_code)] +pub struct Game { + pub running: bool, + pub board: Board, + pub difficulty: Difficulty, + pub uncovered: Vec>, + pub flags: Vec<(usize, usize)> +} +impl Game { + pub fn new(width: usize, height: usize, difficulty: Difficulty) -> Game { + let mut board = Board::new(width, height); + let mut uncovered = Vec::new(); + for _y in 0..height { + let mut row = Vec::new(); + for _x in 0..width { + row.push(false); + } + uncovered.push(row); + } + let num_bombs = difficulty_lookup(width, height, difficulty.clone()); + for _ in 0..num_bombs { + loop { + let x = random(width); + let y = random(height); + if board.get(x, y) == &Cell::Empty { + board.set(x, y, Cell::Bomb); + break; + } + } + } + Game { + running: true, + board, + difficulty, + uncovered, + flags: Vec::new() + } + } + pub fn get_valid_neighbors(&self, x: usize, y: usize) -> Vec { + let x = x as isize; + let y = y as isize; + let mut cells_to_check = vec![ + Coord::new(x - 1, y - 1), + Coord::new(x, y - 1), + Coord::new(x + 1, y - 1), + Coord::new(x - 1, y ), + Coord::new(x, y ), + Coord::new(x + 1, y ), + Coord::new(x - 1, y + 1), + Coord::new(x, y + 1), + Coord::new(x + 1, y + 1) + ]; + let mut valid_cells = Vec::new(); + for i in 0..cells_to_check.len() { + if cells_to_check[i].x < 0 || cells_to_check[i].y < 0 { + cells_to_check[i].valid = false; + } + if cells_to_check[i].x >= self.board.width as isize || cells_to_check[i].y >= self.board.height as isize { + cells_to_check[i].valid = false; + } + if cells_to_check[i].valid { + valid_cells.push(cells_to_check[i]); + } + } + valid_cells + } + pub fn get_bomb_count(&self, x: usize, y: usize) -> usize { + let mut bomb_count = 0; + for cell in self.get_valid_neighbors(x, y) { + if self.board.get(cell.x as usize, cell.y as usize) == &Cell::Bomb { + bomb_count += 1; + } + } + bomb_count + } + pub fn check_uncovered(&self, x: usize, y: usize) -> bool { + self.uncovered[y][x] + } + pub fn uncover(&mut self, x: usize, y: usize) { + if !self.check_uncovered(x, y) || !self.check_flagged(x, y) { + self.uncovered[y][x] = true; + if self.get_bomb_count(x, y) == 0 { + for neighbor in self.get_valid_neighbors(x, y) { + if !self.check_uncovered(neighbor.x as usize, neighbor.y as usize) { + self.uncover(neighbor.x as usize, neighbor.y as usize); + } + } + } + if self.board.get(x, y) == &Cell::Bomb { + println!("Uncovered a bomb!"); + self.uncover_all(); + println!("{}", self.get_board()); + self.running = false; + } + } + } + pub fn uncover_all(&mut self) { + let mut uncovered = Vec::new(); + for _y in 0..self.board.height { + let mut row = Vec::new(); + for _x in 0..self.board.width { + row.push(true); + } + uncovered.push(row); + } + self.uncovered = uncovered; + } + pub fn check_flagged(&self, x: usize, y: usize) -> bool { + for flag in &self.flags { + if *flag == (x, y) { + return true; + } + } + false + } + pub fn flag(&mut self, x: usize, y: usize) { + if !self.check_flagged(x, y) { + self.flags.push((x, y)); + } else { + for i in 0..self.flags.len() { + if self.flags[i] == (x, y) { + self.flags.remove(i); + break; + } + } + } + } + pub fn get_board(&self) -> String { + let mut out = String::new(); + out.push_str(" "); + if self.board.height > 10 { + out.push_str(" "); + } + for x in 0..self.board.width { + out.push_str(&format!("{} ", x)); + if x < 10 { + out.push_str(" "); + } + } + out.push_str("\n"); + if self.board.height > 10 { + out.push_str(" "); + } + out.push_str(" +"); + for _ in 0..self.board.width { + out.push_str("---"); + } + out.push_str("\n"); + for y in 0..self.board.height { + let mut temp = String::new(); + temp.push_str(&format!("{}", y)); + if y < 10 && self.board.height > 10 { + temp.push_str(" "); + } + temp.push_str("|"); + out.push_str(&temp); + for x in 0..self.board.width { + if self.check_uncovered(x, y) { + let bomb_count = self.get_bomb_count(x, y); + let count = &format!("{}", bomb_count); + out.push_str(match self.board.get(x, y) { + Cell::Empty => if bomb_count == 0 { "." } else { count }, + Cell::Bomb => "@", + }); + } else { + if self.check_flagged(x, y) { + out.push_str("F"); + } else { + out.push_str("_"); + } + } + out.push_str(" "); + } + out.push_str("\n"); + } + out.pop(); // Remove trailing newline. + out + } +} + +pub fn difficulty_lookup(width: usize, height: usize, difficulty: Difficulty) -> usize { + let area = (width * height) as f32; + let num_bombs = match difficulty { + Difficulty::Easy => 0.1 * area, + Difficulty::Medium => 0.2 * area, + Difficulty::Hard => 0.4 * area, + }; + num_bombs as usize +} +pub fn random(max: usize) -> usize { + let rand_amt: f64 = rand::random(); + ((max as f64) * rand_amt) as usize +} +pub fn read_num() -> isize { + std::io::stdout().flush().unwrap(); + let mut input = String::new(); + match std::io::stdin().read_line(&mut input) { + Ok(_bytes_read) => { + input.pop(); // Remove trailing newline + std::io::stdout().flush().unwrap(); + return input.parse::().expect("input was not isize"); + } + Err(err) => { + eprintln!("Error reading input: {}", err); + return 0; + } + } +} +pub fn read() -> String { + std::io::stdout().flush().unwrap(); + let mut input = String::new(); + match std::io::stdin().read_line(&mut input) { + Ok(_bytes_read) => { + input.pop(); // Remove trailing newline + std::io::stdout().flush().unwrap(); + return input; + } + Err(err) => { + eprintln!("Error reading input: {}", err); + return String::new(); + } + } +} +pub fn prompt(options: &str, min: isize, max: isize) -> usize { + loop { + print!("{}", options); + let num = read_num(); + if num >= min && num <= max { + return num as usize; + } else { + println!("Please choose between {} and {}", min, max); + } + } +} +pub fn prompt_coords(xmin: usize, xmax: usize, ymin: usize, ymax: usize) -> Coord { + loop { + print!("Which coordinates? "); + let s = read(); + let mut current = 0; + while char_at(&s, current) != ' ' && char_at(&s, current) != '\0' { + current += 1; + } + if current >= str_len(&s) { + println!("X and Y must be separated by a space."); + continue; + } + let x = substring(&s, 0, current).parse::().expect("input was not usize"); + let y = substring(&s, current + 1, str_len(&s)).parse::().expect("input was not usize"); + if x < xmin || x > xmax || y < ymin || y > ymax { + println!("X and Y must be within range."); + continue; + } + return Coord::new(x as isize, y as isize); + } +} +fn str_len(s: &String) -> usize { + s.chars().count() +} +fn char_at(s: &String, pos: usize) -> char { + if pos >= str_len(&s) { + '\0' + } else { + s.chars().skip(pos).next().unwrap() + } +} +fn substring(s: &String, start: usize, mut end: usize) -> String { + if start > end || start >= str_len(&s) { + "".to_owned() + } else { + if end > str_len(&s) { + end = str_len(&s); + } + let text: String = s.chars().skip(start).take(end - start).collect(); + text + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3074ed2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + rustsweeper::run(); +}