Percursos
/
Rust
Rust
/
Exercícios
/
Jogo de interpretação de papéis
Jogo de interpretação de papéis

Jogo de interpretação de papéis

Exercício de aprendizagem

Introdução

Referências nulas

Se já alguma vez usaste outra linguagem de programação (C/C++, Python, Java, Ruby, Lisp, etc.), é provável que já tenhas encontrado null ou nil. É através de null ou nil que essas linguagens indicam que uma determinada variável não tem valor. No entanto, isso torna o uso acidental de uma variável que aponta para null um erro fácil (e frequente) de cometer. Como podes imaginar, tentar chamar uma função que não existe, ou aceder a um valor que não existe, pode dar origem a todo o tipo de bugs e falhas. O criador do null chegou ao ponto de o chamar o seu 'erro de mil milhões de dólares'.

O tipo Option

Para evitar estes problemas, o Rust não usa referências nulas. No entanto, continua a precisar de uma forma segura de indicar que uma determinada variável não tem valor. É aqui que entra o Option. Em vez de teres uma variável sem valor, as variáveis do Rust podem usar o enum Option. Este enum tem duas variantes: None, o equivalente a null no Rust; e Some(T), em que T é um valor de qualquer tipo.

Tem este aspeto:

enum Option<T> {
    None,
    Some(T),
}

Podes pensar no Option como uma camada de segurança entre ti e os problemas que as referências nulas podem causar, mantendo ainda assim a sua utilidade conceptual.

Usar Option

Definir uma variável como None é bastante simples:

let nothing: Option<u32> = None;  // Variable nothing is set to None

No entanto, se quiseres que o tipo Option transporte um valor, não podes atribuir esse valor diretamente. Uma variável do tipo Option e, digamos, uma variável do tipo i32 não são equivalentes. Vais precisar de usar Some:

let wrong_way: Option<i32> = -4; // This will not work

let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>

É também por esta razão que o seguinte não funciona:

let number = 47;
let option_number = Some(15);

let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types

Se quiseres obter o valor contido em Some, primeiro tens de verificar que ele existe:

let mut some_words = Some("choose something to say"); // some_words set to something

match some_words {
    Some(str) => println!("Here, we will {}", str),
    None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"

some_words = None; // some_words now set to None

// exactly the same match block as above
match some_words {
    Some(str) => println!("Here, we will {}", str),
    None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"

Além do match, o Rust tem outras ferramentas para verificar e aceder aos valores contidos no Option, mas o match já te deve ser familiar a esta altura.

Além disso, encara isto como uma demonstração da razão pela qual o Rust usa Option em vez de uma referência nula. O ponto principal é que tens mesmo de verificar se a variável Option é Some (caso em que podes avançar, extrair e usar o valor que está lá dentro) ou None. Qualquer outra coisa, e o teu programa não compila; o compilador está a proteger-te do null.

Instruções

Estás a trabalhar na implementação de um jogo de RPG. A personagem do jogador é representada pelo seguinte:

pub struct Player {
    health: u32,
    mana: Option<u32>,
    level: u32,
}

Neste jogo, os jogadores têm de atingir o nível 10 antes de desbloquearem uma reserva de mana, para poderem começar a lançar feitiços. Antes disso, a mana do Player é None.

Estás a trabalhar em duas funcionalidades deste jogo: o mecanismo de reanimação e o mecanismo de lançamento de feitiços.

O método revive deve verificar que o Player está mesmo morto (a sua saúde chegou a 0) e, se estiver, deve devolver uma nova instância de Player com 100 de saúde. Se o nível do Player for 10 ou superior, também deve ser reanimado com 100 de mana. Se o nível do Player for inferior a 10, a sua mana deve ser None. O método revive deve preservar o nível do Player.

let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }

Se o método revive for chamado num Player cuja saúde seja 1 ou superior, o método deve devolver None.

let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None

O método cast_spell recebe uma referência mutável ao Player, bem como um parâmetro mana_cost que indica quanta mana o feitiço custa. Devolve a quantidade de dano que o feitiço lançado provoca, que será sempre o dobro do custo de mana do feitiço, se este for lançado com sucesso.

  • Se o jogador não tiver acesso a uma reserva de mana, tentar lançar o feitiço deve diminuir a sua saúde no valor do custo de mana do feitiço. O dano devolvido deve ser 0.

    let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
    assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
    assert_eq!(not_a_wizard_yet.health, 74);
    assert_eq!(not_a_wizard_yet.mana, None);
    
  • Se o jogador tiver uma reserva de mana mas mana insuficiente, o método não deve afetar a reserva e deve, em vez disso, devolver 0

    let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
    assert_eq!(low_mana_wizard.cast_spell(10), 0);
    assert_eq!(low_mana_wizard.health, 93);
    assert_eq!(low_mana_wizard.mana, Some(3));
    
  • Caso contrário, o mana_cost deve ser deduzido da reserva de mana do Player e deve ser devolvida a quantidade de dano adequada.

    let wizard = Player { health: 123, mana: Some(30), level: 18 };
    assert_eq!(wizard.cast_spell(10), 20);
    assert_eq!(wizard.health, 123);
    assert_eq!(wizard.mana, Some(20));
    

Diverte-te!

Editar via GitHub A ligação abre numa nova janela ou separador
Rust Exercism

Estás pronto para começar Jogo de interpretação de papéis?

Inscreve-te no Exercism para aprenderes e dominares Rust com 99 exercícios, e mentoria humana real, tudo grátis.