Desenvolvendo o jogo 2048 usando programação funcional (final)

Renato Cassino
Grupo OLX Tech
Published in
4 min readFeb 15, 2019
2048 game

No início do meu desenvolvimento fiquei perguntando como faria para ter um board que se altera a todo momento sem poder reinstanciar nenhuma variável. Ao desenvolver do tutorial, podemos ver que sempre passamos o board inteiro como parâmetro e recebemos um novo board.

Primeiro, vamos alterar a função de adicionar número em uma posição

Para o jogo, não será diferente, para cada movimento, será gerado um novo board e será feito o print do mesmo na tela.

Portanto, o jogo terá uma função recursiva parecida com:

const game = board => {
// code here
return game(board);
}

Vamos começar criando a função usando o readline para perguntar ao jogador qual movimento ele deseja fazer. Vamos instalar a lib “readline-sync”.

// In terminal: npm i --save readline-sync
const readlineSync = require('readline-sync');

Agora ao chamar a função “ask()” será chamado o readline.
Vamos criar o método que pergunta ao jogador qual movimento ele deseja fazer.

const askMovement = () => { // To ask in readline
while(true) {
const movement = readlineSync.question("Make your movement using the keys [w,a,s,d]:");
if(movement.match(/^[wasd]$/)) {
return movement;
}
}
};
const move = (board) => { // Move
const movement = askMovement();
switch(movement) {
case 'w': return moveBoardUp(board);
case 'a': return moveBoardLeft(board);
case 's': return moveBoardDown(board);
case 'd': return moveBoardRight(board);
}
};

Já temos o fluxo de movimento a partir de perguntas ao usuário. Agora vamos começar o jogo de uma maneira bem simples:

const game = board => (
compose(
game,
printBoard,
addRandomNumber,
move
)(board)
)
const board = [[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0]];
game(board);

Woow! Conseguimos jogar!

Existem diversas falhas na execução desse jogo. Uma delas é que se o board ficar sem números 0 será retornado um erro em tempo de execução. Para isso, iremos fazer algumas modificações na função “game”.

const game = board => {
if (isWinner(board)) {
console.log("You win the game! Congrats! ;D");
return;
}
if (isLooser(board)) {
console.log("You loose the game!");
return;
}
return compose(
game,
addRandomNumber,
move,
printBoard
)(board);
};
// Add two numbers here to start the game
const board = [[2,2,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0]];
game(board);

Conseguimos jogar normalmente agora, porém, ao ganhar ou perder o jogador é informado com uma mensagem.

Já está razoavelmente bom o jogo, porém existe um bug ainda onde os jogadores podem trapacear.

Suponha que na lateral direita do board todos os números sejam diferentes de 0 e você fez um movimento para a direita. Ao fazer isso, nenhum bloco será movimentado e será adicionado um novo número. Só deve adicionar um novo número se for feito um movimento anteriormente.

Primeiro vamos fazer uma função que compara dois arrays. Vamos fazer uma função super simples para compará-las.

const arraysEqual = (arr1, arr2) => JSON.stringify(arr1) === JSON.stringify(arr2);

Podemos então alterar nosso método de movimento (método “move”) para verificar se o movimento foi válido.

const moveTo = (board, movement) => {
switch(movement) {
case 'w': return moveBoardUp(board);
case 'a': return moveBoardLeft(board);
case 's': return moveBoardDown(board);
case 'd': return moveBoardRight(board);
}
};
const move = (board) => {
const movement = askMovement();
const movedBoard = moveTo(board, movement);
return (arraysEqual(movedBoard, board))
? move(board)
: movedBoard;
};

Nesse novo método eu primeiro armazeno o retorno do movimento do board em uma variável e depois comparo com o board recebido. Se o board for igual, então a função “move” é chamada novamente.

O jogo já está funcionando perfeitamente!

Refactor in game

Se você notar a função “game”, notará que ela pode retornar “undefined” ou retornar um board. O caso de saída do jogo é quando não é chamado a recursão.

A programação funcional tem a tipagem forte como parte de sua metodologia. A linguagem Javascript por ser fracamente tipada é possível ter soluções como a abordada no exemplo acima, porém, por boas práticas iremos trocar o meio de verificar se o jogador venceu ou perdeu a partida.

Vamos alterar o método game e criar um novo método.

const finishGame = (board) => {
if (isWinner(board)) {
console.log("You win the game! Congrats! ;D");
process.exit(0);
}
if (isLooser(board)) {
console.log("You lost the game!");
process.exit(0);
}
return board;
};
const game = board => compose(
game,
addRandomNumber,
move,
printBoard,
finishGame
)(board);

Agora temos sempre o mesmo retorno na função.

Definimos no início o board com dois números definidos manualmente.
Para ter o jogo completo e 100% funcional, basta agora trocar a chamada “game” no final do arquivo para sortear nos números antes de chamar o jogo.

const board = [[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0]];compose(
game,
addRandomNumber,
addRandomNumber
)(board);

Está finalizado o jogo :D

O código completo você pode conferir abaixo:

Comentários, dúvidas e sugestões são todas bem vindas.

--

--