From a6c372c9a9b539fbb77f57a136dcc1c54dabad9c Mon Sep 17 00:00:00 2001 From: luca0N! Date: Thu, 31 Dec 2020 14:23:58 -0300 Subject: [PATCH] =?UTF-8?q?C=C3=B3digo=20original=20adicionado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + cliente.js | 39 +++ config.exemplo.json | 23 ++ filtro.json | 13 + index.js | 760 ++++++++++++++++++++++++++++++++++++++++++++ iniciar.sh | 3 + pontuação.js | 39 +++ resposta.js | 35 ++ sala.js | 277 ++++++++++++++++ util.js | 125 ++++++++ 10 files changed, 1317 insertions(+) create mode 100644 .gitignore create mode 100644 cliente.js create mode 100644 config.exemplo.json create mode 100644 filtro.json create mode 100644 index.js create mode 100755 iniciar.sh create mode 100644 pontuação.js create mode 100644 resposta.js create mode 100644 sala.js create mode 100644 util.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9aef452 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config.json +node_modules/* + diff --git a/cliente.js b/cliente.js new file mode 100644 index 0000000..04a5a0c --- /dev/null +++ b/cliente.js @@ -0,0 +1,39 @@ +/* + Força: um (livre) clone do jogo Forca + Copyright (C) 2020 luca0N! + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Entre em contato comigo por e-mail via . +*/ + +class Cliente { + constructor(endereço, apelido, soquete){ + this.endereço = endereço; + this.apelido = apelido; + this.soquete = soquete; + this.uas = Date.now(); // Última Ação Significativa: determina o tempo em que o último evento significativo deste jogador ocorreu. Utilizado para detectar usuários inativos. + this.pontos = 0; + + this.opções = []; + this.pings = []; + } + acrescentarPontos(pontos){ + this.pontos += pontos; + return pontos; + } +} + +module.exports = Cliente; + diff --git a/config.exemplo.json b/config.exemplo.json new file mode 100644 index 0000000..9977ecf --- /dev/null +++ b/config.exemplo.json @@ -0,0 +1,23 @@ +{ + "variáveis":{ + "servidor.porta": 12501, + "sala.clientes.limite": 6, + "sala.pontos.pontuaçãoMeta": 120, + "sala.tin.limite": 6, + "sala.uas.limite": 180000, + + "cliente.opções.padrão": { + "bate-papo.filtro.habilitado": true + }, + + "sala.opções.padrão":{ + "filtro.temasAdultos.habilitado": true + }, + + "cliente.ping.tempoMáximo": 50000, + "cliente.uas.limite": 180000, + "misc.tempo.pings": 10000, + "misc.tempo.checarInatividade": 10000 + } +} + diff --git a/filtro.json b/filtro.json new file mode 100644 index 0000000..c5ec14d --- /dev/null +++ b/filtro.json @@ -0,0 +1,13 @@ +[ + "dnRuYw==", "dnRj", "c2Zk", "Y3Js", "cGF1", "ZmRw", "Y2FjdGU=", "azc=", "Y2FyYWk=", "a3JhaQ==", + "cGludA==", "cHVuaGV0", "YnVjZXQ=", "YnVjdA==", "Z296", "Zm9k", "ZnVk", "dmFkaQ==", "cG9ycg==", "Y2FyYWxo", "a3JhbA==", "dGV0", "Y2FjZXQ=", "cGlyb2M=", + + + "ZGljaw==", "ZnVjaw==", "ZnVjYw==", "Y29jaw==", "Y3Vt", "aGFuZGpvYg==", "Zm9vdGpvYg==", "ZGlsZG8=", "Y3VudA==", "Yml0Y2g=", "aG9l", "d2hvcmU=", "aG9va2Vy", "YmFsbHNhYw==", "Ym93c2Fj", "dGhpYw==", + "bmln", "ZmFn", "Y29vY2g=", "cHVzcw==", "ZnVja2lu", "Zmxlc2hsaQ==", + + + "dnVsdmE=", "YW5hbA==", "aGVycGVz", "YWlkcw==", "aGl2", "cGVuaXM=", "bmF6aQ==", + "dHVyYmE=", "dGVyYmE=", "cHJvc3RpdHV0", "c2V4" +] + diff --git a/index.js b/index.js new file mode 100644 index 0000000..f06db2a --- /dev/null +++ b/index.js @@ -0,0 +1,760 @@ +/* + Força: um (livre) clone do jogo Forca + Copyright (C) 2020 luca0N! + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Entre em contato comigo por e-mail via . +*/ + +const WebSocket = require('ws'); +const fs = require('fs'); + +const Resposta = require('./resposta.js'); +const Cliente = require('./cliente.js'); +const Sala = require('./sala.js'); +const Util = require('./util.js'); + +const Pontos = require('./pontuação.js'); + +var vars = JSON.parse(fs.readFileSync('config.json', 'UTF8')).variáveis; + +const wss = new WebSocket.Server({ port: vars["servidor.porta"] }); + +var clientes = [], + salas = []; + +console.log('Servidor Força, v0.1-beta'); + +var filtros = JSON.parse(fs.readFileSync('filtro.json', 'UTF8')); + +var ids = {}; + +ids.pings = setInterval(verificarPings, vars["misc.tempo.pings"]); +ids.checarInativos = setInterval(checarInativos, vars["misc.tempo.checarInatividade"]); + +console.log('Disponível.'); + +wss.on('connection', ws => { + //var end = ws._socket.remoteAddress; + let cliente = new Cliente(ws._socket.remoteAddress, undefined, ws); + cliente.opções = vars["cliente.opções.padrão"]; // Definir opções padrão + console.log('Nova conexão! Fonte: ' + cliente.endereço); + + let aguardandoEntrada = true; + + ws.on('message', msg => { + console.log(' <' + cliente.endereço + '> ' + msg); + try { + resposta = JSON.parse(msg); + } catch (e){ + // A resposta enviada não está no formato JSON válido. + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "RESPOSTA_CORROMPIDA" ))); + return; + } + if (aguardandoEntrada){ + aguardandoEntrada = false; + cliente.apelido = resposta.fonte.split('.')[1]; + // Problema #2: O apelido é válido? + let apelidoRegex = /[^A-Za-z0-9_]/g; + if ( + cliente.apelido.length < 3 // O apelido tem menos que três caracteres? + || cliente.apelido.length > 16 // O apelido tem mais que 16 caracteres? + || apelidoRegex.exec(cliente.apelido) !== null // O apelido contém algum caractere que não seja permitido? + || Util.filtrarMensagem(cliente.apelido, filtros) != cliente.apelido // O apelido contém termos filtrados? + ){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "NOME_ILEGAL" ))); + ws.terminate(); + } + } + + let salaObj = cliente.sala !== undefined ? pesquisarSala(cliente.sala) : undefined; + + // Interpretar a mensagem do cliente. + switch (resposta.pedido){ + case "ENTRAR":{ + // Entrar em uma sala ou criar ela caso inexistente. + // O parâmetro extra "sala" é necessário. Caso inexistente, retornar um erro ao cliente. + let sala = resposta.extra.sala, + salaNum, + adicionarCliente = false; + + if (sala === undefined){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", undefined, { motivo: "PARÂMETRO_FALTANDO"}))); + break; + } + + // Problema #2: O apelido é válido? + let salaRegex = /[^a-z0-9]/g; + if ( + sala.length < 3 // O nome tem menos que três caracteres? + || sala.length > 8 // O nome tem mais que 16 caracteres? + || salaRegex.exec(sala) !== null // O nome contém algum caractere que não seja permitido? + || Util.filtrarMensagem(sala, filtros) != sala // O nome contém termos filtrados? + ){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "NOME_ILEGAL" ))); + ws.terminate(); + } + + + // Agora que verificamos que os parâmetros necessários foram providenciados, vamos verificar a existência da sala. + if (salaExiste(sala)){ + salaObj = pesquisarSala(sala); + // Encontrar a sala existente na lista de salas. + for (var x = 0; x < salas.length; x++){ + if (salas[x].sala === sala){ + //salas[x].clientes.push(cliente); + salaNum = x; + adicionarCliente = true; + salaObj = salas[x]; + break; + } + } + // O IP do cliente está banido? + if (salaObj.banido(cliente.soquete.remoteAddress)){ + ws.send(JSON.stringify(new Resposta("FATAL", "SERVIDOR", "BANIDO"))); + return; + } + // A sala está em partida? + if (salaObj.estado === "EM_PARTIDA"){ + ws.send(JSON.stringify(new Resposta("FATAL", "SERVIDOR", "SALA_EM_PARTIDA"))); + return; + } + // A sala está cheia? + let limiteJogadores = vars["sala.clientes.limite"]; + if (salaObj.clientes.length >= limiteJogadores){ + // A sala está cheia. Retornar um código de erro ao jogador. + ws.send(JSON.stringify(new Resposta("FATAL", "SERVIDOR", "SALA_CHEIA"))); + return; + } + + // Gerar uma lista com os apelidos dos clientes conectados. + let jogadores = []; + for (let x = 0; x < salaObj.clientes.length; x++){ + let jogadorApelido = salaObj.clientes[x].apelido; + // Verificar se já existe um usuário com o mesmo apelido na sala. + if (jogadorApelido === cliente.apelido){ + // Alertar o cliente. + ws.send(JSON.stringify(new Resposta("FATAL", "SERVIDOR", "APELIDO_EXISTENTE"))); + // Fechar a conexão. + ws.terminate(); + return; + } + + jogadores.push(jogadorApelido); + } + + // Retornar um código de sucesso ao cliente, incluindo a lista de jogadores conectados, estado e o líder da sala. + ws.send(JSON.stringify(new Resposta("SUCESSO", "SERVIDOR", "SALA_EXISTENTE", { opções: salaObj.opções, líder: salaObj.líder, estado: salaObj.estado, jogadores: jogadores }))); + } else { + // A sala não existe. Vamos criar ela e adicionar o usuário nela. + // salas.push(new Sala(sala, [cliente])); + // Retornar um código de sucesso ao cliente. + ws.send(JSON.stringify(new Resposta("SUCESSO", "SERVIDOR", "SALA_CRIADA"))); + } + + cliente.sala = sala; + clientes.push(cliente); + if (salaObj !== undefined){ + salas[salaNum].clientes.push(cliente); + salaObj = salas[salaNum]; + } else{ + var salaNova = new Sala(sala, [cliente], undefined, undefined, vars["sala.opções.padrão"]); + salaNova.pontuaçãoMeta = vars['sala.pontos.pontuaçãoMeta']; + salas.push(salaNova); + salaObj = salaNova; + } + console.log(' [' + cliente.endereço + '] Conexão estabelecida.'); + // Alertar todos os jogadores sobre a entrada do jogador. + alertarClientesEvento('EVENTO_JOGADOR_ENTROU', sala, { apelido:cliente.apelido }); + if (salaObj !== undefined) + // A sala possui a quantidade suficiente de jogadores para iniciar? + verificarPossibilidadeDePartida(salaObj); + break; + } + case "ENVIAR_MENSAGEM":{ + // Verificar se a mensagem desejada foi providenciada pelo cliente. + if (resposta.extra.mensagem === undefined){ + // A mensagem não foi providenciada pelo cliente. Retornar uma resposta de erro para o cliente. + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "PARAMETRO_FALTANDO"))); + return; + } + // Verificar se a mensagem excede o limite de caracteres proposto pelo cliente. + if (resposta.extra.mensagem.length > 128) { + // Cancelar este pedido e retornar uma mensagem de erro para o cliente. + ws.send(JSON.stringify(new Resposta("PEDIDO_CANCELADO", "SERVIDOR", "MENSAGEM_MUITO_LONGA"))); + return; + } + cliente.uas = Date.now(); + + var mensagem = '<' + cliente.apelido + '> ' + resposta.extra.mensagem; + // Enviar a mensagem para todos os clientes. + alertarClientesEvento('EVENTO_JOGADOR_MENSAGEM', cliente.sala, { apelido: cliente.apelido, mensagem: mensagem }); + break; + } + case "INICIAR_PARTIDA":{ + // O cliente que está tentando iniciar a partida é do líder? + if (salaObj.líder !== cliente.apelido){ + // Este cliente não é do líder da sala. Retornar um erro ao cliente e cancelar a tarefa. + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SEM_PERMISSÃO"))); + break; + } + // A sala já está em partida? + if (salaObj.estado === "EM_PARTIDA"){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SALA_EM_PARTIDA"))); + break; + } + + cliente.uas = Date.now(); + salaObj.uas = Date.now(); + + // Agora que verificamos que o cliente é do líder da sala, vamos enviar a todos os usuários um evento alertando sobre o início da partida. + + salaObj.novaPartida(); + + alertarClientesEvento("EVENTO_SALA_ESTADO_ATUALIZADO", cliente.sala, { estado: salaObj.estado }); + + // E vamos alertar todos os clientes sobre o jogador escolhido. + alertarClientesEvento("EVENTO_JOGADOR_ESCOLHIDO", cliente.sala, { jogador: salaObj.vezDe }); + break; + } + case "ESCOLHER_PALAVRA":{ + // A sala está em partida? + if (salaObj.estado !== "EM_PARTIDA"){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SALA_ESTADO_NÃO_EM_PARTIDA"))); + break; + } + + // Verificar se é realmente a vez do cliente que enviou este pedido. + if (salaObj.vezDe !== cliente.apelido){ + // Não é a vez deste cliente! + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SEM_PERMISSÃO"))); + break; + } + + // Certo, é a vez deste cliente. + cliente.uas = Date.now(); + salaObj.uas = Date.now(); + // A sala está com o filtro de temas adultos habilitado? + if (salaObj.opções["filtro.temasAdultos.habilitado"]){ + // Verificar se esta palavra está filtrada. + let palavraFiltrada = Util.filtrarMensagem(resposta.extra.palavra, filtros); + let temaFiltrado = Util.filtrarMensagem(resposta.extra.tema, filtros); + // Caso há variação da palavra/tema, então a palavra/tema escolhido contém um ou mais termos impróprios. + if (palavraFiltrada !== resposta.extra.palavra + || temaFiltrado !== resposta.extra.tema){ + ws.send(JSON.stringify(new Resposta("PEDIDO_CANCELADO", "SERVIDOR", "PALAVRA_OU_TEMA_PROIBIDO"))); + break; + } + } + + // Redefinir tin (contagem de tentativas incorretas). + alterarTin(salaObj, 0); + + salaObj.jpa = cliente.apelido; + + // Escolher um jogador aleatório para escolher a primeira letra. + var clienteEscolhido = salaObj.gerarJogadorAleatório(); + + salaObj.palavra = resposta.extra.palavra.toUpperCase(); + salaObj.definirPalavraLetrasQtd(); + salaObj.termoVezDe = clienteEscolhido; + salaObj.estadoTermo = "AGUARDANDO_LETRA"; + salaObj.jf.push(clienteEscolhido); + + // Vamos alertar todos os clientes sobre este evento. + alertarClientesEvento("EVENTO_PALAVRA_ESCOLHIDA", cliente.sala, { palavra: { tamanho: resposta.extra.palavra.length, espaços: salaObj.procurar(' '), hífens: salaObj.procurar('-') }, tema: resposta.extra.tema, termoVezDe: salaObj.termoVezDe }); + break; + } + case "ENVIAR_TERMO":{ + // A sala está em partida? + if (salaObj.estado !== "EM_PARTIDA"){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SALA_ESTADO_NÃO_EM_PARTIDA"))); + break; + } + + // Verificar se é a vez do cliente que enviou este pedido. + if (salaObj.termoVezDe !== cliente.apelido){ + ws.send(JSON.stringify(new Resposta("PEDIDO_CANCELADO", "SERVIDOR", "AGUARDE_SUA_VEZ"))); + break; + } + cliente.uas = Date.now(); + salaObj.uas = Date.now(); + + // Verificar o tipo do termo enviado. + let letra = resposta.extra.termo.length === 1; + let salaClientes = salaObj.clientes; + let e = Util.receberClientePorApelido(salaObj, salaObj.vezDe); // escolhedor + + /*// A sala está aceitando apenas letras? + if (salaObj.estadoTermo === "AGUARDANDO_LETRA" && !letra){ + // A sala não está esperando termos. + ws.send(JSON.stringify(new Resposta("PEDIDO_CANCELADO", "SERVIDOR", "TERMO_INVÁLIDO_TERMO_INESPERADO"))); + break; + }*/ + // O jogador deseja pular? + if (salaObj.estadoTermo === "AGUARDANDO_TERMO" && + resposta.extra.termo === '?'){ + alertarClientesEvento("EVENTO_JOGADOR_PULOU_TERMO", cliente.sala, { jogador: cliente.apelido }); + // Escolher outro jogador. + escolherJogadorTermo(salaObj); + return; + } + + // A sala está aceitando apenas termos? + if (salaObj.estadoTermo !== "AGUARDANDO_LETRA" && letra){ + // A sala não está esperando letras. + ws.send(JSON.stringify(new Resposta("PEDIDO_CANCELADO", "SERVIDOR", "TERMO_INVÁLIDO_LETRA_INESPERADA"))); + break; + } + let resultadoTermo = salaObj.processarTermo(resposta.extra.termo); + let lp = resultadoTermo.letrasPreenchidas; + let r = resultadoTermo.resultado; + + let dif = 0, // diferença da nova pontuação do jogador que enviou o termo + dife = 0; // diferença da nova pontuação do jogador que enviou a palavra atual + switch (r){ + case "LETRAS_PREENCHIDAS":{ + alertarClientesEvento("EVENTO_TERMO_ESCOLHIDO", cliente.sala, { tipo: letra ? "LETRA" : "TERMO", termo: resposta.extra.termo, resultado: r, ldc: salaObj.ldc }); + + // Também acrescentar pontuação ao jogador. + for (let x = 0; x < lp; x++){ + dif += cliente.acrescentarPontos(Pontos.EVENTO_JOGADOR_LETRA_ACERTADA); + dife += e.acrescentarPontos(Pontos.EVENTO_JOGADOR_SALA_LETRA_ACERTADA); + } + // Alertar os clientes da nova pontuação deste jogador. + alertarClientesEvento("EVENTO_JOGADOR_PONTUAÇÃO_ALTERADA", cliente.sala, { jogador: cliente.apelido, pontos: cliente.pontos, diferença: dif }); + alertarClientesEvento("EVENTO_JOGADOR_PONTUAÇÃO_ALTERADA", cliente.sala, { jogador: e.apelido, pontos: e.pontos, diferença: dife }); + break; + } + case "LETRA_INEXISTENTE":{ + alertarClientesEvento("EVENTO_TERMO_ESCOLHIDO", cliente.sala, { tipo: letra ? "LETRA" : "TERMO", termo: resposta.extra.termo, resultado: r }); + if (alterarTin(salaObj)) return; + break; + } + case "PALAVRA_CORRETA":{ + alertarClientesEvento("EVENTO_TERMO_ESCOLHIDO", cliente.sala, { acertou: true, palavra: salaObj.palavra, resultado: r }); + // Acrescentar pontos ao jogador por acertar a palavra. + // Pontos pelos campos que antes estavam vazios e pontos por acertar a palavra. + + let extra = salaObj.estadoTermo === 'AGUARDANDO_LETRA'; // caso verdadeiro, dar pontos extras por acertar a palavra cedo + for (let x = 0; x < lp; x++){ + dif += cliente.acrescentarPontos(extra ? Pontos.EVENTO_JOGADOR_LETRA_ACERTADA_CEDO : Pontos.EVENTO_JOGADOR_LETRA_ACERTADA); + dife += e.acrescentarPontos(extra ? Pontos.EVENTO_JOGADOR_SALA_LETRA_ACERTADA_CEDO : Pontos.EVENTO_JOGADOR_SALA_LETRA_ACERTADA); + } + + dif += cliente.acrescentarPontos(Pontos.EVENTO_JOGADOR_PALAVRA_ACERTADA); + dife += e.acrescentarPontos(Pontos.EVENTO_JOGADOR_SALA_PALAVRA_ACERTADA); + // Alertar os clientes da nova pontuação deste jogador. + alertarClientesEvento("EVENTO_JOGADOR_PONTUAÇÃO_ALTERADA", cliente.sala, { jogador: cliente.apelido, pontos: cliente.pontos, diferença: dif }); + alertarClientesEvento("EVENTO_JOGADOR_PONTUAÇÃO_ALTERADA", cliente.sala, { jogador: e.apelido, pontos: e.pontos, diferença: dife }); + + // Verificar se um jogador atingiu a meta de pontos. + let vencedor = salaObj.receberVencedor(); + if (vencedor !== null){ + // Há um vencedor; alertar os clientes na sala que houve um vencedor. + alertarClientesEvento("EVENTO_JOGADOR_VITÓRIA", cliente.sala, { jogador: vencedor.apelido }); + // Encerrar esta partida. + verificarPossibilidadeDePartida(salaObj, true); + return; + } + + // Escolher outro jogador para escolher a palavra. + salaObj.novaRodada(); + // E vamos alertar todos os clientes sobre o jogador escolhido. + alertarClientesEvento("EVENTO_JOGADOR_ESCOLHIDO", cliente.sala, { jogador: salaObj.vezDe }); + return; + } + case "PALAVRA_INCORRETA":{ + alterarTin(salaObj); + // Penalizar o jogador caso ele tenha errado a palavra cedo. + if (salaObj.estadoTermo === 'AGUARDANDO_LETRA') + for (let x = 0; x < resposta.extra.termo.length; x++) + dif += cliente.acrescentarPontos(Pontos.EVENTO_JOGADOR_LETRA_ERRADA_CEDO); + alertarClientesEvento("EVENTO_JOGADOR_PONTUAÇÃO_ALTERADA", cliente.sala, { jogador: cliente.apelido, pontos: cliente.pontos, diferença: dif }); + + alertarClientesEvento("EVENTO_TERMO_ESCOLHIDO", cliente.sala, { acertou: false, palavra: resposta.extra.termo, resultado: r }); + break; + } + } + escolherJogadorTermo(salaObj); + // Enviar um alerta para todos os clientes da sala. + // Não é necessário especificar o apelido do jogador. Os clientes já vão estar esperando uma resposta do cliente em questão. + /*alertarClientesEvento("EVENTO_TERMO_ESCOLHIDO", cliente.sala, { tipo: letra ? "LETRA" : "TERMO", termo: resposta.extra.termo });*/ + break; + } + case "CHUTAR_CLIENTE":{ + // Problema #60: O cliente que enviou este pedido é o líder da sala? + if (salaObj.líder !== cliente.apelido){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SEM_PERMISSÃO"))); + return; + } + let jogador = resposta.extra.jogador; + if (jogador === undefined){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", undefined, { motivo: "PARÂMETRO_FALTANDO"}))); + break; + } + + cliente.uas = Date.now(); + + // Encerrar conexão com o cliente alvo. + let clienteAlvo = Util.receberClientePorApelido(salaObj, jogador); + alertarClientesEvento("EVENTO_CLIENTE_CHUTADO", cliente.sala, { cliente: clienteAlvo.apelido }); + + // Remover o cliente da sala. + clienteAlvo.soquete.terminate(); + break; + } + case "BANIR_CLIENTE":{ + // Problema #60: O cliente que enviou este pedido é o líder da sala? + if (salaObj.líder !== cliente.apelido){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SEM_PERMISSÃO"))); + return; + } + let jogador = resposta.extra.jogador; + if (jogador === undefined){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", undefined, { motivo: "PARÂMETRO_FALTANDO" }))); + break; + } + cliente.uas = Date.now(); + + // Encerrar conexão com o cliente alvo. + let clienteAlvo = Util.receberClientePorApelido(salaObj, jogador); + alertarClientesEvento("EVENTO_CLIENTE_BANIDO", cliente.sala, { cliente: clienteAlvo.apelido }); + + // Remover o cliente da sala. + clienteAlvo.soquete.terminate(); + // Adicionar o cliente na lista de banidos da sala. + salaObj.banidos.push(clienteAlvo.soquete.remoteAddress); + break; + } + case "ALTERAR_OPÇÃO":{ + let opção = resposta.extra.opção, + valor = resposta.extra.valor; + if (opção === undefined || valor === undefined){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", undefined, { motivo: "PARÂMETRO_FALTANDO"}))); + break; + } + cliente.uas = Date.now(); + + // Definir opção. + if (resposta.extra.alvo === "CLIENTE"){ + cliente.opções[opção] = valor; + + ws.send(JSON.stringify(new Resposta("SUCESSO", "SERVIDOR", "OPÇÃO_ALTERADA", { opção: opção, novoValor: cliente.opções[opção] }))); + } else if (resposta.extra.alvo === "SALA") { + // O cliente que está enviando este pedido é o líder da sala? + if (salaObj.líder !== cliente.apelido){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SEM_PERMISSÃO"))); + break; + } + + // A sala já está em partida? + if (salaObj.estado === "EM_PARTIDA"){ + ws.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SALA_EM_PARTIDA"))); + break; + } + // Agora que verificamos que este cliente é o líder, vamos alterar a configuração da sala. + salaObj.opções[opção] = valor; + ws.send(JSON.stringify(new Resposta("SUCESSO", "SERVIDOR", "OPÇÃO_ALTERADA", { alvo: resposta.extra.alvo, opção: opção, novoValor: salaObj.opções[opção] }))); + // Vamos alertar os clientes a opção alterada. + alertarClientesEvento("EVENTO_SALA_OPÇÃO_ALTERADA", cliente.sala, { opção: opção, valor: valor}); + } + break; + } + case "PING":{ + // Adicionar tempo para a lista de pings do cliente. + cliente.pings.push(Date.now()); + ws.send(JSON.stringify(new Resposta("SUCESSO", "SERVIDOR", "PONG", { tempo: resposta.extra.tempo }))); + break; + } + } + }); + + ws.on('error', erro => { + console.log(' [' + cliente.endereço + '] Conexão fechada devido a erro.'); + // Remover o cliente da lista de clientes. + removerCliente(cliente.endereço); + }); + + ws.on('close', function(){ + console.log(' [' + cliente.endereço + '] Conexão fechada.'); + // Remover o cliente da lista de clientes. + removerCliente(cliente, 'MOTIVO_CONEXÃO_FECHADA'); + }); + let resposta = new Resposta("TESTE_CON", "SERVIDOR"); + ws.send(JSON.stringify(resposta)); +}); + +/** + * Remove um cliente da lista de clientes e da sala conectada. + * @param Cliente O objeto Cliente. + * @param string O motivo pelo qual o jogador está sendo removido. + * @since 12 de outubro de 2020, 12:51 (UTC -03:00). + */ +function removerCliente(cliente, motivo){ + let sala = pesquisarSala(cliente.sala); + + // Verifica se o cliente está em uma sala. + if (sala === undefined){ + //cliente.soquete.send(JSON.stringify(new Resposta("ERRO", "SERVIDOR", "SEM_SALA"))); + return -1; + } + + // Remover o cliente da lista de clientes. + clientes.splice(clientes.indexOf(cliente), 1); + + // Verificar se este é o único cliente conectado na sala. Se este for o caso, então apagar a sala. + if (sala.clientes.length === 1){ + salas.splice(salas.indexOf(sala)); + return; + } + + let líder = sala.líder === cliente.apelido; + + if (líder) // Caso o líder sair da sala, então apagar a sala. + alertarClientesEvento('EVENTO_LÍDER_SAIU', sala.sala, {}); + + // Este cliente está na sala? + //if (sala.clientes.indexOf(cliente.apelido) === -1) + //return; + + // Alertar os clientes na sala sobre o jogador que está sendo removido caso um motivo tenha sido especificado. + alertarClientesEvento('EVENTO_JOGADOR_SAIU', cliente.sala, { apelido: cliente.apelido, motivo: motivo }); + + if(líder) // Remover todos os jogadores da sala. + for (let x = sala.clientes.length; x > 0; x--) + sala.clientes[x-1].soquete.terminate(); + + // Remover o cliente da lista de clientes da sala. + sala.clientes.splice(sala.clientes.length === 2 ? + sala.clientes.indexOf(cliente.apelido) + : sala.clientes.indexOf(cliente.apelido) - 1, 1); + + // Remover o jogador da fila gerada. + for (let x = 0; x < sala.jfr.length; x++) + if (sala.jfr[x] === cliente.apelido){ + sala.jfr.splice(x, 1); + break; + } + + // Este jogador havia escolhido a palavra atual? + if (sala.vezDe === cliente.apelido){ + // Escolher outro jogador. + sala.novaRodada(); + // E vamos alertar todos os clientes sobre o jogador escolhido. + alertarClientesEvento("EVENTO_JOGADOR_ESCOLHIDO", cliente.sala, { jogador: sala.vezDe }); + } + // Este jogador estava escolhendo um termo? + else if (sala.termoVezDe === cliente.apelido){ + // Removê-lo da fila de jogadores. + for (let x = 0; x < sala.jf.length; x++) + if (sala.jf[x] === cliente.apelido){ + sala.jf.splice(x, 1); + break; + } + // Escolher outro jogador. + escolherJogadorTermo(sala); + } + + // Se a sala estiver pronta para iniciar uma nova partida, ela ainda continuará preparada após a saída deste cliente? + if (!líder) verificarPossibilidadeDePartida(sala); +} + +/** + * Verifica a existência de uma sala. + * @param string sala O código da sala. + * @returns boolean Verdadeiro caso a sala seja existente. + * @since 12 de outubro de 2020, 13:15 (UTC -03:00). + */ +function salaExiste(sala){ + for (var x = 0; x < salas.length; x++) + if (salas[x].sala === sala) + return true; + + // Nenhuma sala foi encontrada no ciclo. Retornar falso. + return false; +} + +/** + * Alerta todos os clientes de uma sala sobre um evento. + * @param string O ID do evento. + * @param string salaNome A sala desejada. + * @param object extra Os dados extra do evento. + * @since 12 de outubro de 2020, 13:32 (UTC -03:00). + */ +function alertarClientesEvento(evento, salaNome, extra){ + // Pesquisar pela sala desejada. + let sala = pesquisarSala(salaNome); + + // Cancelar alerta caso a sala não seja existente. + if (sala === undefined) + return; + + let msgFiltrada = null; + + for (let x = 0; x < sala.clientes.length; x++){ + let e = extra; + if (e !== undefined && e.mensagem !== undefined){ + // Evitar problemas de formatação com colchetes. + e.mensagem = e.mensagem.replace(/\[/g, "\\[") + .replace(/\]/g, "\\]"); + if (sala.clientes[x].opções["bate-papo.filtro.habilitado"]){ + if (msgFiltrada === null) // Evitar de filtrar a mensagem caso ela já tenha sido filtrada. + msgFiltrada = Util.filtrarMensagem(e.mensagem, filtros); + e.mensagem = msgFiltrada; + } + } + + sala.clientes[x].soquete.send(JSON.stringify(new Resposta("EVENTO", "SERVIDOR", evento.substring(7), e))); + } +} + +function alertarClienteEvento(evento, salaNome, extra, cliente){ + // Pesquisar pela sala desejada. + let sala = pesquisarSala(salaNome); + + // Cancelar alerta caso a sala não seja existente. + if (sala === undefined) + return; + for (let x = 0; x < sala.clientes.length; x++){ + if (sala.clientes[x].apelido === cliente.apelido){ + let mensagem = '<' + extra.apelido + '> ' + extra.mensagem; + sala.clientes[x].soquete.send(JSON.stringify(new Resposta("EVENTO", "SERVIDOR", evento.substring(7), extra))); + break; + } + } +} + +/** + * Pesquisa por uma sala com o nome desejado e retorna um objeto Sala correspondente. + * @param string sala O nome da sala. + * @returns Sala O objeto da sala. + * @since 12 de outubro de 2020, 19:49 (UTC -03:00). + */ +function pesquisarSala(sala){ + for (var x = 0; x < salas.length; x++) + if (salas[x].sala === sala) + return salas[x]; +} + +/** + * Verifica se há a possibilidade de iniciar uma nova partida baseado na quantidade de clientes conectados à sala especificada. Caso exista essa possibilidade, então alterar o estado da sala e alertar todos os jogadores (incluindo o líder) sobre isso. + * @param Sala sala O objeto da sala. + * @return boolean Verdadeiro caso exista a possibilidade, falso caso contrário. + * @since 14 de outubro de 2020. + */ +function verificarPossibilidadeDePartida(sala, forçar){ + if (sala.estado === "EM_PARTIDA" && sala.clientes.length > 1 && !forçar) + return true; + + // Alterar o estado da sala. + sala.estado = sala.clientes.length === 1 ? "AGUARDANDO_JOGADORES" : "AGUARDANDO_LÍDER"; + + // Alertar os clientes. + alertarClientesEvento("EVENTO_SALA_ESTADO_ATUALIZADO", sala.sala, { estado: sala.estado }); + return sala.clientes.length > 1; +} + +/** + * Incrementa o tin da sala especificada e alerta a todos os clientes sobre o tin atualizado. + * @param Object sala O objeto da sala. + * @param int valor O novo valor do tin. Caso este parâmetro não esteja definido, então o tin terá seu valor incrementado por 1. + * @returns Verdadeiro caso uma nova partida tenha sido iniciada. + * @since 24 de dezembro de 2020. + */ +function alterarTin(sala, valor){ + if (valor === undefined) + sala.tin++; + else + sala.tin = valor; + alertarClientesEvento("EVENTO_TIN_ATUALIZADO", sala.sala, { tin: sala.tin }); + // Tin máximo atingido? + if (sala.tin >= vars['sala.tin.limite']){ + alertarClientesEvento("EVENTO_TIN_MÁXIMO_ATINGIDO", sala.sala, { palavra: sala.palavra }); + // Nova rodada. + // Escolher outro jogador para escolher a palavra. + sala.novaRodada(); + // E vamos alertar todos os clientes sobre o jogador escolhido. + alertarClientesEvento("EVENTO_JOGADOR_ESCOLHIDO", sala.sala, { jogador: sala.vezDe }); + return true; + } +} + +function escolherJogadorTermo(sala){ + // Escolher outro jogador para enviar o próximo termo. + let ja = sala.gerarJogadorAleatório(); + + sala.jf.push(ja); + sala.termoVezDe = ja; + + // É hora de descobrir a palavra inteira? + let possívelDescoberta = sala.possívelDescoberta(); + if (possívelDescoberta) + sala.estadoTermo = "AGUARDANDO_TERMO"; + + alertarClientesEvento("EVENTO_JOGADOR_ESCOLHIDO_TERMO", sala.sala, { jogador: ja, tipo: sala.possívelDescoberta() ? "TERMO" : "LETRA" }); +} + +/** + * Verifica os pings enviados pelos clientes. Caso um ping recente não tenha sido enviado pelo cliente, então a sua conexão será terminada. + * @since 30 de dezembro de 2020. + */ +function verificarPings(){ + for (let x = 0; x < clientes.length; x++){ + let cliente = clientes[x], + pings = cliente.pings; + if (pings.length >= 10) + pings.splice(0, 1); + + let delta = Date.now() - pings[pings.length - 1], + máx = vars["cliente.ping.tempoMáximo"]; + + if (delta >= máx){ + console.log("[" + cliente.endereço + "] O cliente não enviou pings recentemente. Fechando conexão."); + cliente.soquete.terminate(); + } + } +} + +/** + * Fecha conexões e salas inativas. + * @since 30 de dezembro de 2020. + */ +function checarInativos(){ + /////////////// + // Jogadores // + /////////////// + for (let x = 0; x < clientes.length; x++){ + if (Date.now() - clientes[x].uas >= vars["cliente.uas.limite"]){ + console.log("[" + clientes[x].endereço + "] O jogador parece estar inativo. Fechando conexão."); + // Alertar o cliente. + clientes[x].soquete.send(JSON.stringify(new Resposta("EVENTO", "SERVIDOR", "CHUTADO_POR_INATIVIDADE"))); + clientes[x].soquete.terminate(); + } + } + + /////////// + // Salas // + /////////// + for (let x = 0; x < salas.length; x++) + if (Date.now() - salas[x].uas >= vars["sala.uas.limite"]){ + console.log(salas[x].sala + ": esta sala parece estar inativa. Removendo ela."); + // Esta sala está inativa. Alertar aos clientes de que eles serão desconectados desta sala por inatividade. + alertarClientesEvento("EVENTO_SALA_INATIVA", salas[x].sala); + // Terminar conexão com o líder, que automaticamente irá desconectar todos da sala. + Util.receberClientePorApelido(salas[x], salas[x].líder).soquete.terminate(); + } +} + diff --git a/iniciar.sh b/iniciar.sh new file mode 100755 index 0000000..14fc6ec --- /dev/null +++ b/iniciar.sh @@ -0,0 +1,3 @@ +#!/bin/sh +clear && node . + diff --git a/pontuação.js b/pontuação.js new file mode 100644 index 0000000..3ea4c4d --- /dev/null +++ b/pontuação.js @@ -0,0 +1,39 @@ +/* + Força: um (livre) clone do jogo Forca + Copyright (C) 2020 luca0N! + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Entre em contato comigo por e-mail via . +*/ + +class Pontos { + //////////////// + // Ao jogador // + //////////////// + static EVENTO_JOGADOR_LETRA_ACERTADA = 2; // qtd de pts dados a um jogador que acertou uma letra. + static EVENTO_JOGADOR_PALAVRA_ACERTADA = 4; // qtd de pts dados a um jogador que acertou uma palavra. + static EVENTO_JOGADOR_LETRA_ACERTADA_CEDO = 4; // qtd de pts dados a um jogador que acertou uma palavra cedo. + static EVENTO_JOGADOR_LETRA_ERRADA_CEDO = -4; // qtd de pts dados a um jogador que errou uma palavra cedo. + + ///////////////////////////////////// + // Ao jogador que enviou a palavra // + ///////////////////////////////////// + static EVENTO_JOGADOR_SALA_LETRA_ACERTADA = Pontos.EVENTO_JOGADOR_LETRA_ACERTADA / 2; // qtd de pts dados a um jogador quando alguém acerta uma letra de sua palavra. + static EVENTO_JOGADOR_SALA_PALAVRA_ACERTADA = Pontos.EVENTO_JOGADOR_PALAVRA_ACERTADA / 2; // qtd de pts dados a um jogador quando alguém acerta a sua palavra. + static EVENTO_JOGADOR_SALA_LETRA_ACERTADA_CEDO = Pontos.EVENTO_JOGADOR_LETRA_ACERTADA_CEDO / 2; // qtd de pts dados a um jogador quando alguém acerta a sua palavra cedo. +} + +module.exports = Pontos; + diff --git a/resposta.js b/resposta.js new file mode 100644 index 0000000..55b57ea --- /dev/null +++ b/resposta.js @@ -0,0 +1,35 @@ +/* + Força: um (livre) clone do jogo Forca + Copyright (C) 2020 luca0N! + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Entre em contato comigo por e-mail via . +*/ + +// Oct 11, 2020, 23:25 (UTC -03:00) + +class Resposta { + constructor(tipo, fonte, pedido, extra){ + this.tipo = tipo; + this.fonte = fonte; + this.pedido = pedido; + + if (extra !== undefined) + this.extra = extra; + } +} + +module.exports = Resposta; + diff --git a/sala.js b/sala.js new file mode 100644 index 0000000..1640a5f --- /dev/null +++ b/sala.js @@ -0,0 +1,277 @@ +/* + Força: um (livre) clone do jogo Forca + Copyright (C) 2020 luca0N! + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Entre em contato comigo por e-mail via . +*/ + +class Sala { + /** + * Objeto Sala. + * @param string sala O nome da sala. + * @param Array A lista de clientes. + * @param string O apelido do líder. + * @param string O estado da sala. + */ + constructor(sala, clientes, líder, estado, opções){ + this.sala = sala; + this.clientes = clientes; + this.opções = opções; + this.uas = Date.now(); // Última Ação Significante: marca o tempo em que um último evento significante tenha acontecido. Caso este evento tenha acontecido muito tempo atrás, esta sala será apagada. + this.líder = líder === undefined ? clientes[0].apelido : líder; + this.estado = estado === undefined ? "AGUARDANDO_JOGADORES" : estado; + this.pontuaçãoMeta = 0; + this.tin = 0; // tentativas incorretas + + this.jfr = []; // fila de jogadores para rodadas + + this.banidos = []; + } + + + /** + * Inicia uma nova partida nesta sala. + * @returns null + * @since 19 de outubro de 2020. + */ + novaPartida(){ + this.estado = 'EM_PARTIDA'; + // Popular a fila JFR. + + let tmpJfr = []; // JFR temporária, utilizado para popular a JFR verdadeira. + + for (let x = 0; x < this.clientes.length; x++) + tmpJfr.push(this.clientes[x].apelido); + + for (let x = tmpJfr.length; x > 0; x--){ + // Pegar um jogador aleatório da fila. + let num = Sala.gerarNúmeroAleatório(tmpJfr.length); + this.jfr.push(tmpJfr[num]); + tmpJfr.splice(num, 1); + } + + this.novaRodada(); + } + + /** + * Inicia uma nova rodada nesta sala, escolhendo um jogador aleatório. + * @returns null + * @since 05 de novembro de 2020. + */ + novaRodada(){ + this.lei = []; // Letras escolhidas inexistentes + this.lee = []; // Letras escolhidas existentes + this.ldc = []; // Letras descobertas + this.jpa = null; // Jogador que escolheu a palavra atual (Jogador Palavra Atual) + this.palavra = null; + this.jf = []; + this.tin = 0; + + // Vamos escolher um jogador aleatório. + if (this.jfr.indexOf(this.vezDe) === this.jfr.length - 1) + // Fim da fila JFR atingido. Voltar para o começo. + this.vezDe = null; + + this.vezDe = this.vezDe === undefined || this.vezDe === null ? this.jfr[0] : this.jfr[this.jfr.indexOf(this.vezDe) + 1]; // Escolher o primeiro jogador na fila caso o limite tenha sido atingido, ou escolher o próximo jogador na fila caso contrário. +} + + /** + * Calcula e define a quantidade de letras na palavra atual. + * @since 23 de outubro de 2020. + */ + definirPalavraLetrasQtd(){ + let letrasQtd = 0; + for (let x = 0; x < this.palavra.length; x++) + if (this.palavra[x] !== ' ') + letrasQtd++; + this.palavraLetrasQtd = letrasQtd; + } + + /** + * Deduz se é possível que um jogador descubra a palavra atual baseado na quantidade de letras descobertas da palavra. + * @returns boolean Verdadeiro caso mais de 75% da palavra tenha sido descoberto ou caso apenas falte uma letra para descobrir a palavra inteira. + * @since 23 de outubro de 2020. + */ + possívelDescoberta(){ + let ld = 0; + for (let x = 0; x < this.palavra.length; x++) + if (this.ldc[x] !== undefined) + ld++; + + // Pelo menos 75% da palavra deve ter sido descoberta para retornar o evento de descoberta de palavra. + // Caso a palavra seja pequena, então pelo menos uma letra restante deve estar faltando. + + if(ld / this.palavraLetrasQtd >= 0.75){ + return true; + } else { + // Menos de 75% da palavra foi descoberta. Caso apenas uma letra esteja faltando, então retornar verdadeiro de qualquer maneira. + return this.palavraLetrasQtd - 1 === ld; + } + } + + /** + * Retorna uma lista contendo as posições dos espaços na palavra atual. + * @param String busca O alvo para procurar na palavra atual. + * @returns Array Lista contendo as posições dos espaços na palavra atual. + * @since 22 de outubro de 2020. + */ + procurar(busca){ + let espaços = []; + for (var x = 0; x < this.palavra.length; x++){ + if (this.palavra[x] === busca) + espaços.push(x); + } + return espaços; + } + + /** + * Esta função processa o termo enviado por um jogador. + * @param string termo O termo enviado pelo jogador escolhido. + * @returns "LETRA_INEXISTENTE" caso a letra inserida não exista na palavra atual; "LETRAS_PREENCHIDAS" caso a letra inserida apareça na palavra atual. + * @since 19 de outubro de 2020. + */ + processarTermo(termo){ + termo = termo.toUpperCase(); + let resultadoTermo = { letrasPreenchidas: 0, resultado: null }; + if (termo.length === 1){ + // A palavra escolhida possui a letra enviada? + if (this.palavra.indexOf(termo) === -1) + resultadoTermo.resultado = 'LETRA_INEXISTENTE'; + else { + resultadoTermo.letrasPreenchidas = this.preencherCampos(termo); + resultadoTermo.resultado = 'LETRAS_PREENCHIDAS'; + } + return resultadoTermo; + } else { + let palavraCorreta = this.palavra === termo; + resultadoTermo.resultado = palavraCorreta ? "PALAVRA_CORRETA" : "PALAVRA_INCORRETA"; + if (palavraCorreta){ + for(var x = 0; x < this.palavra.length; x++) + if (this.ldc[x] === undefined) + resultadoTermo.letrasPreenchidas++; + } + } + return resultadoTermo; + } + + /** + * Gera um jogador aleatório. Esta função é utilizada para escolher a vez de alguma pessoa aleatoriamente sem ecolher pessoas que já foram escolhidas. + * @returns string O apelido do jogador escolhido. + * @since 19 de outubro de 2019. + */ + gerarJogadorAleatório(){ + // Se todos os jogadores já tiverem sido escolhidos, então redefinir a fila de jogadores. + if (this.jf.length === this.clientes.length - 1) + this.jf = []; + + var clientesPossíveis = []; + for (var x = 0; x < this.clientes.length; x++){ + var ca = this.clientes[x]; // Cliente atual + if (ca.apelido !== this.jpa // A pessoa que escolheu a palavra não pode ser escolhida. + && this.jf.indexOf(ca.apelido) === -1) { // Jogadores que já foram escolhidos não podem ser escolhidos novamente. + clientesPossíveis.push(ca.apelido); + } + } + var je = clientesPossíveis[Sala.gerarNúmeroAleatório(clientesPossíveis.length)]; // Jogador escolhido + return je; + } + + /** + * Gera um jogador aleatório para uma nova rodada. + * @returns String O apelido do jogador escolhido. + * @since 25 de dezembro de 2020. + */ + gerarJogadorAleatórioRodada(){ + // Se todos os jogadores já tiverem sido escolhidos, então redefinir a fila de jogadores. + if (this.jfr.length === this.clientes.length - 1) + this.jfr = []; + + let clientesPossíveis = []; + for (let x = 0; x < this.clientes.length; x++){ + let ca = this.clientes[x]; // Cliente atual + if (this.jfr.indexOf(ca.apelido) === -1) // Jogadores que já foram escolhidos não podem ser escolhidos novamente. + clientesPossíveis.push(ca.apelido); + } + let je = clientesPossíveis[Sala.gerarNúmeroAleatório(clientesPossíveis.length)]; // Jogador escolhido + return je; + } + + + /** + * Preenche os campos com a letra especificada. + * @param string letra A letra desejada. + * @since 19 de outubro de 2020. + */ + preencherCampos(letra){ + let qtd = 0; // quantidade de campos preenchidos + for (let x = 0; x < this.palavra.length; x++){ + if (this.palavra[x] === letra){ + this.ldc[x] = letra; + qtd++; + } + } + return qtd; + } + /** + * Retorna o vencedor da partida atual caso exista. + * @returns Object O cliente do jogador que venceu a partida ou nulo caso não há um vencedor. + * @since 23 de dezembro de 2020. + */ + receberVencedor(){ + let pv = []; // Lista de possíveis vencedores. Adicionar jogadores que ultrapassaram a meta aqui. Caso mais de um cliente seja adicionado aqui, então conceder vitória ao jogador que tiver a maior quantidade de pontos ou retornar nulo caso houve um empate. + for (let x = 0; x < this.clientes.length; x++) + if (this.clientes[x].pontos >= this.pontuaçãoMeta) + pv.push(this.clientes[x]); + + if (pv.length === 1) + return pv[0]; + else if (pv.length === 0) + return null; + else { + pv.sort(function (a, b){ + return b.pontos - a.pontos; + }); + // Houve um empate? + return pv[0].pontos === pv[1].pontos ? null // Houve um empate; retornar nulo. + : pv[0]; // Não houve um empate. Retornar o cliente que ficou no topo. + } + } + /** + * Checa se o endereço dado está banido nesta sala. + * @param String endereço O endereço remoto do cliente. + * @returns Verdadeiro caso o endereço esteja banido nesta sala, falso caso contrário. + * @since 24 de dezembro de 2020. + */ + banido(endereço){ + for (let x = 0; x < this.banidos.length; x++) + if (this.banidos[x] === endereço) + return true; + return false; + } + + /** + * Gera um número aleatório baseado no argumento providenciado. + * @param int máximo O número máximo desejado. + * @since 15 de outubro de 2020. + */ + static gerarNúmeroAleatório(máximo){ + var rnd = Math.random(); + var num = Math.floor(rnd * máximo); + return num; + } +} + +module.exports = Sala; diff --git a/util.js b/util.js new file mode 100644 index 0000000..b934a3f --- /dev/null +++ b/util.js @@ -0,0 +1,125 @@ +/* + Força: um (livre) clone do jogo Forca + Copyright (C) 2020 luca0N! + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Entre em contato comigo por e-mail via . +*/ + +class Util { + /** + * Alerta todos os clientes de uma sala sobre um evento. + * @param string O ID do evento. + * @param string salaNome A sala desejada. + * @param object extra Os dados extra do evento. + * @since 12 de outubro de 2020, 13:32 (UTC -03:00). + */ + static alertarClientesEvento(evento, salaNome, extra){ + // Pesquisar pela sala desejada. + //var sala = Util.pesquisarSala(salaNome); + // Cancelar alerta caso a sala não seja existente. + if (!(sala instanceof Object)) + throw new Error('Sala deve ser um objeto'); + + for (let x = 0; x < sala.clientes.length; x++){ + let mensagem = '<' + extra.apelido + '> ' + extra.mensagem; + sala.clientes[x].soquete.send(JSON.stringify(new Resposta("EVENTO", "SERVIDOR", evento.substring(7), extra))); + } + } + /** + * Pesquisa por uma sala com o nome desejado e retorna um objeto Sala correspondente. + * @param string sala O nome da sala. + * @returns Sala O objeto da sala. + * @since 12 de outubro de 2020, 19:49 (UTC -03:00). + */ + static pesquisarSala(sala){ + for (let x = 0; x < salas.length; x++) + if (salas[x].sala === sala) + return salas[x]; + } + + /** + * Retorna o objeto de um cliente baseado em seu apelido e sala. + * @param Object sala O objeto da sala. + * @param String apelido O apelido dado pelo jogador. + * @since 23 de dezembro de 2020. + */ + static receberClientePorApelido(sala, apelido){ + for (let x = 0; x < sala.clientes.length; x++) + if (sala.clientes[x].apelido === apelido) + return sala.clientes[x]; + } + + /** + * Filtra a mensagem recebida. + * @param String mensagem A mensagem para filtrar. + * @param Array filtro A lista de palavras filtradas, codificadas em Base64. + * @since 25 de dezembro de 2020. + */ + static filtrarMensagem(mensagem, filtro){ + for (let x = 0; x < filtro.length; x++){ + let filtragem; + let dec = Buffer.from(filtro[x], 'base64').toString(); // Termo Base64 decodificado + let regex = Util.compilarRegex(dec); + mensagem = mensagem.replace(regex, "[CENSURADO]") + } + return mensagem; + } + + /** + * Transforma o padrão providenciado em um RegEx avançado. + * @param String regex O RegEx básico. + * @param String flags As opções para serem passadas para o objeto RegExp. Este parâmetro não é obrigatório, e, quando não é especificado, o padrão é "gi". + * @returns RegExp Um novo objeto RegExp criado baseado no padrão dado. + * @since 28 de dezembro de 2020. + */ + static compilarRegex(regex, flags){ + if (flags === undefined) // caso nenhuma opção tenha sido dada + flags = "gi"; + + let subst = { // possíveis substituições. + "a": "(a|â|ã|á|à|ä|4|@)", + "i": "(i|î|ĩ|í|ì|ï|1|!|l)", + "e": "(e|ê|ẽ|é|è|ë|3)", + "s": "(s|ŝ|ś|5)", + "b": "(b|6|8)", + "t": "(t|ẗ|7)", + "o": "(o|ô|õ|ó|ò|ö|0)", + "u": "(u|û|ũ|ú|ù|ü|v)", + "c": "(c|ĉ|ć|k)" + } + + let letras = []; + let detec = "(\\s|\\.|\\*)*"; // detector de separação de termos + for (let x = 0; x < regex.length; x++){ + letras.push(regex[x]); + if (x != regex.length - 1) + letras.push(detec); + } + + for (let x = 0; x < letras.length; x++) + if (letras[x][0] !== '(' && subst[letras[x]] !== undefined) + letras[x] = subst[letras[x]]; + + let cr = ""; + for (let x = 0; x < letras.length; x ++) + cr += letras[x]; + + return new RegExp(cr, flags); + } +} + +module.exports = Util; +