Manter uma ficha de pontuação válida entre rodadas

RustBeginner
Pratique Agora

Introdução

Uma pontuação é apenas uma decisão. Uma partida completa precisa lembrar as categorias usadas e manter os mesmos dados enquanto você corrige uma escolha inválida. Você terminará um jogo de três rodadas e salvará uma ficha que pode ser verificada recalculando os pontos a partir dos dados e das categorias registrados.

As regras de pontuação do primeiro laboratório já estão instaladas neste projeto independente. Você deve conhecer essas regras, funções Rust, enum, match, Option, arrays e caminhos básicos no terminal. Este laboratório apresenta as estruturas preparadas, os métodos mutáveis, Result e as fatias no momento de usá-los. Reserve cerca de 55 minutos, incluindo leitura e erros comuns; essa é uma estimativa do autor.

Confirmar somente uma escolha válida

Nesta etapa, planeje e implemente a transição de estado de uma escolha aceita. Comece na aba Terminal. Este projeto independente já tem regras de pontuação completas. Não usa arquivos do primeiro laboratório. Configure o caminho de busca deste shell e entre no projeto; repita os comandos em um novo shell:

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

Comece com uma pontuação normal funcional e preveja-a antes de executar:

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

Espere os dados exibidos e pair: 8. Isso comprova que a camada de regras fornecida é utilizável antes de acrescentar estado à partida. Leia a estrutura inicial:

cat src/state.rs

Um struct reúne campos nomeados que descrevem um objeto. Round registra cinco dados, uma categoria e pontos. Game contém current: Option<Dice> e rounds: Vec<Round>. Some(dice) significa que uma escolha está pendente; None significa que não há rolagem atual. Vec<Round> é uma coleção expansível; Vec::new() cria uma vazia e push acrescenta um item.

Leia também o chamador no programa de entrada preparado:

cat src/main.rs

Localize match game.submit(input.trim()). Seu ramo Err imprime uma rejeição. Seu ramo Ok imprime pontos e só obtém outra rolagem se houver menos de três rodadas registradas. Você não edita esse programa. Seu método deve deixar o estado inalterado após rejeitar, para que a próxima solicitação mostre os mesmos dados e contagem de rolagens.

Preveja os campos depois de cada tipo de confirmação:

Confirmação rounds após a chamada current após a chamada
Sem dados pendentes, palavra desconhecida ou categoria usada Inalterado Inalterado
Categoria válida não usada, incluindo pontuação zero Um novo registro completo acrescentado None

O programa de entrada, não submit, fornece a próxima rolagem após sucesso. Não obtenha dados aleatórios no método. impl Game agrupa métodos de Game. &mut self permite atualizar essa partida por uma referência mutável. Result<u16, &'static str> significa Ok(points) ou Err(message); &'static str é o tipo de texto fixo das mensagens de erro. Diferentemente de Option, Result pode explicar uma falha.

ok_or(message) transforma Some em Ok e None em Err. Um ? seguinte continua com o valor bem-sucedido ou retorna o erro imediatamente. Por exemplo, a verificação de rolagem pendente pode ser:

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

Aplique a mesma ideia a Category::parse(input). Antes de alterar qualquer campo, tome os registros anteriores por empréstimo com um laço for sobre &self.rounds e rejeite uma categoria correspondente. Você escolhe como organizar as verificações; a tabela é o contrato. Depois da validação, obtenha points com score(dice, category). A confirmação bem-sucedida tem três ações:

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

Os campos abreviados usam variáveis locais com os mesmos nomes. Um resultado zero ainda é sucesso. Nenhuma dessas alterações pertence a um caminho rejeitado.

Mantenha src/main.rs, Cargo.toml, os tipos públicos, nomes e tipos das funções e as exportações de módulo inalterados. use crate::{score, Category, Dice} importa da biblioteca atual; mod state e pub use em src/lib.rs conectam os módulos. Esta etapa edita somente submit em src/state.rs, preservando as estruturas, o método new e replay incompleto.

Antes de editar, faça uma cópia única desse arquivo permitido. Como ensinado, mktemp -d substitui seis X por um sufixo único. $() guarda o caminho impresso; && executa cp -p somente depois da criação bem-sucedida do diretório. O caminho entre aspas continua como um argumento. Se falhar, leia o erro e obtenha uma cópia antes de editar.

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

Abra o arquivo no nano; -c mostra a posição do cursor:

nano -c src/state.rs

Pressione Ctrl+W, procure pub fn submit e pressione Enter. Examine as chaves de abertura e fechamento desse método. Renomeie a variável de parâmetro não usada _input para input sem mudar seu tipo. Substitua apenas o corpo incompleto por sua implementação das verificações e da confirmação bem-sucedida acima. Preserve os outros métodos e definições de dados. Use setas e Backspace/Delete para editar. Salve com Ctrl+O, Enter e saia com Ctrl+X. A notação ^ do nano significa Ctrl. Se houver uma edição acidental ainda não salva, Ctrl+X e N à pergunta sobre salvar a descartam; isso não desfaz uma edição salva antes.

Leia o método salvo antes de executar:

cat src/state.rs

Use a sequência fixa conhecida para observar suas decisões de estado. --fixture seleciona rolagens divulgadas em vez da fonte aleatória:

cargo run --quiet -- play --fixture

Aguarde cada solicitação, digite uma palavra e pressione Enter:

Primeiro digite pairs. É desconhecida: espere os mesmos [2, 2, 4, 4, 6] e rolls 1 após a rejeição. Depois digite pair. Vale 8 e avança para [1, 2, 3, 4, 6], rolls 2. Digite pair novamente. A categoria usada é rejeitada, preservando esses dados e rolls 2. Depois digite straight. Essa escolha válida vale zero e avança para a terceira rolagem. Por fim digite sum. Vale 20.

O erro final esperado é Error: replay not implemented, com status 1. Suas confirmações funcionaram, mas replay ainda é a função original incompleta, portanto o programa se recusa a salvar uma ficha não verificada. Este é o estado intermediário planejado, não uma partida salva com sucesso. Se escolher outra categoria válida por engano, termine as opções não usadas ou use Ctrl+C e inicie intencionalmente uma nova partida fixa. Dentro de uma partida, uma rejeição não deve reiniciar nem rolar nada novamente.

Para um erro do compilador, leia o arquivo/linha indicado, reabra nano, procure o método afetado e repare-o. Para um erro salvo que não consiga reparar, restaure seu arquivo anterior a submit e implemente e teste submit novamente:

Somente ao recuperar um erro já salvo: no Terminal original, a variável do checkpoint ainda contém seu caminho. Em um novo Terminal, primeiro repita os comandos anteriores de PATH e diretório do projeto. Liste as cópias de segurança desta etapa:

ls -d .edit-before-submit.*

ls -d lista nomes de diretórios, não seu conteúdo; * corresponde aos sufixos dos diretórios únicos desta fase. Esses nomes foram realmente gerados, portanto não digite um sufixo inventado. Se nenhuma cópia corresponder, confirme o diretório do projeto e se você criou este checkpoint; não copie de uma fase sem relação. Para selecionar um dos caminhos exibidos, execute:

read rs02_before_submit

O shell aguarda uma linha. Cole o nome exato do diretório escolhido e pressione Enter. read guarda essa linha na variável indicada para esta sessão. Confirme o conteúdo da cópia antes de restaurar:

cat "$rs02_before_submit/state.rs"

Ela deve conter o código inicial anterior a submit. Se o caminho ou a etapa estiver errado, repita read com o caminho correto exibido e examine novamente. Só então use o comando de restauração. Essa verificação também vale no Terminal original se você não souber ao certo qual checkpoint a variável identifica.

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

Esse comando substitui intencionalmente apenas o arquivo de estado editável. A variável da cópia é local a este Terminal; use as instruções condicionais de recuperação em outro shell. Não reinicialize setup nem substitua o programa de entrada preparado para corrigir um erro de estado.

Recalcular e salvar uma ficha completa

Nesta etapa, implemente uma verificação independente de cada rodada registrada antes de aceitar um total. Continue no mesmo Terminal, em /home/labex/project/dice-game; repita export PATH e cd anteriores se necessário. Preserve seu método submit concluído.

Primeiro confira à mão a ficha fixa proposta: pair em [2, 2, 4, 4, 6] vale 8, straight em [1, 2, 3, 4, 6] vale 0 e sum em [6, 5, 4, 3, 2] vale 20. O total recalculado é 28. Se alguém escreveu 10 em vez de 8 na primeira linha, somar seus pontos salvos ocultaria o registro errado. Uma ficha não pode comprovar sua correção com seu próprio total ou pontos salvos.

replay recebe &[Round], uma fatia: uma visão emprestada de registros, sem transferência de posse nem modificação. Planeje as verificações antes de escrever. Exija exatamente três registros. Para cada um, rejeite valores fora de 1 a 6 antes de chamar score, pois uma implementação de pontuação pode usar um valor como índice de array. Registre as categorias usadas em um pequeno Vec; contains testa se a categoria atual já apareceu. Rejeite duplicatas. Recalcule pontos a partir dos dados e categoria reais e compare com os pontos salvos. Some apenas o resultado recalculado ao total e só retorne o total depois que todos os registros forem válidos.

Você pode usar return Err("...") antecipado para uma condição que falhou. Em for round in rounds, cada rodada é emprestada; round.dice é um pequeno array Copy que pode examinar sem alterar o registro. A condição !(1..=6).contains(&face) significa que o valor está fora do intervalo válido inclusivo. Escolha mensagens de rejeição significativas; sua implementação pode organizar essas decisões de outra forma preservando o contrato.

Salve um novo ponto de restauração contendo seu submit já funcional. Ele não pode ser confundido com a cópia inicial anterior:

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

Abra o mesmo arquivo e procure pub fn replay com Ctrl+W:

nano -c src/state.rs

Renomeie a variável de parâmetro não usada _rounds para rounds, preservando seu tipo, e implemente apenas o corpo dessa função. Deixe submit, as estruturas e conexões de módulo como estavam. Use suas verificações ordenadas e acumulador; não retorne apenas 28, não confie nos pontos salvos nem ignore silenciosamente um registro inválido. Salve com Ctrl+O, Enter e saia com Ctrl+X. Leia o código salvo:

cat src/state.rs

Execute a sequência fixa novamente:

cargo run --quiet -- play --fixture

Digite pair, depois straight e sum, aguardando cada solicitação. A saída final inclui:

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

O programa preparado escreve match.tsv somente depois que uma ficha completa passa por replay. Cada linha tem cinco valores separados por espaços, uma tabulação, categoria, outra tabulação e pontos. O programa preparado trata esse formato; você não implementa um analisador de arquivos. Uma nova partida concluída substitui essa ficha do curso, enquanto uma incompleta deixa a anterior intacta.

Leia e recalcule seu arquivo realmente salvo:

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

Espere os mesmos registros e total 28, sem Saved match.tsv. Replay lê o caminho exato sem alterá-lo nem recorrer a um exemplo. Para verificar que não fixou a sequência anterior de categorias em submit, jogue a sequência fixa mais uma vez:

cargo run --quiet -- play --fixture

Use straight, sum e depois pair. Preveja 0, 16 e 0: as três escolhas são válidas e o total recalculado é 16. Essa partida concluída agora substitui a ficha anterior. Recalcule esse arquivo atual:

cargo run --quiet -- replay match.tsv

Seus registros devem refletir essa segunda sequência, não a partida anterior de 28 pontos. Um total diferente é esperado porque a regra escolhida para cada rolagem mudou. A próxima etapa usa dados realmente aleatórios com as mesmas decisões.

Para um erro do compilador, reabra o arquivo indicado e repare a linha de replay informada. Se necessário, restaure seu checkpoint anterior a replay, que preserva submit, e implemente replay novamente:

Somente ao recuperar um erro já salvo: no Terminal original, a variável do checkpoint ainda contém seu caminho. Em um novo Terminal, primeiro repita os comandos anteriores de PATH e diretório do projeto. Liste as cópias de segurança desta etapa:

ls -d .edit-before-replay.*

ls -d lista nomes de diretórios, não seu conteúdo; * corresponde aos sufixos dos diretórios únicos desta fase. Esses nomes foram realmente gerados, portanto não digite um sufixo inventado. Se nenhuma cópia corresponder, confirme o diretório do projeto e se você criou este checkpoint; não copie de uma fase sem relação. Para selecionar um dos caminhos exibidos, execute:

read rs02_before_replay

O shell aguarda uma linha. Cole o nome exato do diretório escolhido e pressione Enter. read guarda essa linha na variável indicada para esta sessão. Confirme o conteúdo da cópia antes de restaurar:

cat "$rs02_before_replay/state.rs"

Ela deve conter submit funcional antes de replay. Se o caminho ou a etapa estiver errado, repita read com o caminho correto exibido e examine novamente. Só então use o comando de restauração. Essa verificação também vale no Terminal original se você não souber ao certo qual checkpoint a variável identifica.

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

Restaurar o código-fonte não altera uma ficha existente. Uma partida concluída posterior só a substitui depois do sucesso de replay; partidas incompletas e a leitura de uma ficha não. A referência completa abaixo serve para comparar uma função específica depois de sua tentativa, não para substituir o módulo como operação normal de aprendizado.

Referência completa de estado para comparação e recuperação

Preserve seus campos e o trabalho anterior bem-sucedido. Compare apenas a função que está reparando, edite-a no nano e execute a sequência fixa e o recálculo da ficha real novamente.

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)
}

Jogar e reutilizar sua própria partida

Nesta etapa, você usará as regras completas com dados novos e corrigirá um erro comum de caminho de arquivo. Continue no mesmo Terminal, em /home/labex/project/dice-game.

Sem --fixture, o programa preparado lê bytes aleatórios de /dev/urandom. Ele produz cinco valores de 1 a 6 em cada rolagem. Este é um jogo local de prática. Você não precisa atingir um total específico nem seguir uma estratégia vencedora, mas os pontos devem respeitar as regras. Cada uma das categorias sum, pair e straight deve ser usada uma vez, em qualquer ordem. Palavras rejeitadas e categorias repetidas preservam a rolagem atual.

Comece uma nova partida:

cargo run --quiet -- play

Leia os primeiros dados, escolha uma categoria ainda não utilizada e pressione Enter. Repita na segunda e na terceira rolagem. Você pode digitar pairs uma vez de propósito para observar uma rejeição e então corrigir para uma categoria disponível. Observe que os dados e o contador rolls não mudam durante a correção. Após três escolhas aceitas, examine os três registros e o total recalculado. Os valores aleatórios podem diferir da sequência fixa, e a partida concluída substitui match.tsv.

Leia a nova ficha novamente:

cargo run --quiet -- replay match.tsv

Os registros devem corresponder à sua partida mais recente, incluindo zeros legítimos. Para praticar um erro comum de caminho, o próximo comando solicita intencionalmente um arquivo inexistente. Espere a mensagem Error: missing-card.tsv: ... e status 1; ele não deve produzir uma ficha substituta:

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

Corrija usando o nome real do arquivo:

cargo run --quiet -- replay match.tsv

Sua ficha continua legível e não é alterada por nenhum dos dois recálculos. Preserve src/main.rs, Cargo.toml e a API pública. A verificação executa sua implementação original em uma cópia isolada e lê o verdadeiro match.tsv; ela não cria uma ficha por você.

Resumo

Você modelou uma partida e seus registros, retornou sucesso ou rejeição explícitos e alterou o estado somente após a validação. Escolhas inválidas preservaram a rolagem, zeros legítimos fizeram a partida avançar e o recálculo verificou uma ficha completa. Você jogou com novos dados e reutilizou o arquivo realmente salvo sem depender de uma VM anterior.