Eu estava em dúvida em que projeto fazer neste ponto, foi difícil pensar em algo e, porque não algo que eu nunca fiz?
Então vamos lá fazer o "jogo da cobrinha", vamos começar criando o projeto, usando o comando cargo new snake-game.
Teremos a estrutura padrão do projeto:
├── Cargo.toml
└── src
└── main.rs
Vamos adicionar um arquivo chamado lib.rs na pasta src, este arquivo sera usado para declarar os nossos módulos. Em seguida criamos um arquivo chamado "ponto.rs" e nele iremos criar uma struct para as localizações no nosso jogo, vamos criar uma implementação a essa struct para facilitar a instânciação dessa strutc.
Para que este arquivo seja reconhecido no projeto, vamos adicionar no nosso arquivo lib.rs a seguinte linha pub mod point;. Note que tanto a struct quanto seus atributos e a implementação do método new estão com a palavra pub, que faz eles serem visíveis fora desse módulo.
Vamos printar o campo onde a cobrinha ira andar, e para testar vamos adicionar um ponto nesse tabuleiro.
fnmain() {
let ponto = Ponto::new(7, 7);
let (x, y) = (15, 15);
for x in0..x {
for y in0..y {
if ponto == (x, y) {
print!("# ")
} else {
print!("- ");
}
}
println!();
}
}
Note que comparamos a nossa struct Ponto, com uma tupla de (x, y), para isso ser possível, precisamos implementar uma trait chamada PartialEq a implementação para isso é relativamente simples. A trait recebe um parâmetro genérico na implementação vamos falar que esse parâmetro genérico é uma tupla (usize, usize). E a partir dai implementamos nossa comparação.
Agora vamos criar a struct da nossa cobrinha, para isso vamos adicionar a cabeça - que é um ponto - e uma lista de pontos para o corpo. Criamos o arquivo "cobra.rs" e adicionamos o pub mod cobra no arquivo lib.rs, e no arquivo "cobra.rs" adicionamos a struct.
A implementação da traitdefault serve para termos um valor padrão para a struct. Vamos separar a nossa função de desenhar o tabuleiro e vamos passar uma referência para a struct da cobra, então com base nos dados passados ali vamos desenhar a nossa cobra.
#![allow(unused)]fnmain() {
fnprint_board(cobra: &Cobra) {
let (x, y) = (15, 15);
for y in0..y {
for x in0..x {
if cobra.cabeca == (x, y) {
print!("0 ")
} elseif cobra.corpo.contains(&Ponto::new(x, y)) {
print!("# ");
} else {
print!("- ");
}
}
println!();
}
}
}
Temos a função e agora é só chamar ela na nossa função main.
Agora temos a cabeça e o corpo, precismos começar a definir uma direção que a cobra irá seguir e movimentar o corpo da cobra.
Para isso criamos um enumarado de direções, seguimos o mesmo passo a passo, criamos um arquivo "direcao.rs" e adicionamos no arquivo lib.rs a declaração do módulo pub mob direcao.
Então adicionamos as 4 direções possíveis ao nosso enum.
Criamos o enum já implementando a trait default para nos auxiliar, como o padrão de início da cobra sempre vai ser para a direita, colocamos o retorno do método o valor Self::Direita. Já derivamos as traits, Clone e Copy para não precisar passar esse enum como referência todas às vezes.
Agora na nossa struct da cobra, vamos adicionar o atributo da direção.
Agora iremos adicionar a lógica para a cobra se mover, precisaremos de um método para mover a cabeça que é quem vai definir se o movimento é valido, se vai bater na parede, se vamos alterar a direção e já vamos adicionar os testes que consiste em, encerrar o jogo caso bata na parede, validar a posição dos pontos após algum movimento, etc.
#![allow(unused)]fnmain() {
fnmover_cabeca(&mutself, board: &(usize, usize)) -> Result<(), &'staticstr> {
matchself.direcao {
Direcao::Cima ifself.cabeca.y == 0 => Err("fim de jogo, esbarrou na parede de cima"),
Direcao::Baixo ifself.cabeca.y >= board.1 => Err("fim de jogo, esbarrou na parede de baixo"),
Direcao::Esquerda ifself.cabeca.x == 0 => Err("fim de jogo, esbarrou na parede da esquerda"),
Direcao::Direita ifself.cabeca.x >= board.0 => Err("fim de jogo, esbarrou na parede da direita"),
_ => {
self.cabeca.alterar(self.direcao);
Ok(())
}
}
}
}
Temos nessa implementação o uso de um if que segue o valor do enum, afinal o que é isso?
Isso faz parte do Pattern Match, é algo que chamamos de guards, do modo em que essa implementação é feita, temos duas validações para cair nesse ponto, o enum deve bater ali e a condição deve ser verdadeira, caso uma das duas condições falhe ele segue para o próximo match.
Na função de mover o corpo temos a lógica para mover o restante da cobra, guardamos a posição do ponto antes de ser alterada e fazemos o próximo item a ser iterado a obter essa posição. Para isso usamos o método da biblioteca padrão do Rust, swap, esse método troca o valor de duas referências que são passadas.
#![allow(unused)]fnmain() {
fnmover_corpo(&mutself, posicao_anterior_cabeca: Ponto) {
let corpo = &mutself.corpo;
letmut posicao_anterior = posicao_anterior_cabeca;
for ponto in corpo.iter_mut() {
std::mem::swap(&mut posicao_anterior, ponto);
}
}
}
Agora temos a lógica de mover a cobra, mas temos um problema nela, no método de alterar a direção, não temos uma validação para saber se o jogador, selecionou a opção de direção contraria da que a cobra esta seguindo, vamos adicionar agora.
No nosso enum de direção, vamos adicionar um método para pegar a direção contraria.
E agora no nosso método de alterar a direção, faremos a validação, também deixo por sua conta esta alteração, e os testes da mesma.
Agora que temos o tabuleiro do jogo sendo desenhado, e temos a movimentação da cobra programada, vamos adicionar o petisco que iremos ter que pegar no jogo. O petisco é um ponto, então não precisamos criar outra struct para ela, apenas vamos gerar um ponto aleatório e fazer o nosso render renderiza-lo.
#![allow(unused)]fnmain() {
fngerar_petisco(cobra: &Cobra, tabuleiro: &(usize, usize)) -> Point {
letmut petisco;
loop {
let x = rand::thread_rng().gen_range(0..=tabuleiro.0 - 1);
let y = rand::thread_rng().gen_range(0..=tabuleiro.1 - 1);
petisco = Point::new(x, y);
if cobra.cabeca != petisco && !cobra.corpo.contains(&petisco) {
break;
}
}
petisco
}
}
Para esse rand funcionar precisamos ir em nosso Cargo.toml e adicionar a seguinte dependência rand = "0.8.5" logo abaixo do [dependencies], nesse método temos validações para não gerar um petisco em cima da cobra, ou seja, se o valor aleatório cair na cabeça ou em alguma parte do corpo da cobra, outro valor sera gerado. Quando o valor respeitar essa condição o loop para.
Agora precisamos aumentar o tamanho da cobra, para isso adicionamos um método que ira adicionar um ponto, no fim do corpo da cobra.
Para testar esse método é interessante, validarmos o tamanho do corpo e se a posição do ponto adicionado, é igual à posição do último ponto anterior após a cobra se mover.
Agora temos que fazer o jogo funcionar, estamos quase lá.
Vamos criar um arquivo jogo onde teremos a struct Jogo. Aquele mesmo processo de sempre, cria o arquivo, adiciona na lib.rs.
A struct é a mais simples possível, ela é apenas.
#![allow(unused)]fnmain() {
structJogo;
}
Então vamos alterar o método que desenha o tabuleiro para gerar uma String, vamos usa-la para desenhar o tabuleiro inteiro de uma vez e também vamos movê-la para a struct Jogo.
Agora que fizemos essa alteração, vamos jogar o gerador do petisco para essa mesma struct.
#![allow(unused)]fnmain() {
impl Jogo {
...
fngerar_petisco(cobra: &Cobra, tabuleiro: &(usize, usize)) -> Point {
letmut petisco;
loop {
let x = rand::thread_rng().gen_range(0..=tabuleiro.0 - 1);
let y = rand::thread_rng().gen_range(0..=tabuleiro.1 - 1);
petisco = Point::new(x, y);
if cobra.cabeca != petisco && !cobra.corpo.contains(&petisco) {
break;
}
}
petisco
}
}
}
E estamos quase lá, falta apenas um loop infinito, onde, movemos a cobra, limpamos a tela anterior, redesenhamos a tela e capturamos a tecla acionada.
Para facilitar o processo vamos adicionar mais uma dependência no arquivo Cargo.toml, sera dependência termion.
Esse método agrupa tudo o que nós precisamos, criamos a cobra, criamos o primeiro petisco, definimos o tamanho do tabuleiro e começamos a trabalhar.
Na linha let mut stdin = termion::async_stdin().keys(); criamos um modo assincrono de capturar as teclas digitadas pelo usuário, utilizando a dependencia do termion, assim que entramos no loop, fazemos as primeiras verificações, que são:
Validar se a cabeça da cobra está na mesma posição de um petisco
Se sim => seu tamanho aumenta e outro petisco é gerado.
Se não => valida se a cabeça está na mesma posição de seu corpo
Se sim => encerra o jogo com a mensagem de fim de jogo
Se não => continua a execução
Então geramos o tabuleiro e armazenamos em uma variável. Com um método do termion, limpamos o terminal, posicionamos o mouse na primeira posição e escondemos o cursor. Logo em sequência desenhamos tabuleiro do jogo. Transformamos a saída em raw mode, lemos uma tecla e caso alguma tecla tenha sido pressionada validamos qual foi, em um match assim alteramos a direção que a cobra esta andando caso necessário. Limpamos a saída, esperamos meio segundo com o método thread::sleep(Duration::from_milis(500)) e então fazemos a cobra dar mais um passo. O processo todo se repete.
Adicionamos a o nosso main a chamada a esse método: