UNPKG

@andsfonseca/term-cli

Version:

O clássico jogo de adivinhação de palavras agora para linha de comando. Uma nova palavra a cada dia!

413 lines (412 loc) 16.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Game = void 0; const os_1 = require("os"); const palavras_pt_br_1 = require("@andsfonseca/palavras-pt-br"); const view_1 = require("./view"); const chalk_1 = __importDefault(require("chalk")); const discord_rpc_1 = require("discord-rpc"); const clipboardy = require('clipboardy'); const storage = require('node-persist'); /** * Instância do Term-cli, contém a lógica do jogo necessária para seu funcionamento. */ class Game { //#endregion /** * Carrega a Base de dados da biblioteca @andsfonseca/palavras-pt-br. */ static initializeDatabase() { palavras_pt_br_1.Word.library = [...palavras_pt_br_1.BRISPELL, ...palavras_pt_br_1.UNVERSEDV2]; this.words = palavras_pt_br_1.Word.getAllWords(this.WORD_SIZE, false, false, false, false); palavras_pt_br_1.Word.library = this.words; this.wordsWithoutAccents = this.words.map(a => a.normalize("NFD").replace(/[\u0300-\u036f]/g, "")); palavras_pt_br_1.Word.library = this.wordsWithoutAccents; if (this.isARandomWord) { this.dailyWord = palavras_pt_br_1.Word.getRandomWord(); } else { this.dailyWord = palavras_pt_br_1.Word.getDailyWord(); } } /** * Inicializa o teclado com as letra do Alfabeto. */ static initalizeKeyboard() { for (let i = 0; i < this.ALLOWED_LETTERS.length; i++) { this.keyboard[this.ALLOWED_LETTERS[i]] = this.DEFAULT_TEXT; } } /** * Loop do Jogo, responsável por controlar o stdin e esperar por uma tecla do jogador. */ static loop() { let stdin = process.stdin; stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8'); stdin.on('data', (key) => { if (!this.isOver) this.onDetectAnyKeyDuringGame(key); else { process.exit(); } }); } /** * Ao detectar uma tecla durante o jogo, contém o pipeline do que fazer quando jogador presssiona uma letra, enter, backspace e delete. * @param key Tecla usada pelo jogador */ static onDetectAnyKeyDuringGame(key) { let warning = ""; let win = false; let keyAsString = key.toString(); // ctrl-c -> Sair do Jogo //@ts-ignore if (key === '\u0003') { process.exit(); } // backspace ou delete -> Apagar Palavra //@ts-ignore else if (key === '\b' || key == '\x1B[3~') { this.currentLetters.pop(); } //Se Enter -> Próximo estado //@ts-ignore else if (key === '\r') { let word = this.currentLetters.join('').toLowerCase(); if (this.currentLetters.length != 5) { warning = "Letra(s) faltando!"; } else if (!palavras_pt_br_1.Word.checkValid(word)) { warning = "Esta palavra não existe!"; } else { let validations = palavras_pt_br_1.Word.wordleValidator(this.dailyWord, word); let withAccent = this.words[this.wordsWithoutAccents.indexOf(word)]; this.triedWords.push(withAccent.toUpperCase().split("")); this.triedWordsValidated.push(validations); this.currentAttempt++; //Estado de Win if (validations.every(v => v.exact === true)) { this.isOver = true; win = true; } //Estado de Perda else if (this.currentAttempt == this.ATTEMPTS) { this.isOver = true; } else { this.currentLetters = []; } this.UpdateRichPresence(this.currentAttempt + 1, this.generateBoard([validations], false), this.isOver); view_1.View.renderKeyboard(this.keyboard, validations, this.WORD_SIZE, false); } } //Se ainda pode escrever faça else if (this.currentLetters.length < this.WORD_SIZE && this.ALLOWED_LETTERS.indexOf(keyAsString.toUpperCase()) > -1) { this.currentLetters.push(keyAsString.toUpperCase()); } else { return; } this.loadBoard(warning, true, !this.isOver); if (this.isOver) { if (win) this.currentAttempt--; this.final(win, this.currentAttempt).then(() => { console.log("Clique em qualquer tecla para sair..."); }); } } /** * Carrega o tabuleiro do jogo. * @param additionalWarning Aviso adicional dado pelo jogador. * @param clearBeforeRender Informa se deve apagar o tabuleiro antes. * @param renderCleanTry Informa se deve renderizar a ultima tentativa do jogador. */ static loadBoard(additionalWarning = "", clearBeforeRender = false, renderCleanTry = true) { if (clearBeforeRender) view_1.View.clearLine(this.boardSize); this.boardSize = 4; view_1.View.renderWarning("Tentativas restantes: " + (this.ATTEMPTS - this.currentAttempt), 1); for (let i = 0; i < this.currentAttempt; i++, this.boardSize++) { view_1.View.renderStatus(this.triedWords[i], this.triedWordsValidated[i]); } if (renderCleanTry) { view_1.View.renderStatus(this.currentLetters); this.boardSize++; } view_1.View.renderWarning(additionalWarning); view_1.View.renderKeyboard(this.keyboard); } /** * Inicializa o jogo */ static start(isARandomWord) { this.isARandomWord = isARandomWord; this.EnableRichPresence().then((found) => { }); //Visualização Inicial view_1.View.clear(); view_1.View.renderTitle(this.title); this.loadTips(); //Carrega a Base de Dados this.initializeDatabase(); //Cria o teclado this.initalizeKeyboard(); //Carrega o tabuleiro this.loadBoard(); //Game Loop this.loop(); } /** * Responsável por mostra a tela de final do Jogo ao usuário e salvar suas estatísticas * @param win Se ganhou * @param position Posição da tentativa */ static final(win, position = 6) { return __awaiter(this, void 0, void 0, function* () { yield storage.init({ dir: os_1.homedir + "/.term-cli" }); let count = yield storage.getItem('count'); if (count == undefined) { yield this.resetStats(); count = 0; } //Recupera as variavéis let wins = yield storage.getItem('wins'); let stats = yield storage.getItem('stats'); let lastGameString = yield storage.getItem('lastGame'); let lastWinString = yield storage.getItem('lastWin'); let lastGameDate = (lastGameString) ? new Date(new Date(lastGameString).toLocaleString("en-US", { timeZone: 'America/Recife' })) : new Date(2020, 0, 1); let lastWinDate = (lastWinString) ? new Date(lastWinString) : new Date(2020, 0, 1); //Condições para salvar as estatistícas //1. O ultimo jogo não deve ter sido jogado no mesmo dia //2. Não deve ser uma palavra aleatória let currentDate = new Date(); if ((lastGameDate == undefined || lastGameDate.getDate() != currentDate.getDate() || lastGameDate.getMonth() != currentDate.getMonth() || lastGameDate.getFullYear() != currentDate.getFullYear()) && !this.isARandomWord) { lastGameDate = currentDate; yield storage.setItem("lastGame", lastGameDate.toDateString()); if (win) { wins++; lastWinDate = lastGameDate; yield storage.setItem("lastWin", lastWinDate.toDateString()); } count++; stats[position]++; yield storage.setItem("count", count); yield storage.setItem("wins", wins); yield storage.setItem("stats", stats); } let auxLastWin = ""; if (lastWinString || win) { auxLastWin = lastWinDate.toLocaleDateString('pt-Br', { dateStyle: 'short' }); } let wordWithAccents = this.words[this.wordsWithoutAccents.indexOf(this.dailyWord)]; yield this.textToClipboard("Joguei term-cli! " + (position == 6 ? "❌" : (position + 1) + "/6"), this.generateBoard(this.triedWordsValidated)); view_1.View.renderStaticts(count, wins, stats, lastGameDate.toLocaleDateString('pt-Br', { dateStyle: 'short' }), auxLastWin, wordWithAccents); view_1.View.renderWarning("Estatísticas do jogo copiadas para a área de transferência"); }); } /** * Dada uma sequência validada, retorna uma representação em emoction (🟩,🟨,🟥) * @param validation Validação da palavra * @returns Representação da validação em emoction */ static getBoardEmoction(validation) { if (validation.exact) return "🟩"; if (validation.contains) return "🟨"; return "🟥"; } /** * Gera um tabuleiro em forma de emoction * @param validations Validações de uma palavra * @param includeSpaceOnFinal Informa se deve colocar espaço no final * @returns String com o tabuleiro */ static generateBoard(validations, includeSpaceOnFinal = true) { let s = ""; for (let i = 0, len = validations.length; i < len; i++) { for (let j = 0; j < 5; j++) { s += this.getBoardEmoction(validations[i][j]); } if (i == len - 1 && !includeSpaceOnFinal) continue; s += "\n"; } return s; } /** * Copia um texto para a área de transferência * @param message Mensagem a ser copiado para a área de transferência * @param board Tabuleiro */ static textToClipboard(message, board) { return __awaiter(this, void 0, void 0, function* () { let s = message + "\n\n" + board + "\n\nInstale também em: https://www.npmjs.com/package/@andsfonseca/term-cli"; yield clipboardy.write(s); }); } /** * Reinicializa as estatísticas do jogo * @param store Serviço de armazenamento */ static resetStats(store = undefined) { return __awaiter(this, void 0, void 0, function* () { if (store == undefined) { yield storage.init({ dir: os_1.homedir + "/.term-cli" }); store = storage; } yield store.setItem("count", 0); yield store.setItem("wins", 0); yield store.setItem("stats", [0, 0, 0, 0, 0, 0, 0]); yield store.setItem("lastGame", undefined); yield store.setItem("lastWin", undefined); }); } /** * Carrega as dicas do jogo */ static loadTips() { view_1.View.renderSection("O objetivo é descobrir qual é a palavra correta em apenas 6 tentativas.", false); view_1.View.renderSection("A cada letra digitada que faz parte da palavra correta dicas serão exibidas, de acordo com as cores das letras, veja abaixo:", false); view_1.View.renderStatus(["P", "A", "L", "C", "O"], [{ exact: false, contains: false, word: "" }, { exact: true, contains: false, word: "A" }, { exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "" }]); view_1.View.renderSection("A letra " + chalk_1.default.green("A") + " está na posição correta.", false); view_1.View.renderStatus(["C", "E", "S", "T", "O"], [{ exact: false, contains: true, word: "C" }, { exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "" }]); view_1.View.renderSection("A letra " + chalk_1.default.yellow("C") + " contém na palavra, mas em outra posição.", false); view_1.View.renderStatus(["L", "E", "I", "T", "E"], [{ exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "" }, { exact: false, contains: false, word: "T" }, { exact: false, contains: false, word: "" }]); view_1.View.renderSection("A letra " + chalk_1.default.red("T") + " não contém na palavra.", false); view_1.View.renderSection("Os acentos não são considerados nas dicas."); } /** * Habilita o Discord Rich Presence */ static EnableRichPresence() { return __awaiter(this, void 0, void 0, function* () { this.discordClient = new discord_rpc_1.Client({ transport: "ipc" }); this.discordCurrentActivity = { details: "Tentando a palavra diária [1/6]", state: "Pensando...", assets: { large_image: "term-cli", large_text: "term-cli", }, timestamps: { start: Date.now() }, instance: true }; this.discordClient.on("ready", () => { this.discordClientIsReady = true; this.discordClient.request("SET_ACTIVITY", { pid: process.pid, activity: this.discordCurrentActivity }); }); try { yield this.discordClient.login({ clientId: "943272235521675306" }); return true; } catch (e) { return false; } }); } /** * Atualiza a atividade do Discord Rich Presence */ static UpdateRichPresence(attempt, board, isOver = false) { if (this.discordClientIsReady) { if (!isOver) this.discordCurrentActivity.details = "Tentando a palavra diária [" + attempt + "/6]"; else this.discordCurrentActivity.details = "Vendo as estatísticas..."; this.discordCurrentActivity.state = board, this.discordClient.request("SET_ACTIVITY", { pid: process.pid, activity: this.discordCurrentActivity }); } } } exports.Game = Game; //#region Constantes /** * Tamanho da Palavra. */ Game.WORD_SIZE = 5; /** * Número de tentativas. */ Game.ATTEMPTS = 6; /** * Letras permitidas. */ Game.ALLOWED_LETTERS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; /** * Função que recebe uma string e retorna a mesma string. * @param s String a ser recebida. * @returns Retorna a string recebida. */ Game.DEFAULT_TEXT = (s) => { return s; }; //#endregion //#region Atributos privados /** * Todas as palavras usadas no jogo. */ Game.words = []; /** * Todas as palavras usadas no jogo, sem acentos. */ Game.wordsWithoutAccents = []; /** * Palavra diária. */ Game.dailyWord = ""; /** * Tentativa atual do jogador. */ Game.currentAttempt = 0; /** * Palavras testadas pelo jogador. */ Game.triedWords = []; /** * Validação das palabras do jogador. */ Game.triedWordsValidated = []; /** * Letras escolhidas pelo jogador em uma rodada. */ Game.currentLetters = []; /** *Contém um dicionário de teclas usados, com um função de texto para cada valor. */ Game.keyboard = {}; /** * Tamanho do tabuleiro. */ Game.boardSize = 5; /** * Informa se é uma palavra aleatória. */ Game.isARandomWord = false; /** * Informa se o jogo acabou. */ Game.isOver = false; //#endregion //#region Atributos públicos /** * Título do Jogo. */ Game.title = "Game";