Aventura de Texto com Rust
Faça um jogo de aventura baseado em texto com Rust!
A programação em Rust te intriga, porém você não tem ideia de onde começar?
Então, nesse caso, esse workshop é para você! Não importa se você é um iniciante ou já está familiarizado com o Rust - Todo o processo será escrito com o máximo de detalhes possíveis, e eu me certifiquei de incluir muitos recursos adicionais durante todo o workshop!
Nota: Este workshop foi elaborado com todos os tipos de níveis de habilidade em mente e pode levar de 30 minutos a uma hora e meia, dependendo do nível de familiaridade com Rust.
- Se você está familiarizado com programação, sinta-se à vontade para pular para a primeira parte da Parte 3.
- Se você também estiver familiarizado com o básico do Rust e com a criação de um novo projeto em Rust, pule para a Parte 4.
- Se você for trabalhar com outras pessoas neste workshop, certifique-se de que esteja familiarizado com o nível de habilidade delas. Esta oficina explica tudo o que está sendo feito com detalhes para acomodar a todos.
Demo
Você pode ver uma demonstração de um exemplo que preparamos no asciinema.org.
O código completo pode ser visto no GitHub e no repl.it.
Parte 1: Configuração 🔰
A fim de agilizar o processo, utilizaremos o repl.it.
Preparando seu projeto
Usar o repl.it é vantajoso no sentido de que você não precisa de nada além de um navegador para usá-lo. Você pode criar uma conta aqui e criar um novo projeto Rust indo para https://repl.it/languages/rust.
Depois de fazer isso, execute cargo init --name app-texto-rust
na aba do Shell.
Parte 2: Estrutura de arquivos e o gerente de pacotes de carga 🏗
Seu projeto no Rust, no mínimo, deve ter este aspecto:
Cargo.toml
main.rs
Cargo é o gestor de pacotes que é usado entre a grande maioria dos desenvolvedores do Rust (também conhecida como “Rustaceanos”). É uma ferramenta conveniente que lida com muitas coisas, incluindo, mas não se limitando às seguintes:
-
Construir: O processo que usamos para descrever o processo onde o compilador (neste caso, o cargo) traduz o código que você pode escrever e ler para um formato que seu computador possa entender.
-
Instalar bibliotecas de software (“caixas”) e dependências: O software depende de outras bibliotecas de software. O Cargo permite que você baixe bibliotecas de software que você encontrou na internet para seus projetos. Por exemplo, o Hack Clubber @anirudhb usou a biblioteca Serenity para interagir com o Discord de uma maneira muito mais fácil.
-
Distribuir seus programas: Compartilhar é cuidar! O Cargo também pode ajudá-lo a publicar seu código no crates.io junto com alguns detalhes adicionais que explicarei daqui a pouco. É particularmente útil em situações em que você deseja ser creditado por seu trabalho ou se você quiser usar versões. Todos estes detalhes estão incluídos no arquivo
Cargo.toml
, que explicarei em pouco tempo.
Parte 2.1: O Manifesto (Cargo.toml)
O arquivo cargo.toml
é o manifesto do seu projeto, que descreve seu projeto e suas dependências. Eis como seria um exemplo do arquivo Cargo.toml
:
Cargo.toml
:
[package]
name = "app-texto-rust"
version = "0.1.0"
authors = ["João Silva <joao.silva@exemplo.com>"]
edition = "2018"
name
define como seu programa é chamado.version
define a versão de seu programa. É particularmente útil se você quiser emitir atualizações para as pessoas que estão utilizando seu programa!authors
é onde você coloca os nomes das pessoas que escreveram o programa.edition
descreve a versão de Rust que você quer que este projeto utilize. Você deve mantê-la como2018
para ter certeza de utilizar os recursos mais recentes.
Se você quiser adicionar mais de um autor, você deve separar cada autor com uma vírgula e usar aspas:
authors = ["João Silva <joao.silva@exemplo.com>", "Maria Silva <maria.silva@exemplo.com>"]
Há também alguns items adicionais como tables e keys que você deve dar uma olhada, tais como license
ou [[bin]]
. Você pode ler mais sobre o manifesto aqui: https://doc.rust-lang.org/cargo/reference/manifest.html
Parte 3: Começando com o Rust 💫
Viva! Agora que a burocracia está fora do caminho, podemos finalmente chegar à parte divertida: Escrever código!
Para começar, vamos começar com algo simples para nos aconchegar escrevendo um programa “Olá, Mundo!”.
Se você está familiarizado com o essencial da programação e do Rust, sinta-se à vontade para pular esta seção.
- Abra o arquivo
main.rs
. Você verá o seguinte (ou algo semelhante):
fn main() {
println!("Hello World!");
}
Vamos fazer um rápido resumo do que está acontecendo aqui:
fn main()
define uma nova “função” chamada main().
Esta é uma função muito importante! Quando os programas são executados, eles geralmente fazem o que a função main() lhes diz para fazer.
println!()
é uma função diferente que se destina a “imprimir” informações na tela do jogador.
Essa operação é chamada de “imprimir” porque executar um programa nem sempre envolvia telas - ao invés disso, impressoras reais estavam envolvidas!
- Ponto-e-vírgula significa que a instrução termina aí. Eles estão lá para dar ordem e coerência ao nosso código.
A fim de entender a importância dos ponto-e-vírgula, farei com que o computador imprima duas coisas diferentes em uma linha:
main.rs
:
fn main() {
println!("Olá, Mundo");
println!("Está um dia lindo lá fora!");
}
É possível perceber duas coisas a a partir daqui;
-
Os espaços não importam. (Mas eles fazem tudo parecer muito mais agradável, não é mesmo?)
-
Usar ponto-e-vírgula é como dividir as instruções que você está dando ao computador.
Agora, vamos executar o código que você escreveu. Abra um terminal na pasta em que seu projeto reside e execute os seguintes comandos:
cargo build
./target/debug/app-texto-rust
Se você tiver sorte o suficiente, agora você deve ver um pedaço de texto alegre dizendo Hello World!
Lembrete: Se você estiver usando o repl.it, você também pode simplesmente clicar no botão “Run” (Executar)!
Parte 4: Escrevendo o jogo ✍
Muito bem, nosso objetivo é escrever um programa que essencialmente narrará uma história ao jogador, dando-lhe instruções específicas. O jogador será capaz de afetar o curso da história com as decisões que ele entrará no programa.
Vamos começar!
Parte 4.1: Criando um pouco de texto
Neste momento, nosso código se parece com isso.
fn main() {
println!("Hello World!");
}
Porém, Hello World!
não se parece como algo que se veria em um jogo, certo? Então, vamos mudar isso. Mude o texto dentro das citações para algo mais apropriado. Vamos mudá-lo para Olá corajoso explorador! Quer embarcar em uma aventura?
.
Agora execute seu programa novamente. Em vez de Hello World!
, coloque o que você escreveu acima.
Parte 4.2: Entrada do usuário
Um jogo precisa dar a possibilidade de interagir com ele e modificar a aventura.
Assim, vamos escrever uma nova função que aceitará ler a resposta do jogador e reagir de acordo.
Para simplificar, o jogador só deverá responder com uma palavra começando com “S”, que inclui palavras como “Sim”, “Si”, “SimSim”, ou “Sanduíche” (como uma consequência engraçada não intencional), da mesma forma com a letra “N”. Para isso, vamos criar um novo arquivo com o nome de prompts.rs
e vamos escrever o seguinte nele:
main.rs
:
use std::io;
fn prompt() -> bool {
let mut entrada = String::new();
io::stdin().read_line(&mut entrada).unwrap();
return entrada.to_ascii_lowercase().starts_with("s");
}
fn main() {...}
Eu sei que isto é bastante para absorver, mas não se preocupe! Vamos passar pelo que está acontecendo aqui mais uma vez, linha por linha:
-
use std::io;
A importação de bibliotecas nos permite utilizar código instalado pelo cargo, ou empacotado no Rust (biblioteca padrão). Ao importar
std::io
(ondestd
significa biblioteca padrão, também conhecida como Rust), ganhamos acesso a mais funções que nos permitem realizar operações de “entrada/saída” (comoprintln!()
, mas mais avançadas!). -
fn prompt() -> bool {
Todas as funções destinam-se a devolver alguma saída, que é visível para o jogador, para outros programas ou partes do mesmo programa. Neste caso, definimos uma nova função chamada
prompt()
, que retornará um valor conhecido comobool
, que significa “boolean” (booleano). Os tipos de dados booleanos só podem aceitar dois valores, que são denotados com as palavrastrue
(verdadeiro) oufalse
(falso) na linguagem de programação Rust. Eles recebem o nome do matemático George Boole e são possivelmente a instância mais simples da álgebra booliana no campo da ciência da computação. -
let mut entrada = String::new();
Aqui, criamos um ‘String’ mutável. Mutabilidade define se você pode alterar o valor de uma variável. Já que estamos inicializando uma nova string (o que significa que declaramos que a variável
entrada
existe e é umaString
, mas ainda não houve nenhum valor que tenha sido declarado a ela). -
io::stdin().read_line(&mut entrada).unwrap();
Aqui, utilizando o módulo std
io
novamente, capturamos o que quer que o jogador esteja digitando até que ele aperteEnter
dentro da variávelentrada
. A&mut entrada
que você vê é a sintaxe de Rust para criar uma referência mutável para uma variável. Neste caso, é assim que o métodoread_line
é capaz de ler a partir de seu buffer de entrada padrão; ele coloca os bytes na memória referenciada pela referência mutável que é passada. Neste caso, você está passando uma referência mutável paraentrada
, então a linha resultante será colocada diretamente naquelaString
sem que nenhuma nova “string” seja criada. Note que tivemos que adicionar explicitamente o especificadormut
à nossa referência; as referências e variáveis no Rust são instanciadas como imutáveis, a menos que especifiquemos o contrário. Em nosso caso, estamos especificando-a como mutável para que possa ser, bem, mutada pelo métodoread_line
. -
return entrada.to_ascii_lowercase().starts_with("s");
E finalmente, verificamos se a
entrada
com todas as letras minúsculas começa coms
. Se isso acontecer, significa que a entrada foi sim (ou similar), e retornatrue
(verdadeiro). Se não, é seguro assumir que o jogador respondeu negativamente. Portanto, devolvemos o valorfalse
(falso).
Parte 4.3: Verificando a resposta!
A fim de escrever uma história, faremos nosso programa reagir com base nas respostas que o jogador der. Em outras palavras, esta é a parte em que você escreve a lógica de seu próprio jogo! Dentro da função main
, e em cima da macro println!
(= uma função cujo nome termina com !
), adicione o seguinte:
let resposta = prompt();
Agora você tem uma variável (imutável) chamada resposta
, cujo tipo é um bool
eano ( verdadeiro
ou falso
). Agora podemos verificar a resposta do jogador e tomar um curso de ação dependendo da situação:
Vamos avaliar a resposta que o jogador nos deu!
// Se a resposta do jogador for sim, imprima a mensagem
if resposta == true {
println!("Êêê! Suas aspirações irão trazer coisas boas em sua vida.");
// caso contrário, se a respota for não (ou qualquer outra coisa!)
// imprima uma mensagem diferente, menos amigável
} else {
println!("Nãão, acho que deu ruim!");
}
Agora você pode criar um jogo adicionando, de forma semelhante, mais prompts, depois utilizando-os nos blocos if... else
do prompt anterior e conectá-los.
Parte 4.4: Módulos do Rust
Uau… já são muitas linhas escritas. Se acrescentássemos somente mais uma, isso já ficaria muito confuso! Já que estamos prestes a escrever muitas linhas diferentes que aparecerão dependendo das escolhas que o jogador fizer, pode valer a pena dividir nosso jogo em arquivos diferentes, em vez de apenas colocar tudo no main.rs
!
Então, vamos criar um novo arquivo chamado prompts.rs
! A estrutura de nosso diretório deve se parecer com esta:
Cargo.toml
main.rs
prompts.rs
A fim de importar as funções que vamos escrever no prompts.rs
, devemos acrescentar mod prompts
na nossa primeira linha, para informar o compilador sobre a existência de nosso novo arquivo. Seu código deve agora estar assim:
main.rs
:
use std::io;
mod prompts;
fn prompt() -> bool {
let mut entrada = String::new();
io::stdin().read_line(&mut entrada).unwrap();
return entrada.to_ascii_lowercase().starts_with("s");
}
fn main() {
let resposta = prompt();
// Se a resposta do jogador for sim, imprima a mensagem
if resposta == true {
println!("Êêê! Suas aspirações irão trazer coisas boas em sua vida.");
// caso contrário, se a respota for não (ou qualquer outra coisa!)
// imprima uma mensagem diferente, menos amigável
} else {
println!("Nãão, acho que deu ruim!");
}
}
Você pode ler mais sobre os módulos Rust aqui: https://doc.rust-lang.org/rust-by-example/mod.html
O Rust é instruído a executar a função chamada intro
, que é exportada do prompts
. Esta é uma função associada. Mas, atualmente, ela não existe. Vamos implementá-la agora?
Abra o arquivo prompts.rs
, e cole o seguinte:
prompts.rs
:
use std::io;
fn prompt() -> bool {
let mut entrada = String::new();
io::stdin().read_line(&mut entrada).unwrap();
return entrada.to_ascii_lowercase().starts_with("s");
}
pub fn intro() {
let resposta = prompt();
// Se a resposta do jogador for sim, imprima a mensagem
if resposta == true {
println!("Êêê! Suas aspirações irão trazer coisas boas em sua vida.");
// caso contrário, se a respota for não (ou qualquer outra coisa!)
// imprima uma mensagem diferente, menos amigável
} else {
println!("Nãão, acho que deu ruim!");
}
}
Você notará que o código é semelhante, mas ele tem algumas pequenas mudanças:
- Antes de mais nada, removemos
mod prompts;
. Não queremos utilizar o arquivo com o qual estamos trabalhando. - Renomeamos a função
main
paraintro
. Como a funçãomain
têm um nome especial para Rust, tivemos que mudar seu nome. - Também acrescentamos o modificador
pub
à nossa função, o que nos permite chamar essa função de outros arquivos (que importamprompt.rs
).
Vamos agora utilizar a função que escrevemos no main.rs
. Substitua todo o código na função main.rs
por apenas uma chamada para a função intro, como abaixo:
main.rs
:
mod prompts;
fn main() {
prompts::intro();
}
Parte 4.4: Escrevendo um jogo completo
A fim de tornar o jogo mais satisfatório, divertido e cativante, é preciso haver mais diálogo!
main.rs
:
mod prompts;
fn main() {
prompts::intro();
}
prompts.rs
:
use std::io;
// retorna um bool, aceitando texto como entrada
fn prompt() -> bool {
let mut entrada = String::new();
io::stdin().read_line(&mut entrada).unwrap();
return entrada.to_ascii_lowercase().starts_with("s");
}
// primeiro prompt
pub fn intro() -> () {
println!("Alguém está te ligando, você atende?");
// se prompt() retorna true (verdadeiro), é suficiente para o rust continuar!
// algumas outras expressões que também são verdadeiras são 2 == 2,
// 1 > 0, ou apenas a palvara true.
if prompt() {
telefone_atendido();
} else {
println!("Você desligou o celular. Você não descobriu do que se tratava. Quem sabe um dia você descubra...");
}
}
// segundo prompt
pub fn telefone_atendido() -> () {
println! ("Você ouve uma voz do outro lado da linha: 'Já faz muito tempo, vamos se encontrar?'");
println!("Sua voz parece vagamente familiar, e ela soa um pouco angustiada. Você aceita?");
if prompt() {
desafio_aceito();
}
else {
println!("Você desligou o celular. Você não descobriu do que se tratava. Quem sabe um dia você descubra..");
println!("VOCÊ GANHOU! Continua...");
}
}
// terceiro prompt
pub fn desafio_aceito() -> () {
println!("Você se encontra com um velho amigo e ele lhe entrega uma bolsa cheia de dinheiro");
println!("Seu amigo diz: 'Pegue a bolsa, não faça perguntas.");
println!("Parabéns! Você agora é rico e já cobriu o suficiente suas necessidades materiais, mas será que isso representa a verdadeira felicidade? Você pode ter ganho o jogo, mas não ganhou na vida.");
println!("GAME OVER!");
}
O que está acontecendo aqui é bastante simples: Se o jogador atender seu telefone (telefone_atendido()
), ele prossegue. Se não atender, ele perde. Depois de ouvir o que o chamador tem a dizer, ele perderá se aceitar a proposta do chamador (desafio_aceito()
), mas ele ganhará se não o fizer.
Não apenas enganamos o jogador tomando as decisões mais óbvias, e depois jogando esta bomba nuclear moral na cara dele, mas também podemos preparar o terreno para uma sequência, caso eles decidam que receber muito dinheiro de graça é muito bom para ser verdade!
Além dos comentários no próprio código, há alguns detalhes importantes a serem observados:
- Neste exemplo, temos duas respostas possíveis, portanto, a história segue sempre dois caminhos totalmente diferentes. A maioria dos videogames acaba dando ao jogador a ilusão de escolha, fazendo-o passar por consequências de curto prazo e depois distorcendo a história de tal forma que tudo funciona da mesma maneira ou de uma maneira ligeiramente diferente, e eles acabam dando-lhe um final diferente. Títulos aclamados pela crítica, como The Last of Us, The Witcher, Night in the Woods, Mass Effect ou Assassin’s Creed fazem isso.
- No entanto, ainda assim, acabamos dando ao jogador um diálogo diferente a cada vez. Em nosso caso, é que quase todas as decisões erradas expulsam o jogador do jogo.
- Você pode querer lidar com todas as cláusulas
if... else
e a lógica do jogo em uma função, em vez de lidar com a lógica por todo o lado. No entanto, acreditamos que a maneira como escrevemos deixa espaço para mais flexibilidade. - Também movemos todas as respostas corretas para sua própria função, para manter nosso código mais claro
Parabéns! Você acaba de escrever seu primeiro jogo em Rust. 🎉
Parte 5: Ideias! 💡
Aqui estão um monte de ideias que o ajudarão a dar um passo além:
- Você pode fazer um jogo de dados onde o jogador pode lançar um dado se eles entrarem com a palavra “lançar”?
- Você pode incluir mais opções em seu jogo para que o jogador possa escolher? (Dica: Booleanos aceitam apenas dois valores. Poderia retornar outro tipo de valor que lhe permita inserir mais respostas, e depois avaliar a resposta no próprio prompt?)
- Você pode fazer um jogo em que seu programa tenha um número aleatório, o jogador tenta adivinhá-lo e se ele acertar, o jogo parabeniza o jogador? (Dica: Em vez de um booleano, a função
prompt()
deve retornar um número em vez disso) - Você pode fazer um jogo baseado na sorte, onde você escolhe tamanhos de dados e tem que lançar um número mais alto do que o computador?
- E se você pudesse construir jogos completos com gráficos?
Aqui estão algumas dicas (em inglês):
- The Rust Programming Language - Data Types
- The Rust Programming Language - Patterns and Matching
- Are we game yet?
Você também pode conferir este remix do código fonte deste workshop caso você fique preso, que implementa algumas das ideias apresentadas:
Parte 6: Leitura adicional 📖
- The Rust Programming Language - an in-depth guide to Rust. Este workshop foi inspirado no Capítulo 2 do livro
- Rust By Example - Para aqueles que preferem exemplos de código em vez de páginas de documentação
- Rust’s learning resources page - incluindo guias para cursos avançados sobre outros tópicos
Você também pode estar interessado em ouvir a mixtape do Charalampos. Propaganda sem vergonha, eu sei.
Agora que você terminou de construir este maravilhoso projeto, compartilhe sua bela criação com outras pessoas! Lembre-se, é só mandar a URL do seu projeto!
Você provavelmente conhece as melhores maneiras de entrar em contato com seus amigos e familiares, mas se você quiser compartilhar seu projeto com a comunidade brasileira do Hack Club, não há melhor lugar para fazer isso do que no Discord do Hack Club Brasil.✨
- Clique aqui para fazer parte da nossa comunidade!
- Depois, poste o link do seu projeto no canal
💡┇criações
para compartilhá-lo com todos os Hack Clubbers!
A comunidade te espera!🎉🎉