타입이 있는 선택으로 규칙 표현하기

RustBeginner
지금 연습하기

소개

주사위 5개를 사용하는 이 게임에는 sum, pair, straight라는 세 가지 선택지가 있습니다. 같은 주사위 결과라도 어떤 항목을 선택하느냐에 따라 점수가 달라집니다. [2, 2, 4, 4, 6]의 합계는 18이고, 가장 높은 페어는 8점입니다. 이 짧은 규칙을 타입이 있는 Rust 코드로 표현하고, 정해진 결과와 무작위 결과를 모두 시험합니다.

Rust 함수, 배열, enum, match, Option과 기본적인 터미널 경로 사용법을 알아야 합니다. Cargo와 입력 처리 프로그램은 준비되어 있으며, 여러분은 점수를 결정하는 로직을 구현합니다. 이 랩은 다른 VM에 의존하지 않습니다. 읽기와 일반적인 실수 처리를 포함해 약 40분을 예상합니다. 이는 작성자의 추정입니다.

선택을 모델링하고 합계 계산하기

이 단계에서는 준비된 타입을 읽고 점수를 예상한 뒤 실제 게임 라이브러리에 짧고 작동하는 합계 계산을 작성합니다. Terminal 탭에서 시작하세요. 준비 과정에서 /home/labex/project/dice-game에 Cargo 프로젝트를 만들었습니다. src/lib.rs는 규칙 라이브러리이고 src/main.rs는 입력과 무작위 주사위를 제공합니다. src/main.rs와 Cargo.toml은 변경하지 마세요.

도구 모음은 /home/labex/.cargo/bin에 있습니다. export는 셸이 명령을 찾는 위치 목록인 PATH에 이 디렉터리를 추가합니다. 이 설정은 현재 Terminal 세션에만 적용되므로 새 셸에서는 반복하세요. cd로 프로젝트를 선택하고 pwd로 위치를 확인합니다.

export PATH=/home/labex/.cargo/bin:$PATH
cd /home/labex/project/dice-game
pwd

/home/labex/project/dice-game이 표시되어야 합니다. 경로 오타는 표시된 cd로 고치고 환경을 초기화하지 마세요. 편집 전에 [2, 2, 4, 4, 6]의 합계를 예상합니다. 18입니다. 먼저 이 일반적인 점수 명령이 작동하게 만듭니다.

파일을 바꾸지 않고 출력하는 cat으로 기존 라이브러리를 읽으세요.

cat src/lib.rs

Dice는 [u8; 5]의 별칭으로 부호 없는 1바이트 값이 정확히 5개임을 뜻합니다. 입력 프로그램은 1부터 6까지의 눈만 허용합니다. Category는 세 가지 선택지의 이름입니다. Category::parse는 인식한 단어에 Some(category), 알 수 없는 단어에 None을 반환합니다. sum과 sums에 무엇을 반환할지 예상한 뒤 해당 match 분기를 찾으세요. 입력 거절은 입력 프로그램이 담당합니다. 인식한 항목의 점수가 0인 것은 정당한 결과일 수 있습니다.

타입과 보조 메서드는 제공되어 있습니다. pub은 입력 프로그램에서 이름을 사용할 수 있게 합니다. impl Category는 해당 타입의 메서드를 모으고 그 안의 Self는 Category를 뜻합니다. derive는 이 열거형에 복사, 디버그 표시, 동등 비교 기능을 제공합니다. name은 표시할 고정 문자열 리터럴을 반환합니다. 이 정의들을 유지하세요. 편집 범위는 현재 모든 항목에 0을 반환하는 score로 한정됩니다.

이번 편집 전에 되돌릴 지점을 만드세요. mktemp -d는 X 여섯 개를 고유 접미사로 바꾸어 이전 백업을 재사용하지 않고 새 디렉터리를 만듭니다. $()는 출력된 경로를 세션 변수 rs02_before_sum에 저장합니다. &&는 디렉터리 생성이 성공한 경우에만 복사를 실행합니다. cp -p는 소스를 복사하고 메타데이터를 보존하며 따옴표는 변수의 경로를 하나로 유지합니다. 백업에는 편집 가능한 라이브러리만 있고 준비된 입력 프로그램이나 Cargo 설정은 없습니다. 어느 작업이든 실패하면 오류를 읽고 편집 전에 백업에 성공해야 합니다.

rs02_before_sum=$(mktemp -d .edit-before-sum.XXXXXX) && cp -p src/lib.rs "$rs02_before_sum/lib.rs"

기존 파일을 nano로 엽니다. -c는 커서 위치를 표시하므로 컴파일러 줄 번호를 따라갈 때 유용합니다.

nano -c src/lib.rs

Nano의 ^ 표기는 Ctrl 키를 뜻합니다. Ctrl+W를 누르고 pub fn score를 입력한 뒤 Enter로 함수를 찾으세요. 화살표 키로 이동하고 Backspace/Delete로 문자를 바꿉니다. 편집 전에 함수의 여는 {부터 대응하는 }까지 읽으세요. 앞의 열거형이나 보조 메서드는 삭제하지 마세요.

초기 코드의 사용하지 않는 매개변수 이름은 _dice, _category입니다. 본문에서 사용할 수 있도록 그 바인딩 이름만 dice, category로 바꾸고 함수명과 매개변수/반환 타입은 유지하세요. 하나의 0 표현식을 match category 표현식으로 교체합니다. 분기에는 패턴, =>, 결과가 있습니다. 예를 들어 임시 분기는 Category::Pair => 0,입니다. Category::Straight에도 같은 임시 0 결과를 주세요. 이 두 규칙은 다음에 구현합니다.

Category::Sum에는 가변 누적값으로 시작해 모든 눈을 방문하고 확장한 값을 더하는 블록을 사용하세요. 이 짧은 루프가 숫자 연산을 보여 줍니다.

let mut total = 0;
for face in dice {
    total += u16::from(face);
}

u16::from(face)는 더하기 전에 숫자 타입을 확장합니다. 6이 5개여도 u16에 충분히 들어갑니다. 루프 뒤에서 total을 Sum 분기의 마지막 표현식으로 두세요. 마지막 표현식 뒤에는 세미콜론이 없어야 분기가 값을 반환합니다. match와 함수의 중괄호를 모두 닫습니다. 기존 프로그램에 실제 항목 하나를 구현했고 나머지 두 선택지는 의도적으로 미완성 상태입니다.

Ctrl+O로 쓰고 Enter로 src/lib.rs를 확인한 뒤 Ctrl+X로 nano에서 나갑니다. 편집기가 열린 동안 터미널 명령을 입력하지 마세요. 컴파일 전에 저장한 편집을 읽습니다.

cat src/lib.rs

Cargo는 준비된 프로그램을 컴파일하고 실행합니다. --quiet는 일반 빌드 메시지를 숨기고 --는 Cargo 옵션과 프로그램 인수를 구분합니다. 예상했던 같은 눈 5개로 실행하세요.

cargo run --quiet -- score sum 2 2 4 4 6
Dice: [2, 2, 4, 4, 6]
sum: 18

주사위는 바뀌지 않았고 선택한 규칙이 점수를 만들었습니다. 계속하기 전에 루프가 6 다섯 개에 대해 30을 계산하는지도 예상해 보세요.

Cargo가 문법 오류를 알리면 파일명과 줄 번호를 읽고 nano -c src/lib.rs를 다시 열어 pub fn score를 검색한 뒤 해당 중괄호, 분기, 표현식을 고칩니다. 저장하고 나가 실패한 명령을 다시 실행하세요. 아직 저장하지 않은 편집은 Ctrl+X 다음 저장 질문의 N으로 버릴 수 있습니다. 이미 쓴 변경까지 되돌리지는 않습니다.

이미 저장했고 고칠 수 없는 실수라면 아래 명령으로 자신의 편집 전 라이브러리를 복원합니다. 허용된 src/lib.rs만 의도적으로 교체하며 입력 프로그램이나 Cargo 파일은 복원하지 않습니다. 백업 변수는 현재 Terminal 세션에 속합니다. 다른 셸에서는 아래 조건부 복구 절차로 실제 이전 백업을 선택하고 살펴보세요.

저장한 실수를 복구할 때만: 원래 Terminal에서는 체크포인트 변수에 경로가 남아 있습니다. 새 Terminal에서는 먼저 앞의 PATH와 프로젝트 디렉터리 명령을 반복하세요. 이 단계의 백업을 나열합니다.

ls -d .edit-before-sum.*

ls -d는 내용이 아니라 디렉터리 이름을 나열하고 *는 이 단계의 고유 디렉터리 접미사에 일치합니다. 실제 생성된 이름이므로 접미사를 지어내 입력하지 마세요. 일치하는 백업이 없으면 프로젝트 디렉터리와 체크포인트 생성 여부를 확인하세요. 무관한 단계에서 복사하지 마세요. 출력된 경로 하나를 선택하려면 다음을 실행합니다.

read rs02_before_sum

셸이 한 줄을 기다립니다. 선택한 정확한 디렉터리 이름을 붙여 넣고 Enter를 누르세요. read는 그 줄을 현재 세션의 지정한 변수에 저장합니다. 복원 전에 백업 내용을 확인하세요.

cat "$rs02_before_sum/lib.rs"

Sum 구현 전의 초기 코드가 있어야 합니다. 경로나 단계가 틀리면 올바르게 출력된 경로로 read를 반복하고 다시 살펴보세요. 그 뒤에만 복원 명령을 사용합니다. 변수에 어떤 체크포인트가 있는지 확신하지 못한다면 원래 Terminal에서도 이 확인을 합니다.

cp -p "$rs02_before_sum/lib.rs" src/lib.rs

초기 코드를 복원하므로 Sum 구현과 점수 명령을 다시 수행하세요. 다음 단계를 위해 이 Terminal 세션과 작동하는 Sum을 유지합니다.

가장 높은 페어와 스트레이트 계산하기

이 단계에서는 작동하는 Sum 분기를 유지하면서 나머지 점수 판단을 설계하고 구현합니다. 같은 Terminal의 /home/labex/project/dice-game에서 계속하세요. 새 셸에서는 앞의 export PATH와 cd 명령을 반복합니다.

페어는 두 번 이상 나타나는 가장 큰 눈의 두 배를 얻습니다. 같은 눈이 3개, 4개, 5개여도 주사위 두 개만 계산합니다. [2, 2, 4, 4, 6]에서 두 페어를 모두 더할까요, 4의 페어만 계산할까요? 4의 페어만 계산해 8점입니다. 6이 다섯 개면 30이 아니라 12점입니다. 페어가 없어 0점인 것도 유효합니다.

유용한 표현 하나는 [0u8; 7]입니다. 인덱스 06의 카운터 7개이며 0은 쓰지 않습니다. 각 눈에 대해 usize::from(face)로 배열 인덱스를 구하고 counts[usize::from(face)] += 1로 개수를 늘립니다. 두 번째 루프는 양 끝을 포함하는 1..=6의 눈을 확인할 수 있습니다. 개수가 둘 이상이면 가변 최고 점수를 그 눈의 두 배로 교체하세요. 작은 눈부터 방문하므로 마지막에는 가장 높은 유효 페어가 남습니다. as u16은 그 작은 숫자 결과를 점수 타입으로 변환합니다. 동등한 개수 세기나 정렬 알고리즘을 구성해도 되지만 페어 두 개가 있거나 같은 눈이 35개일 때도 올바르게 처리해야 합니다.

스트레이트는 서로 다른 눈이 순서와 관계없이 정확히 15 또는 26일 때만 15점입니다. [6, 5, 4, 3, 2]와 [1, 2, 3, 4, 6]의 차이를 예상하세요. 첫 번째는 스트레이트이고 두 번째에는 빈 숫자가 있습니다. let mut sorted = dice로 복사하고 sorted.sort()를 호출하면 원본 결과를 바꾸지 않고 정렬한 배열을 정확한 두 배열과 비교할 수 있습니다. ||는 둘 중 한 비교가 참이면 된다는 뜻입니다. 15와 0을 언제 반환할지 정하세요. 중복된 눈은 통과하면 안 됩니다.

편집 전에 이미 작동하는 단계를 보존합니다. 앞서처럼 고유 디렉터리는 이전 백업 덮어쓰기를 막고, 디렉터리 생성이 성공한 뒤에만 복사합니다.

rs02_before_rules=$(mktemp -d .edit-before-rules.XXXXXX) && cp -p src/lib.rs "$rs02_before_rules/lib.rs"

기존 파일을 열고 Ctrl+W로 pub fn score를 찾아 Pair와 Straight 분기만 바꾸세요. Sum, 공개 API, 모든 보조 정의를 유지합니다. 모듈 전체를 교체하지 말고 이 규칙으로 구현을 작성하세요.

nano -c src/lib.rs

Ctrl+O, Enter로 저장하고 Ctrl+X로 나갑니다. 실행 전에 저장한 분기를 읽고 각 결과를 예상하세요.

cat src/lib.rs
cargo run --quiet -- score pair 2 2 4 4 6
Dice: [2, 2, 4, 4, 6]
pair: 8

이제 같은 눈 다섯 개, 두 스트레이트 범위, 유효한 0점을 비교하세요.

cargo run --quiet -- score pair 6 6 6 6 6
cargo run --quiet -- score straight 6 5 4 3 2
cargo run --quiet -- score straight 1 2 3 4 5
cargo run --quiet -- score pair 1 2 3 4 6
cargo run --quiet -- score straight 1 2 3 4 6

순서대로 12, 15, 15, 0, 0이 나와야 합니다. 0은 인식한 규칙에서 점수를 얻는 조합이 없다는 뜻이지 입력 거절이 아닙니다. 유효한 주사위 5개 예시를 하나 더 직접 고르고 항목 하나의 점수를 예상한 뒤 해당 항목과 눈 5개로 score를 실행하세요. 다르면 예시를 바꿔 숨기지 말고 규칙으로 설명합니다.

눈 5개를 생략하면 운영체제의 난수 원천을 사용합니다. 보안이나 도박 시스템이 아닌 로컬 게임입니다. 한 번 실행하고 원하면 반복하세요.

cargo run --quiet -- score pair

주사위와 점수는 달라질 수 있고 무작위 값이 우연히 반복될 수도 있습니다. 표시된 눈에 최고 페어 규칙을 독립적으로 적용하세요. 알 수 없는 단어는 유효한 0점과 다릅니다. 다음 의도적인 철자 오류는 상태 1로 종료하며 주사위 점수를 계산하지 않고 Error: choose sum, pair, or straight를 출력합니다.

cargo run --quiet -- score pairs 2 2 4 4 6

명령줄에서 항목을 고치세요.

cargo run --quiet -- score pair 2 2 4 4 6

pair: 8이 나와야 합니다. 컴파일 오류는 소스를 가리킵니다. nano에서 score를 다시 열어 표시된 줄을 수정하고 저장한 뒤 재시도하세요. 필요하면 자신의 작동하는 Sum 단계로 복원한 뒤 나머지 두 분기를 다시 구현합니다.

저장한 실수를 복구할 때만: 원래 Terminal에서는 체크포인트 변수에 경로가 남아 있습니다. 새 Terminal에서는 먼저 앞의 PATH와 프로젝트 디렉터리 명령을 반복하세요. 이 단계의 백업을 나열합니다.

ls -d .edit-before-rules.*

ls -d는 내용이 아니라 디렉터리 이름을 나열하고 *는 이 단계의 고유 디렉터리 접미사에 일치합니다. 실제 생성된 이름이므로 접미사를 지어내 입력하지 마세요. 일치하는 백업이 없으면 프로젝트 디렉터리와 체크포인트 생성 여부를 확인하세요. 무관한 단계에서 복사하지 마세요. 출력된 경로 하나를 선택하려면 다음을 실행합니다.

read rs02_before_rules

셸이 한 줄을 기다립니다. 선택한 정확한 디렉터리 이름을 붙여 넣고 Enter를 누르세요. read는 그 줄을 현재 세션의 지정한 변수에 저장합니다. 복원 전에 백업 내용을 확인하세요.

cat "$rs02_before_rules/lib.rs"

Pair와 Straight 구현 전의 작동하는 Sum이 있어야 합니다. 경로나 단계가 틀리면 올바르게 출력된 경로로 read를 반복하고 다시 살펴보세요. 그 뒤에만 복원 명령을 사용합니다. 변수에 어떤 체크포인트가 있는지 확신하지 못한다면 원래 Terminal에서도 이 확인을 합니다.

cp -p "$rs02_before_rules/lib.rs" src/lib.rs

편집 가능한 라이브러리만 의도적으로 복원합니다. 환경을 초기화하거나 src/main.rs, Cargo.toml을 덮어쓰지 마세요. 다음 참고 코드는 시도한 뒤 특정 함수를 비교하거나 복구할 때 사용하며 기본 편집 순서는 아닙니다.

비교와 복구를 위한 완전한 라이브러리 참고 코드

관련 분기를 비교하고 필요한 수정만 nano로 옮기세요. 알려진 예시와 경계 예시를 다시 실행합니다. 아래 완전한 라이브러리는 다음 랩의 이미 완성된 점수 규칙을 준비하는 데 사용하는 유일한 유지보수 소스이기도 합니다.

pub type Dice = [u8; 5];

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Category {
    Sum,
    Pair,
    Straight,
}

impl Category {
    pub fn parse(input: &str) -> Option<Self> {
        match input {
            "sum" => Some(Self::Sum),
            "pair" => Some(Self::Pair),
            "straight" => Some(Self::Straight),
            _ => None,
        }
    }
    pub fn name(self) -> &'static str {
        match self {
            Self::Sum => "sum",
            Self::Pair => "pair",
            Self::Straight => "straight",
        }
    }
}

pub fn score(dice: Dice, category: Category) -> u16 {
    match category {
        Category::Sum => {
            let mut total = 0;
            for face in dice {
                total += u16::from(face);
            }
            total
        }
        Category::Pair => {
            let mut counts = [0u8; 7];
            for face in dice {
                counts[usize::from(face)] += 1;
            }
            let mut best = 0;
            for face in 1..=6 {
                if counts[face] >= 2 {
                    best = (face * 2) as u16;
                }
            }
            best
        }
        Category::Straight => {
            let mut sorted = dice;
            sorted.sort();
            if sorted == [1, 2, 3, 4, 5] || sorted == [2, 3, 4, 5, 6] {
                15
            } else {
                0
            }
        }
    }
}

요약

타입이 있는 항목, match, 누적 합계, 눈의 개수 카운터, 정렬한 복사본으로 세 가지 규칙을 완성했습니다. 고정 예제는 결과의 이유를 설명하고, 무작위 결과는 같은 규칙을 새로운 주사위에 적용하게 합니다. 점수를 얻는 조합이 없으면 0점이며, 알 수 없는 항목은 입력 오류입니다.