From b0f4607b2d5168c501b7e80bd968c37708b3e47a Mon Sep 17 00:00:00 2001 From: geekiot Date: Mon, 13 Oct 2025 22:59:09 +0500 Subject: [PATCH] Add hangman game: the gallows cli game --- hangman_game/Cargo.toml | 1 + hangman_game/src/game.rs | 13 ++ hangman_game/src/game/actions.rs | 29 +++ hangman_game/src/game/calls.rs | 4 + hangman_game/src/game/cli.rs | 193 +++++++++++++++++++ hangman_game/src/game/graphics.rs | 75 +++++++ hangman_game/src/game/objects.rs | 7 + hangman_game/src/game/objects/dictionary.rs | 121 ++++++++++++ hangman_game/src/game/objects/gallows.rs | 62 ++++++ hangman_game/src/game/objects/guessed_row.rs | 33 ++++ hangman_game/src/game/terminal.rs | 65 +++++++ hangman_game/src/lib.rs | 186 ++++++++++++++++++ hangman_game/src/main.rs | 4 +- 13 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 hangman_game/src/game.rs create mode 100644 hangman_game/src/game/actions.rs create mode 100644 hangman_game/src/game/calls.rs create mode 100644 hangman_game/src/game/cli.rs create mode 100644 hangman_game/src/game/graphics.rs create mode 100644 hangman_game/src/game/objects.rs create mode 100644 hangman_game/src/game/objects/dictionary.rs create mode 100644 hangman_game/src/game/objects/gallows.rs create mode 100644 hangman_game/src/game/objects/guessed_row.rs create mode 100644 hangman_game/src/game/terminal.rs create mode 100644 hangman_game/src/lib.rs diff --git a/hangman_game/Cargo.toml b/hangman_game/Cargo.toml index ac37114..6dd47f0 100644 --- a/hangman_game/Cargo.toml +++ b/hangman_game/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +rand = "0.9.2" diff --git a/hangman_game/src/game.rs b/hangman_game/src/game.rs new file mode 100644 index 0000000..ba12f41 --- /dev/null +++ b/hangman_game/src/game.rs @@ -0,0 +1,13 @@ +mod actions; +mod calls; +mod cli; +mod graphics; +mod objects; +mod terminal; + +use self::actions::GameActions; +use self::graphics::TerminalRender; +use self::objects::{Dictionary, Gallows, GuessedRow}; +pub use calls::GameCall; +pub use cli::GameCLI; +pub use terminal::Terminal; diff --git a/hangman_game/src/game/actions.rs b/hangman_game/src/game/actions.rs new file mode 100644 index 0000000..d87a6f3 --- /dev/null +++ b/hangman_game/src/game/actions.rs @@ -0,0 +1,29 @@ +#[derive(Clone)] +pub enum GameActions { + StartSession, + ChangeSettings, + CheckStats, + Exit, +} + +impl GameActions { + pub fn all() -> Vec { + vec![ + GameActions::StartSession, + GameActions::ChangeSettings, + GameActions::CheckStats, + GameActions::Exit, + ] + } + + pub fn describe(&self) -> String { + let description = match self { + GameActions::StartSession => "Начать новую игру", + GameActions::ChangeSettings => "Изменить настройки игры", + GameActions::CheckStats => "Посмотреть игровую статистику", + GameActions::Exit => "Выйти из игры", + }; + + description.to_string() + } +} diff --git a/hangman_game/src/game/calls.rs b/hangman_game/src/game/calls.rs new file mode 100644 index 0000000..5b761f4 --- /dev/null +++ b/hangman_game/src/game/calls.rs @@ -0,0 +1,4 @@ +pub enum GameCall { + Nothing, + Exit, +} diff --git a/hangman_game/src/game/cli.rs b/hangman_game/src/game/cli.rs new file mode 100644 index 0000000..e53caef --- /dev/null +++ b/hangman_game/src/game/cli.rs @@ -0,0 +1,193 @@ +use crate::game::GameActions; +use crate::game::GameCall; +use crate::game::TerminalRender; +use crate::game::{Dictionary, Gallows, GuessedRow}; +use crate::Terminal; + +struct GameData { + sessions_cnt: u8, + wins_cnt: u8, +} + +impl GameData { + fn new() -> GameData { + GameData { + sessions_cnt: 0, + wins_cnt: 0, + } + } +} + +pub struct GameCLI { + gallows: Gallows, + guessed_row: GuessedRow, + dictionary: Dictionary, + first_startup: bool, + game_data: GameData, +} + +impl GameCLI { + pub fn new(max_attemps: u16, has_game_words: bool) -> GameCLI { + GameCLI { + gallows: Gallows::new(max_attemps), + guessed_row: GuessedRow::new(), + dictionary: Dictionary::new(has_game_words), + first_startup: true, + game_data: GameData::new(), + } + } + + pub fn draw_start_message(&mut self) { + if self.first_startup { + println!("Добро пожаловать в HangmanGame v0.1.0"); + println!("На этой странице вы можете увидеть основное меню игры."); + } else { + println!("Основное меню игры, HangmanGame v0.1.0"); + } + println!("\nСписок действий:"); + + let actions = GameActions::all(); + for option_number in 1..=actions.len() { + println!( + "{} - {}", + option_number, + actions[option_number - 1].describe() + ) + } + + self.first_startup = false; + } + + pub fn setting_game(&mut self, max_attemps: u16, new_words: &Vec, terminal: &Terminal) { + self.gallows.max_attemps = max_attemps; + self.dictionary.add_new_words(new_words); + println!("Ваши изменения успешно применены!"); + terminal.wait_enter(); + } + + pub fn draw_game_settings(&self) { + println!("Ваше кол-во попыток: {}", self.gallows.max_attemps); + println!("Ваши слова: {}", self.dictionary.render()); + } + + pub fn make_session(&mut self, terminal: &Terminal) -> Result { + self.gallows.current_attemp = 1; + self.gallows.is_alive = true; + + if self.dictionary.get_len() <= 2 { + return Err("Ваш словарь должен состоять как минимум из 3 слов."); + } + + println!("Во время разгадки слова вы можете воспользоваться следующими командами:"); + println!("1. Если вы устали отгадывать слово, то введите \"сдаюсь\""); + println!("2. Если вы захотите выключить приложение, то введите \"стоп\""); + terminal.wait_enter(); + terminal.clear_screen(); + + let random_word = self.dictionary.get_random_word().unwrap(); + let random_word_len = random_word.chars().count(); + + self.guessed_row.generate_new(&random_word, random_word_len); + let is_win: bool; + + loop { + println!("{}\n", self.gallows.render()); + self.draw_session_stats(random_word_len); + println!("\n{}\n", self.guessed_row.render()); + + let guess = match terminal.get_input("Введите ваше слово.".to_string()) + { + Ok(input) => { + let input = input.to_lowercase(); + + if input == "сдаюсь".to_string() { + is_win = false; + break; + } + + if input == "стоп".to_string() { + return Ok(GameCall::Exit); + } + + let input_len = input.chars().count(); + + if input_len == random_word_len { + input + } else { + println!( + "\nВаше слово должно иметь такую же длину!\nНадо: {}, а у вас: {}", + random_word_len, input_len, + ); + terminal.wait_enter(); + terminal.clear_screen(); + continue; + } + } + Err(_) => { + println!("Некорректный ввод!"); + terminal.wait_enter(); + terminal.clear_screen(); + continue; + } + }; + + self.guessed_row.update_row(&guess); + + if self.guessed_row.is_guessed() { + is_win = true; + break; + } + + self.gallows.next_stage(); + + if !self.gallows.is_alive { + is_win = false; + break; + } + + terminal.clear_screen(); + } + + terminal.clear_screen(); + + if is_win { + self.game_data.wins_cnt += 1; + } else { + self.gallows.current_attemp = self.gallows.max_attemps; + self.gallows.is_alive = false; + } + self.game_data.sessions_cnt += 1; + + if is_win { + println!("Ура, вы победили!"); + println!( + "Потрачено попыток: {} из {}", + self.gallows.current_attemp, self.gallows.max_attemps + ); + } else { + println!("О нееет, вы проиграли!"); + println!("{}", self.gallows.render()); + } + + Ok(GameCall::Nothing) + } + + pub fn draw_session_stats(&self, random_word_len: usize) { + println!("Текущая попытка: {}", self.gallows.current_attemp); + println!("Всего попыток: {}", self.gallows.max_attemps); + println!("Длина загаданного слова: {}", random_word_len); + } + + pub fn draw_end_message(&self) { + println!("Спасибо за игру!"); + println!("Мы собрали 2.56 GiB телеметрии о вас и отправили её в Гонконг."); + println!(":>"); + } + + pub fn draw_game_stats(&self) { + println!("Вы в меню просмотра статистики."); + println!("\nСловарь: {}", self.dictionary.render()); + println!("Кол-во игр: {}", self.game_data.sessions_cnt); + println!("Кол-во побед: {}", self.game_data.wins_cnt); + } +} diff --git a/hangman_game/src/game/graphics.rs b/hangman_game/src/game/graphics.rs new file mode 100644 index 0000000..b091d7c --- /dev/null +++ b/hangman_game/src/game/graphics.rs @@ -0,0 +1,75 @@ +use crate::game::{Dictionary, Gallows, GuessedRow}; + +pub trait TerminalRender { + fn render(&self) -> String { + return String::from("|||"); + } +} + +impl TerminalRender for Dictionary { + fn render(&self) -> String { + if self.words.len() > 0 { + let string = self.words.join(", "); + string + } else { + String::from("<Ваш словарь пуст!>") + } + } +} + +impl TerminalRender for Gallows { + fn render(&self) -> String { + let percent = self.get_percent(); + + let mut string: String = "\n".to_string(); + + if percent <= 25 { + string.push_str("\n"); + string.push_str(" | \n"); + string.push_str(" | \n"); + string.push_str(" | \n"); + string.push_str(" | \n"); + string.push_str("--|-- "); + } else if percent <= 50 { + string.push_str(" ---- \n"); + string.push_str(" | \n"); + string.push_str(" | \n"); + string.push_str(" | \n"); + string.push_str(" | \n"); + string.push_str("--|-- "); + } else if percent <= 75 { + string.push_str(" ---- \n"); + string.push_str(" | |\n"); + string.push_str(" | 0\n"); + string.push_str(" | \n"); + string.push_str(" | \n"); + string.push_str("--|-- "); + } else if percent <= 99 { + string.push_str(" ---- \n"); + string.push_str(" | | \n"); + string.push_str(" | 0 \n"); + string.push_str(" | /|\\\n"); + string.push_str(" | \n"); + string.push_str("--|-- "); + } else { + string.push_str(" ---- \n"); + string.push_str(" | | \n"); + if self.is_alive { + string.push_str(" | 0 -спасите! \n"); + } else { + string.push_str(" | 0 - x_x \n"); + } + string.push_str(" | /|\\\n"); + string.push_str(" | / \\ \n"); + string.push_str("--|-- "); + } + + string + } +} + +impl TerminalRender for GuessedRow { + fn render(&self) -> String { + self.current_guessed.join(" ") + } +} diff --git a/hangman_game/src/game/objects.rs b/hangman_game/src/game/objects.rs new file mode 100644 index 0000000..c41cad0 --- /dev/null +++ b/hangman_game/src/game/objects.rs @@ -0,0 +1,7 @@ +mod dictionary; +mod gallows; +mod guessed_row; + +pub use dictionary::Dictionary; +pub use gallows::Gallows; +pub use guessed_row::GuessedRow; diff --git a/hangman_game/src/game/objects/dictionary.rs b/hangman_game/src/game/objects/dictionary.rs new file mode 100644 index 0000000..5fdf1d0 --- /dev/null +++ b/hangman_game/src/game/objects/dictionary.rs @@ -0,0 +1,121 @@ +use rand::Rng; + +const DEFAULT_WORDS: [&str; 11] = [ + "банан", + "пончик", + "граната", + "помидор", + "торнадо", + "паркет", + "иллюзия", + "ссора", + "барабулька", + "рыбак", + "сторож", +]; + +pub struct Dictionary { + pub words: Vec, +} + +impl Dictionary { + pub fn new(has_game_words: bool) -> Dictionary { + let mut words = Vec::new(); + + if has_game_words { + for default_word in DEFAULT_WORDS { + words.push(default_word.to_string()); + } + } + + Dictionary { words: words } + } + + pub fn get_len(&self) -> usize { + return self.words.len(); + } + + pub fn add_new_words(&mut self, words: &Vec) { + for word in words { + if !self.words.contains(word) { + self.words.push(word.clone()); + } + } + } + + pub fn get_random_word(&self) -> Result { + if self.words.len() == 0 { + return Err("Can't get random word from empty dictionary."); + } + + let random_index = rand::rng().random_range(0..self.get_len()); + Ok(self.words[random_index].clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_empty_words() { + let dict = Dictionary::new(false); + let real_words: Vec = Vec::new(); + + assert_eq!(real_words, dict.words); + } + + #[test] + fn check_default_words() { + let dict = Dictionary::new(true); + + let needed_words: Vec = DEFAULT_WORDS.iter().map(|&s| s.to_string()).collect(); + + assert_eq!(needed_words, dict.words); + assert_eq!(needed_words.len(), dict.get_len()); + } + + #[test] + fn check_add_words_func() { + let mut dict = Dictionary::new(false); + + let needed_words: Vec = vec![String::from("Dog"), String::from("Cat")]; + + dict.add_new_words(&vec![String::from("Dog")]); + dict.add_new_words(&vec![String::from("Cat")]); + + assert_eq!(needed_words, dict.words); + } + + #[test] + fn check_random_word_contains() { + let dict = Dictionary::new(true); + + let random_word = dict.get_random_word().unwrap(); + + assert_eq!(true, dict.words.contains(&random_word)); + } + + #[test] + fn check_random_of_one_word() { + let mut dict = Dictionary::new(false); + + dict.add_new_words(&vec![String::from("Dog")]); + + let random_word = dict.get_random_word().unwrap(); + + assert_eq!(String::from("Dog"), random_word); + } + + #[test] + fn check_random_of_empty_dictionary() { + let dict = Dictionary::new(false); + + let random_word = dict.get_random_word(); + + assert_eq!( + Err("Can't get random word from empty dictionary."), + random_word + ) + } +} diff --git a/hangman_game/src/game/objects/gallows.rs b/hangman_game/src/game/objects/gallows.rs new file mode 100644 index 0000000..5e7271b --- /dev/null +++ b/hangman_game/src/game/objects/gallows.rs @@ -0,0 +1,62 @@ +pub struct Gallows { + pub current_attemp: u16, + pub max_attemps: u16, + pub is_alive: bool, +} + +impl Gallows { + pub fn new(max_attemps: u16) -> Gallows { + if max_attemps == 0 { + return Gallows { + current_attemp: 0, + max_attemps: 0, + is_alive: false, + }; + } + + return Gallows { + current_attemp: 1, + max_attemps: max_attemps, + is_alive: true, + }; + } + + pub fn next_stage(&mut self) { + if !self.is_alive { + return; + } + + self.current_attemp += 1; + self.is_alive = self.current_attemp <= self.max_attemps; + } + + pub fn get_percent(&self) -> u16 { + let percent = self.current_attemp * 100 / self.max_attemps; + percent as u16 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn try_to_give_zero() { + let gallows = Gallows::new(0); + + assert_eq!(false, gallows.is_alive); + } + + #[test] + fn try_to_make_dead() { + let mut gallows = Gallows::new(3); + assert_eq!(true, gallows.is_alive); + + gallows.next_stage(); + gallows.next_stage(); + assert_eq!(true, gallows.is_alive); + + gallows.next_stage(); + assert_eq!(false, gallows.is_alive); + } +} diff --git a/hangman_game/src/game/objects/guessed_row.rs b/hangman_game/src/game/objects/guessed_row.rs new file mode 100644 index 0000000..7f561f3 --- /dev/null +++ b/hangman_game/src/game/objects/guessed_row.rs @@ -0,0 +1,33 @@ +pub struct GuessedRow { + pub current_guessed: Vec, + secret_word: String, +} + +impl GuessedRow { + pub fn new() -> GuessedRow { + GuessedRow { + current_guessed: Vec::new(), + secret_word: String::new(), + } + } + + pub fn generate_new(&mut self, random_word: &String, random_word_len: usize) { + self.current_guessed.clear(); + self.secret_word = random_word.clone(); + for _ in 0..random_word_len { + self.current_guessed.push("_".to_string()) + } + } + + pub fn update_row(&mut self, guess: &String) { + for (idx, pair) in guess.chars().zip(self.secret_word.chars()).enumerate() { + if pair.0 == pair.1 { + self.current_guessed[idx] = pair.0.to_string(); + } + } + } + + pub fn is_guessed(&self) -> bool { + !self.current_guessed.contains(&"_".to_string()) + } +} diff --git a/hangman_game/src/game/terminal.rs b/hangman_game/src/game/terminal.rs new file mode 100644 index 0000000..0e14283 --- /dev/null +++ b/hangman_game/src/game/terminal.rs @@ -0,0 +1,65 @@ +use std::{ + env, + io::{self, Error, Write}, + process::Command, +}; + +enum Platform { + Unix, + Windows, +} + +pub struct Terminal { + platform: Platform, +} + +impl Terminal { + pub fn new() -> Terminal { + let os = env::consts::OS; + + let platform = if os == "windows" { + Platform::Windows + } else { + Platform::Unix + }; + + Terminal { platform: platform } + } + + pub fn get_input(&self, question: String) -> Result { + println!("{}", question); + print!("> "); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + + match io::stdin().read_line(&mut input) { + Ok(_) => { + let input: String = input.trim().to_string(); + + return Ok(input); + } + Err(err) => { + return Err(err); + } + }; + } + + pub fn clear_screen(&self) { + match self.platform { + Platform::Windows => { + Command::new("cls").status().unwrap(); + } + Platform::Unix => { + Command::new("clear").status().unwrap(); + } + } + } + + pub fn wait_enter(&self) { + println!("\nНажмите, чтобы продолжить..."); + let mut string: String = String::new(); + + io::stdin().read_line(&mut string).unwrap(); + } +} diff --git a/hangman_game/src/lib.rs b/hangman_game/src/lib.rs new file mode 100644 index 0000000..08abd22 --- /dev/null +++ b/hangman_game/src/lib.rs @@ -0,0 +1,186 @@ +mod game; + +use game::{GameCLI, GameCall, Terminal}; + +fn get_u16_number(question: String, default: u16, terminal: &Terminal) -> u16 { + match terminal.get_input(question) { + Ok(input) => match input.parse() { + Ok(num) => num, + Err(_) => { + println!("Будет использовано значение по-умолчанию..."); + default + } + }, + Err(_) => { + println!("Некорректный ввод! Будет использовано значение по-умолчанию..."); + default + } + } +} + +fn get_max_attemps(terminal: &Terminal) -> u16 { + let max_attemps_default: u16 = 10; + let max_attemps_question: String = format!( + "Введите максимальное кол-во попыток. [по-умолчанию: {}]\n(0 < x < 100)", + max_attemps_default, + ); + let max_attemps: u16 = get_u16_number(max_attemps_question, max_attemps_default, &terminal); + + let max_attemps = if max_attemps > 0 && max_attemps < 100 { + max_attemps + } else { + println!("Будет использовано значение по-умолчанию!"); + max_attemps_default + }; + + return max_attemps; +} + +fn get_new_words(terminal: &Terminal) -> Vec { + let mut words = Vec::new(); + + println!("Приступаем к добавлению новых слов..."); + println!("Вводите слова по одному! Нажмите ENTER для прекращения вывода."); + terminal.wait_enter(); + terminal.clear_screen(); + + loop { + let word: String = match terminal.get_input("Введите ваше слово или ENTER.".to_string()) + { + Ok(input) => { + let input = input.to_lowercase(); + + if input.len() == 0 { + break; + } else { + input + } + } + Err(_) => { + println!("Некорректный ввод!"); + terminal.wait_enter(); + terminal.clear_screen(); + continue; + } + }; + + words.push(word); + terminal.clear_screen(); + } + + words +} + +pub fn run() { + let terminal = Terminal::new(); + + println!("Давайте начнем первичкую настройку...\n"); + + let max_attemps: u16 = get_max_attemps(&terminal); + + println!(); + + let has_game_words_default: bool = true; + let has_game_words_question: String = format!( + "Хотите ли вы использовать игровой словарь? [да/нет, по-умолчанию: {}]", + if has_game_words_default { + "да" + } else { + "нет" + }, + ); + let has_game_words: bool = match terminal.get_input(has_game_words_question) { + Ok(input) => { + let input = input.to_lowercase(); + let input = input.as_str(); + + if input == "да" { + true + } else if input == "нет" { + false + } else { + println!("Некорректный ввод! Будет использовано значение по-умолчанию..."); + has_game_words_default + } + } + Err(_) => { + println!("Некорректный ввод! Будет использовано значение по-умолчанию..."); + has_game_words_default + } + }; + + let mut game_cli = GameCLI::new(max_attemps, has_game_words); + + terminal.wait_enter(); + + 'game_loop: loop { + terminal.clear_screen(); + game_cli.draw_start_message(); + + let action_default = 1; + let action_question = format!( + "\nВыберите действие. [1/2/3/4, по-умолчанию: {}]", + action_default + ); + let action_number = get_u16_number(action_question, action_default, &terminal); + + let action_number = if action_default <= 4 && action_default > 0 { + action_number + } else { + action_default + }; + + terminal.clear_screen(); + + if action_number == 1 { + match game_cli.make_session(&terminal) { + Ok(status) => match status { + GameCall::Exit => { + terminal.clear_screen(); + break 'game_loop; + } + GameCall::Nothing => {} + }, + Err(message) => { + println!("Ошибка: {}", message); + terminal.wait_enter(); + continue; + } + } + } else if action_number == 2 { + println!("Добро пожаловать в настройки!"); + terminal.wait_enter(); + terminal.clear_screen(); + + println!("Вывожу ваши текущие настройки...\n"); + game_cli.draw_game_settings(); + terminal.wait_enter(); + terminal.clear_screen(); + + println!("Для начала изменение кол-ва попыток...\n"); + let max_attemps = get_max_attemps(&terminal); + terminal.wait_enter(); + terminal.clear_screen(); + + let new_words = get_new_words(&terminal); + terminal.clear_screen(); + + game_cli.setting_game(max_attemps, &new_words, &terminal); + terminal.clear_screen(); + + println!("Вывожу ваши текущие настройки...\n"); + game_cli.draw_game_settings(); + terminal.wait_enter(); + terminal.clear_screen(); + continue; + } else if action_number == 3 { + game_cli.draw_game_stats(); + } else { + break 'game_loop; + } + + terminal.wait_enter(); + } + + game_cli.draw_end_message(); +} diff --git a/hangman_game/src/main.rs b/hangman_game/src/main.rs index e7a11a9..793676b 100644 --- a/hangman_game/src/main.rs +++ b/hangman_game/src/main.rs @@ -1,3 +1,5 @@ +use hangman_game::run; + fn main() { - println!("Hello, world!"); + run(); }