556 lines
15 KiB
JavaScript
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 = [];
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|