Пишем игру-угадайку на Rust

Rust Примечания
Пишем игру-угадайку на Rust

Эта статья была впервые опубликована вОфициальный сайт Луожу, добро пожаловать на станцию.

Угадай числовой код игры вguessing_game, добро пожаловать на клонирование.

Эта статья является третьей в серии Pomodoro Rust, которая на данный момент включает:

За последние два дня Луожу был опрыскан слишком большим количеством овощей, а статьи все еще публикуются повсюду😭. Я извиняюсь перед всеми здесь. Смысл моих извинений в том, что я не должен отправлять его в различные группы переднего плана для разоблачения. Я все равно буду настаивать на написании этой заметки и настаивать на том, чтобы разослать ее для всеобщего обозрения. —— Восхитительно, но увлекательно

После двух дней размышлений и объяснений здоровяка я понял, что настоящий смысл таков:Лучший способ освоить новый навык — это иметь интерес, место, где можно поделиться, и сеть единомышленников.В конце статьи есть QR-код для группы обмена Rust Добро пожаловать к нам.

Для того, чтобы облегчить всем обучение, нужно бежать (cargo run) Я делал скриншоты гифок везде, где видел эффект. Если вам это нравится, вы можете поощрить Луо Чжу 👍🏻.

окрестности

  • ржавчина:1.50.0 (cb75ad5db 2021-02-10)

  • груз:1.50.0 (f04e7fab7 2021-02-04)

  • randящик:0.8.3

  • vscode plugins

    • Better TOML
    • CodeLLDB
    • crates
    • rust-analyzer
  • о-мой-зш тема: ys

  • Программа для скриншотов:Kap

Обработка пользовательского ввода

Первая часть игры в угадайку попросит пользователя ввести приведенный выше код:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failded to read line");

    println!("You guessed: {}", guess);
}

декларативный импорт

  • use std::io;Заявление: поставить стандартную библиотеку (std)серединаioМодули импортируются в текущую область.
  • По умолчанию Rust автоматически вводит в область видимости каждой программы записи в модулях prelude, которые содержат небольшой набор довольно распространенных типов. Если нужного вам модуля нет в предварительно импортированном модуле, то мы должны использоватьuseОператор для явного объявления импорта.
  • std::idБиблиотека содержит множество полезных функций, которые мы можем использовать для получения входных данных пользователя.

Используйте переменные для хранения значений

  • let mut guess: создаетguessпеременные переменные, переменные неизменяемы по умолчанию,mutКлючевое слово представляет изменяемую переменную, а концепции переменных и изменчивости находятся вЗаметки по изучению грамматики Rust для двух ПомидоровЕсть вводные.
  • String::new: возвращает новыйStringПример
    • ::грамматика показываетnewдаStringассоциированная функция типа — известная в других языках как статический метод
    • StringЭто строковый тип в стандартной библиотеке, который использует внутреннюю кодировку UTF-8 и может увеличивать свой размер по мере необходимости.
  • io::stdin:возвращениеstd::io::Stdin, который используется в качестве дескриптора для обработки стандартного ввода в терминале.
  • .read_lineМетод используется для получения пользовательского ввода.
    • read_lineпараметры&mut guessПредставляет ссылку на изменяемую переменную.
    • &Указывает, что текущий параметр является ссылкой, которая также существует в Go. Потому что большинство этих языков программирования системного уровня предоставляют разрешения на манипулирование памятью, а JavaScript не дает пользователям этих функций.

Дескрипторы: дескрипторы широко использовались в управлении памятью операционных систем в 1980-х годах, таких как дескрипторы Mac OS и Windows. Файловые дескрипторы в системах Unix тоже в основном являются дескрипторами. Как и в других средах рабочего стола, Windows API широко использует дескрипторы для идентификации объектов в системе и для создания каналов связи между операционной системой и пользовательским пространством. Например, форма на рабочем столе состоит изHWNDДескриптор типа для идентификации.

Обработка возможных сбоев

Давайте сначала посмотрим на обработку ошибок в Go:

func main() {
  result,err := getResult();
  if err != nil {
    panic(err)
  }
  fmt.Printf("The result is %s", result);
}

func getResult() string,(string, err error) {
  // some codes
}

Как правило, если функция может сообщить об ошибке, она в то же время вернет результат.error. Это не специфично для Go, это то же самое в Rust, просто реализовано по-другому.

В предыдущем кодеread_lineсохранит пользовательский ввод в строку, которую мы передали, но в то же время он также возвращаетio::Resultценность. В стандартной библиотеке Rust вы можете найти множествоResultименованные типы, которые обычно находятся в различных подмодуляхResultконкретные версии дженериков, как здесьio::Result.

Resultявляется типом перечисления. Тип перечисления состоит из фиксированного ряда значений, которые называются вариантами перечисления. (Как мы все знаем, в Javascript нет типа перечисления)

заResult, она имеетOkиErrдва варианта. один из нихOkВарианты указывают на то, что текущая операция выполнена успешно, а результирующее значение создается кодом. Соответственно,ErrВарианты указывают на сбой текущей операции с указанием конкретной причины сбоя.

expectдаResultОдин из серии методов для значений типа. еслиio::ResultСтоимость экземпляраErr,ТакexpectМетод прервет текущую программу и отобразит входящий строковый параметр.

read_lineМетод может возвращатьErrрезультат.

Соответственно, еслиio::ResultСтоимость экземпляраOk,Такexpectбудет извлеченOkи вернуть его пользователю в качестве результата. В нашем случае это значение — количество байтов пользовательского ввода.

даже если мы не вызываем в конце оператораexpect, эту программу также можно скомпилировать, но в процессе компиляции вы увидите предупреждающее сообщение, показанное ниже. Это в основном то же самое, что и Go.

Компилятор Rust напоминает намread_lineметод возвращенResultЗначение еще не обработано, что обычно означает, что наша программа не обработала возможную ошибку.

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

пройти черезprintln!Заполнитель в выводе соответствует значению

println!("You guessed: {}", guess);Пользовательский ввод, который мы сохранили, можно распечатать. Первым параметром этого вызова макроса является строка, используемая для форматирования, а фигурные скобки в строке{}является заполнителем.

узнать по аналогии:{}Поскольку заполнители не являются специфическими для Rust, в оболочках есть похожие приложения, о которых я знаю, напримерls | xargs -I {} tar zcvfm {}.tar.bz {}Этот скрипт означает, чтоlsВыходные значения передаются в последующие команды одна за другой,{}Это заполнитель для получения значения, переданного конвейером. Кстати, этот скрипт у меня есть вtuya-panel-demoЕсть польза. Выкопайте здесь яму, и последует серия руководств по сценариям оболочки.

попробуй запустить код

Теперь воспользуемсяcargo runкоманда, чтобы попробовать запустить этот код:

сгенерировать секретный номер

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

Представить ранд пакет

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

Примечание. Крейт в Rust представляет собой набор файлов с исходным кодом. Проект, который мы сейчас создаем, представляет собой бинарный крейт для создания исполняемых программ, и мы представляемrandПакет — это библиотечный пакет (libray crate, code package), используемый для повторного использования функций.

Чтобы использовать третью сторонуcrate, мы должныCargo.tomlДобавьте зависимости в файл:

...
[dependencies]

rand = "0.3.14"

существуетCargo.tomlВ файле все, от одного заголовка до другого, относится к одной и той же области. здесь[denpendencies]Область используется для объявления всех зависимостей и их номеров версий, которые необходимо использовать в проекте.

Номер версии в книге относительно старый, и плагин vscode crates подсказывает версию❌

Мы можем щелкнуть любую версию, чтобы выбрать эту версию в качестве версии, от которой мы зависим, и щелкнуть документы в скобках, чтобы перейти к документации библиотеки.

Давайте пересоберем проект напрямую, без изменения кода:

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

cates.io в экосистеме Rust — это веб-сайт, на котором люди могут делиться различными проектами Rust с открытым исходным кодом.

Теперь, если вы не внесли никаких изменений, немедленно перезапуститеcargo build, то появится только сообщение Готово. Cargo автоматически проанализирует, что в данный момент загружено или скомпилировано, и пропустит шаги, которые не нужно повторять.

генерировать случайное число

+ use rand::Rng;
use std::io;

fn main() {
    println!("Guess the number!");

+   let secret_number = rand::thread_rng().gen_range(1..101);

+   println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failded to read line");

    println!("You guessed: {}", guess);
}
  • Мы добавили дополнительную строкуuseЗаявление:use rand::Rng. Здесь Rng — трейт, определяющий набор методов, которые должен реализовать генератор случайных чисел. Чтобы использовать эти методы, нам нужно явно ввести его в текущую область видимости.
  • rand::thread_rng()вернет определенный генератор случайных чисел. Затем мы вызываем генераторgen_rangeметод.
  • gen_rangeМетод был только что введен в область примененияRngtrait, который принимает тип Range (1..100) в качестве аргумента и генерирует случайное число в диапазоне, предшествующем обоим.

Напоминание: в книгеgen_rangeОн получает два параметра, не удивляйтесь, когда прочтете, версия другая.

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

Со сторонними пакетами неизбежно часто проверять документацию. Вы можете перейти к официальной документации, чтобы увидеть, нет никаких проблем. Но вы также можете использоватьcargo doc --openКоманда создает локальную документацию по всем зависимостям и автоматически открывает документацию в вашем браузере, чтобы вы могли ознакомиться с ней:

В приведенном выше коде мы распечатываем секретный номер только для отладки, а затем удаляем этот код.

Сравнение угаданных чисел с секретными числами

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

use rand::Rng;
+ use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failded to read line");

    println!("You guessed: {}", guess);

+   match guess.cmp(&secret_number) {
+       Ordering::Less => println!("Too small!"),
+       Ordering::Greater => println!("Too big!"),
+       Ordering::Equal => println!("You win!"),
+   }
}

  • Мы импортировали из стандартной библиотекиstd::cmp::Orderingтип. Как и Result, Ordering также является типом перечисления, который имеетLess,Greater,EqualЭти 3 варианта. (cmp для сравнения)
  • matchВыражение состоит из нескольких ветвей (ветвей), каждая из которых содержит шаблон для сопоставления и соответствующий код, который должен быть выполнен после успешного совпадения.

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

Приведенный выше код в настоящее время не может скомпилироваться:

Причина ошибки в том, что мы сохранилиguessТип переменной — String, секретное число — целочисленный тип, и разные типы не могут сопоставляться (в статических языках нет неявного преобразования типов).

Чтобы операция сравнения работала правильно, нам нужно преобразовать ввод, прочитанный в программе, из типа String в числовой тип:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failded to read line");

+   let guess: i32 = guess.trim().parse().expect("Please type a number");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
  • Здесь мы создаем новую неизменяемую переменнуюguess, у которого то же имя, что и у предыдущего, но это допустимо, и Rust позволяет новой переменной с тем же именем скрывать значение старой переменной. Эта функция часто используется в сценариях, где необходимо преобразовать типы значений, и в этом случае позволяет нам повторно использовать предположение имени переменной без необходимости создавать другое имя, такое как Guess_str .

  • guess.trim()заключается в удалении всех пробелов в начале и в конце.

  • нитьparseМетод попытается преобразовать текущую строку в числовое значение. Поскольку этот метод может обрабатывать различные числовые типы, нам нужно передать операторlet guess: i32для явного объявления нужных нам числовых типов.

Теперь снова запустим нашу программу:

Игра в основном сформирована, но игрок может сделать только одно предположение, чего явно недостаточно.Далее мы добавим цикл для улучшения игры.

Используйте циклы для реализации нескольких предположений

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    // `..` 语法是标准库中提供的 Range
    let secret_number = rand::thread_rng().gen_range(1..101);
+   loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failded to read line");

        let guess: i32 = guess.trim().parse().expect("Please type a number");

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
+           Ordering::Equal => {
+               println!("You win!");
+               break;
+           }
        }
+   }
}
  • Мы переместили все после того, как предложили пользователю принять угадывающее решениеloopсередина. В ржавчинеloopключевое слово создает бесконечный цикл.
  • Мы также добавилиbreakоператор, чтобы игрок мог нормально выйти из игры после правильного угадывания числа.

Запустите программу, игрок правильно угадает число, после вывода **You Win!** программа завершит работу:

Обработка недопустимого ввода

в преобразованииguessСтрокаnumberтип, мы используемexpect("Please type a number")Для обработки возможных ошибок мы пытаемся ввести не-numberЗапустите значение типа:

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

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    // `..` 语法是标准库中提供的 Range
    let secret_number = rand::thread_rng().gen_range(1..101);
    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failded to read line");

        let guess: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
              println!("Please type a number!");
              continue;
            }
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
  • мы использовалиmatchвыражение для замены предыдущегоexpectметод, который является полезным инструментом для нас, чтобы иметь дело с плохим поведением.
  • parseвернет тип результата иResultтип включаетOkиErrдва варианта.

В общем, давайте запустим этот проект и попробуем:

История не окончена

Оглядевшись, я не нашел группу по обмену Rust, поэтому создал ее сам.

У Tuya Smart большое количество качественных НС, присоединяться могут все желающие, вам нужно добавить меня в WeChat yang_jun_ning, или отправить свое резюме прямо на почтуyoungjuning@aliyun.com

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