Skip to content

Instantly share code, notes, and snippets.

@carboleda
Last active February 9, 2019 17:07
Show Gist options
  • Save carboleda/7c8fa08aaa168d0bc1666a6391ea6d00 to your computer and use it in GitHub Desktop.
Save carboleda/7c8fa08aaa168d0bc1666a6391ea6d00 to your computer and use it in GitHub Desktop.
nodejs-tictactoe-server
module.exports = {
//Tamaño del tablero, este caso es de 3x3
GAME_SIZE: 3,
//Una posición sin marcar
UNSELECTED_POSITION: ' ',
//Identificador del jugador 1
PLAYER_INDEX_1: 1,
//Identificador del jugador 2
PLAYER_INDEX_2: 2,
//Caracteres de los jugadores:
//CURSOR (indica la posicion actual de cada jugador en el tablero),
//MARK (indica las posiciones marcadas por cada jugador en el tablero)
GAMER_CHAR: {
GAMER1_CURSOR: '⦿',
GAMER1_MARK: 'O',
GAMER2_CURSOR: '⦿',
GAMER2_MARK: 'X',
},
//Posible estados de un juego
GAME_STATE: {
IN_PROGRESS: '1',
FINISHED: '2',
TIED: '3',
},
//Jugadas en las que un jugador puede ganar
PLAYS_TO_WIN: [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
]
}
const dockerNames = require('docker-names');
const Match = require('../modules/match');
const ACTIVE_MATCHES = {};
module.exports = function(io) {
io.on('connection', function (client) {
client.on('new match', function ({ nickName }) {
const matchName = dockerNames.getRandomName().split('_').shift();
const mMatch = new Match(matchName, io);
client.nickName = nickName;
client.matchName = matchName;
mMatch.joinPlayer(client);
ACTIVE_MATCHES[matchName] = mMatch;
});
client.on('join to match', function ({ nickName, matchName }) {
client.nickName = nickName;
client.matchName = matchName;
ACTIVE_MATCHES[matchName].joinPlayer(client);
});
client.on('get available matches', () => {
const matches = Object.keys(ACTIVE_MATCHES)
.filter(matchName => ACTIVE_MATCHES[matchName].hasAvailablePlace() == true);
client.emit('receive available matches', matches);
});
client.on('disconnect', () => {
console.log('disconnect::playerSocket.playerPlace', client.playerPlace);
if (client.matchName) {
const match = ACTIVE_MATCHES[client.matchName];
if (match) {
match.playerCount--;
match.players[client.playerPlace] = null;
if (match.playerCount == 0) {
console.log('destroy match', client.matchName);
delete ACTIVE_MATCHES[client.matchName];
} else {
match.resetGame();
io.to(client.matchName).emit('game reset', {
gameBoardData: match.gameBoardData,
currentTurn: match.currentTurn,
});
io.to(client.matchName).emit('waiting player', client.matchName);
}
}
}
});
});
}
const {
GAME_SIZE,
GAME_STATE,
UNSELECTED_POSITION,
PLAYER_INDEX_1,
PLAYER_INDEX_2
} = require('../../helpers/constants');
const tictactoe = require('./tictactoe');
/**
* Clase que implementa la logica de una partida de tictactoe
*/
class Match {
/**
* Constructor de la clase
* @param {String} matchName nombre de la partida
* @param {Object} io referencia del servidor socket.io
*/
constructor(matchName, io) {
this.matchName = matchName;
this.io = io;
//Tablero de juego de la partida
this.gameBoardData = null;
//Indicador del turno actual, contiene el playerPlace (index) del jugador
this.currentTurn = null;
//Contador de cuantas conexiones hay en la partida
this.playerCount = 0;
//Objeto que almacena los sockets de los jugaodres
this.players = {
[PLAYER_INDEX_1]: null,
[PLAYER_INDEX_2]: null
};
this.resetGame();
}
/**
* Funcion encargada de resetar o inicializar la partida
*/
resetGame() {
this.currentTurn = null;
//Se crea un array de acuerdo al tamaño de tablero con todas las posiciones sin seleccionar
this.gameBoardData = Array(GAME_SIZE * GAME_SIZE).fill(UNSELECTED_POSITION);
}
/**
* Funcion encargada de obtener el place que le recorresponde a un jugador
*/
getPlayerPlace() {
//Genera un array con las llaves del objeto players y luego
//busca la primera llave cuyo valor este null lo cual significa que el place esta libre
const place = Object
.keys(this.players)
.find(place => this.players[place] === null);
//Retorna el place obtenido y en caso de de estar definido lo convierte a numerico (+)
return !place ? null : +place;
}
/**
* Funcion encargada de verificar si aun hay lugares disponibles en la partida
*/
hasAvailablePlace() {
return this.getPlayerPlace() !== null;
}
/**
* Funcion encargada de obtener un turno de forma aleatoria
*/
getRandomTurn() {
const playerIndexes = Object.keys(this.players);
return Math.ceil(Math.random() * playerIndexes.length);
}
getNextTurn() {
return this.currentTurn == PLAYER_INDEX_1 ? PLAYER_INDEX_2 : PLAYER_INDEX_1;
}
/**
* Permite que un jugador se una a una partida
* @param {WebSocket} playerSocket socket del nuevo jugador
*/
joinPlayer(playerSocket) {
//Obtiene el place para el jugador
const playerPlace = this.getPlayerPlace();
//Si aun hay lugares disponibles
if(playerPlace) {
this.playerCount++;
this.players[playerPlace] = playerSocket;
playerSocket.playerPlace = playerPlace;
console.log(`Current connections on ${this.matchName}: ${this.playerCount}`);
console.log(`Player playerPlace: ${playerPlace}`);
//Se une el jugador a la partida, pasando un callback para
//identificar cuando el jugador ya se haya unido
playerSocket.join(this.matchName, () => onPlayerJoined(this));
//Evento para cuando una posicion del tablero es marcada por un juagador
playerSocket.on('position marked',
currentGame => onPositionMarked(this, playerSocket, currentGame));
} else {
playerSocket.emit('game full');
}
}
}
/**
* Funcion callback para cuando un jugador se unió correctamente a una partida
* @param {Match} match instancia de la partida
*/
function onPlayerJoined(match) {
//Si aun hay lugares disponibles
if(match.hasAvailablePlace()) {
//Se le indica al jugador que se unió que se esta esperando otro jugador
match.io.to(match.matchName).emit('waiting player', match.matchName);
} else {
//Se obtiene el turno
match.currentTurn = match.getRandomTurn();
//Se envia la configuración del juego a cada uno de
//los jugadores en la partida
Object.keys(match.players).forEach(playerPlace => {
playerPlace = +playerPlace;
match.players[playerPlace].emit('init', {
gameBoardData: match.gameBoardData,
unselectedPosition: UNSELECTED_POSITION,
gameSize: GAME_SIZE,
playerPlace,
nickName: match.players[playerPlace].nickName,
currentTurn: match.currentTurn
});
})
}
}
/**
* Funcion que maneja el evento sobre cuando una posicion es macrada en el tablero
* @param {Match} match instancia de la partida
* @param {WebSocket} playerSocket socket del jugador
* @param {Array} currentGame array con el estado del juego
*/
function onPositionMarked(match, playerSocket, currentGame) {
match.gameBoardData = currentGame;
//Se invoca la funcion que aplica la logica del juego cada vez que se marca una posicion
tictactoe.positionMarked(match.gameBoardData, playerSocket.playerPlace)
.then((result) => {
//Se obtiene el siguiente turno
match.currentTurn = match.getNextTurn();
const gameState = {
gameBoardData: match.gameBoardData,
currentTurn: match.currentTurn
};
//Si despues de marcada una posicion el juego sigue estando en progreso
switch(result.state) {
case GAME_STATE.IN_PROGRESS:
console.log('position marked:gameBoardData', match.gameBoardData);
console.log('position marked:currentTurn', match.currentTurn);
//Se notifica a los jugadores en la partida sobre la posición que fue marcada
//y el nuevo estado del tablero
match.io.to(match.matchName).emit('position marked', gameState);
break;
default:
//TODO: GUARDAR LA PARTIDA EN LA BASE DE DATOS
//De lo contrario es porque el juego quedo empatado o finalizado,
//entonces se notifica a todos los jugadores en la partida
//el nuevo estado de la partida
match.io.to(match.matchName).emit('game state', { ...gameState, ...result });
match.resetGame();
}
});
}
module.exports = Match;
const {
PLAYS_TO_WIN,
GAME_STATE,
UNSELECTED_POSITION,
GAMER_CHAR
} = require('../../helpers/constants');
const Utilities = require('../../helpers/utilities');
/**
* Realiza las vaidaciones sobre el tablero despues de cada juagada (posicion marcada)
* @param {Array} currentGame tablero de juego
* @param {Integer} playerPlace index del jugador
* @returns {Object} un objeto con el resultado de las validaciones. El objeto contiene:
* {
* state: {@link helpers/constants.js#GAME_STATE},
* winnerPlace: null o place del jugador que gano la partida,
* winningPlay: array con las posiciones que marco el jugador para ganar la partida
* }
*/
function positionMarked(currentGame, playerPlace) {
return new Promise((resolve, reject) => {
const result = {
state: GAME_STATE.IN_PROGRESS,
winnerPlace: null,
winningPlay: []
};
//Se obtiene el caracter de marcado del jugador
const playerMark = Utilities.getPlayerMarker(GAMER_CHAR, playerPlace);
//Se verifica si el jugador gano la partida con esta jugada
const winningPlay = isWinnerPlayer(currentGame, playerMark);
//Si winningPlay es un array (significa que viene la juagada ganadora)
if (Array.isArray(winningPlay)) {
//Se determina el estado del juego como finalizado...
result.state = GAME_STATE.FINISHED;
result.winnerPlace = playerPlace;
result.winningPlay = winningPlay;
} else {
//De lo contrario se verifica si aun hay posiciones hay disponibles en la partida
const availablePositions = getAvailablePositions(currentGame);
//Si ya no hay posiciones disponibles se deterina el estado del juego como empatado
if(!availablePositions || availablePositions.length == 0) {
result.state = GAME_STATE.TIED;
}
}
resolve(result);
});
}
/**
* Verifica si el jugador gano la partida de acuerdo al estado actual del tablero
* @param {Array} currentGame tablero de juego
* @param {*} playerMark caracter de marcado del jugador
* @returns {Array|Boolean} un array con la jugada si el jugador ganó o false en caso contrario
*/
function isWinnerPlayer(currentGame, playerMark) {
//Recorre cada una de las posibles juagada con las que se puede ganar una partida
for (let i = 0; i < PLAYS_TO_WIN.length; i++) {
const playerWins = currentGame
//Se filra del tablero de juego solo las posiciones correspondientes a la juagada
.filter((position, index) => PLAYS_TO_WIN[i].indexOf(index) !== -1)
//Se verifica si cada una de las posiciones tiene la marca del jugador
.every(position => position === playerMark);
//Si el resultado es true, siginifica que el jugador gano la partida,
//entonces se retorna la jugada con la que gano
if(playerWins) {
return PLAYS_TO_WIN[i];
}
}
//De lo contrario se retorna false
return false;
}
/**
* Devuelve las posiciones que aun estan sin marcar
* @param {*} currentGame tablero de juego
*/
function getAvailablePositions(currentGame) {
return currentGame.filter(position => position === UNSELECTED_POSITION);
}
module.exports = {
positionMarked
};
/**
* Función encargada de convertir un array a una matriz,
* dividiendo el array en subarrays de tamaño cols
* @param {Array} array array a convertir
* @param {Integer} cols cantidad de columnas de la matriz
*/
function arrayToMatrix(array, cols) {
const matrix = [];
for(let i=0; i<=array.length-cols; i=i+cols) {
matrix.push(array.slice(i, i + cols));
}
return matrix;
}
/**
* Función encargada de convertir una matriz en un array,
* concatenando cada fila de la matriz a un array
* @param {Array} matrix array de arrays a convertir
*/
function matrixToArray(matrix) {
return matrix.reduce((array, row) => {
return [...array, ...row];
}, []);
}
/**
* Funcion encargada de quitar los caracteres unicode que representan el color
* o el tipo de fuente asignados usando el paquete colors.
* https://www.npmjs.com/package/colors
* @param {String} str cadena de texto
*/
function stripColors(str) {
return str.replace(/\x1B\[\d+m/g, '');
}
/**
* Funcion encargada de generar un array con los caracteres de indicador de posicion (GAME_CHAR)
* de cada jugador incluyendo las variantes de color.
* NOTA: Una variante de color es el color que se configura para diferenciar cuando el cursor
* esta sobre una posicion ya marcada
* @param {Object} gameChars objeto con la definicion de los caracteres de los jugadores
* @see {@link helpers/constants.js#GAMER_CHAR}
*/
function getPlayerCursors(gameChars) {
const normal = Object.keys(gameChars)
.filter(key => key.indexOf('_CURSOR') !== -1)
.map(key => gameChars[key]);
const colors = normal.map(item => item.bold.yellow);
return [ ...normal, ...colors ];
}
/**
* Funcion encargada de obtener el caracter de indicador de posicion de un jugador
* @param {*} gameChars objeto con la definicion de los caracteres de los jugadores
* @param {*} plyerPlace indice de la posicion de un jugador {@link helpers/constants.js#PLAYER_INDEX_1}
* y {@link helpers/constants.js#PLAYER_INDEX_2}
*/
function getPlayerCursor(gameChars, plyerPlace) {
return gameChars[`GAMER${plyerPlace}_CURSOR`];
}
/**
* Funcion encargada de generar un array con los caracteres de posicion marcada (GAME_CHAR)
* de cada jugador incluyendo las variantes de color.
* NOTA: Una variante de color es el color que se configura para diferenciar cuando el cursor
* esta sobre una posicion ya marcada
* @param {Object} gameChars objeto con la definicion de los caracteres de los jugadores
* @see {@link helpers/constants.js#GAMER_CHAR}
*/
function getPlayerMarkers(gameChars) {
const normal = Object.keys(gameChars)
.filter(key => key.indexOf('_MARK') !== -1)
.map(key => gameChars[key]);
const colors = normal.map(item => item.bold.yellow);
return [ ...normal, ...colors ];
}
/**
* Funcion encargada de obtener el caracter de posicion marcada de un jugador
* @param {*} gameChars objeto con la definicion de los caracteres de los jugadores
* @param {*} plyerPlace indice de la posicion de un jugador {@link helpers/constants.js#PLAYER_INDEX_1}
* y {@link helpers/constants.js#PLAYER_INDEX_2}
*/
function getPlayerMarker(gameChars, plyerPlace) {
return gameChars[`GAMER${plyerPlace}_MARK`];
}
module.exports = {
arrayToMatrix,
matrixToArray,
stripColors,
getPlayerCursors,
getPlayerCursor,
getPlayerMarkers,
getPlayerMarker,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment