반복문과 게임 완성하기
추리 게임에 반복문을 추가하고 정답과 잘못된 입력을 처리하여 최종 프로그램을 완성합니다.
반복문을 이용하여 여러 번의 추리 허용하기
loop 키워드는 무한루프를 제공합니다.
루프를 추가하여 사용자들에게 숫자를 추리할 기회를 더 주겠습니다.
// --생략--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --생략--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
보시다시피, 추릿값 입력 프롬프트부터 모든 것을 루프로 옮겼습니다.
루프 내부의 줄을 각각 네 칸씩 들여 쓰고 프로그램을 다시 실행하세요.
이제 프로그램이 계속해서 다른 추리를 요청하고, 새로운 문제가 발생했군요.
사용자가 이 프로그램을 종료할 수 없는 것 같습니다!
사용자는 ctrl-c 단축키를 이용하여 프로그램을 멈출 수 있습니다.
하지만 ‘비밀 숫자와 추릿값을 비교하기’에서 parse 메서드에 대해 언급했던 것처럼 이 만족할 줄 모르는 괴물에게서 빠져나올 다른 방법이 있습니다.
만약 사용자가 숫자가 아닌 답을 적는다면 프로그램이 멈춥니다.
이를 활용하여 아래와 같이 사용자가 종료할 수 있도록 할 수 있습니다.
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracequit를 입력하면 게임이 종료되지만, 보시다시피 숫자가 아닌 다른 입력값에도 게임이 종료됩니다.
정확한 숫자를 맞혔을 때 게임이 종료되기를 원하기 때문에 이는 차선책이라고 할 수 있습니다.
정답을 맞힌 후 종료하기
사용자가 정답을 맞혔을 때 게임이 종료되도록 break문을 추가합니다.
// --생략--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
You win! 뒤에 break 라인을 추가하면 사용자가 비밀 숫자를 맞혔을 때 프로그램이 루프를 종료하게 됩니다.
루프가 main의 마지막 부분이므로 루프의 종료는 프로그램의 종료를 의미합니다.
잘못된 입력값 처리하기
게임의 동작을 더욱 다듬기 위해 사용자가 숫자가 아닌 값을 입력할 때 프로그램을 종료시키는 대신 이를 무시하여 사용자가 계속 추릿값을 입력할 수 있도록 만들어 봅시다.
예제 1-5에 나온 것처럼 guess가 String에서 u32로 변환되는 줄을 변경하면 그렇게 할 수 있습니다.
잘못된 입력은 버리고 정답은 반복을 끝낸다
새 회차 입력→parse 실패 continue 또는 숫자 비교→Less/Greater 재시도/Equal break의 실제 반복 상태이다. 같은 secret과 새 String을 구분한다.
Rust · loop · Result · Ordering
잘못된 입력은 이번 회차만 버리고, 정답은 루프를 끝낸다
두 오류 정책을 구분하세요. read_line(...).expect(...)는 I/O 실패 시 그대로 종료하지만, parse의 Err(_)는 메시지 없이 현재 문자열만 버리고 continue합니다.
1. 입력 I/O 정책은 그대로
read_line(&mut guess).expect(...)가 남아 있으므로 실제 I/O 실패는 종료됩니다.
2. 숫자가 아니면 조용히 재시도
Err(_) => continue는 오류 메시지를 출력하지 않고 현재 문자열만 버린 뒤 새 회차로 이동합니다.
3. 숫자만 cmp로 이동
Ok(num)가 만든 u32를 비밀 숫자와 비교합니다.
4. Less · Greater는 다시 입력
Too small! 또는 Too big!을 출력한 뒤 루프 끝에서 다음 회차로 이어집니다.
5. Equal만 루프 종료
You win!을 출력하고 break가 반복을 끝냅니다.
expect를 모두 없앤 것이 아니다
바뀐 부분은 parse().expect(...)뿐입니다. read_line의 I/O 오류와 잘못된 숫자 입력은 서로 다른 실패입니다.
재시도와 종료의 경계
continue는 지금 회차를 중단하고, break는 loop 전체를 중단합니다. Less와 Greater는 회차 끝까지 간 뒤 자연스럽게 반복됩니다.
완성 코드의 비밀 숫자는 rand 0.10.2 API인 rand::random_range(1..=100)로 한 번 생성되며, 반복되는 것은 사용자 입력과 판정입니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 원 완성 코드의 8노드 9관계다. 회차→readline→parse에서 Err는 continue로 다음 회차로 재진입하고, Ok는 cmp로 향한다. Less/Greater는 안내 후 다음 회차로, Equal만 break 종료점으로 이어진다. 재진입은 secret 재생성이 아니라 새 String/새 입력이다. I/O Err는 기존 expect 정책이며 EOF 정상 종료 추가 버전과 섞지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 회차 입력 같은 secret에 새 String 입력을 받는다. |
|
|
| 파싱 분기 실패는 continue, 성공은 비교로 진행한다. |
|
|
| 재시도·종료 Less/Greater는 반복하고 Equal은 break한다. |
|
|
잘못된 입력은 버리고 정답은 반복을 끝낸다
새 회차 입력→parse 실패 continue 또는 숫자 비교→Less/Greater 재시도/Equal break의 실제 반복 상태이다. 같은 secret과 새 String을 구분한다.
Rust · loop · Result · Ordering
잘못된 입력은 이번 회차만 버리고, 정답은 루프를 끝낸다
두 오류 정책을 구분하세요. read_line(...).expect(...)는 I/O 실패 시 그대로 종료하지만, parse의 Err(_)는 메시지 없이 현재 문자열만 버리고 continue합니다.
1. 입력 I/O 정책은 그대로
read_line(&mut guess).expect(...)가 남아 있으므로 실제 I/O 실패는 종료됩니다.
2. 숫자가 아니면 조용히 재시도
Err(_) => continue는 오류 메시지를 출력하지 않고 현재 문자열만 버린 뒤 새 회차로 이동합니다.
3. 숫자만 cmp로 이동
Ok(num)가 만든 u32를 비밀 숫자와 비교합니다.
4. Less · Greater는 다시 입력
Too small! 또는 Too big!을 출력한 뒤 루프 끝에서 다음 회차로 이어집니다.
5. Equal만 루프 종료
You win!을 출력하고 break가 반복을 끝냅니다.
expect를 모두 없앤 것이 아니다
바뀐 부분은 parse().expect(...)뿐입니다. read_line의 I/O 오류와 잘못된 숫자 입력은 서로 다른 실패입니다.
재시도와 종료의 경계
continue는 지금 회차를 중단하고, break는 loop 전체를 중단합니다. Less와 Greater는 회차 끝까지 간 뒤 자연스럽게 반복됩니다.
완성 코드의 비밀 숫자는 rand 0.10.2 API인 rand::random_range(1..=100)로 한 번 생성되며, 반복되는 것은 사용자 입력과 판정입니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 원 완성 코드의 8노드 9관계다. 회차→readline→parse에서 Err는 continue로 다음 회차로 재진입하고, Ok는 cmp로 향한다. Less/Greater는 안내 후 다음 회차로, Equal만 break 종료점으로 이어진다. 재진입은 secret 재생성이 아니라 새 String/새 입력이다. I/O Err는 기존 expect 정책이며 EOF 정상 종료 추가 버전과 섞지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 회차 입력 같은 secret에 새 String 입력을 받는다. |
|
|
| 파싱 분기 실패는 continue, 성공은 비교로 진행한다. |
|
|
| 재시도·종료 Less/Greater는 반복하고 Equal은 break한다. |
|
|
// --생략--
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}");
// --생략--
이전의 guess.trim().parse().expect("Please type a number!") 호출만 match 표현식으로 바꾸어, 정수 변환 실패를 직접 처리하도록 했습니다.
입력 장치의 I/O 실패를 처리하는 read_line(&mut guess).expect("Failed to read line")는 그대로 남아 있습니다. 즉 이 예제에서 바뀐 것은 parse의 오류 처리뿐입니다.
parse 메서드가 Result 타입을 반환한다는 점, 그리고 Result는 Ok나 Err 배리언트를 가진 열거형임을 기억하세요.
여기서는 cmp 메서드의 Ordering 결과에서와 마찬가지로 match 표현식을 사용하고 있습니다.
만약 parse가 성공적으로 문자열을 정수로 변환할 수 있으면 결괏값이 들어있는 Ok를 반환합니다.
이 Ok 값은 첫 번째 갈래의 패턴에 매칭되고, match 표현식은 parse가 생성하여 Ok 값 안에 넣어둔 num 값을 반환합니다.
그 값은 새로 만드는 guess 변수에 바로 저장됩니다.
만약 parse가 문자열을 정수로 바꾸지 못한다면, 에러에 대한 더 많은 정보를 담은 Err를 반환합니다.
Err는 첫 번째 갈래의 패턴인 Ok(num)과는 매칭되지 않지만, 두 번째 갈래의 Err(_)와는 매칭됩니다.
밑줄 _은 캐치올(catchall) 값이라 합니다. 모든 값에 매칭될 수 있으며, 이 예제에서는 Err 내에 무슨 값이 있든지 상관없이 모든 Err를 매칭하도록 했습니다.
따라서 프로그램은 두 번째 갈래의 코드인 continue를 실행합니다. 잘못 입력한 현재 문자열은 별도 메시지 없이 버리고, loop의 다음 반복으로 가서 또 다른 추릿값을 요청합니다.
따라서, 프로그램은 효과적으로 parse에서 발생할 수 있는 모든 에러를 무시합니다!
이제는 프로그램의 모든 부분이 예상한 대로 동작해야 합니다.
한번 시도해봅시다.
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!멋집니다!
마지막 미세 조정만 추가하여 추리 게임을 마무리하겠습니다.
프로그램이 여전히 비밀 숫자를 출력하고 있다는 것을 떠올리세요.
테스트 때는 괜찮지만 게임을 망치게 됩니다.
정답을 미리 알려 주는 println!을 삭제합시다.
예제 1-6은 최종 코드를 보여줍니다.
예제 1-6: 완성된 추리 게임 코드use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::random_range(1..=100);
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;
}
}
}
}
이 시점에서는 추리 게임의 기본 기능이 완성된 상태입니다.
반복마다 바뀌는 값과 게임 동안 고정되는 값
secret_number는 loop 밖에서 한 번 뽑으므로 한 게임 안에서는 고정됩니다. guess의 String은 매 회차 새로 만들어 이전 입력을 누적하지 않습니다. 비밀 숫자를 1..=100에서 뽑는 규칙과 사용자의 입력을 같은 범위로 제한하는 규칙은 다릅니다. 현재 코드는 u32 변환만 검사하므로 0과 101도 숫자로 받아 비교합니다.
정답을 미리 고정한 독립 연습
실험용 사본에서 비밀 숫자 생성 한 줄만 let secret_number = 42_u32;로 바꾸세요. foo, 0, 50, 42를 차례로 한 줄씩 입력할 때 You guessed가 출력되는 회차, 비교 결과, 입력 안내 횟수를 먼저 예상하세요. 여기서는 모든 입력 줄이 정상적으로 읽히는 조건입니다. 원래 게임을 실행할 때는 rand 호출로 되돌립니다.
네 회차의 풀이
foo는 parse 실패로 continue하여 You guessed와 비교 출력을 건너뜁니다. 0은 변환 성공 뒤 Too small!, 50은 Too big!, 42는 You win!을 출력하고 break합니다. 입력 안내는 네 번, You guessed는 세 번입니다. continue는 현재 회차만, break는 반복 전체를 끝내며 비밀 숫자를 매 회차 다시 뽑지 않습니다.
입력이 끝났을 때도 종료시키기
위 완성 코드는 대화형으로 추리를 계속 입력하는 기본 실습입니다. 파일·파이프의 끝인 EOF에서 read_line은 오류가 아니라 Ok(0)을 반환합니다. 원래 코드는 이 반환값을 버린 뒤 빈 문자열의 parse 실패로 continue하므로 EOF에서 계속 입력 안내를 출력할 수 있습니다. 표준 입력의 반환 규칙에 따라 읽은 바이트 수를 확인하면 정답 이외의 정상 종료도 정할 수 있습니다.
아래는 예제 1-6의 입력 처리만 바꾼 별도 완전한 프로그램입니다. src/main.rs 내용을 이것으로 교체하며, Cargo.toml의 rand 조건은 그대로 사용합니다. I/O 실패에는 기존 expect 정책을 유지하고, EOF만 정상 종료로 추가합니다.
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::random_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
let bytes_read = io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
if bytes_read == 0 {
println!("Input ended.");
break;
}
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;
}
}
}
}이 버전에서도 숫자가 아닌 한 줄은 재시도하고, 읽을 입력 자체가 끝나면 Input ended.를 한 번 출력하고 종료합니다. 빈 줄은 개행을 읽었으므로 바이트 수가 0보다 크고 parse 실패로 재시도합니다. EOF와 빈 줄은 서로 다른 조건입니다. 파일의 마지막 줄이 42이고 개행이 없어도 그 숫자 바이트를 먼저 읽어 숫자 변환과 비교가 가능하며, 읽기 전부터 EOF인 상태만 즉시 종료됩니다.
마무리 요약
이 프로젝트는 let, match, 메서드, 연관 함수, 외부 크레이트 사용과 같이 많은 새로운 러스트 개념들을 소개하기 위한 실습이었습니다.
다음 장들에서는 이 개념들의 세부적인 내용을 배울 것입니다.
2장은 대부분의 프로그래밍 언어들이 가지고 있는 변수, 데이터 타입, 함수를 소개하고 러스트에서의 사용법을 다룹니다.
3장에서는 다른 프로그래밍 언어와 차별화된 러스트의 특성인 소유권을 다룹니다.
4장에서는 구조체와 메서드 문법을 다루며 5장에서는 열거형에 대해 다룹니다.