Artigo original: Promises and Pokemon — how I learned to think in async
Escrito por: Kalalau Cantrell
Se você vem aprendendo JavaScript, já deve ter ouvido falar de promises e de como elas são ótimas.
Então, você deve ter pensado em pesquisar o básico a respeito. Talvez, você já tenha visto a documentação da MDN sobre promises ou artigos muito legais como este, escrito pelo Eric Elliott (em inglês), ou este, escrito pelo Brandon Morelli (em inglês). Se você leu esses artigos ou outros mais, já deve ter visto o exemplo clássico de promises em ação.
/* Exemplo clássico de promises */
let p = new Promise((resolve) => {
setTimeout(function() {
resolve('delícia');
}, 1000);
});
p.then((mensagem1) => {
console.log(mensagem1); //imprime 'delícia' depois de 1s
return new Promise((resolve) => {
setTimeout(function() {
resolve('de molho');
}, 1000);
});
}).then((mensagem2) => {
console.log(mensagem2); //imprime 'de molho' depois de mais 1s
});
Depois de ver alguns desses exemplos, no entanto, você pode ter se perguntado se estava realmente entendendo promises. Nesse ponto, se você é como eu, entendeu conceitualmente o que as torna incríveis — elas permitem que você escreva código assíncrono em um padrão síncrono — mas deve estar se remoendo para ver um exemplo onde elas façam mais do que exibir uma série de linhas no console.log
que disparem em tempos diferentes.
Então, o que foi que eu fiz? Criei um jogo simples de Pokemon, com uma batalha com base em turnos contra um Electabuzz (em inglês).
Este artigo assume que você entendeu o exemplo de promises referido acima. Confira os recursos nos links do parágrafo introdutório se precisar refrescar sua memória.
Funcionalidade básica
O Electabuzz e o jogador começam com uma certa quantidade de pontos de vida (HP – do inglês, hit points). A primeira coisa a acontecer é um ataque do Electabuzz, onde o jogador perde um pouco de HP. Em seguida, o jogo aguarda até que o jogador escolha um ataque e o utilize contra o Electabuzz.
Sim, o jogo simplesmente aguarda… e aguarda… essa é a parte onde eu comecei a de fato apreciar o valor do uso das promises. No momento em que o jogador opta por um ataque, o Electabuzz perde um pouco de HP e, em seguida, ataca novamente. Esse laço de repetição continua até que o HP do Electabuzz ou o HP do jogador chegue a zero.
O pseudocódigo
/* O Electabuzz e o jogador começam com uma certa quantidade de
pontos de vida (HP).
A primeira coisa que acontece é um ataque do Electabuzz
e o jogador perde um pouco de HP. */
function iniciarJogo() {
//...define o HP da CPU e do jogador como 40
}
function vezDaCPU() {
//...Electabuzz ataca! -5 HP para o jogador
}
/*
.
.
.
*/
iniciarJogo();
vezDaCPU();
Até aqui, é bastante simples. É o momento de ajustarmos isso um pouco, de maneira que o Electabuzz ataque em um tempo mais natural. Eu queria fazer parecer que ele estava "pensando" sobre qual movimento fazer.
//...
function vezDaCPU() {
setTimeout(function() {
//...Electabuzz ataca! -5 HP para o jogador
}, 1000); //o ataque acontece após 1 segundo
}
//...
Enquanto isso, vamos colocar um pouco da magia das promises aqui para que possamos encadear funções que serão disparadas quando o Electabuzz termina seu ataque e somente depois disso, nem um milissegundo antes. Afinal, esse é um jogo com base em turnos.
//...
function vezDaCPU() {
return new Promise((resolve) => {
setTimeout(function() {
//...Electabuzz ataca! -5 HP para o jogador
resolve();
}, 1000); //o ataque acontece após 1 segundo,
}); //que é o tempo que a promise leva para resolver
}
//...
Ótimo! A configuração acima nos permitirá, mais tarde, fazer o seguinte:
//...
vezDaCPU()
.then(() => vezDoJogador());
Agora, passemos para o código do jogador.
/*O jogo aguarda até que o jogador selecione um ataque
para usar no Electabuzz. Quando ele escolhe o ataque,
o Electabuzz perde um pouco de HP e ataca novamente.*/
//...
function vezDoJogador() {
//....????....????
}
//...
Como escreveremos uma função que, ao ser chamada, aguardará pela entrada do usuário antes de terminar sua execução? Sabemos que isso precisa envolver de alguma maneira um event listener (em português, algo como "escutador" ou observador de eventos) para a parte da entrada do usuário. Também sabemos que deve poder usar promises de algum jeito para a parte assíncrona… como faremos, porém, para juntar as duas coisas?
O que descobri foi que você poder 1) criar uma promise, 2) dentro da promise, adicionar um event listener para, em nosso caso, um evento de clique de um botão e 3) se a função for chamada pelo event listener, a promise ser resolvida, o que fará com que você consiga o efeito de aguardar.
//...
function vezDoJogador() {
return new Promise((resolve) => { // (1)
botaoDoJogador.addEventListener('click', function() { // (2)
//...O jogador ataca! -5 HP para o Electabuzz
resolve(); // (3)
});
});
}
//...
Aí está! Agora, conseguimos fazer isto:
//...
vezDaCPU()
.then(() => vezDoJogador())
.then(() => vezDaCPU())
.then(() => vezDoJogador());
Observe que cada chamada de vezDoJogador()
no código acima simplesmente aguardará… e aguardará um pouco mais… até que o jogador selecione seu ataque. Somente se a execução continuar o turno do Electabuzz chegará.
No entanto, por que devemos escrever assim quando o mesmo código pode ser obtido na forma equivalente em async/await, que parece muito mais limpa? Se você seguiu o que fiz com promises até aqui, não há uma grande diferença para aquilo que async/await faz. Compare o código abaixo com o código acima e verá que são equivalentes, mas o código abaixo parece mais bem organizado.
//...
async function sequenciaDeJogo() {
await vezDaCPU();
await vezDoJogador();
await vezDaCPU();
await vezDoJogador();
}
sequenciaDeJogo();
Se quiser saber mais sobre async/await, confira este artigo (em inglês) de Tiago Lopes Ferreira ou estes slides (em inglês) de Wes Bos.
Bem, nosso código agora consegue executar algumas rodadas de combate com base em turnos com o Electabuzz. Porém, ainda precisamos de um modo de fazer o jogo terminar.
//...
async function sequenciaDeJogo() {
await vezDaCPU();
if (/*HP do jogador é 0*/) {
//...fim de jogo
}
await vezDoJogador();
if (/*HP do Electabuzz é 0*/) {
//...fim de jogo
}
await vezDaCPU();
if (/*HP do jogador é 0*/) {
//...fim de jogo
}
await vezDoJogador();
if (/*HP do Electabuzz é 0*/) {
//...fim de jogo
}
// poderíamos repetir essa lógica manualmente tanto quanto
// precisássemos..., mas não há um jeito mais inteligente?
}
sequenciaDeJogo();
Por fim, queremos que o programa continue a ser executado por conta própria até que as condições de fim de jogo sejam atendidas. Em vez de repetir manualmente as lógicas de cpuTurn()
e playerTurn()
como viemos fazendo, podemos fazer isso recursivamente, chamando a função gameLoop()
até que uma das condições seja atendida.
//...
async function sequenciaDeJogo() {
await vezDaCPU();
if (/*HP do jogador é 0*/) {
//...fim de jogo
}
await vezDoJogador();
if (/*HP do Electabuzz é 0*/) {
//...fim de jogo
} else {
sequenciaDeJogo(); //sequenciaDeJogo chama a si mesmo até que
} //atenda as condições de fim de jogo
}
sequenciaDeJogo();
A função sequenciaDeJogo
será executada e continuará a chamar a si mesma e a executar até que o HP do Electabuzz ou o HP do jogador chegue a zero. Se quiser saber mais sobre recursão, assista este vídeo no YouTube (em inglês), de MPJ. Enquanto estiver por lá, confira outros vídeos do canal dele, o Fun Fun Function. Ele explica muito bem tópicos complexos de um modo divertido.
Vamos agora olhar o pseudocódigo completo:
function iniciarJogo() {
//...define o HP da CPU e do jogador como 40
}
function vezDaCPU() {
return new Promise((resolve) => {
setTimeout(function() {
//...O Electabuzz ataca! -5 HP para o jogador
resolve();
}, 1000); //o ataque acontece após 1 segundo,
}); //que é o tempo para a promise resolver
}
function vezDoJogador() {
return new Promise((resolve) => {
botaoDoJogador.addEventListener('click', function() {
//...O jogador ataca! -5 HP para o Electabuzz
resolve(); //essa promise não resolve até que
}); //a entrada do usuário desejada é recebida
});
}
async function sequenciaDeJogo() {
await vezDaCPU();
if (/*HP do jogador é 0*/) {
//...fim de jogo
}
await vezDoJogador();
if (/*HP do Electabuzz é 0*/) {
//...fim de jogo
} else {
sequenciaDeJogo(); //sequenciaDeJogo chama a si mesmo até que
} //atenda as condições de fim de jogo
}
iniciarJogo();
sequenciaDeJogo();
O código
Agora que terminamos o pseudocódigo, veja neste Pen como eu implementei a lógica com JavaScript de verdade:
Conclusão
Obrigado pela leitura. Esse pequeno experimento com promises me fez ver que as promises simplificam muito no que se trata de compor código assíncrono. Embora o exemplo típico de promises com console.logs e setTimeouts ilustrasse bem o conceito, eu não me empolguei muito com ele, por isso decidi criar esse jogo simples para me empolgar com as promises de vez. Espero que você tenha captado um pouco dessa empolgação e ficado empolgado com elas como eu. Se houver algum especialista em código assíncrono lendo este artigo, seria ótimo saber sua opinião quanto a maneiras melhores de se obter a mesma funcionalidade (como generators, por exemplo). Se algo não ficou claro no artigo, mande mensagem e eu tentarei esclarecer.
Fique à vontade para entrar em contato pelo Twitter.