// Copyright (C) 2020 Fatcat560/Mario Spies // Copyright (C) 2020 Arc676/Alessandro Vinciguerra // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation (version 3) // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::str::FromStr; use std::fs::File; use std::fmt; use std::fmt::Display; use std::io; use std::io::{Seek, SeekFrom, BufReader, BufRead}; use std::path::Path; use rand::Rng; //region Structs pub struct Hangman { attempts: u32, max_attempts: u32, secret_word: String, current_status: String, guesses: Vec } //endregion //region Implementations impl Hangman { /// Creates a new instance of Hangman with the provided secret word. /// # Errors /// This functions returns an Error if the secret contains an illegal character for the secret. /// # Example /// ``` /// use hangman::hangman::Hangman; /// /// let hangman = Hangman::new("supersecret", 8); /// assert!(hangman.is_ok()); /// let invalid = Hangman::new("is-not-allowed", 8); /// assert!(invalid.is_err()); /// ``` pub fn new(secret: T, max_attempts: u32) -> Result where T: AsRef { match Hangman::validate_word(secret) { Ok((secret_word, current_status)) => Ok(Hangman { attempts: 0, max_attempts, current_status, secret_word, guesses: Vec::new() }), Err(err) => Err(err) } } /// Creates a new instance of Hangman with a randomly chosen secret word /// from the provided word list. /// # Errors /// This function returns an Error if any disk I/O operation fails while /// obtaining the secret from the word list. /// # Example /// ``` /// use hangman::hangman::Hangman; /// use std::path::Path; /// /// let hangman = Hangman::new_from_word_list(Path::new("my words.txt"), 8); /// ``` pub fn new_from_word_list

(word_list: P, max_attempts: u32) -> Result where P: AsRef { let (secret_word, current_status) = loop { let temp = match Hangman::get_word(word_list.as_ref()) { Ok(w) => w, Err(why) => { return Err(why.to_string()); } }; if let Ok(pair) = Hangman::validate_word(temp) { break pair; } }; Ok(Hangman { attempts: 0, max_attempts, current_status, secret_word, guesses: Vec::new() }) } /// Validates the secret word for the Hangman game and generates the /// initial status of the game with spaces already provided. /// # Errors /// This function returns an Error if the provided secret is invalid. fn validate_word(word: T) -> Result<(String, String), String> where T: AsRef { if word.as_ref().chars().any(|c| c.is_ascii_digit() || c.is_ascii_punctuation()) { return Err("Digits and punctuation are not allowed in the secret".to_string()); } let res = word.as_ref().to_lowercase(); let status = res.chars().map(|c| if c.is_whitespace() {c} else {'_'}).collect::(); Ok((res, status)) } /// Obtains a random word from a list of newline-separated words. /// This function is used internally when constructing a new game /// from a word list. /// # Errors /// This function returns an io::Error if any disk operation fails. fn get_word

(word_list: P) -> io::Result where P: AsRef { let file = File::open(word_list.as_ref())?; let mut bf = BufReader::new(file); let end = bf.seek(SeekFrom::End(0))?; if rand::thread_rng().gen_range(0u64, end) < 10 { bf.seek(SeekFrom::Start(0))?; let mut result = String::new(); bf.read_line(&mut result)?; return Ok(result.trim().to_string()); } let result = loop { let rand_pos: u64 = rand::thread_rng().gen_range(0u64, end); let mut pos = bf.seek(SeekFrom::Start(rand_pos))?; let mut line = String::new(); let read_bytes = bf.read_line(&mut line)?; line.clear(); let mut word_len = bf.read_line(&mut line)?; if word_len > 0 { break line; } else { let delta = read_bytes as u64 + 50; if pos < delta { pos = 0; } else { pos -= delta; } bf.seek(SeekFrom::Start(pos))?; word_len = bf.read_line(&mut line)?; if word_len > 0 { break line; } } }; Ok(result.trim().to_string()) } /// This function checks if a given single character is part of the secret and adjusts the current game status accordingly. /// Returns true, if the guess was correct, else false. /// # Example /// ``` /// use hangman::hangman::Hangman; /// /// let mut hangman = Hangman::new("supersecret", 8).unwrap(); /// assert_eq!("___________", hangman.get_current_status()); /// //Make a guess /// assert!(hangman.guess('s')); /// // The game status gets updated automatically /// assert_eq!("s____s_____", hangman.get_current_status()); /// ``` pub fn guess(&mut self, c: char) -> bool { let positions: Vec = self.secret_word.chars().enumerate().filter(|(_, cha)| cha == &c).map(|(i,_)| i).collect(); self.guesses.push(c.to_string()); if positions.is_empty() { self.attempts += 1; false } else { let new_stat = self.current_status.chars().enumerate().map(|(i, ch)| if positions.contains(&i) {c} else {ch}).collect::(); self.current_status = new_stat; true } } /// This function is used if a participant tries to guess the whole secret word. This only returns true if the guess was exactly like the secret. /// Note that the given string does not get checked for invalid inputs. /// # Example /// ``` /// use hangman::hangman::Hangman; /// /// let mut hangman = Hangman::new("supersecret", 8).unwrap(); /// assert_eq!("___________", hangman.get_current_status()); /// //A wrong guess won't change the word status /// assert!(!hangman.guess_str("randomguess")); /// assert_eq!("___________", hangman.get_current_status()); /// // A right guess will immediately set the current status to the secret /// assert!(hangman.guess_str("supersecret")); /// assert_eq!("supersecret", hangman.get_current_status()); /// ``` pub fn guess_str(&mut self, guess: T) -> bool where T: AsRef { let g = guess.as_ref().to_lowercase(); if self.secret_word == g { self.current_status = g; true } else { self.attempts += 1; self.guesses.push(g); false } } /// Manual check if game is ongoing pub fn game_ongoing(&self) -> bool { self.attempts < self.max_attempts && self.secret_word != self.current_status } /// Returns the current status of the guess pub fn get_current_status(&self) -> &str { &self.current_status } /// Returns the secret key pub fn get_secret(&self) -> &str { &self.secret_word } /// Returns the maximum number of attempts pub fn get_max_attempts(&self) -> u32 { self.max_attempts } /// Returns the number of used attempts pub fn get_attempts(&self) -> u32 { self.attempts } /// Determines whether a certain guess has already been made pub fn has_guessed(&self, guess: T) -> bool where T: AsRef { let g = guess.as_ref(); match self.guesses.iter().find(|x| x == &&g) { Some(_) => true, None => false } } /// Convenience function to automatically handle a guess. /// # Errors /// This function will return an error if the given guess contains an illegal character. /// # Example /// ``` /// use hangman::hangman::Hangman; /// /// let mut hangman = Hangman::new("supersecret", 8).unwrap(); /// assert_eq!("___________", hangman.get_current_status()); /// let mut guess = hangman.handle_guess("e"); /// assert!(guess.is_ok() && guess.unwrap()); /// assert_eq!("___e__e__e_", hangman.get_current_status()); /// guess = hangman.handle_guess("supersecret"); /// assert!(guess.is_ok() && guess.unwrap()); /// assert_eq!("supersecret", hangman.get_current_status()); /// ``` pub fn handle_guess(&mut self, guess: T) -> Result where T: AsRef { let g = guess.as_ref(); if g.chars().any(|c| c.is_ascii_digit() || c.is_ascii_punctuation()) { Err("The guess contains an invalid character!".to_string()) } else { match char::from_str(g) { Ok(x) => Ok(self.guess(x)), Err(_) => Ok(self.guess_str(g)), } } } } impl Display for Hangman { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let guesses = { if self.guesses.is_empty(){ "".to_string() } else { self.guesses.join(", ") } }; write!(f, "{}\n{} attempts of {} remaining\n\n{}", self.current_status, (self.max_attempts - self.attempts), self.max_attempts, guesses) } } //endregion //region Tests #[cfg(test)] mod tests { use crate::hangman::Hangman; #[test] fn check_hangman_new() { let h = Hangman::new("abcde", 8); assert!(h.is_ok()); let h = h.unwrap(); assert_eq!(h.attempts, 0); assert_eq!(h.secret_word.len(), h.current_status.len()); assert_eq!(h.current_status, String::from("_____")); } #[test] fn check_numbers_not_allowed() { let h = Hangman::new("3lvis", 8); assert!(h.is_err()) } #[test] fn test_guess_single() { let h = Hangman::new("Hello", 8); assert!(h.is_ok()); let mut h = h.unwrap(); assert!(h.guess('l')); assert_eq!(h.current_status, String::from("__ll_")); assert!(h.guess('h')); assert_eq!(h.current_status, String::from("h_ll_")); assert!(!h.guess('r')); assert_eq!(h.attempts, 1); } #[test] fn test_guess_whole() { let h = Hangman::new("Roberto", 8); assert!(h.is_ok()); let mut h = h.unwrap(); assert!(!h.guess_str("Rainer")); assert!(h.guess_str("Roberto")); assert_eq!(h.secret_word, h.current_status) } } //endregion