라운드마다 올바른 점수표 유지하기

RustBeginner
지금 연습하기

소개

점수 하나를 계산하는 것은 한 번의 판단일 뿐입니다. 완전한 게임은 사용한 항목을 기억하고, 잘못된 선택을 고치는 동안 같은 주사위를 유지해야 합니다. 3라운드 게임을 완성하고, 기록된 주사위와 항목을 바탕으로 점수를 다시 계산해 검증할 수 있는 점수표를 저장합니다.

이 독립 프로젝트에는 첫 번째 랩의 점수 규칙이 이미 설치되어 있습니다. 그 규칙과 Rust 함수, enum, match, Option, 배열, 기본 터미널 경로를 알아야 합니다. 준비된 구조체, 상태를 바꾸는 메서드, Result, 슬라이스는 사용할 때 설명합니다. 읽기와 일반적인 실수 처리를 포함해 약 55분을 예상합니다. 이는 작성자의 추정입니다.

유효한 선택만 확정하기

이 단계에서는 받아들인 선택 하나의 상태 전환을 설계하고 구현합니다. Terminal 탭에서 시작하세요. 이 독립 프로젝트에는 점수 규칙이 이미 완성되어 있습니다. 첫 번째 랩의 파일은 사용하지 않습니다. 이 셸의 검색 경로를 설정하고 프로젝트로 이동하세요. 새 셸에서도 이 명령을 반복합니다.

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

정상적으로 작동하는 점수부터 시작하며 실행 전에 예상해 보세요.

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

표시된 주사위와 pair: 8이 나와야 합니다. 게임 상태를 추가하기 전에 제공된 규칙 계층을 사용할 수 있음을 확인합니다. 코드 뼈대를 읽으세요.

cat src/state.rs

struct는 객체 하나를 설명하는 이름 있는 필드들을 묶습니다. Round는 주사위 5개, 항목, 점수를 기록합니다. Game은 current: Option<Dice>와 rounds: Vec<Round>를 보관합니다. Some(dice)는 선택을 기다린다는 뜻이고 None은 현재 결과가 없다는 뜻입니다. Vec<Round>는 크기가 늘어나는 모음이며 Vec::new()는 빈 모음을 만들고 push는 항목 하나를 추가합니다.

준비된 입력 프로그램의 호출부도 읽으세요.

cat src/main.rs

match game.submit(input.trim())를 찾으세요. Err 분기는 거절을 출력합니다. Ok 분기는 점수를 출력한 뒤 기록된 라운드가 셋 미만일 때만 다음 주사위를 얻습니다. 이 입력 프로그램은 편집하지 않습니다. 거절 시 상태를 그대로 유지해야 다음 프롬프트에 같은 주사위와 굴림 수가 표시됩니다.

제출 종류별로 호출 뒤의 필드를 예상해 보세요.

제출 호출 뒤 rounds 호출 뒤 current
대기 중인 주사위 없음, 알 수 없는 단어, 이미 사용한 항목 변화 없음 변화 없음
0점을 포함한 유효하고 사용하지 않은 항목 완전한 새 기록 하나 추가 None

성공 후 다음 주사위를 제공하는 것은 submit이 아니라 입력 프로그램입니다. 메서드에서 주사위를 얻지 마세요. impl Game은 Game의 메서드를 모읍니다. &mut self는 가변 참조를 통해 그 게임을 갱신하게 합니다. Result<u16, &'static str>은 Ok(points) 또는 Err(message)를 뜻하고 &'static str은 고정 오류 메시지 텍스트 타입입니다. Option과 달리 Result는 실패를 설명할 수 있습니다.

ok_or(message)는 Some을 Ok로, None을 Err로 바꿉니다. 뒤의 ?는 성공 값을 사용해 계속하거나 오류를 즉시 반환합니다. 예를 들어 대기 중인 결과 검사는 다음과 같습니다.

let dice = self.current.ok_or("match finished")?;

Category::parse(input)에도 같은 생각을 적용하세요. 필드를 바꾸기 전에 &self.rounds를 순회하는 for 루프로 이전 기록을 빌려 읽고 같은 항목이 있으면 거절합니다. 검사 구성은 직접 선택하되 표의 계약을 따라야 합니다. 검증 후 score(dice, category)로 points를 구합니다. 성공 확정에는 세 동작이 있습니다.

self.rounds.push(Round { dice, category, points });
self.current = None;
Ok(points)

짧은 필드 이름은 같은 이름의 지역 변수를 사용합니다. 결과가 0이어도 성공입니다. 거절 경로에서는 이 변경들을 수행하면 안 됩니다.

src/main.rs, Cargo.toml, 공개 타입, 함수명과 타입, 모듈 내보내기를 유지하세요. use crate::{score, Category, Dice}는 현재 라이브러리에서 가져오고 src/lib.rs의 mod state와 pub use는 모듈을 연결합니다. 이번 단계에서는 src/state.rs의 submit만 편집하고 구조체, new 메서드, 미완성 replay는 그대로 둡니다.

편집 전에 이 허용된 파일의 고유 백업을 만듭니다. 앞서 배운 대로 mktemp -d가 X 여섯 개를 고유 접미사로 바꿉니다. $()는 출력 경로를 저장하고 &&는 디렉터리 생성이 성공해야 cp -p를 실행합니다. 따옴표로 감싼 경로는 인수 하나로 유지됩니다. 실패하면 오류를 읽고 편집 전에 백업을 만드세요.

rs02_before_submit=$(mktemp -d .edit-before-submit.XXXXXX) && cp -p src/state.rs "$rs02_before_submit/state.rs"

nano로 엽니다. -c는 커서 위치를 표시합니다.

nano -c src/state.rs

Ctrl+W로 pub fn submit을 검색하고 Enter를 누르세요. 메서드의 여는 중괄호와 닫는 중괄호를 살펴봅니다. 사용하지 않는 _input 매개변수 바인딩만 타입을 유지하며 input으로 바꾸세요. 미완성 본문만 위의 검사와 성공 확정 구현으로 교체합니다. 다른 메서드와 데이터 정의는 보존하세요. 화살표와 Backspace/Delete로 편집합니다. Ctrl+O, Enter로 저장하고 Ctrl+X로 나갑니다. Nano의 ^는 Ctrl입니다. 저장하지 않은 실수는 Ctrl+X 다음 저장 질문의 N으로 버릴 수 있지만 이전에 저장한 편집까지 되돌리지는 않습니다.

실행 전에 저장한 메서드를 읽으세요.

cat src/state.rs

알려진 고정 예제로 상태 판단을 관찰합니다. --fixture는 난수 대신 공개된 눈을 고릅니다.

cargo run --quiet -- play --fixture

각 프롬프트를 기다린 뒤 단어 하나를 입력하고 Enter를 누르세요.

먼저 pairs를 입력합니다. 알 수 없는 항목이므로 거절 후 같은 [2, 2, 4, 4, 6]과 rolls 1이 나와야 합니다. 다음으로 pair를 입력합니다. 8점을 얻고 [1, 2, 3, 4, 6], rolls 2로 진행합니다. 다시 pair를 입력합니다. 사용한 항목은 거절하고 주사위와 rolls 2를 유지합니다. 이어서 straight를 입력합니다. 유효한 선택으로 0점을 얻고 세 번째 결과로 진행합니다. 마지막으로 sum을 입력하면 20점을 얻습니다.

예상된 마지막 오류는 Error: replay not implemented이며 상태 1입니다. 제출은 작동했지만 replay는 여전히 원래 미완성 함수여서 입력 프로그램이 검증되지 않은 점수표 저장을 거부합니다. 계획된 중간 상태이며 성공적으로 저장한 게임이 아닙니다. 실수로 다른 유효한 항목을 골랐다면 남은 항목으로 끝내거나 Ctrl+C 후 의도적으로 새 고정 게임을 시작하세요. 같은 게임 안의 거절은 재시작이나 재굴림을 일으키면 안 됩니다.

컴파일 오류는 보고된 파일/줄을 읽고 nano에서 해당 메서드를 검색해 고칩니다. 저장한 오류를 고칠 수 없다면 자신의 submit 전 파일을 복원하고 submit을 다시 구현해 시험하세요.

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

ls -d .edit-before-submit.*

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

read rs02_before_submit

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

cat "$rs02_before_submit/state.rs"

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

cp -p "$rs02_before_submit/state.rs" src/state.rs

이 명령은 편집 가능한 상태 파일만 의도적으로 교체합니다. 백업 변수는 이 Terminal에만 있으므로 다른 셸에서는 조건부 복구 절차를 사용하세요. 상태 오류를 고치려고 setup을 초기화하거나 준비된 입력 프로그램을 교체하지 마세요.

완전한 점수표를 재계산하고 저장하기

이 단계에서는 합계를 받아들이기 전에 기록된 각 라운드를 독립적으로 확인하는 기능을 구현합니다. 같은 Terminal의 /home/labex/project/dice-game에서 계속하세요. 필요하면 앞의 export PATH와 cd를 반복합니다. 완성한 submit 메서드는 유지하세요.

먼저 제안된 고정 점수표를 손으로 확인합니다. [2, 2, 4, 4, 6]의 pair는 8, [1, 2, 3, 4, 6]의 straight는 0, [6, 5, 4, 3, 2]의 sum은 20으로 재계산한 합계는 28입니다. 누군가 첫 줄에 8 대신 10을 썼을 때 저장된 점수를 더하면 잘못된 기록을 숨깁니다. 점수표는 자신의 저장된 합계나 점수로 자신을 증명할 수 없습니다.

replay는 소유권을 넘기거나 수정하지 않고 기록을 빌려 보는 슬라이스 &[Round]를 받습니다. 작성 전에 검사를 설계하세요. 정확히 세 기록을 요구합니다. 점수 구현이 눈을 배열 인덱스로 쓸 수 있으므로 각 기록에서 score를 부르기 전에 1~6 밖의 눈을 거절하세요. 작은 Vec로 사용 항목을 추적하고 contains로 현재 항목이 이미 나왔는지 검사합니다. 중복을 거절하세요. 실제 주사위와 항목으로 점수를 재계산해 저장된 점수와 비교합니다. 재계산한 결과만 합계에 더하고 모든 기록이 유효한 뒤에만 합계를 반환하세요.

실패 조건에는 빠른 return Err("...")를 사용할 수 있습니다. for round in rounds에서 각 round는 빌려 오며 round.dice는 기록을 바꾸지 않고 확인할 수 있는 작은 Copy 배열입니다. !(1..=6).contains(&face)는 눈이 양 끝을 포함한 유효 범위 밖이라는 뜻입니다. 의미 있는 거절 메시지를 고르세요. 계약을 유지한다면 판단을 다르게 구성해도 됩니다.

이미 작동하는 submit을 포함하는 새 복구 지점을 저장하세요. 이전 초기 코드 백업과 혼동하면 안 됩니다.

rs02_before_replay=$(mktemp -d .edit-before-replay.XXXXXX) && cp -p src/state.rs "$rs02_before_replay/state.rs"

같은 파일을 열고 Ctrl+W로 pub fn replay를 검색하세요.

nano -c src/state.rs

사용하지 않는 _rounds 바인딩을 타입은 유지하며 rounds로 바꾸고 이 함수 본문만 구현합니다. submit, 구조체, 모듈 연결은 그대로 둡니다. 순서 있는 검사와 누적값을 사용하세요. 28을 그대로 반환하거나 저장된 점수를 믿거나 잘못된 기록을 조용히 건너뛰면 안 됩니다. Ctrl+O, Enter로 저장하고 Ctrl+X로 나간 뒤 저장한 코드를 읽습니다.

cat src/state.rs

고정 예제를 다시 실행합니다.

cargo run --quiet -- play --fixture

각 프롬프트를 기다리며 pair, straight, sum 순서로 입력하세요. 최종 출력에는 다음이 포함됩니다.

1 | [2, 2, 4, 4, 6] | pair | 8
2 | [1, 2, 3, 4, 6] | straight | 0
3 | [6, 5, 4, 3, 2] | sum | 20
Replayed total: 28
Saved match.tsv

준비된 입력 프로그램은 완전한 점수표가 replay를 통과한 뒤에만 match.tsv를 씁니다. 각 줄은 공백으로 구분한 눈 5개, 탭, 항목, 또 하나의 탭, 점수로 이루어집니다. 이 형식은 준비된 프로그램이 처리하며 파일 파서를 구현할 필요는 없습니다. 새 게임을 끝내면 이 코스 소유 점수표를 교체하고 미완성 게임은 이전 점수표를 그대로 둡니다.

실제 저장한 파일을 읽고 재계산하세요.

cat match.tsv
cargo run --quiet -- replay match.tsv

Saved match.tsv 없이 같은 기록과 합계 28이 나와야 합니다. 재계산은 지정한 정확한 경로를 바꾸거나 예제로 대체하지 않고 읽습니다. 이전 항목 순서를 submit에 고정하지 않았는지 확인하려면 고정 예제를 한 번 더 플레이하세요.

cargo run --quiet -- play --fixture

straight, sum, pair를 사용합니다. 0, 16, 0을 예상하세요. 세 선택은 모두 유효하고 재계산 합계는 16입니다. 이 완성된 게임이 이전 점수표를 교체합니다. 현재 파일을 재계산하세요.

cargo run --quiet -- replay match.tsv

이전 28점 게임이 아니라 두 번째 순서의 기록이어야 합니다. 각 주사위에 선택한 규칙이 바뀌었으므로 합계가 다른 것이 정상입니다. 다음 단계에서는 같은 판단으로 실제 무작위 주사위를 사용합니다.

컴파일 오류는 표시된 파일을 다시 열고 해당 replay 줄을 고칩니다. 필요하면 submit을 유지하는 자신의 replay 전 체크포인트로 복원한 뒤 replay를 다시 구현하세요.

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

ls -d .edit-before-replay.*

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

read rs02_before_replay

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

cat "$rs02_before_replay/state.rs"

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

cp -p "$rs02_before_replay/state.rs" src/state.rs

소스 복원은 기존 점수표를 바꾸지 않습니다. 이후 완료한 게임은 replay 성공 뒤에만 교체하고 실패한 게임이나 점수표 읽기는 교체하지 않습니다. 아래 완전한 참고 코드는 시도 후 특정 함수를 비교할 때 쓰며 정상 학습 작업으로 모듈을 교체하기 위한 것이 아닙니다.

비교와 복구를 위한 완전한 상태 참고 코드

필드와 앞서 성공한 작업을 유지하세요. 고치는 함수만 비교하고 nano에서 편집한 뒤 고정 예제와 실제 점수표 재계산을 다시 수행합니다.

use crate::{score, Category, Dice};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Round {
    pub dice: Dice,
    pub category: Category,
    pub points: u16,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Game {
    pub current: Option<Dice>,
    pub rounds: Vec<Round>,
}

impl Game {
    pub fn new(first: Dice) -> Self {
        Self { current: Some(first), rounds: Vec::new() }
    }

    pub fn submit(&mut self, input: &str) -> Result<u16, &'static str> {
        let dice = self.current.ok_or("match finished")?;
        let category = Category::parse(input).ok_or("unknown category")?;
        for round in &self.rounds {
            if round.category == category {
                return Err("category already used");
            }
        }
        let points = score(dice, category);
        self.rounds.push(Round { dice, category, points });
        self.current = None;
        Ok(points)
    }
}

pub fn replay(rounds: &[Round]) -> Result<u16, &'static str> {
    if rounds.len() != 3 {
        return Err("card needs three rounds");
    }
    let mut used = Vec::new();
    let mut total = 0;
    for round in rounds {
        for face in round.dice {
            if !(1..=6).contains(&face) {
                return Err("card has an invalid face");
            }
        }
        if used.contains(&round.category) {
            return Err("card repeats a category");
        }
        let expected = score(round.dice, round.category);
        if expected != round.points {
            return Err("card has incorrect points");
        }
        used.push(round.category);
        total += expected;
    }
    Ok(total)
}

직접 게임하고 자신의 기록 재사용하기

이 단계에서는 완성한 규칙을 새로운 주사위에 적용하고 일반적인 파일 경로 오류에서 복구합니다. 같은 Terminal의 /home/labex/project/dice-game에서 계속하세요.

--fixture가 없으면 준비된 프로그램은 /dev/urandom에서 난수 바이트를 읽습니다. 매번 1부터 6까지의 눈 5개를 생성합니다. 이 프로그램은 로컬 연습 게임입니다. 목표 합계나 이기는 전략은 필요 없지만 점수는 규칙을 따라야 합니다. sum, pair, straight를 순서와 관계없이 한 번씩 사용해야 합니다. 거절된 단어와 중복 항목은 현재 주사위 결과를 유지합니다.

새 게임을 시작합니다.

cargo run --quiet -- play

첫 주사위 결과를 읽고 아직 쓰지 않은 항목을 골라 Enter를 누릅니다. 두 번째와 세 번째도 반복합니다. 거절을 보기 위해 한 번 pairs를 의도적으로 입력한 다음 사용 가능한 항목으로 고쳐도 됩니다. 수정하는 동안 주사위와 rolls 수가 바뀌지 않는지 보세요. 선택 3개를 받아들인 뒤 기록 3개와 재계산한 합계를 확인합니다. 무작위 값은 고정 예제와 다를 수 있으며, 끝난 게임은 match.tsv를 교체합니다.

새 점수표를 다시 읽습니다.

cargo run --quiet -- replay match.tsv

기록은 유효한 0점까지 포함해 가장 최근 게임과 일치해야 합니다. 일반적인 경로 실수를 연습하기 위해 다음 명령은 존재하지 않는 파일을 일부러 요청합니다. Error: missing-card.tsv: ... 메시지와 상태 1을 예상하세요. 대체 점수표를 생성해서는 안 됩니다.

cargo run --quiet -- replay missing-card.tsv

실제 파일명으로 복구합니다.

cargo run --quiet -- replay match.tsv

어느 재계산에서도 점수표는 바뀌지 않으며 계속 읽을 수 있습니다. src/main.rs, Cargo.toml, 공개 API를 유지하세요. 검사는 격리된 복사본에서 원래 구현을 실행하고 실제 match.tsv를 읽습니다. 여러분 대신 점수표를 만들지는 않습니다.

요약

게임과 기록을 모델링하고, 성공 또는 거절을 명시적으로 반환하며, 검증 후에만 상태를 바꿨습니다. 잘못된 선택은 현재 결과를 유지하고, 유효한 0점은 게임을 진행하며, 재계산은 완전한 점수표를 확인했습니다. 이전 VM에 의존하지 않고 새로운 주사위로 게임을 하고 실제 저장 파일을 재사용했습니다.