O jogo começa com um segredo de quatro cores. Ao todo existem seis cores diferentes que podem ser combinadas de qualquer forma: azul, amarelo, rosa, verde; azul, azul, amarelo, amarelo; azul, azul, azul e azul. Não há limite para cores repetidas.
Para descobrir o segredo, o jogador faz testes, escolhendo cores que o computador compara com o segredo. Se as combinações forem iguais, o jogador ganha; se forem diferentes, joga outro turno. Em 10 turnos, o jogador perde.
Mas é preciso algo a mais para descobrir o segredo em menos que 1296 tentativas aleatórias. Cada teste retorna pontos: a cada ponto preto, há uma cor certa no lugar certo; a cada ponto branco, há uma cor certa no lugar errado.
Agora, vamos criar esse jogo. Para entender como funciona, experimente aqui a versão pronta.
As ferramentas utilizadas
Para criar esse jogo, vamos utilizar duas ferramentas principais. Com o React, vamos criar a interface, gerenciar os estados da aplicação por meio da Context API e operar os inputs do jogador.
Com o Lodash — conjunto de funções que expande[1] as possibilidades de manipulação de estruturas de dados em Javascript, vamos manipular os arrays e objetos para montar testes, comparar testes ao segredo e calcular pontos. Escolhi o Lodash porque a inspiração para recriar esse jogo veio do Mastermind in Haskell[2], uma implementação feita com funções puras que roda no terminal.
Gerando o segredo
Vamos começar pelo mais fácil. O segredo não é nada mais do que uma combinação aleatória de 4 das 6 cores disponíveis. As funções shuffle e take do Lodash nos permitem resolver isso em uma quatro linhas.
import _ from 'lodash'; | |
function genSecret(arr,n){ | |
let secret = _.map(arr, x => _.sample(arr)) | |
return {pegs: _.take(secret,n), display: false} | |
} |
Sample colhe elementos aleatórios de arr, que são então mapeados em um novo array, e a partir dele take gera um novo array com os primeiros n elementos. É importante ressaltar que as funções são puras e não alteram a fonte arr. Um beijo para o time lambda 🧙.[3]
Assim, para começar um novo jogo, basta gerar um novo segredo com algo como let secret = genSecret(cores, 4).
Gerando os testes
Gerar o segredo é a parte mais fácil. Agora vamos ver como gerar o teste com as cores escolhidas pelo jogador. Para isso, vamos usar o hook useReducer do React para adicionar, remover e resetar cores no teste, que aqui será chamado de state.
import React, {useReducer} from 'react' | |
import _ from 'lodash' | |
import { v4 as uuid } from 'uuid'; | |
function reducer(state, action){ | |
switch (action.type){ | |
case 'add': | |
if (state.guess.length <= 3){ | |
return {guess: _.concat(state.guess, {...action.payload, id: uuid()})}}; | |
case 'remove': | |
return {guess: _.filter(state.guess, el => (el.id !== action.payload.id))}; | |
case 'reset': | |
return {guess: []}; | |
default: | |
throw new Error(); | |
} | |
} | |
//in component function: | |
let [state,dispatch] = useReducer(reducer,{guess: []}), |
No componente, iniciamos o teste (state) como um array vazio. A cada evento que dispara uma ação do dispatch, um novo objeto teste é emitido de acordo com o tipo da ação (“add”, “remove”, “reset”): adicionando a cor escolhida (action.payload) ao teste, removendo-a ou resetando tudo.
Mais uma vez, as ações não alteram o objeto do teste, mas sim criam um novo objeto que o substitui, a cada ação.
Gerando a pontuação
Essa é a parte mais interessante. Se você leu até aqui, aí vai um desafio: dados um conjunto teste e um conjunto segredo, ambos de quatro elementos, como descobrir quantos são os elementos iguais na posição correta e os elementos iguais na posição errada?
Descobrir as cores certas na posição certa (pontos pretos) é fácil, é claro. Comparamos os arrays lado a lado: se na posição 0 as cores forem iguais, 1; se forem diferentes, 0; e somamos os pontos. Mas e os pontos brancos, isto é, as cores certas na posição errada?
import _ from 'lodash'; | |
function getNotInGuess(secret,guess){ | |
let guess_ = guess.slice(); | |
let res = []; | |
secret.map(el => { | |
if (guess_.includes(el)) { | |
guess_.splice(guess_.indexOf(el),1); | |
} else { res.push(el) } | |
}) | |
return res | |
} | |
function getScore(guess, secret){ | |
if(guess.length === 4 && secret.length === 4){ | |
let guess_ = _.map(guess, x => x.num), | |
secret_ = _.map(secret, x => x.num), | |
zip_ = _.zip(guess_,secret_), | |
rights = _.map(zip_, x => { | |
if(x[0] === x[1]){ return 1} | |
else { return 0} }), | |
rights_ = _.reduce(rights, (x,y) => x + y, 0), | |
sNotInGuess = _.difference(secret_,guess_), | |
wrongs_ = secret.length - sNotInGuess.length - rights_ | |
return {guess: guess, blacks: rights_, whites: wrongs_} | |
} else { | |
return {} | |
} | |
} |
Mais uma vez, o Lodash nos dá uma série de ferramentas para juntar, filtrar e comparar os arrays. solução para o desafio: os pontos brancos são o numero de cores do segredo, menos as cores que o teste não acertou, menos os pontos pretos.
Condições de vitória e derrota
Falamos sobre como começar o jogo — gerar o segredo — e como fazer cada turno — criar um teste, calcular os pontos. Agora, preciso falar sobre o funcionamento geral do jogo, isto é, como registrar os testes realizados e quais são as condições de vitória e derrota.
Para o jogo funcionar, eu preciso de acesso a uma informação em todos os componentes (tabuleiro, header, mão do jogador, app). Essa informação é a lista de todos os testes já realizados.
No React, essa necessidade nos leva diretamente à ideia de uma ferramenta de gerenciamento de estado global da aplicação, como o Redux. Para criar este jogo, optei pela ferramenta nativa do React, a Context API.
import React, {createContext, useState, useContext} from 'react' | |
const GuessContext = createContext(); | |
export default function GuessProvider({children}){ | |
let [guesses, setGuesses] = useState([]) | |
return( | |
<GuessContext.Provider value={{guesses,setGuesses}}> | |
{children} | |
</GuessContext.Provider> | |
) | |
} | |
export function useGuesses(){ | |
const context = useContext(GuessContext); | |
if(!context) throw new Error("useGuess must be used within a provider") | |
const {guesses, setGuesses} = context; | |
return {guesses, setGuesses} | |
} | |
//in App component, use: | |
// <GuessProvider> | |
// <GameScreen/> | |
// </GuessProvider> |
A lista de testes (“guesses”) agora pode estar disponível por meio do uso do custom hook useGuesses. A variável guesses lista todos os testes já realizados, e a função setGuesses é usada para incluir novos testes à lista.[4]
Para terminar, precisamos falar sobre como ganhar o jogo, e também como perder. O jogador ganha quando o teste é igual ao segredo, e perde quando já lançou 10 testes errados. Agora que temos a lista completa e constantemente atualizada dos testes, fica fácil implementar essas condições. Para isso, utilizaremos o hook useEffect, um método que roda a cada atualização do componente.
import React, {useEffect} from 'react' | |
import _ from 'lodash'; | |
useEffect(() => { | |
if(gameStatus.active && guesses.length > 1 && _.last(guesses).blacks === 4){ | |
setGameStatus({active: false}); | |
setMessage("You win") | |
setSecret({pegs: secret.pegs, display: true}) | |
} | |
else if (gameStatus.active && guesses.length > 9){ | |
setGameStatus({active: false}); | |
setMessage("You lose!") | |
setSecret({pegs: secret.pegs, display: true}) | |
} | |
}) |
Se o último teste tiver 4 pontos pretos, o jogador ganhou. Se o número de testes for maior que 9, o jogador perdeu.
Conclusão
Desde que eu comecei a programar, meu mentor sempre me disse que criar jogos é uma das maneiras mais eficazes de estudar, treinar, aprender. Isso porque é ao mesmo tempo divertido e complexo.
Neste exercício, recriei o jogo de Mastermind que pode ser jogado pelo browser, no computador ou no celular. Ainda teria muita coisa para falar, como a organização dos componentes do React ou a estilização responsiva com Material UI. Mas acho que nesse artigo consegui explicar o que eu queria: como o jogo funciona logicamente, e como usar funções puras para manipular estruturas de dados para obter os resultados desejados.
Se você gostou, pode conferir o código no meu github, jogar o jogo pelo browser ou deixar o like no artigo. Obrigado e um abraço!
1.Talvez a expressão correta seja “facilita”; afinal, não há razão para as operações feitas com o Lodash não poderem ser feitas com o Javascript puro. Mas “expandir” é mais bonito, vamos concordar.
Mastermind in Haskell, por Christophe Delord. Acesse aqui.
Na primeira versão, eu utilizei a função shuffle do Lodash, que gera um array de elementos embaralhados. Como um colega apontou, dessa maneira o jogo não conseguia gerar segredos com elementos repetidos, e alterei para usar a função sample.
Para criar esse Context, foi indispensável o tutorial do Guilherme Rodz. Obrigado, meu caro! Acesse aqui.
Top comments (0)