import fs from 'fs'; import Discord from 'discord.js'; import { LOG_TYPES } from './constants'; import { toUSD } from './helper'; import { DISCORD_SETTINGS, DISCORD_SOURCES, DISCORD_WEIGHTS, PUSH_SUGGESTION_SECONDS, SEND_SUGGESTION_SECONDS, SUGGEST_USING_DELTAS, TEST_MODE, } from './config'; export default class Scraper { constructor(game, logger) { this.readClients = []; this.botClient = null; this.players = []; this.question = null; this.collect = false; this.googleWeightsPosted = false; this.timer = null; this.ticker = 1; this.logger = logger; this.game = game; this.questionNumber = 0; this.questionsRight = 0; this.pusher = null; this.searcher = null; this.answers = []; } async connect() { DISCORD_SETTINGS.SCRAPER_TOKENS.map(token => { const scraperClient = new Discord.Client(); this.readClients.push(scraperClient); scraperClient.on('ready', () => this.logger.log(`Scraper running as ${scraperClient.user.tag}!`)); scraperClient.on('message', msg => this.collect && this.scrapeContents(msg)); scraperClient.login(token); }); if (DISCORD_SETTINGS.PUBLISH_TOKEN) { this.botClient = new Discord.Client(); this.botClient.on('ready', this.handleConnect.bind(this)); this.botClient.login(DISCORD_SETTINGS.PUBLISH_TOKEN); } } close() { const { questionsRight, questionNumber } = this; if (questionNumber > 0) { const accuracy = Math.round(questionsRight / questionNumber * 10000) / 100; this.broadcastMessage({ embed: { color: 0xd35400, title: 'Game Finished', description: `TAS.js got ${questionsRight} questions right and ${questionNumber - questionsRight} questions wrong for an accuracy of ${accuracy}%!`, }, }); } } handleConnect() { this.logger.log(`Discord bot logged in!`); const prize = toUSD(this.game.prize); this.broadcastMessage({ embed: { color: 0x2ecc71, title: `TAS.js Connected`, description: `Game is live and about to begin! Tonight\'s game is for ${prize}!`, }, }); } broadcastMessage(embeddedMessage, callback = () => {}) { const { PUBLISH_CHANNELS } = DISCORD_SETTINGS; if (this.botClient) { PUBLISH_CHANNELS.forEach(channelId => { const channel = this.botClient.channels.get(channelId); if (channel) { channel.send(embeddedMessage).then(msg => callback(msg)); } }); } } startQuestion(question) { this.question = { ...question, suggestedPicks: [true, true, true], weights: [0, 0, 0], lastNormalizedWeights: [0, 0, 0], lastDeltas: [0, 0, 0], }; this.questionNumber = question.questionNumber; this.collect = true; this.ticker = 1; this.googleWeightsPosted = false; if (this.searcher && !TEST_MODE) { this.searcher.search(question, weights => this.question.searchWeights = weights); } this.timer = setInterval(this.update.bind(this), 1000); this.update(); this.showQuestion(question); } endQuestion(question) { this.collect = false; if (this.timer) clearInterval(this.timer); } update() { if (!this.collect) return; const deltas = this.getDeltas(); const normalizeWeights = this.normalizeWeights(); const searchWeights = this.question.searchWeights; if (searchWeights && !this.googleWeightsPosted) { const searchOutput = `[${searchWeights[0]}%, ${searchWeights[1]}%, ${searchWeights[2]}%]`; this.broadcastMessage(`**Google Weights**\`\`\`${searchOutput}\`\`\``); if (this.pusher) { this.pusher.sendAll(`HQ Question ${this.questionNumber}`, `Google Weights ${searchOutput}`); } this.googleWeightsPosted = true; } if (this.ticker >= SEND_SUGGESTION_SECONDS) { const weights = `[${normalizeWeights[0]}%, ${normalizeWeights[1]}%, ${normalizeWeights[2]}%]`; const delta = `[${deltas[0]}, ${deltas[1]}, ${deltas[2]}]`; const suggestion = this.getSuggestion(); this.broadcastMessage(`**Normalized Weights + Threshold Deltas**\`\`\`${weights}\t\t${delta}\t\t${suggestion}\`\`\``); if (this.pusher && this.ticker === PUSH_SUGGESTION_SECONDS) { this.pusher.sendAll(`HQ Question ${this.questionNumber}`, `${suggestion} ${weights}`); } } this.ticker++; this.question.lastNormalizedWeights = normalizeWeights; } showQuestion(question) { const { questionNumber, questionId, question: text, answers } = question; const query = encodeURI(`${text} "${answers[0].text}"|"${answers[1].text}"|"${answers[2].text}"`); this.broadcastMessage({ embed: { color: 0x3498db, title: `Question #${questionNumber}`, url: `https://www.google.com/search?q=${query}&oq=${query}`, description: text, fields: [ { name: 'Choice 1', value: answers[0].text, }, { name: 'Choice 2', value: answers[1].text, }, { name: 'Choice 3', value: answers[2].text, }, ], }, }); } showSummary(questionSummary, answerIndex) { const { answerCounts, advancingPlayersCount, eliminatedPlayersCount, question } = questionSummary; const { answerId } = answerCounts[answerIndex]; const percentAdvancing = Math.round(advancingPlayersCount / (advancingPlayersCount + eliminatedPlayersCount) * 100); const savagePercentage = 100 - Math.round(percentAdvancing); let savage = false; if (percentAdvancing <= 33) { savage = true; } this.scoreUsers(answerIndex, savage, savagePercentage); this.saveContents(); let choice1 = 'Choice 1'; let choice2 = 'Choice 2'; let choice3 = 'Choice 3'; if (answerCounts[0].answerId === answerId) choice1 = `${choice1} :ballot_box_with_check:`; if (answerCounts[1].answerId === answerId) choice2 = `${choice2} :ballot_box_with_check:`; if (answerCounts[2].answerId === answerId) choice3 = `${choice3} :ballot_box_with_check:`; const correct = this.question.suggestedPicks[answerIndex]; if (correct) { this.questionsRight++; } this.broadcastMessage({ embed: { color: 0x3498db, title: `Summary`, description: question, fields: [ { name: choice1, value: answerCounts[0].text || answerCounts[0].answer || 'undefined', }, { name: choice2, value: answerCounts[1].text || answerCounts[1].answer || 'undefined', }, { name: choice3, value: answerCounts[2].text || answerCounts[2].answer || 'undefined', }, ], footer: { text: `${eliminatedPlayersCount} players eliminated / ${advancingPlayersCount} players advancing`, }, }, }, msg => { if (correct) { msg.react('👍'); } else { msg.react('👎'); } if (savage) { msg.react('💯'); } }); } scrapeContents(msg) { const { channel, guild } = msg; if (!guild || !channel) return; const validServer = DISCORD_SOURCES.find(({ id }) => id === guild.id); if (!validServer) return; const validChannel = validServer.channels.find(({ id }) => id === channel.id); if (!validChannel) return; if (validChannel.isTestChannel && !TEST_MODE) return; const { author, content } = msg; const { id: userId, username } = author; let cleanedMessage = content.trim().replace(' ', '').replace('\'', '').toLowerCase(); let cleanedUserId = userId; let cleanedUsername = username; /*try { jsonMessage = JSON.parse(content); cleanedMessage = jsonMessage.content.trim().replace(' ', '').replace('\'', '').toLowerCase(); cleanedUserId = jsonMessage.id; const user = this.readClient.users.get(jsonMessage.id); cleanedUsername = user ? user.username : 'Unknown Elite User'; } catch (e) {};*/ const matchedMessage = /((w|not|its)?([123])(\?|apg|apb)?)/g.exec(cleanedMessage); if (!matchedMessage || matchedMessage.length === 0) return; const fullAnswer = matchedMessage[0]; const answer = { pre: matchedMessage[2], number: matchedMessage[3], post: matchedMessage[4], }; let weight = this.weightAnswer(cleanedUserId, answer); weight = weight * validChannel.weightModifier; switch (this.ticker) { case 5: weight = weight * 2; break; case 6: weight = weight * 3; break; case 7: weight = weight * 3; break; case 8: weight = weight * 5; break; case 9: weight = weight * 10; break; } this.logAnswer(cleanedUserId, cleanedUsername, answer, weight); } logAnswer(userId, username, answer, weight) { const userAnswer = { userId, username, answer: { ...answer, }, }; const { number } = answer; if (['1','2','3'].includes(number)) { this.question.weights[number - 1] += weight; } this.answers.push(userAnswer); } scoreUsers(answerIndex, savage, savagePercentage) { const correctAnswer = answerIndex + 1; this.answers = this.answers.map(userAnswer => { const { answer, userId, username } = userAnswer; const { pre, number } = answer; const negated = pre ? pre === 'not' : false; let correct = false; if (negated) { correct = number !== correctAnswer.toString(); } else { correct = number === correctAnswer.toString(); } let user = this.players.find(player => player.userId === userId); if (!user) { this.players.push({ userId, username, score: 1, }); } user = this.players.find(player => player.userId === userId); if (correct) { user.score += savage ? savagePercentage : 1; } else { user.score -= 5; } return { ...userAnswer, correct, } }); } weightAnswer(userId, answer) { let weight = 0; const { pre, post } = answer; const player = this.players.find(player => player.userId === userId); if (player) { if (parseInt(player.score) < 0) return 0; weight += parseInt(player.score); } if (pre) { weight += DISCORD_WEIGHTS[pre] || 0; } if (post) { weight += DISCORD_WEIGHTS[post] || 0; } if (!pre && !post) { weight += DISCORD_WEIGHTS['standard']; } return weight; } normalizeWeights(numberOfPlayers = 100) { let fullWeights = this.question.weights; if (this.question.searchWeights && this.question.searchWeights.length === 3) { fullWeights = this.question.weights.map((weight, index) => { return weight + this.question.searchWeights[index]; }); } let nonZeroWeights = fullWeights.filter(weight => weight >= 0); if (nonZeroWeights.length === 0) { nonZeroWeights = [0, 0, 0]; } let totalWeight = nonZeroWeights .reduce((accumulator, currentValue) => accumulator + currentValue); let normalized = []; normalized = fullWeights.map(weight => { if (totalWeight === 0) return Math.round((1 / 3) * numberOfPlayers); const zeroNegatives = weight < 0 ? 0 : weight; return Math.round((zeroNegatives / totalWeight) * numberOfPlayers); }); return normalized; } getDeltas() { const weights = this.normalizeWeights(); const lastWeights = this.question.lastNormalizedWeights; const deltas = weights.map((weight, index) => { return weight - lastWeights[index]; }); if (deltas[0] > 0 || deltas[1] > 0 || deltas[2] > 0) { this.question.lastDeltas = deltas; } return deltas; } getSuggestion() { if (this.ticker < 5) { return 'Wait'; } const { searchWeights } = this.question; const deltas = this.question.lastDeltas; const normalizedWeights = this.normalizeWeights(); let suggestion; if (SUGGEST_USING_DELTAS && (deltas[0] !== 0 || deltas[1] !== 0 ||deltas[2] !== 0)) { const highestValueIndex = deltas.reduce((highestIndex, delta, index) => { if (delta > deltas[highestIndex]) { return index; } return highestIndex; }, 0); switch (highestValueIndex) { case 0: suggestion = 'Choose 1'; break; case 1: suggestion = 'Choose 2'; break; case 2: suggestion = 'Choose 3'; break; } this.question.suggestedPicks = [ highestValueIndex === 0, highestValueIndex === 1, highestValueIndex === 2, ]; return suggestion; } const majority = normalizedWeights.find(weight => weight > 66); if (majority) { switch (normalizedWeights.indexOf(majority)) { case 0: suggestion = 'Choose 1'; break; case 1: suggestion = 'Choose 2'; break; case 2: suggestion = 'Choose 3'; break; } this.question.suggestedPicks = [ normalizedWeights.indexOf(majority) === 0, normalizedWeights.indexOf(majority) === 1, normalizedWeights.indexOf(majority) === 2, ]; return suggestion; } let splitChoices = normalizedWeights.filter(weight => weight >= 20); if (splitChoices.length === 3 || splitChoices.length === 0) { this.question.suggestedPicks = [true, true, true]; suggestion = 'Split 1 2 3'; if (searchWeights) { const searchPick = searchWeights.indexOf(Math.max(...searchWeights)); suggestion += `\t\tLikely ${searchPick + 1}`; } return suggestion; } splitChoices = normalizedWeights.map((weight, index) => { if (weight >= 20) return index; return null; }); const nullChoices = splitChoices.filter(choice => choice === null); if (nullChoices.length < 2) { suggestion = 'Split'; } else { suggestion = 'Choose'; } this.question.suggestedPicks = [false, false, false]; let suggestionSearchWeights = []; splitChoices.forEach(choiceIndex => { if (choiceIndex !== null) { suggestionSearchWeights.push(searchWeights[choiceIndex]); suggestion += ` ${choiceIndex + 1}`; this.question.suggestedPicks[choiceIndex] = true; } }); if (suggestion.toLowerCase().includes('split')) { if (searchWeights) { const searchPick = searchWeights.indexOf(Math.max(...suggestionSearchWeights)); suggestion += `\t\tLikely ${searchPick + 1}`; } } return suggestion; } getSuggestionArray() { return this.question.suggestedPicks; } saveContents() { fs.readFile('playerAnswers.json', 'utf8', (err, data) => { if (err) { this.logger.logSilent('Error reading playerAnswers for writing: ', err); } else { const playerAnswers = JSON.parse(data); this.players.forEach(player => { const playerAnswersPlayer = playerAnswers.find(({ userId }) => userId === player.userId); if (playerAnswersPlayer) { playerAnswersPlayer.score += player.score; } else { playerAnswers.push(player); } }); const playerAnswersJSON = JSON.stringify(playerAnswers, null, 2); fs.writeFile('playerAnswers.json', playerAnswersJSON, 'utf8', (err, data) => { if (err) { this.logger.logSilent('Error saving playerAnswers: ', err); } this.answers = []; }); } }); } }