Expressar regras com escolhas tipadas

RustBeginner
Pratique Agora

Introdução

Este jogo de cinco dados oferece três opções: sum, pair e straight. A mesma rolagem pode render pontuações diferentes conforme sua escolha. Para [2, 2, 4, 4, 6], a soma é 18, enquanto o maior par vale 8. Você transformará essas regras curtas em código Rust tipado e experimentará rolagens conhecidas e aleatórias.

Você deve conhecer funções, arrays, enum, match e Option em Rust, além de caminhos básicos no terminal. Cargo e o programa de entrada estão preparados; você implementa as decisões de pontuação. Este laboratório é independente de outras VMs. Reserve cerca de 40 minutos, incluindo leitura e erros comuns; essa é uma estimativa do autor.

Modelar uma escolha e calcular a soma

Nesta etapa, leia os tipos preparados, preveja uma pontuação e escreva uma pequena soma funcional na biblioteca real do jogo. Comece na aba Terminal. A preparação criou /home/labex/project/dice-game com um projeto Cargo. src/lib.rs é a biblioteca de regras; src/main.rs fornece entrada e dados aleatórios. Mantenha src/main.rs e Cargo.toml inalterados.

O conjunto de ferramentas está em /home/labex/.cargo/bin. export adiciona esse diretório a PATH, a lista onde o shell procura comandos. Essa configuração vale para esta sessão do Terminal; repita-a em um novo shell. cd seleciona o projeto e pwd confirma sua localização.

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

Espere /home/labex/project/dice-game. Corrija um erro de caminho com o cd mostrado; não reinicialize o ambiente. Antes de editar, preveja a soma de [2, 2, 4, 4, 6]. É 18. Primeiro você fará esse comando comum de pontuação funcionar.

Leia a biblioteca existente com cat, que imprime um arquivo sem alterá-lo:

cat src/lib.rs

Dice é um alias para [u8; 5], exatamente cinco valores sem sinal de um byte. O programa de entrada admite apenas valores de 1 a 6. Category nomeia três opções. Category::parse retorna Some(category) para uma palavra reconhecida e None para uma desconhecida. Preveja o retorno para sum e sums e encontre seus ramos match. A rejeição de entrada pertence ao programa de entrada; uma categoria reconhecida pode legitimamente valer zero.

Os tipos e métodos auxiliares são fornecidos. pub torna nomes disponíveis ao programa de entrada. impl Category agrupa métodos desse tipo; Self significa Category dentro dele. derive fornece cópia, exibição de depuração e igualdade para esse enum. name retorna um literal de string fixo para exibição. Preserve essas definições. Sua edição fica restrita a score, cujo corpo atual retorna zero para todas as categorias.

Guarde um ponto de restauração antes desta edição. mktemp -d cria um diretório novo, substituindo os seis caracteres X por um sufixo único em vez de reutilizar uma cópia antiga. $() captura o caminho impresso na variável de sessão rs02_before_sum. && executa a cópia somente se a criação do diretório tiver sucesso. cp -p copia o código-fonte e preserva seus metadados; as aspas mantêm o caminho da variável unido. Essa cópia contém apenas a biblioteca editável, não o programa de entrada nem a configuração Cargo preparados. Se alguma operação falhar, leia o erro e faça uma cópia bem-sucedida antes de editar.

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

Abra esse arquivo existente no nano. -c exibe a posição do cursor, útil para acompanhar números de linha do compilador:

nano -c src/lib.rs

A notação ^ do nano significa a tecla Ctrl. Pressione Ctrl+W, digite pub fn score e pressione Enter para encontrar a função. Use as setas para mover e Backspace/Delete para alterar caracteres. Leia da { de abertura da função até a } correspondente antes de editar. Não apague o enum nem os métodos auxiliares anteriores.

O código inicial nomeia parâmetros não usados como _dice e _category. Renomeie apenas essas variáveis de parâmetro para dice e category para usá-las no corpo; preserve o nome da função e os tipos dos parâmetros e retorno. Substitua sua única expressão 0 por uma expressão match category. Um ramo tem um padrão, => e um resultado, por exemplo o ramo temporário Category::Pair => 0,. Dê a Category::Straight o mesmo resultado temporário zero. Você implementará essas duas regras a seguir.

Para Category::Sum, use um bloco que começa com um acumulador mutável, visita cada valor e soma o valor ampliado. Este laço curto mostra a operação numérica:

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

u16::from(face) amplia o número antes da soma. Até cinco valores seis cabem em u16. Depois desse laço, faça total ser a expressão final do ramo Sum: sem ponto e vírgula depois dessa expressão final, para que o ramo retorne o valor. Feche as chaves do match e da função. Você implementou uma categoria real no programa existente, deixando deliberadamente incompletas as duas opções restantes.

Pressione Ctrl+O para gravar, Enter para confirmar src/lib.rs e Ctrl+X para sair do nano. Não digite comandos do terminal enquanto o editor ainda estiver aberto. Leia a edição salva antes de compilar:

cat src/lib.rs

Cargo compila e executa o programa preparado. --quiet oculta mensagens comuns de compilação; -- separa opções Cargo de argumentos do programa. Execute os mesmos cinco valores que previu:

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

Os dados não mudaram; a regra escolhida produziu os pontos. Confira também se seu laço prevê 30 para cinco valores seis antes de continuar.

Se Cargo informar erro de sintaxe, leia o arquivo e número da linha, reabra nano -c src/lib.rs, procure pub fn score e repare a chave, o ramo ou a expressão correspondente. Salve, saia e repita o comando que falhou. Enquanto uma edição ainda não foi salva, Ctrl+X e N à pergunta sobre salvar a descartam; isso não desfaz alterações já gravadas.

Para um erro já salvo que não consiga reparar, restaure sua própria biblioteca anterior à edição com o comando abaixo. Ele substitui intencionalmente apenas src/lib.rs, que é permitido; não restaura o programa de entrada nem os arquivos Cargo. A variável da cópia pertence a esta sessão do Terminal. Em outro shell, use as instruções condicionais abaixo para selecionar e examinar sua cópia anterior real.

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-sum.*

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_sum

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_sum/lib.rs"

Ela deve conter o código inicial anterior a Sum. 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_sum/lib.rs" src/lib.rs

Isso restaura o código inicial, então repita a implementação de Sum e seu comando de pontuação. Mantenha esta sessão do Terminal e sua Sum funcional para a próxima etapa.

Pontuar o maior par e uma sequência

Nesta etapa, planeje e implemente as decisões restantes de pontuação, preservando seu ramo Sum funcional. Continue no mesmo Terminal, em /home/labex/project/dice-game. Em um novo shell, repita os comandos anteriores export PATH e cd.

Um par vale o dobro do maior valor que aparece pelo menos duas vezes. Três, quatro ou cinco valores iguais ainda contribuem com apenas dois dados. Em [2, 2, 4, 4, 6], ambos os pares devem contribuir ou somente os quatro? Somente os quatro: 8 pontos. Com cinco valores seis, o resultado é 12, não 30. Sem par, zero é válido.

Uma representação útil é [0u8; 7]: sete contadores indexados de 0 a 6, com 0 não usado. Para cada dado, usize::from(face) converte seu valor em índice do array; counts[usize::from(face)] += 1 incrementa a contagem desse valor. Um segundo laço pode examinar valores em 1..=6, que inclui os dois extremos. Quando uma contagem for pelo menos dois, substitua uma pontuação máxima mutável pelo dobro desse valor; a visita crescente deixa o maior par elegível. as u16 converte esse pequeno resultado numérico no tipo da pontuação. Você pode organizar um algoritmo equivalente de contagem ou ordenação; ele ainda deve tratar corretamente dois pares disponíveis e três a cinco dados iguais.

Uma sequência vale 15 somente para exatamente os valores distintos de 1 a 5 ou de 2 a 6, em qualquer ordem. Preveja a diferença entre [6, 5, 4, 3, 2] e [1, 2, 3, 4, 6]. O primeiro é uma sequência; o segundo tem uma lacuna. Copiar com let mut sorted = dice e chamar sorted.sort() permite comparar o array ordenado com os dois arrays exatos sem alterar a rolagem original. || significa que qualquer comparação pode ser verdadeira. Decida quando retornar 15 e quando retornar zero; repetições não devem passar.

Preserve a etapa já funcional antes de editar. Como antes, o diretório único impede sobrescrever uma cópia anterior, e a cópia só ocorre após a criação bem-sucedida do diretório:

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

Abra o arquivo existente, procure pub fn score com Ctrl+W e altere somente os ramos Pair e Straight. Preserve Sum, a API pública e todas as definições auxiliares. Escreva a implementação a partir dessas regras em vez de substituir o módulo inteiro.

nano -c src/lib.rs

Salve com Ctrl+O, Enter e saia com Ctrl+X. Antes de executar, leia os ramos salvos e preveja cada resultado:

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

Agora compare cinco dados iguais, os dois intervalos de sequência e zeros válidos:

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

Espere 12, 15, 15, 0 e 0, respectivamente. Zero significa que a regra reconhecida não encontrou combinação que pontue, não que a entrada foi rejeitada. Escolha mais um exemplo válido próprio de cinco dados, preveja sua pontuação para uma categoria e execute score com essa categoria e todos os cinco valores. Explique qualquer divergência pela regra em vez de mudar o exemplo para escondê-la.

Omitir os cinco valores usa a fonte aleatória do sistema operacional. Este é um jogo local, não um sistema de segurança ou apostas. Execute uma vez; pode repetir:

cargo run --quiet -- score pair

Seus dados e pontuação podem variar, e uma rolagem aleatória pode se repetir por acaso. Aplique independentemente a regra do maior par aos dados exibidos. Uma palavra desconhecida difere de um zero válido: este erro deliberado encerra com status 1 e imprime Error: choose sum, pair, or straight sem pontuar uma rolagem:

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

Corrija a categoria na linha de comando:

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

Espere pair: 8. Já um erro do compilador aponta para o código-fonte: reabra nano em score, repare a linha indicada, salve e tente novamente. Se necessário, restaure sua etapa própria com Sum funcional e reimplemente os dois ramos restantes:

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-rules.*

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_rules

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_rules/lib.rs"

Ela deve conter Sum funcional antes de Pair e Straight. 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_rules/lib.rs" src/lib.rs

Isso restaura deliberadamente apenas sua biblioteca editável. Não reinicialize o ambiente nem sobrescreva src/main.rs ou Cargo.toml. A referência abaixo fica disponível depois de sua tentativa para comparar ou recuperar uma função específica; não é a sequência normal de edição.

Referência completa da biblioteca para comparação e recuperação

Compare o ramo relevante e transfira apenas o reparo necessário pelo nano. Execute novamente os exemplos conhecidos e de limites depois. A biblioteca completa abaixo também é a única fonte mantida usada para preparar as regras já completas do próximo laboratório.

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

Resumo

Você usou uma categoria tipada, match, uma soma acumulada, contadores de valores e uma cópia ordenada para expressar três regras completas. Os exemplos fixos explicam os resultados, enquanto as rolagens aleatórias permitem aplicar as mesmas regras a novos dados. Quando não existe uma combinação que pontue, o resultado é zero; uma categoria desconhecida é um erro de entrada.