DEV Community

React Mastermind

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}
}
view raw header.js hosted with ❤ by GitHub

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: []}),
view raw PlayerHand.js hosted with ❤ by GitHub

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 {}
}
}
view raw PlayerHand.js hosted with ❤ by GitHub

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})
}
})
view raw GameScreen.js hosted with ❤ by GitHub

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.

  1. Mastermind in Haskell, por Christophe Delord. Acesse aqui.

  2. 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.

  3. Para criar esse Context, foi indispensável o tutorial do Guilherme Rodz. Obrigado, meu caro! Acesse aqui.

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more