diff --git a/60_Mastermind/README.md b/60_Mastermind/README.md index 2e74ed1cd..7820eed7b 100644 --- a/60_Mastermind/README.md +++ b/60_Mastermind/README.md @@ -28,7 +28,7 @@ As published in Basic Computer Games (1978): Downloaded from Vintage Basic at http://www.vintage-basic.net/games.html -###How the computer deduces your guess. +### How the computer deduces your guess. The computer takes the number of black pegs and white pegs that the user reports and uses that information as a target. It then assumes its guess is the answer diff --git a/60_Mastermind/rust/Cargo.toml b/60_Mastermind/rust/Mastermind/Cargo.toml similarity index 89% rename from 60_Mastermind/rust/Cargo.toml rename to 60_Mastermind/rust/Mastermind/Cargo.toml index 3b1d02f52..83729722a 100644 --- a/60_Mastermind/rust/Cargo.toml +++ b/60_Mastermind/rust/Mastermind/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rust" +name = "mastermind" version = "0.1.0" edition = "2021" diff --git a/60_Mastermind/rust/README.md b/60_Mastermind/rust/Mastermind/README.md similarity index 100% rename from 60_Mastermind/rust/README.md rename to 60_Mastermind/rust/Mastermind/README.md diff --git a/60_Mastermind/rust/src/main.rs b/60_Mastermind/rust/Mastermind/src/main.rs similarity index 99% rename from 60_Mastermind/rust/src/main.rs rename to 60_Mastermind/rust/Mastermind/src/main.rs index afe1802d3..92fc852a5 100644 --- a/60_Mastermind/rust/src/main.rs +++ b/60_Mastermind/rust/Mastermind/src/main.rs @@ -324,7 +324,7 @@ fn main() { all_possibilities.iter_mut().enumerate().for_each(|b| { if *b.1 { //filter out ones we already know aren't possible let mut tmp_guess = GUESS::new(CODE::new_from_int(b.0, num_colors, num_positions)); - tmp_guess.evaluate(&answer); + tmp_guess.evaluate(&guess.code); //compare with computer guess if blacks != tmp_guess.blacks || whites != tmp_guess.whites { //if number of blacks/whites is different, set it to false *b.1 = false; } @@ -378,7 +378,6 @@ fn print_board(guesses: &Vec) { for guess in guesses.iter().enumerate() { println!("{}\t{}\t\t{}\t{}", guess.0,guess.1.code._as_human_readible_chars(),guess.1.blacks,guess.1.whites); } - } /** diff --git a/60_Mastermind/rust/Mastermind_refactored_for_conventions/Cargo.toml b/60_Mastermind/rust/Mastermind_refactored_for_conventions/Cargo.toml new file mode 100644 index 000000000..14762c87e --- /dev/null +++ b/60_Mastermind/rust/Mastermind_refactored_for_conventions/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mastermind_refactored_for_conventions" +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" \ No newline at end of file diff --git a/60_Mastermind/rust/Mastermind_refactored_for_conventions/README.md b/60_Mastermind/rust/Mastermind_refactored_for_conventions/README.md new file mode 100644 index 000000000..7e85f9a15 --- /dev/null +++ b/60_Mastermind/rust/Mastermind_refactored_for_conventions/README.md @@ -0,0 +1,3 @@ +Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) + +Conversion to [Rust](https://www.rust-lang.org/) by Anthony Rubick [AnthonyMichaelTDM](https://github.com/AnthonyMichaelTDM) diff --git a/60_Mastermind/rust/Mastermind_refactored_for_conventions/src/lib.rs b/60_Mastermind/rust/Mastermind_refactored_for_conventions/src/lib.rs new file mode 100644 index 000000000..1fd92d62a --- /dev/null +++ b/60_Mastermind/rust/Mastermind_refactored_for_conventions/src/lib.rs @@ -0,0 +1,463 @@ +/* + lib.rs contains all the logic of the program +*/ +use rand::{Rng, prelude::thread_rng}; //rng +use std::error::Error; //better errors +use std::io::{self, Write}; //io interactions +use std::{str::FromStr, fmt::Display}; //traits + +//DATA +const COLORS: [&str;8] = ["Black ", "White ","Red ","Green ","Orange ","Yellow ", "Purple ", "Tan "]; //all available colors +const LETTERS: &str = "BWRGOYPT"; //letters representing the above colors + +/// handles setup for the game +pub struct Config { + num_colors: usize, + num_positions: usize, + num_rounds: usize, + num_guesses: usize, + total_possibilities: usize, +} +impl Config { + /// creates and returns a new Config from user input + pub fn new() -> Result> { + //DATA + let mut config: Config = Config { + num_colors: 0, + num_positions: 0, + num_rounds: 0, + num_guesses: 0, + total_possibilities: 0, + }; + + //get data from user input + //input loop + loop {match get_number_from_input("NUMBER OF COLORS: ", 2, COLORS.len()) { + Ok(num) => { + config.num_colors = num; + break; + }, + Err(e) => eprintln!("{}",e), + }} + //input loop + loop {match get_number_from_input("NUMBER OF POSITIONS: ", 2, 10) { + Ok(num) => { + config.num_positions = num; + break; + }, + Err(e) => eprintln!("{}",e), + }} + //input loop + loop {match get_number_from_input("NUMBER OF ROUNDS: ", 1, 10) { + Ok(num) => { + config.num_rounds = num; + break; + }, + Err(e) => eprintln!("{}",e), + }} + //input loop + loop {match get_number_from_input("NUMBER OF GUESSES: ", 10, 0) { + Ok(num) => { + config.num_guesses = num; + break; + }, + Err(e) => eprintln!("{}",e), + }} + //calc total posibilities + config.total_possibilities = config.num_colors.pow(config.num_positions as u32); + + //return new config + return Ok(config); + } +} + +/// run the program +pub fn run(config: &Config) -> Result<(), Box> { + //DATA + let mut human_score: usize = 0; + let mut computer_score: usize = 0; + + //print number of possibilities + println!("\nTOTAL POSSIBILITIES = {}\n", config.total_possibilities); + + //print color letter table + print_color_letter_table(config.num_colors); + + //for every round + for round in 1..=config.num_rounds { + //print round number + println!("\n\nROUND NUMBER: {}", round); + + //computer as code-maker + if let Some(score) = play_round_computer_codemaker(config) { + human_score += score; + } else {break;} + + //human as code-maker + if let Some(score) = play_round_human_codemaker(config) { + computer_score += score; + } else {break;} + + //print scores + print_scores(human_score,computer_score); + } + + //return to main + Ok(()) +} + +/// run a round with computer as code-maker +/// returns the number of turns it takes the human to guess the secret code +fn play_round_computer_codemaker(config: &Config) -> Option { + //DATA + let mut rng = thread_rng(); + let mut guesses: Vec = Vec::new(); + let secret: Code; + + //generate secret + secret = Code::new_from_int(rng.gen_range(0..config.num_colors.pow(config.num_positions.try_into().unwrap())), config); + + //round loop + for human_moves in 1..=config.num_guesses { + //get guess from user input + //input loop + let mut guess = loop { + //get input + let user_input = get_string_from_user_input(format!("\nMOVE # {} GUESS: ", human_moves).as_str()).expect("something went wrong getting user guess"); + + //parse input + if user_input.trim().eq_ignore_ascii_case("board") { //print the board state + print_board(&guesses); + continue; //run input loop again + } else if user_input.trim().eq_ignore_ascii_case("quit") { //quit the game + println!("QUITTER! MY COMBINATION WAS: {}\nGOOD BYE", secret.as_human_readible_chars()); + return None; //exit the game + } else { + //parse input for a code + match Code::new_from_string(&user_input, &config) { + Ok(code) => { + //ensure code is correct length + if code.code.len() != config.num_positions { // if not + println!("BAD NUMBER OF POSITIONS."); + continue; //run loop again + } + else {break code;}//break with the code + }, + Err(e) => {eprintln!("{}",e); continue;}, //run loop again + } + } + }; + + //evaluate guess + guess.evaluate(&secret).expect("something went wrong evaluating user guess"); + + //tell user the results + if guess.black_pins >= config.num_positions { //guessed it correctly + println!("YOU GUESSED IT IN {} MOVES!", human_moves); + return Some(human_moves); //exit function + } else { //didn't + println!("YOU HAVE {} BLACKS AND {} WHITES.", guess.black_pins, guess.white_pins); + } + + //add guess to the list of guesses + guesses.push(guess); + } + + //only runs if user doesn't guess the code + println!("YOU RAN OUT OF MOVES! THAT'S ALL YOU GET!"); + println!("THE ACTUAL COMBINATION WAS: {}", secret.as_human_readible_chars()); + return Some(config.num_guesses); //max score gain per round +} + +/// run a round with human as code-maker +/// returns the number of turns it takes the computer to guess the secret code +fn play_round_human_codemaker(config: &Config) -> Option{ + //DATA + let mut rng = thread_rng(); + let mut all_possibilities = vec![true; config.total_possibilities]; + let _secret: Code; + + + //get a secret code from user input + println!("\nNOW I GUESS. THINK OF A COMBINATION.\nHIT RETURN WHEN READY: "); + // input loop + _secret = loop { + //get input + let user_input = get_string_from_user_input("").expect("something went wrong getting secret from user"); + + //parse input + if let Ok(code) = Code::new_from_string(&user_input, config) { + if code.code.len() == config.num_positions {break code;} //exit loop with code + else {println!("CODE MUST HAVE {} POSITIONS", config.num_positions);continue;} //tell them to try again + } + println!("INVALID CODE. TRY AGAIN"); //if unsuccessful, this is printed and the loop runs again + }; + + //round loop + for computer_moves in 1..=config.num_guesses { + let mut guess: Code = Code::new(); + + //randomly generate a guess //770 + let mut guess_int = rng.gen_range(0..config.total_possibilities); + // if possible, use it //780 + if all_possibilities[guess_int] { + guess = Code::new_from_int(guess_int, &config); //create guess + } + else {// if not possible: + // search all possibilities after guess, use first valid one //790 + for g in guess_int..config.total_possibilities { + if all_possibilities[g] { + guess_int=g; + guess = Code::new_from_int(guess_int, &config); //create guess + break; + } + } + // if none was found + // search all possibilities before guess, use first valid one //820 + if guess.code.is_empty() { + for g in (0..guess_int).rev() { + if all_possibilities[g] { + guess_int=g; + guess = Code::new_from_int(guess_int, &config); //create guess + break; + } + } + } + + // if none where found, tell the user and start over #850 + if guess.code.is_empty() { + println!("YOU HAVE GIVEN ME INCONSISTENT INFORMATION."); + println!("PLAY AGAIN, AND THIS TIME PLEASE BE MORE CAREFUL."); + return None; //exit game + }; + } + + //convert guess into something readible #890 + //print it #940 + println!("MY GUESS IS: {}", guess.as_human_readible_chars()); + + //ask user for feedback, #980 + // input loop for black pegs + loop {match get_number_from_input("BLACKS: ", 0, config.num_positions) { + Ok(num) => { + guess.black_pins = num; + break; + }, + Err(e) => eprintln!("{}",e), + }} + // input loop for white pegs + loop {match get_number_from_input("WHITES: ", 0, config.num_positions) { + Ok(num) => { + guess.white_pins = num; + break; + }, + Err(e) => eprintln!("{}",e), + }} + + //if computer guessed it, end #990 + if guess.black_pins >= config.num_positions { //guessed it correctly + println!("I GOT IT IN {} MOVES!", computer_moves); + return Some(computer_moves); //exit function + } else { //didn't + all_possibilities[guess_int] = false; + //if we didn't, eliminate the combinations that don't work + //we know the number of black and white pegs for a valid answer, so eleminate all that get different amounts + all_possibilities.iter_mut().enumerate().for_each(|b| { + if *b.1 { //filter out ones we already know aren't possible + let mut tmp_guess = Code::new_from_int(b.0, &config); + tmp_guess.evaluate(&guess).expect("something went wrong evaluation the computer's guess"); //compare with computer guess + if (guess.black_pins != tmp_guess.black_pins) || (guess.white_pins != tmp_guess.white_pins) { //if number of blacks/whites is different, set it to false + *b.1 = false; + } + } + }); + } + } + + //only runs if computer doesn't guess the code + println!("I USED UP ALL MY MOVES!"); + println!("I GUESS MY CPU IS JUST HAVING AN OFF DAY."); + return Some(config.num_guesses); //max moves the computer could've taken +} + +struct Code { + code: Vec, + black_pins: usize, + white_pins: usize, +} +impl Code { + /// create generic, empty code + fn new() -> Code { + return Code{code: Vec::new(), black_pins:0, white_pins:0}; + } + /// converts input_int from base 10 to base num_colors to generate the code + /// input_int must be between 0 and num_colors.pow(num_positions) + fn new_from_int(mut input_int: usize, config: &Config) -> Code { + //DATA + let mut converted_number:Vec<_> = Vec::new(); + assert!(2 <= config.num_colors && config.num_colors <= 36); //if num_colors is outside of this range, things break later on + + //convert input_int into a code by effectively converting input_int from base 10 to base n where n is num_colors, uses some fancy stuff to do this + loop { + converted_number.push(std::char::from_digit((input_int % config.num_colors).try_into().unwrap(), config.num_colors.try_into().unwrap()).unwrap()); // + input_int /= config.num_colors; + if input_int == 0 {break} + } + + while converted_number.len() < config.num_positions {converted_number.push('0');} // fill remaining space with zero's + let converted_number: Vec<_> = converted_number.iter().rev().map(|e| e.to_digit(config.num_colors.try_into().unwrap()).unwrap() .try_into().unwrap()).collect(); //reverse the vector and convert it to integers + return Code{code: converted_number, black_pins:0, white_pins:0}; + } + /// returns a code parsed from the passed string + fn new_from_string(input_string: &str, config: &Config) -> Result> { + let valid_chars = &LETTERS[0..config.num_colors]; + //DATA + let new_code = Code{ + code: { + input_string.to_ascii_uppercase().chars() //get an iterator with all the chars in input string converted to uppercase + .filter( |c| { valid_chars.contains(*c)}) //remove chars that aren't in LETTERS + .map( |x| -> usize {valid_chars.find(x).expect("invalid character")})//convert all the chars into usizes representing their index in LETTERS + .collect() //wrap this iterator up into a vector + }, + black_pins: 0, + white_pins: 0, + }; + //if code is empty, return None, otherwise return Some(code) + if new_code.code.is_empty() {return Err(String::from("Input String did not contain enough valid characters").into());} + else {return Ok(new_code);} + } + + /// returns a string containing the code represented as characters + fn as_human_readible_chars(&self) -> String { + return self.code.iter().map(|i|->char{LETTERS.chars().nth(*i).expect("index out of bounds")}).collect(); + } + + /** + * evaulates itself for the number of black and white pegs it should have when compared to a given secret + */ + fn evaluate(&mut self, secret:&Code) -> Result<(),Box> { + //data + let mut consumed = vec![false;secret.code.len()]; + + if self.code.len() != secret.code.len() { + return Err(String::from("only codes of the same length can be compared").into()); + } + + for i in 0..secret.code.len() { + if self.code[i] == secret.code[i] { //correct value correct place + self.black_pins += 1; + consumed[i] = true; + } + else { + //check for correct value incorrect place, don't count positions that are already exact matches + for j in 0..secret.code.len() { + if !consumed[j] && self.code[i] == secret.code[j] && self.code[j] != secret.code[j] { + self.white_pins += 1; + consumed[j] = true; + break; + } + } + } + } + + return Ok(()) + } +} + + + +/// print scores +fn print_scores(human_score:usize, computer_score:usize) { + println!("SCORE\n\tCOMPUTER: {}\n\tHUMAN: {}", computer_score, human_score); +} +/// print the color - letter table +/// only prints the first num_colors pairs +fn print_color_letter_table(num_colors:usize) { + println!("COLOR\tLETTER"); + println!("=====\t======"); + for i in 0..num_colors { + println!("{}\t{}", COLORS[i], &LETTERS[i..i+1]); + } +} +/// prints the board state, previous guesses and the number of black/white pins for each +fn print_board(guesses: &[Code]) { + println!("BOARD"); + println!("MOVE\tGUESS\t\tBLACK\tWhite"); + for guess in guesses.iter().enumerate() { + println!("{}\t{}\t\t{}\t{}", guess.0,guess.1.as_human_readible_chars(),guess.1.black_pins,guess.1.white_pins); + } +} + + + +/// gets a string from user input +fn get_string_from_user_input(prompt: &str) -> Result> { + //DATA + let mut raw_input = String::new(); + + //print prompt + print!("{}", prompt); + //make sure it's printed before getting input + io::stdout().flush().expect("couldn't flush stdout"); + + //read user input from standard input, and store it to raw_input, then return it or an error as needed + raw_input.clear(); //clear input + match io::stdin().read_line(&mut raw_input) { + Ok(_num_bytes_read) => return Ok(String::from(raw_input.trim())), + Err(err) => return Err(format!("ERROR: CANNOT READ INPUT!: {}", err).into()), + } +} +/// generic function to get a number from the passed string (user input) +/// pass a min lower than the max to have minimun and maximun bounds +/// pass a min higher than the max to only have a minumum bound +/// pass a min equal to the max to only have a maximun bound +/// +/// Errors: +/// no number on user input +fn get_number_from_input(prompt: &str, min:T, max:T) -> Result> { + //DATA + let raw_input: String; + let processed_input: String; + + + //input looop + raw_input = loop { + match get_string_from_user_input(prompt) { + Ok(input) => break input, + Err(e) => { + eprintln!("{}",e); + continue; + }, + } + }; + + //filter out num-numeric characters from user input + processed_input = raw_input.chars().filter(|c| c.is_numeric()).collect(); + + //from input, try to read a number + match processed_input.trim().parse() { + Ok(i) => { + //what bounds must the input fall into + if min < max { //have a min and max bound: [min,max] + if i >= min && i <= max {//is input valid, within bounds + return Ok(i); //exit the loop with the value i, returning it + } else { //print error message specific to this case + return Err(format!("ONLY BETWEEN {} AND {}, PLEASE!", min, max).into()); + } + } else if min > max { //only a min bound: [min, infinity) + if i >= min { + return Ok(i); + } else { + return Err(format!("NO LESS THAN {}, PLEASE!", min).into()); + } + } else { //only a max bound: (-infinity, max] + if i <= max { + return Ok(i); + } else { + return Err(format!("NO MORE THAN {}, PLEASE!", max).into()); + } + } + }, + Err(_e) => return Err(format!("Error: couldn't find a valid number in {}",raw_input).into()), + } +} diff --git a/60_Mastermind/rust/Mastermind_refactored_for_conventions/src/main.rs b/60_Mastermind/rust/Mastermind_refactored_for_conventions/src/main.rs new file mode 100644 index 000000000..5a5d01295 --- /dev/null +++ b/60_Mastermind/rust/Mastermind_refactored_for_conventions/src/main.rs @@ -0,0 +1,40 @@ +use std::process;//allows for some better error handling + +mod lib; //allows access to lib.rs +use lib::Config; + +/// main function +/// responsibilities: +/// - Calling the command line logic with the argument values +/// - Setting up any other configuration +/// - Calling a run function in lib.rs +/// - Handling the error if run returns an error +fn main() { + //greet user + welcome(); + + // set up other configuration + let mut config = Config::new().unwrap_or_else(|err| { + eprintln!("Problem configuring program: {}", err); + process::exit(1); + }); + + // run the program + if let Err(e) = lib::run(&mut config) { + eprintln!("Application Error: {}", e); //use the eprintln! macro to output to standard error + process::exit(1); //exit the program with an error code + } + + //end of program + println!("THANKS FOR PLAYING!"); +} + +/// print the welcome message +fn welcome() { + println!(" + MASTERMIND + CREATIVE COMPUTING MORRISTOWN, NEW JERSEY + + + "); +}