Прошло больше года с тех пор, как я впервые познакомился с Rust как с языком программирования, и я собирался написать статью для Medium еще дольше… Итак, теперь я решил сделать решительный шаг и рассказать об одном из первых проектов, которые я создал на Rust, Hangman.

Что такое Rust?

Итак, первый вопрос: что такое Rust и как с ним начать? Если вы хотите узнать больше о Rust как о языке, Книга Rust предоставит вам это. Однако, вкратце, Rust — это язык программирования, разработанный для повышения производительности и безопасности памяти. Он имеет синтаксис, аналогичный C++, но с дополнительным преимуществом, заключающимся в том, что он разработан с учетом безопасности памяти.

Настройка Rust

Опять же, взято прямо из Rust Book…. Чтобы установить Rust в операционных системах MacOS или Linux, вы можете запустить приведенную ниже команду и следовать инструкциям для завершения установки.

$ curl — proto ‘=https’ — tlsv1.2 https://sh.rustup.rs -sSf | ш

Начало проекта

Итак, теперь, когда мы установили и правильно настроили Rust на компьютере, первое, что нужно сделать, это создать рабочее пространство проекта. Для этого в Rust есть встроенный менеджер пакетов Cargo, который облегчит эту задачу. Просто создайте папку, в которой вы хотите работать:

$ mkdir ./Документы/my_first_rust_program

А затем перейдите в этот каталог и инициализируйте запуск проекта Rust:

$ грузовая инициализация

Давайте сделаем здесь паузу и посмотрим, что у нас получилось после инициализации проекта.

  • cargo.toml:содержит сведения о «пакете» или проекте, над которым вы работаете. В нем также перечислены зависимости, на которые опирается код. Без указания зависимости здесь при запуске проекта будет ужасная ошибка!!
  • cargo.lock: автоматически созданный файл с полной информацией об используемых в проекте пакетах. НЕ РЕДАКТИРОВАТЬ ЭТО.
  • src/:папка, которая будет содержать код проекта, включая файл main.rs, который будет быть файлом, который запускается при компиляции кода.

Создание игры из командной строки

Рассмотрев то, что Rust делает для вас, пришло время углубиться в детали того, как создать игру-палач из командной строки. Особенностями этого будут: возможность использовать настраиваемые списки слов для разнообразия игры, выбирать слово из списка случайным образом, разрешать догадки пользователя, проверять, есть ли буква в слове, и раскрывать ее, а также терять жизни, если буква отсутствует. не в слове.

Чтение списка слов

Чтобы позволить программе присваивать имена файлам при запуске, на первом этапе необходимо создать структуру Arguments, которая представляет собой настраиваемый тип данных, который можно использовать повторно.

pub struct Arguments {
    pub flag: String,
    pub filename: String,
}

Чтобы структура могла быть создана, для нее должна быть реализация. В этом случае реализация будет содержать функцию с именем new, которая будет принимать массив строковых аргументов и возвращать нашу пользовательскую структуру данных. Поскольку нам нужны только определенные входные данные для нашей программы, когда она запускается в командной строке, функция new также будет проверять входные данные и выдавать ошибки, если они неверны.

impl Arguments {
    
    pub fn new(args: &[String]) -> Result<Arguments, &'static str> {
      // If there are too few or too many arguments raise an error  
        if args.len() < 2 {
            return Err("Not enough arguments");
        } else if args.len() > 3 {
            return Err("Too many arguments");
        }
        // Having checked there are enough arguments, get the second 
           one from the array as this will be the flag
        
        let f = args[1].clone();
        // If the flag contains the help option and the length of   
           args is equal to 2 display the help menu
        if f.contains("-h") || f.contains("-help") && args.len()==2{
            println!("Usage:
            \n\r -h or -help to show this help menu
            \n\r -g or -game to play the game and specify wordlist 
            \n\r ==========================================
            \n\r Examples:
            \n\r hangman -g simple.txt
            \n\r hangman -g advanced.txt");
            return Err("help");
        } else if f.contains("-h") || f.contains("-help") {
            return Err("Too many arguments, use -h or -help to 
                        display usage guide");
     
        } else if args.len() == 3 {
            // If arguments has a length of 3 and the flag is -g 
               then this is a valid game and return the Arguments     
               data structure with these assigned
            if f.contains("-g") || f.contains("-game"){
                if args[2].ends_with(".txt") {
                    let filename = &args[2].clone();
                    return Ok(Arguments {flag: f, filename:           
                                         filename.to_string()});
                }else {
                    return Err("Invalid filename; must end with  
                                .txt");
                }
            } else {
                return Err("Invalid syntax, use -h or -help to 
                            display usage guide")
            }
        } else {
            return Err("Invalid Syntax");
        }
    }
}

Хорошо, теперь у нас есть структура пути для хранения аргументов, переданных при запуске программы, и функция для проверки их корректности. Теперь пришло время запустить это в основном скрипте и протестировать выходные данные.

use std::env;
use std::fs;
use std::process;
fn main() {
    // Collect the arguments supplied when the program is called       
    let args: Vec<String> = env::args().collect();
    let arguments = misc::Arguments::new(&args).unwrap_or_else(
        |err| {
            if err.contains("help") {
                // if error is help close program
                process::exit(0);
            } else {
                // if error is not help print error then close 
                   program
                eprintln!("{} problem parsing arguments: {}",
                              args[0], err);
                process::exit(0);
            }
        }
    );
    // Read in a wordlist file
    let wordlist_raw = fs::read_to_string(arguments.filename)
        .expect("Something went wrong reading the file");
    let split = wordlist_raw.split("\n");
    let wordlist_vec: Vec<&str> = split.collect();
}

Запуск проекта теперь позволит нам увидеть сообщения об ошибках, которые были написаны при реализации Arguments. Если параметры справки используются в качестве входных данных, отображается меню справки, чтобы предоставить пользователям инструкции о том, как запустить игру. Наконец, если предоставлены правильные входные данные, то файл со списком слов считывается.

Логика палача

Дойдя до этапа, когда список слов считывается в программу, пришло время подумать о логике палача. Во-первых, слово должно быть выбрано случайным образом из списка слов. Для этого в Rust есть пакет под названием Rand, в который встроены функции для генерации случайного целого числа. Используя это целое число, можно случайным образом выбирать слово для списка при каждом запуске игры.

use rand::Rng;
fn main() {
    ....
    let mut rng = rand::thread_rng();
// Select a random word from the wordlist
    let index_val = rng.gen_range(0, wordlist_vec.len());
    let selected_word = wordlist_vec[index_val];
}

Теперь, когда слово выбрано, его нужно каким-то образом скрыть, чтобы человек, играющий в игру, знал количество букв, но не мог видеть слово. Для этого создается изменяемая переменная с именем masked_word, содержащая повторяющиеся «-» до длины слова. Создание этого изменчивого объекта пригодится позже, при обработке верных догадок игрока.

use std::iter;
fn main() {
    ....
    // Mask the string as a String of "-"
    let masked_word = iter::repeat("-")     
                    .take(selected_word.len()).collect::<String>();
}

Теперь, когда есть несколько объектов, за которыми нужно следить в связи с игрой, самое время создать структуру для хранения данных игры. Эта структура должна отслеживать количество жизней, которое осталось у игрока. Обновление оставшихся жизней позволяет создать правильный рисунок в терминале. Далее идут letters_guessed, которые отслеживаются, чтобы убедиться, что пользователь не отправит одно и то же письмо дважды. оставшиеся_буквы также можно отслеживать и обновлять при угадывании букв. Эти буквы отображаются игрокам, чтобы убедиться, что они знают, что доступно. Последние два аспекта для отслеживания каждой игры — это маскированное_слово ислово_для_угадывания.

struct Game {
    lives: i32,
    letters_guessed: String,
    word_to_guess: String,
    masked_word: String,
    remaining_letters: String,
}
fn main(){
    ....
    let mut current_game = Game {
        lives: 6i32,
        letters_guessed: String::from(""),
        word_to_guess: String::from(selected_word),
        masked_word: masked_word,
        remaining_letters: String::from("a b c d e f g h i j k l m n 
                                         o p q r s t u v w x y z"),
    };
}

Последнее, что нужно подготовить перед тем, как перейти к основному игровому циклу, — это создание чертежей терминала для различных этапов игры. Для этого функция print_hangman принимает количество оставшихся жизней и выводит на терминал соответствующий рисунок. В Rust встроен оператор match, который выполняет определенный код при выполнении критерия. Так что в случае с этой игрой каждый раз, когда количество жизней уменьшается, мы можем сопоставить число с одним из рисунков. Чтобы использовать оператор match, должна быть опция «всеобъемлющая», которая будет представлять начальное состояние игры.

fn print_hangman(lives_left: i32) {
    match lives_left {
        0i32 =>
         {
            println!(" _______  ");
            println!("|       |  ");
            println!("|       XO  ");
            println!("|      /|\\ ");
            println!("|      / \\ ");
            println!("|           ");
            println!("|           ");
            println!("____________");
        }
        ....
        _ =>
         {
            println!("          ");
            println!("          ");
            println!("          ");
            println!("            ");
            println!("        O   ");
            println!("       /|\\ ");
            println!("       / \\"); 
            println!("____________");
        }
    }
}

Цикл игры

Проще говоря, игровой цикл — это цикл while, который проверяет, не выполнены ли еще условия, завершающие игру. Это: количество жизней больше нуля и замаскированное слово, не равное слову, которое нужно угадать. Пока эти условия верны, игра продолжается, если любое из этих условий ложно, игра заканчивается.

Итак, что происходит в игре…

Сначала пользователю предоставляется количество оставшихся жизней, в зависимости от количества оставшихся жизней, печатается состояние игры, за которым следуют оставшиеся буквы, а затем ввод, чтобы получить предположение игрока.

На всякий случай в стандартном пакете Rust есть модуль ввода/вывода (std::io). Этот модуль имеет функцию сбора входных данных с терминала. Входные данные сохраняются в виде строк, а символ ввода удаляется перед обработкой предположения.

fn main() {
    ....
    // Main game loop
    while current_game.lives > 0 && 
            current_game.masked_word != current_game.word_to_guess {
        
        println!("You have {} lives remaining", current_game.lives);
        print_hangman::print_hangman(current_game.lives);
        println!("{}", current_game.masked_word);
        println!("Remaining letters to choose from: {}",
                     current_game.remaining_letters);
        println!("Please guess a letter:");
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        guess = guess.replace("\n", "");
        // Clear the terminal        
        print!("{}[2J", 27 as char);
        ....

На этом этапе догадка не имеет ограничения на количество символов и, следовательно, может содержать больше букв, чем слово, и теряет смысл игры. Это также может быть буква, которая уже угадана. Поэтому, прежде чем продолжить проверку слова, необходимо проверить ввод, чтобы убедиться, что он действителен.

fn main() {
    ....
    // Main game loop
    while current_game.lives > 0 && 
            current_game.masked_word != current_game.word_to_guess {
        ....
        if guess.len() > 1 || guess.len() == 0 {
            println!("Invalid guess, enter single character");
        } else if current_game.letters_guessed.contains(&guess) {
            println!("Invalid guess, you've already tried that 
                       letter");
        }
        ....

Если обе эти проверки пройдены, то предположение можно сравнить с word_to_guess. Эта проверка состоит из двух частей: во-первых, проверяется, содержит ли слово букву. Если это так, то masked_word обновляется, чтобы буквы отображались в правильном месте. . Если нет, то игрок теряет жизнь. В обоих случаях буквы_догадки и буквы_оставшиеся структуры игры также обновляются, чтобы отразить предположение.

fn main() {
    ....
    // Main game loop
    while current_game.lives > 0 && 
            current_game.masked_word != current_game.word_to_guess {
        ....
        } else if current_game.word_to_guess.contains(&guess){
            println!("Your guess, {}, is in the word", guess);
            current_game.masked_word = String::new();
            current_game.letters_guessed.push_str(&guess);
            
            for c in current_game.word_to_guess.chars() {
                if current_game.letters_guessed.contains(c){
                    current_game.masked_word.push(c);
                } else {
                    current_game.masked_word.push('-');
                }
            }
        } else {
            println!("Your guess, {}, is not in the word", guess);
            current_game.letters_guessed.push_str(&guess);
            current_game.lives -= 1;
        }
    }
}

Победа или поражение в игре

Ладно, сразу домой. Что происходит, когда критерии цикла while ложны и игра вырывается из него?

Это последняя часть логики, которая будет проверять, выиграл игрок или проиграл игру. Учитывая структуру игры, созданную ранее, все, что нужно сделать, — это проверить, больше ли количество оставшихся жизней нуля. Если это так, то игрок выиграл игру, в противном случае у него не осталось жизней, и он проиграл.

fn main(){
    ....
    if current_game.lives > 0 {
        println!("Congratulations, you guess the word: {}",
                   current_game.word_to_guess);
        } else {
        println!("Better luck next time, the word was: {}",
                   current_game.word_to_guess);
    }
}

После отображения соответствующего результата остается только спросить, не хочет ли игрок сыграть еще одну игру.

fn main() {
    ....
    println!("Would you like to play again? (y/n)");
    let mut play_again = String::new();
    io::stdin()
            .read_line(&mut play_again)
            .expect("Failed to read line");
        if play_again.contains('y'){
        main();
    } else {
        println!("See you again soon!");
        process::exit(0);
    }
}

Теперь осталось только запустить игру и повеселиться!!

Выводы

Для меня это была одна из первых программ, написанных на Rust. Для меня этот опыт охватил широкий спектр концепций Rust, и это было весело и приятно. Раньше я кодировал на других языках, так что это помогало с логикой, но для меня кодирование в игре — это способ сделать это.

Я надеюсь, что изучение этого руководства позволило вам впервые познакомиться с Rust и узнать что-то новое. Полный код можно найти в этом репозитории GitHub.