All Articles

추론 게임 프로그래밍

추측 게임을 통해 Rust의 개념에 대해 알아보고, let, match, method, 관련 function, 외부 crates 사용 방법에 대해 알아보자.

새 프로젝트 생성

cargo new guessing_game
cd guessing_game

processing a guess

추측 게임 프로그램의 첫 부분은 사용자에게 입력을 요청하고, 해당 입력이 예상한 형식인지 확인하는 것 입니다.

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("Failed to read line");

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

사용자의 입력을 받기 위해 std::io 라이브러리를 사용하였습니다.

Rust의 기본 라이브러리 정보는 prelude에서 확인할 수 있습니다.

변수 초기화

다음과 같이 사용자 입력값을 저장할 변수를 초기화 할 수 있습니다.

    let mut guess = String::new();

let은 변수를 생성하는데 사용되는 문법 입니다.

mut은 변수를 수정할 수 있는 특성을 부여하는 문법 입니다.

let apples = 5; // 수정이 불가능한 변수
let mut bananas = 5; // 수정이 가능한 변수

다시 코드로 돌아가서 ley mut guess는 변경 가능한 변수 quess를 의미합니다.

String은 표준 라이브러리 에서 제공하는 문자열 타입이며, new를 이용해 비어있는 새로운 String 타입의 인스턴스를 생성하게 됩니다.

코드 첫 줄에 입력한 use std::io를 통해 터미널 표준 입력에 대한 핸들러 stdin 함수의 사용할 수 있게 됩니다.

    io::stdin()
        .read_line(&mut guess)

.read_line(&mut guess)read_line 메서드를 호출하여 사용자로 부터 입력을 받고 그 값을 guess 변수에 저장합니다.

& 인수를 이용하여 &mut guess를 인수로 전달하면 guess 변수를 수정할 수 있는 참조를 반환합니다.

참조 변수는 메모리를 여러번 복사할 필요 없이 데이터의 코드 위치를 참조하는 것을 허용합니다.

Rust는 메모리 참조 쉽고 안전하게 사용할 수 있도록 제공하며, 세부사항은 신경쓰지 않아도 됩니다.

변수의 선언과 마찬가지로 &mut guess는 변경 가능한 참조 변수, &guess는 변경 불가능한 참조 변수를 의미합니다.

Result 우형으로 잠재적 실패 처리

expectread_line의 실패를 처리하는 데 사용됩니다.

        .expect("Failed to read line");

.method_name()구문을 사용하는 메서드를 호출할 때 긴 줄을 구분하기 위해 개행 및 공백을 사용하는것이 좋습니다.

즉 다음 두 코드는 동일하게 동작하는 것입니다.

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

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

다시 코드로 돌아와서, read_line은 사용자가 입력한 내용을 변수에 전달하는 기능을 하지만, std::Result도 반환 합니다.

사용자가 입력한 내용이 없을 경우 오류를 발생시키는 것을 방지하기 위해 Result 타입을 사용합니다.

Resultenum 타입으로, Ok 또는 Err`를 가지고 있습니다.

Err는 작업 실패를 의미하며, 실패 방법이나 이유에 대한 정보를 가지고 있습니다.

이러현 Result 타입의 목적은 오류 처리 정보를 인코딩 하는 것으로, 이 Result에는 호출 가능한 expect 메서드가 있으며, io::ResultErr일 경우 expect에 의해 메시지가 출력 되는 것 입니다.

또한 exprct를 호출하지 않을 경우 컴파일은 되지만 경고가 출력됩니다.

$ cargo build
   Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 | /     io::stdin()
11 | |         .read_line(&mut guess);
   | |_______________________________^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s

Rust는 read_line에서 오류 처리를 제공하는 Result에 대한 반환 값을 사용하지 않았다고 경고하고 있습니다.

컴파일 단계에서 warning 메시지는 실행에는 문제가 없을지 몰라도, 문제가 발생했을 때 프로그램을 중단시키기 때문에 expect를 이용해 오류 북구를 처리하는 것이 좋습니다.

변수 출력

println!을 이용해 입력받은 guess를 출력 합니다.

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

중괄호를 이용하여 변수를 출력할 수 있습니다.

첫 번째 테스트

cargo run 명령어를 사용하여 추측 게임의 첫 번째 부분을 테스트해 보겠습니다.

cargo run
   Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
10
You guessed: 10

게임의 첫 부분인 키보드 입력을 받고 출력하는 부분까지 완성 되었습니다.

비밀번호 생성

사용자가 추측해야 하는 비밀번호를 생성 해야 합니다.

비밀번호는 매번 달라야 게임이 재미있으며, 너무 어렵지 않도록 1에서 100까지의 숫자를 생성 해야 합니다.

Rust는 아직 표준 라이브러리에서 난수 기능을 제공하지 않고 있기 때문에 rand crates를 사용할 것입니다.

crates 사용하기

crates는 다른 프로젝트에서 사용할 수 있도록 제공되는 Rust 소스 코드 파일의 모음입니다.

다음과 같이 Cargo.toml[dependencies] 섹션 정보에 crates 정보를 추가해 rand crates를 사용할 수 있도록 설정합니다.

rand = "0.8.4"

위 정보는 rand crates 0.8.4 버전을 사용하겠다는 의미입니다.

Cargo는 버전 정보 작성을 위한 Semantic Versioning을 지원합니다.

버전 정보 0.8.4는 실제로는 ^0.8.4의 줄임말이며, 0.8.4이상 0.9.0 이하의 모든 버전을 의미합니다.

Cargo는 ^0.8.4에 해당하는 버전이 호환되는 API를 갖춘것으로 간주하며, 최신 패치 릴리스를 얻을 수 있음을 보장합니다.

코드 변경없이 프로젝트를 빌드해 보겠습니다.

cargo build
    Updating crates.io index
  Downloaded rand_core v0.6.3
  Downloaded cfg-if v1.0.0
  Downloaded rand_chacha v0.3.1
  Downloaded ppv-lite86 v0.2.15
  Downloaded rand v0.8.4
  Downloaded libc v0.2.108
  Downloaded getrandom v0.2.3
  Downloaded 7 crates (740.3 KB) in 0.96s
   Compiling libc v0.2.108
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.15
   Compiling getrandom v0.2.3
   Compiling rand_core v0.6.3
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.4
   Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 25.97s

외부 종속성이 생겼으므로 Cargo는 crates.io의 데이터 복사본인 레지스트리에서 모든 최신 버전을 가져옵니다.

crates.io는 Rust 생태계의 다른 사람들이 사용할 수 있도록 오픈소스 Rust 프로젝트를 게시하는 곳 입니다.

레지스트리를 업데이트한 후 Cargo는 [dependencies] 섹션을 확인하고 가지고 있지 않은 crates를 다운로드 합니다.

Cargo.toml에는 rand만 추가하였지만, rand에 종속된 다른 crates도 다운로드하고 이를 컴파일해, 사용 가능한 종속성을 갖춘 프로젝트를 만들어 내는 것입니다.

변경 없이 다시한번 cargo build를 실행할 경우, Cargo는 이미 종속성을 다운로드하고 컴파일 했기 때문에 Finished 만 출력됩니다.

종속성 정보 변경 없이 코드만 수정되었을 경우에도 종속성 다운로드 없이 빌드만 진행하게 됩니다.

만약 rand crates가 버전업이 되었고, 제공되는 기능이 변경 되어 프로젝트의 코드를 손상시킨다 해도, Cargo.lock에 지정된 버전 정보를 이용하기 때문에 재현 가능한 빌드를 구성할 수 있습니다.

Cargo.lock에 명시된 버전으로 crates 사용함으로 프로젝트가 유지될 수 있습니다.

crates를 명시적으로 업데이트할 경우 cargo update를 이용해 버전을 업데이트할 수 있습니다.

난수 생성하기

rand crates를 추가했으므로, 이제 rand를 사용하여 난수를 생성할 수 있습니다.

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

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("Failed to read line");

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

rand::thread_rng는 난수를 생성하는 랜덤 생성기를 반환합니다.

현재 실행 스레드에 로컬이고 운영 체제에 의해 시드됩니다. 그런다음 난수 생성기에서 gen_range를 사용하여 난수를 생성합니다.

gen_range 메서드는 범위 식을 인수로 사용하고 범위에서 난수를 생성합니다.

여기서 범위 표현식의 형식은 start..end이며, 하한에는 포함되지만 상한에는 배타적이므로, 1에서 100사이의 숫자를 생성하려면 1..101과 같이 지정하거나, 1..=100와 같이 범위를 전달할 수도 있습니다.

crates의 메소드와 함수가 어떤 특성을 사용하는지 알아보기 위해서는 cargo doc를 실행하면 됩니다. cargo doc --open를 실행해 모든 종속 항목에서 제공하는 문서를 로컬에서 빌드하고 브라우저에서 확인할 수 있습니다.

테스트 중 어떤 비밀번호가 생성되는지 확인할 수 있도록 비밀번호를 출력하는 코드도 추가했습니다.

프로그램을 실행해 봅시다.

cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 54
Please input your guess.
10
You guessed: 10

cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 42
Please input your guess.
31
You guessed: 31

1과 100사이의 임의의 숫자가 생성되는 것을 확인할 수 있습니다.

추측과 비밀번호 비교

이제 사용자 입력과 난수가 있으므로 비교할 수 있습니다.

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

fn main() {
    // --snip--

    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은 enum 타입이며, Less, Greater, Equal 형태를 가집니다.

cmp 메서드는 두 값을 비교할 수 있는 모든 값에 대해 호출할 수 있습니다. 비교하려는 대상에 대한 참조가 필요합니다.

여기서 guesssecret_number를 비교하고 use문으로 포함시킨 Ordering enum 타입을 반환합니다.

추측값 guess에 비밀번호 secret_number를 사용하여 cmp에 대한 호출로 반환된 Ordering을 기반으로 다음에 수행할 작업을 결정합니다.

arm은 패턴과 match 표현식의 시작 부분에서 주어진 값이 이 arm 패턴에 맞을 경우 실행 되어야 하는 코드로 구성 됩니다.

Rust는 match에 주어진 값을 받아 각 arm의 패턴을 차례로 확인합니다. 이는 이 패턴이 일치하면 이 값을 반환하고 이 값이 일치하지 않으면 다음 패턴을 확인합니다.

이체 해당 코드를 실행해 봅시다.

cargo run
   Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |
   = note: expected reference `&String`
              found reference `&{integer}`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` due to previous error

오류의 핵심은 타입이 일치하지 않는다는 것 입니다. Rust는 강력한 정적 타입 시스템을 가지고 있습니다.

입력 받은 guessString 타입이고 secret_numberi32 타입입니다.

궁극적으로 String 타입으로 받고 있는 guess를 실수형으로 변환하여 숫자로 secret_number와 비교하도록 수정해야 합니다.

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

정수 타입의 guess를 생성합니다. 하지만 이전에 이미 String 타입의 guess를 선언 했습니다.

Rust는 이미 선언된 타입을 새 값으로 사용하는 새도잉을 허용하기 때문에 이같이 재사용하는 것이 가능합니다.

String 인스턴스의 .trim()은 앞뒤 공백을 제거합니다. 이를 통해 read_line을 충족하기 위해 사용자가 Enter키를 누른 결과인 \n을 제거합니다.

문자열의 parse 메서드는 문자열을 일종의 숫자로 구문 분석을 합니다.

이 방법은 다양한 숫자 타입을 구문 분석할 수 있기 때문에 let guess: u32를 사용하여 정확한 숫자 타입을 Rust에 알려야 합니다.

guess뒤에 콜론(:)은 변수의 유형에 대한 선언을 의미합니다.

parse에 대한 호출은 쉽게 오류를 일으킬 수 있습니다. 이를 처리하기 위해 expect 메서드를 사용합니다. parse 메서드는 read_line와 마찬가지로 Result를 반환합니다.

이제 프로그램을 다시 실행해 보겠습니다.

cargo run
   Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 96
Please input your guess.
44
You guessed: 44
Too small!

사용자의 입력 값과 비밀번호와 비교하고 그 메시지를 출력하는 대부분의 동작이 정상적으로 수행되었습니다.

하지만 단 한번의 실행을 통해 하나의 추측만을 시도할 수 있습니다. 이제 루프를 추가하여 반복적인 비교를 진행할 차례입니다.

루핑으로 여러 춫자를 시도하기

loop 키워드는 무한 루프를 만듭니다.

사용자가 숫자를 추측할 수 있는 더 많은 기회를 제공하기 위해 추가할 것입니다.

    // --snip--

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

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

        // --snip--

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

사용자의 입력을 받고 비교하는 부분을 loop 안에 넣었습니다. 이제 반복적으로 사용자의 입력을 받고 비교할 수 있게 되었습니다.

프로그램을 종료하려면 Ctrl+C를 눌러 실행을 종료할 수 있습니다.

정확한 추측 후 종료

사용자가 정확한 비밀번호를 넣으면 프로그램이 종료되도록 프로그램을 수정해 보겠습니다.

        // --snip--

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

break를 사용하여 사용자가 비밀번호를 올바르게 추측하였을 때 프로그램은 You win!이라는 메시지를 출력한 후 종료하기 됩니다.

breakloop를 종료하는 것을 의미하지만 loop가 프로그램의 마지막 부분이기 때문입니다.

잘못된 입력 처리

게이의 동작을 더욱 구체화 하기위해 사용자가 숫자가 아닌 것을 입력할 때 프로그램이 충돌하는 대신 사용자가 계속 게임을 진행할 수 있도록, 숫자가 아닌 것은 무시하도록 수정합니다.

        // --snip--

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

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

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

        // --snip--

parse로 문자열을 숫자로 성공적으로 변환할 수 있는 경우 Ok 결과에 숫자가 포함 된 값을 반환합니다.

Ok 값은 첫 번째 arm의 패턴과 일치하고 일치 표현식은 구문 분석하여 생성된 num 값을 반환하고 Ok 값 안에 넣습니다.

parse가 문자를 숫자로 변환할 수 없는 경우 오류에 대한 추가 정보가 포함된 Err 값을 반환합니다.

Err는 첫 번재 arm 패턴과는 일치하지 않지만, 두 번째 패턴과 일치하게 됩니다. 밑줄(_)은 포괄적인 값을 의미하며, 내부의 어떤 정보가 있는지 무관하게 모든 Err 값을 일치시키게 됩니다.

Err가 발생하였을 때 continue를 실행하여 루프의 다음 반복으로 이동하여 다른 게임을 계속 진행하도록 지시합니다.

따라서 프로그램은 parse에 대한 모든 오류를 효과적으로 무시하게 됩니다.

이제 모든 것이 예상대로 실행되는지 확인해 보겠습니다.

cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 84
Please input your guess.
44
You guessed: 44
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
81
You guessed: 81
Too small!
Please input your guess.
85
You guessed: 85
Too big!
Please input your guess.
84
You guessed: 84
You win!

모든기능이 잘 동작했습니다. 이제 비밀번호를 누출하는 println!을 삭제해 코드를 완성합니다.

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

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

    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("Failed to read line");

            let guess: u32 = match guess.trim().parse() {
                Ok(num) => num,
                Err(_) => 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;
            }
        }
    }
}

정리

랜덤한 숫자를 생성하고 이를 사용자가 추측하는 게임을 성공적으로 구현했습니다.

이를 통해 let, match, function, 외부 crates 등의 기본적인 Rust 개념을 익혔습니다.

Published Nov 21, 2022

Right Thoughts, Right Words, Right Action