Files
tasJS/Scraper.js
2018-07-20 15:26:41 -04:00

556 lines
15 KiB
JavaScript

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 = [];
});
}
});
}
}