Last active
February 9, 2019 17:07
-
-
Save carboleda/7c8fa08aaa168d0bc1666a6391ea6d00 to your computer and use it in GitHub Desktop.
nodejs-tictactoe-server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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] | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } | |
| } | |
| }); | |
| }); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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