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

533 lines
18 KiB
JavaScript

import moment from 'moment';
import read from 'read';
import request from 'async-request';
import WebSocket from 'ws';
import Player from './Player';
import Pusher from './Pusher';
import Searcher from './Searcher';
import Scraper from './Scraper';
import { accounts } from './accounts.json';
import { LOG_TYPES } from './constants';
import Logger, { toUSD } from './helper';
const { log } = console;
import {
AUTOPLAY,
DISCRETE_MODE,
ENABLE_DISCORD,
ENABLE_GOOGLE,
ENABLE_PUSH,
MASTER_BEARER_TOKEN,
MAX_PERCENTAGE_OF_REMAINING,
MAX_TEST_PLAYERS,
MAX_TEST_QUESTIONS,
MIN_AMOUNT_THRESHOLD,
NEXT_GAME_TIME_BUFFER,
PREDICT_SAVAGE,
SAFE_WINNINGS_MODE,
SERVER_URL,
SPLIT_LAST_ROUND,
SPLIT_MODE,
SPLIT_MODES,
STAGGER_ANSWERS,
STAGGER_AT_INTERVAL,
TEST_MODE,
TEST_SERVER,
TIME_EXPIRE_BUFFER,
USE_EXTRA_LIFE_MIN_PRIZE,
} from './config';
const clearLine = () => {
process.stdout.clearLine();
process.stdout.cursorTo(0);
};
const writeLine = (msg) => process.stdout.write(msg);
export default class Trivia {
constructor() {
this.game = null;
this.socket = null;
this.players = [];
this.masterPingTimeout = null;
this.masterPingInterval = null;
this.logger = null;
this.scraper = null;
this.pusher = null;
this.searcher = null;
}
async run() {
log('Checking for game schedule...');
await this.getGame();
if (this.game.active) {
const testFlag = TEST_MODE ? '-TEST' : '';
const filename = moment(this.game.startTime).format('YYYY-MM-DD-HHmmss');
this.logger = new Logger(`game-${filename}${testFlag}.txt`);
await this.logger.open();
this.logger.log(`Game live at: ${this.game.broadcast.socketUrl}`);
this.players = accounts.map(account => new Player(account));
//this.players = this.players.slice(Math.max(this.players.length - 124, 0));
this.players = this.players.slice(0, 124);
this.players.forEach(player => player.logger = this.logger);
if (TEST_MODE) {
this.players = this.players.slice(0, MAX_TEST_PLAYERS);
}
this.connect();
} else {
log('Game is not live!');
this.scheduleGame(this.game.nextShowTime);
}
}
scheduleGame(nextShowTime) {
const now = new Date().getTime();
let timeDeltaMS = new Date(nextShowTime).getTime() - now;
const countdown = setInterval(() => {
timeDeltaMS -= 1000;
clearLine();
this.showTimeRemaining(timeDeltaMS)
}, 1000);
setTimeout(() => {
clearInterval(countdown);
clearLine();
this.run();
}, timeDeltaMS);
}
showTimeRemaining(timeDeltaMS) {
const nextShowTime = moment.duration(timeDeltaMS);
const hours = nextShowTime.hours();
const minutes = nextShowTime.minutes();
const seconds = nextShowTime.seconds();
writeLine(`Next game is in ${hours} hours, ${minutes} minutes, and ${seconds} seconds for ${this.game.nextShowPrize}!`);
}
ping() {
if (!this.game.active) return;
if (!this.socket) return;
try {
this.socket.ping();
this.masterPingTimeout = setTimeout(() => {
this.socket.terminate();
this.logger.log('Master connection closed. Attempting to reconnect...', LOG_TYPES.ERROR);
this.connect();
}, 2000);
} catch (error) {
this.logger.log('Master connection closed. Attempting to reconnect...', LOG_TYPES.ERROR);
}
}
pong() {
clearTimeout(this.masterPingTimeout);
}
async getGame() {
if (TEST_MODE) {
log('Test mode is enabled. Using test server...');
const getRandomId = () => Math.floor(10000 + Math.random() * 90000);
this.game = {
active: true,
broadcast: {
broadcastId: getRandomId(),
socketUrl: TEST_SERVER,
},
prize: 0,
prizePerPlayer: 0,
startTime: new Date().toJSON().toString(),
};
return;
}
try {
const response = await request(`https://${SERVER_URL}/shows/now`, {
headers: {
Authorization: `Bearer ${MASTER_BEARER_TOKEN}`,
},
});
this.game = JSON.parse(response.body);
} catch (error) {
log(`There was an error getting the game: ${error}`);
}
}
async connect() {
if (!this.game.active) return;
const { broadcast } = this.game;
const { socketUrl } = broadcast;
const options = {
headers: {
Authorization: `Bearer ${MASTER_BEARER_TOKEN}`,
},
};
this.socket = new WebSocket(socketUrl, options);
this.socket.on('error', err => this.logger.log(`An error occurred: ${err}`, LOG_TYPES.ERROR));
this.socket.on('pong', this.pong.bind(this));
this.socket.on('open', this.handleOpen.bind(this));
this.socket.on('close', this.handleClose.bind(this));
this.socket.on('message', this.handleMessage.bind(this));
if (ENABLE_DISCORD) {
this.scraper = new Scraper(this.game, this.logger);
this.scraper.connect();
}
if (ENABLE_PUSH) {
this.pusher = new Pusher(this.logger);
this.pusher.connect();
this.pusher.send('HQ Game Started', `${this.players.length} of your players are playing for ${toUSD(this.game.prize)} tonight!`);
this.pusher.sendAll('HQ Game Started', `Tonight's game is for ${toUSD(this.game.prize)}!`);
if (ENABLE_DISCORD) {
this.scraper.pusher = this.pusher;
}
}
if (ENABLE_GOOGLE) {
this.searcher = new Searcher(this.logger);
if (ENABLE_DISCORD) {
this.scraper.searcher = this.searcher;
}
}
}
async handleOpen() {
const { broadcast } = this.game;
this.masterPingInterval = setInterval(this.ping.bind(this), 10000);
this.logger.log('Master connected to websocket!', LOG_TYPES.SUCCESS);
await this.players.forEach(player => player.connect(this, broadcast));
await this.play();
}
handleClose() {
this.socket = null;
clearInterval(this.masterPingInterval);
clearTimeout(this.masterPingTimeout);
if (this.scraper) {
this.scraper.close();
}
this.logger.log('\nGame is finished!\n');
if (AUTOPLAY) {
const waitTime = moment.duration(NEXT_GAME_TIME_BUFFER).humanize();
this.logger.log(`Waiting ${waitTime} before checking for next game!`);
setTimeout(this.run.bind(this), NEXT_GAME_TIME_BUFFER);
} else {
this.logger.log('Thanks for playing!');
}
this.logger.close();
}
async handleMessage(data) {
const parsed = JSON.parse(data);
switch (parsed.type) {
case 'broadcastStats':
this.logger.logSilent(`Broadcast status: ${data}`);
break;
case 'gameStatus':
this.game.questionCount = TEST_MODE ? MAX_TEST_QUESTIONS : parsed.questionCount;
this.game.savageCount = 0;
this.logger.logSilent(`Game Status: ${data}`);
break;
case 'question':
this.game.questionNumber = parsed.questionNumber;
this.showQuestion(parsed);
if (this.scraper) {
this.scraper.startQuestion(parsed);
}
this.logger.logSilent(`Question: ${data}`);
break;
case 'questionFinished':
this.logger.logSilent(`Question Finished: ${data}`);
if (this.game.questionNumber >= this.game.questionCount) {
this.endGame();
}
break;
case 'questionClosed':
this.logger.logSilent(`Question Closed: ${data}`);
if (this.scraper) {
this.scraper.endQuestion(parsed);
}
break;
case 'questionSummary':
this.checkAnswers(parsed);
this.logger.logSilent(`Question Summary: ${data}`);
break;
}
}
async showQuestion({ answers, category, timeLeftMs, question, questionId, questionNumber }) {
this.logger.log(`\n${'='.repeat(50)}\n`);
this.logger.log(`Question ${questionNumber} of ${this.game.questionCount}:`);
this.logger.log(`\n${question}`);
this.logger.log(`\n1) ${answers[0].text}`);
this.logger.log(`2) ${answers[1].text}`);
this.logger.log(`3) ${answers[2].text}\n`);
if (this.players.length > 0) {
let players = this.players.slice(0);
if (PREDICT_SAVAGE) {
let possiblySavage = false;
/*if (this.game.savageCount === 0 && [4,5].includes(questionNumber)) {
possiblySavage = true;
}*/
if ([0,1].includes(this.game.savageCount) && [8,9,10].includes(questionNumber)) {
possiblySavage = true;
}
if (possiblySavage) {
const savageQuotient = Math.ceil((this.game.questionCount + 1 - questionNumber) / 12 * 10);
const reservePlayerNumber = Math.floor(players.length / savageQuotient);
this.logger.log(`Possible Savage Question: reserving ${reservePlayerNumber} players to split.`);
const reservePlayers = players.splice(0, reservePlayerNumber);
setTimeout(() => {
this.sendAnswers.call(this, questionId, answers, [1, 2, 3], reservePlayers);
}, timeLeftMs - TIME_EXPIRE_BUFFER);
}
}
if (STAGGER_ANSWERS) {
this.logger.logSilent('Stagger Answers is enabled. No manual input allowed.');
let ticker = 1;
let remainingPlayers = players.slice(0);
const timeLeftSeconds = timeLeftMs / 1000;
const staggerTime = timeLeftSeconds - STAGGER_AT_INTERVAL - (TIME_EXPIRE_BUFFER / 1000);
const staggerPlayersAmount = Math.floor(remainingPlayers.length / staggerTime);
this.logger.logSilent(`Stagger Variables: "${timeLeftMs} time left MS" "${staggerTime} stagger time" "${staggerPlayersAmount} stagger players amount" "${remainingPlayers.length} remaining players"`);
const staggerInterval = setInterval(() => {
ticker++;
this.logger.logSilent(`Ticker ${ticker} - Stagger At Interval ${STAGGER_AT_INTERVAL}`);
if (ticker >= STAGGER_AT_INTERVAL) {
if (remainingPlayers.length > staggerPlayersAmount) {
const playersToSend = remainingPlayers.splice(0, staggerPlayersAmount);
this.logger.logSilent(`${playersToSend.length} playersToSend`);
this.autoSend.call(this, questionId, answers, playersToSend);
}
}
}, 1000);
const stopSending = setTimeout(() => {
clearInterval(staggerInterval);
this.logger.logSilent(`Time expired. Sending remaining staggered players ${remainingPlayers.length}!`);
this.autoSend.call(this, questionId, answers, remainingPlayers);
}, timeLeftMs - TIME_EXPIRE_BUFFER);
} else {
let ticker = 1;
const staggerInterval = setInterval(() => {
ticker++;
}, 1000);
let previousChoices = null;
read({
prompt: 'My Answer Choice:',
default: '1,2,3',
input: process.stdin,
output: process.stdout,
timeout: timeLeftMs - TIME_EXPIRE_BUFFER,
}, (error, result) => {
if (error) {
process.stdout.write('\n');
this.logger.log('Time buffer expired. Auto-sending answers!');
this.autoSend(questionId, answers, players);
return;
}
previousChoices = result;
read({
prompt: 'Confirm Choice:',
default: previousChoices || '1,2,3',
input: process.stdin,
output: process.stdout,
timeout: timeLeftMs - (ticker * 1000) - TIME_EXPIRE_BUFFER,
}, (error, result) => {
if (error) {
process.stdout.write('\n');
this.logger.log('Time buffer expired. Auto-sending answers!');
this.autoSend(questionId, answers, players);
return;
}
let choices = result.trim().split(/,| |;/);
if (!choices && previousChoices) {
choices = previousChoices.trim().split(/,| |;/);
}
switch (choices.length) {
case 1:
this.sendAnswers(questionId, answers, [choices[0]], players);
break;
case 2:
this.sendAnswers(questionId, answers, [choices[0], choices[1]], players);
break;
case 3:
this.sendAnswers(questionId, answers, [choices[0], choices[1], choices[2]], players);
break;
default:
this.logger.log('You sent too many answer choices!', LOG_TYPES.ERROR);
this.autoSend(questionId, answers, players);
break;
}
});
});
}
}
}
checkAnswers(questionSummary) {
const { answerCounts, advancingPlayersCount, eliminatedPlayersCount, id: questionId } = questionSummary;
const answerIndex = answerCounts.findIndex(answer => answer.correct);
const correctAnswer = answerCounts[answerIndex];
const percentAdvancing = Math.round(advancingPlayersCount / (advancingPlayersCount + eliminatedPlayersCount) * 100);
this.game.currentPlayersRemaining = advancingPlayersCount;
if (advancingPlayersCount > 0) {
this.game.prizePerPlayer = (this.game.prize / advancingPlayersCount);
}
if (percentAdvancing <= 33) {
this.game.savageCount++;
}
if (this.scraper) {
this.scraper.showSummary(questionSummary, answerIndex);
}
const answerText = correctAnswer.text || correctAnswer.answer || 'undefined';
this.logger.log(`The correct answer was ${answerIndex + 1}) ${answerText}!`, LOG_TYPES.INFO);
const wrongPlayers = [];
this.players.forEach(player => {
if (player.lastAnswerId !== correctAnswer.answerId) {
const potentialPot = this.game.prizePerPlayer * this.players.length;
if (player.hasExtraLife && potentialPot >= USE_EXTRA_LIFE_MIN_PRIZE && this.game.questionNumber < this.game.questionCount) {
player.sendExtraLife(questionId);
} else {
wrongPlayers.push(player.userId);
}
}
});
if (wrongPlayers.length > 0) {
if (wrongPlayers.length === this.players.length) {
this.logger.log(`All remaining ${wrongPlayers.length} players have been eliminated!`, LOG_TYPES.ERROR);
} else {
this.logger.log(`${wrongPlayers.length} players have been eliminated!`, LOG_TYPES.WARNING);
}
} else if (this.players.length > 0) {
this.logger.log(`No players have been eliminated!`, LOG_TYPES.SUCCESS);
}
wrongPlayers.forEach(userId => this.removePlayer(userId));
this.logger.log(`You have ${this.players.length} players still in the game.`);
if (advancingPlayersCount > 0) {
const prizePerPlayer = toUSD(this.game.prizePerPlayer);
const total = toUSD(this.game.prizePerPlayer * this.players.length);
if (this.game.questionNumber >= this.game.questionCount) {
if (this.game.prizePerPlayer * this.players.length > 0) {
this.logger.log(`\nYou won ${total} (${prizePerPlayer} per player)`, LOG_TYPES.SUCCESS);
} else {
this.logger.log(`\nNo winnings this time! Total prize was ${total} (${prizePerPlayer} per player)`)
}
} else {
this.logger.log(`\nPotential Winnings: ${total} (${prizePerPlayer} per player remaining)`);
}
if (this.pusher && this.game.questionNumber >= this.game.questionCount) {
if (this.players.length > 0) {
this.pusher.send('HQ Game Summary', `${this.players.length} of your players won ${prizePerPlayer} for a total of ${total}!`);
} else {
this.pusher.send('HQ Game Summary', 'All of your players were eliminated! No winnings this time.');
}
}
}
}
autoSend(questionId, answers, players = this.players) {
const prizePerPlayer = this.game.prizePerPlayer || 0;
const potentialPot = prizePerPlayer * this.players.length;
let splitMode = SPLIT_MODE;
if (SAFE_WINNINGS_MODE && potentialPot > MIN_AMOUNT_THRESHOLD) {
splitMode = SPLIT_MODES.EVENLY;
this.logger.logSilent(`Safe Winnings Mode activated because ${potentialPot} is greater than ${MIN_AMOUNT_THRESHOLD}.`);
}
if (DISCRETE_MODE) {
if (this.game.currentPlayersRemaining && this.game.currentPlayersRemaining > 0) {
const percentRemaining = Math.floor(this.players.length / this.game.currentPlayersRemaining * 100);
if (percentRemaining > MAX_PERCENTAGE_OF_REMAINING) {
splitMode = SPLIT_MODES.EVENLY;
this.logger.logSilent(`Discrete Mode activated because ${percentRemaining}% is greater than ${MAX_PERCENTAGE_OF_REMAINING}%.`);
}
}
}
if (SPLIT_LAST_ROUND && this.game.questionNumber === this.game.questionCount) {
splitMode = SPLIT_MODES.EVENLY;
this.logger.logSilent(`Split Last Round enabled... splitting last answers evenly.`);
}
switch (splitMode) {
case SPLIT_MODES.WEIGHTS:
case SPLIT_MODES.GOOGLE:
case SPLIT_MODES.SUGGESTION:
if (this.scraper) {
const suggestionArray = this.scraper.getSuggestionArray();
const suggestion = suggestionArray.reduce((acc, suggestion, index) => {
if (suggestion) {
acc.push(index + 1);
}
return acc;
}, []);
this.sendAnswers(questionId, answers, suggestion, players);
break;
}
case SPLIT_MODES.EVENLY:
default:
players.forEach((player, index) => {
const choice = (index % 3) + 1;
player.sendAnswer(questionId, answers[choice - 1], choice);
});
break;
}
}
sendAnswers(questionId, answers, choices, players = this.players) {
players.forEach((player, index) => {
const choice = choices[index % choices.length];
player.sendAnswer(questionId, answers[choice - 1], choice);
});
}
removePlayer(userId) {
const player = this.players.find(player => player.userId === userId);
if (player) {
player.removePlayer();
}
this.players = this.players.filter(player => player.userId !== userId);
}
endGame() {
this.socket.close();
}
async play() {
this.logger.log('Ready to play!');
}
}