Desenvolvendo o jogo 2048 usando programação funcional (part 3)
Nosso jogo já é capaz de fazer um print do board na tela, adicionar números em locais aleatórios onde os blocos tem valor 0 e conseguimos movimentar o board inteiro para cima seguindo as regras do jogo.
Nessa parte do tutorial irei demonstrar como fazer os movimentos para todas as direções e depois com mais algumas linhas de código faremos o jogo rodar no terminal (com algumas pequenas adaptações poderemos rodar no browser).
Movimentando em outras direções
Após terminar o método de movimentar os blocos para cima, pensei na dificuldade que seria movimentar para todos os lados, visto que para as laterais eu precisaria de informações de outras linhas.
Para solucionar isso tive uma ideia que deixou tudo mais simples.
Vamos rotacionar o board :D
Exatamente o que você leu, faremos uma função que vai rotacionar o board para a direita. O processo seguinte será, para cada lado, rotacionar algumas vezes, movimentar e rotacionar mais algumas vezes.
O movimento que faremos será:
Portanto, precisamos pegar os itens da linha e ir preenchendo na coluna. O código ficou dessa maneira:
const flipRight = matrix => (
matrix[0].map((_, index) => (
matrix.map(row => row[index])
)
).reverse());
O algoritmo é um pouco complexo em seu entendimento, mas vamos tentar simplificá-lo explicando linha a linha.
O matrix[0]
é usado somente para a iteração (poderia ser um array [0,0,0,0], se preferir, pode usar o método generateArrayPad
na parte 1 desse tutorial) e esperar um retorno. No retorno, o primeiro parâmetro é o valor do item iterado (que não terá uso, portanto coloquei o nome “_”). O segundo parâmetro é o índice da coluna.
Para cada iteração das colunas, ele itera novamente sobre as colunas (note que ele não itera sobre as linhas e sim sobre as colunas que possuem linhas) e para cada coluna iterada (array) ele retorna o valor do bloco da linha dessa coluna.
Portanto, na primeira iteração, será criado a primeira linha e ela será composta pelos blocos: [(0, 0), (1, 0), (2, 0), (3,0)]. Na segunda linha será [(0,1), (1,1), (2,1), (3,1)] e assim por diante. Para cada nova coluna ele pega um o bloco da coluna, fazendo-o inverter.
O reverse na última serve para inverter a ordem das colunas, pois no algoritmo acima as colunas da última para a primeira.
Moving the game
Agora conseguimos rotacionar o board para o lado direito e movimentar os blocos para cima. Basta juntarmos esses dois métodos que teremos movimentos para todos os lados.
Para cada movimento o board será rotacionado 4 vezes para voltar a posição de origem. Para movimentar o board para a direita, ele será rotacionado 3 vezes, movimentado para cima e depois rotacionado novamente, por exemplo.
Segue um exemplo do código:
const moveBoardRight = (board)=> {
board = flipRight(board);
board = flipRight(board);
board = flipRight(board);
board = moveBoardUp(board);
return flipRight(board);
};
Ok, o código funciona, mas estamos tendo a variável board como mutável. Portanto:
const moveBoardRight = board => (
flipRight(flipRight(flipRight(moveBoardUp(flipRight(board)))));
);
Não temos mais variáveis imutáveis, certo? Mas será que esse código está legível?
Na parte 2 desse tutorial, apresentei a vocês o conceito de composição. Portanto, faremos o uso do método compose (definido na parte 2 do tutorial) para melhorar o código.
const moveBoardRight = board => (
compose(
flipRight, // 5
moveBoardUp, // 4
flipRight, // 3
flipRight, // 2
flipRight // 1
)(board)
);
Obs: O método compose roda as funções do último parâmetro para o primeiro.
Agora a função moveBoardRight chama a função compose. A função compose recebe como parâmetro 5 funções que serão chamadas em sequência, onde o retorno da primeira será o primeiro parâmetro para a segunda.
A função compose retornará uma nova função, que será responsável pela execução em cascata das funções e recebe como parâmetro o board.
Segue o código dos outros movimentos:
const moveBoardDown = (board) => (
compose(
flipRight,
flipRight,
moveBoardUp,
flipRight,
flipRight
)(board)
);const moveBoardLeft = board => (
compose(
flipRight,
flipRight,
flipRight,
moveBoardUp,
flipRight
)(board)
);
O movimento para cima já foi criado na parte 2 desse tutorial e portanto não precisa rotacionar.
Funções do fluxo do jogo
Antes de iniciarmos o jogo, precisamos de algumas funções básicas, para saber se o usuário venceu ou se perdeu o jogo, por exemplo.
Primeiro, iremos fazer uma função que busca por número no board e se existir retorna true.
const hasNumberInBoard = (board, number) => (
board.some(line => line.some((block) => block === number))
);
O método some
itera sobre o array e recebe uma callback como parâmetro. Essa callback é chamada passando o valor corrente do array. Se pelo menos uma dessas funções de iteração retornarem true, então é retornado true na chamada do método “some”. Caso todas as chamadas retornem false, então o retorno do método “some” é falso.
A função acima definida itera sobre as colunas verificando se alguma delas é verdadeira. Para cada iteração de coluna é iterado em cada bloco da linha pesquisando o número passado por parâmetro, caso exista é retornado true na função inteira, caso contrário a função retorna false.
Com essa função podemos pesquisar pelo número 0 em todo o board, caso exista significa que ele não perdeu o jogo, caso contrário, perdeu. Podemos fazer o mesmo para verificar se o número 2048 existe, verificando se ele venceu a partida.
Iremos criar dois métodos para isso.
const isLooser = board => (
!hasNumberInBoard(board, 0)
)const isWinner = board => (
hasNumberInBoard(board, 2048)
)
Já é possível saber se o jogador venceu ou perdeu o jogo.
Criaremos uma função que adiciona o número de valor 2 em um bloco de posição aleatória.
const getRandomPosition = compose(getRandomIndex, getEmptyBlocks);
const addRandomNumber = (board) => {
const position = getRandomPosition(board);
return addNumber(board, position);
}
A função “getRandomPosition” é uma composição que chama o getEmptyBlocks e o getRandomIndex. A função addRandomNumber chama a função citada acima e passa para o addNumber.
[OPCIONAL] O addNumber pode ser transformado em um Currying que recebe primeiro o board e depois o position. Se assim o fizer, podemos mudar o método addRandomNumber para:
const addRandomNumber = (board) => (
compose(
addNumber(board),
getRandomPosition,
)(board)
)
);
Com isso a função addNumber ficaria da seguinte maneira:
const addNumber = (board) => (point) => {
# The same is equals....
Agora temos todas as funções necessárias para iniciar o jogo :D
Conclusão
Nesse momento já temos o jogo praticamente pronto, pois temos todas as funções de fluxo necessárias. O que precisamos agora é fazer o jogo funcionar utilizando todas essas funções. Na próxima (e última) parte do tutorial, será demonstrado como fazer o jogo funcionar.
Comentários, por favor.