Initial Commit

Includes just the basic get/send answers. No discord/google integration at the moment or other functionalities.
This commit is contained in:
eTronic
2018-07-04 13:54:22 -04:00
parent a1f565f42f
commit 9b02ad8371
7 changed files with 3410 additions and 0 deletions

130
Player.js Normal file
View File

@@ -0,0 +1,130 @@
import WebSocket from 'ws';
import { LOG_TYPES } from './constants';
export default class Player {
constructor({
accessToken,
userId,
username,
}) {
this.accessToken = accessToken;
this.userId = userId;
this.username = username;
this.logger = null;
this.broadcast = null;
this.socket = null;
this.lastAnswerId = null;
this.playerPingTimeout = null;
this.playerPingInterval = null;
this.hasExtraLife = false;
}
async connect(broadcast) {
const { broadcastId, socketUrl } = broadcast;
this.broadcast = broadcast;
const options = {
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
};
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));
}
ping() {
if (!this.socket) return;
try {
this.socket.ping();
this.playerPingTimeout = setTimeout(() => {
this.socket.terminate();
this.logger.log(`Player ${this.username} connection closed. Attempting to reconnect...`, LOG_TYPES.ERROR);
this.connect(this.broadcast);
}, 2000);
} catch (error) {
this.logger.log(`Player ${this.username} connection closed. Attempting to reconnect...`, LOG_TYPES.ERROR);
}
}
pong() {
clearTimeout(this.playerPingTimeout);
}
async handleOpen() {
this.playerPingInterval = setInterval(this.ping.bind(this), 10000);
this.logger.log(`Player ${this.username} has connected to websocket!`, LOG_TYPES.SUCCESS);
const { broadcastId } = this.broadcast;
this.socket.send(JSON.stringify({ type: 'subscribe', broadcastId }), () => {
this.logger.log(`Player ${this.username} has subscribed to the game!`, LOG_TYPES.INFO);
});
}
handleClose() {
this.socket = null;
clearInterval(this.playerPingInterval);
clearTimeout(this.playerPingTimeout);
}
removePlayer() {
this.socket.terminate();
this.socket.close();
}
async handleMessage(data) {
const parsed = JSON.parse(data);
switch (parsed.type) {
case 'gameStatus':
const { extraLivesRemaining = 0 } = parsed;
this.hasExtraLife = extraLivesRemaining > 0;
this.logger.logSilent(`Player ${this.username}'s game status: ${data}`);
break;
case 'question':
case 'questionClosed':
case 'questionSummary':
case 'questionFinished':
break;
}
}
sendAnswer(questionId, answer, answerNumber) {
const { answerId } = answer;
const answerPackage = {
type: 'answer',
broadcastId: this.broadcast.broadcastId,
questionId,
answerId,
};
this.lastAnswerId = answerId;
try {
this.socket.send(JSON.stringify(answerPackage));
this.logger.log(`Player ${this.username} sent answer ${answerNumber}) ${answer.text}`);
} catch (error) {
this.logger.log(`WebSocket error: ${this.username} can\'t send answer.`, LOG_TYPES.ERROR);
this.lastAnswerId = null;
}
}
sendExtraLife(questionId) {
const extraLifePackage = {
type: 'useExtraLife',
broadcastId: this.broadcast.broadcastId,
questionId,
};
try {
this.socket.send(JSON.stringify(extraLifePackage));
this.logger.log(`Player ${this.username} has been saved by an extra life!`);
} catch (error) {
this.logger.log(`WebSocket error: ${this.username} can\'t send extra life.`, LOG_TYPES.ERROR);
}
}
}

8
constants.js Normal file
View File

@@ -0,0 +1,8 @@
import chalk from 'chalk';
export const LOG_TYPES = {
ERROR: chalk.bold.red,
SUCCESS: chalk.greenBright,
INFO: chalk.cyan,
WARNING: chalk.yellow,
};

41
helper.js Normal file
View File

@@ -0,0 +1,41 @@
import fs from 'fs';
import { ENABLE_LOGGING, LOG_DIR } from './config';
export default class Logger {
constructor(filename) {
this.filename = filename;
this.logStream = null;
}
async open() {
return new Promise((resolve, reject) => {
if (ENABLE_LOGGING) {
this.logStream = fs.createWriteStream(`${LOG_DIR}${this.filename}`);
this.logStream.on('open', resolve(true));
this.logStream.on('error', reject);
} else {
resolve(true);
}
});
}
log(message, format = msg => msg) {
console.log(format(message));
this.logSilent(message);
};
logSilent(message) {
if (this.logStream) {
this.logStream.write(`${message}\r\n`);
}
}
close() {
if (this.logStream) {
this.logStream.end();
}
}
}
export const toUSD = money => money.toLocaleString('en-US', { style: 'currency', currency: 'USD' });

47
index.js Normal file
View File

@@ -0,0 +1,47 @@
import prompts from 'prompts';
import Trivia from './trivia';
import * as config from './config';
import { LOG_TYPES } from './constants';
const choices = [
{ title: 'Play Trivia', select: () => new Trivia().run() },
{ title: 'Create Account', select: () => console.log('Not available yet!') },
{ title: 'Update Weekly Lives', select: () => console.log('Not available yet!') },
{ title: 'Show Player Summaries', select: () => console.log('Not available yet!') },
{ title: 'Cashout', select: () => console.log('Not available yet!') },
];
const getChoice = async () => {
console.log(LOG_TYPES.WARNING('#######################################'));
console.log(LOG_TYPES.WARNING('# REMEMBER TO VPN WHILE RUNNING #'));
console.log(LOG_TYPES.WARNING('#######################################'));
console.log();
console.log('Welcome to Trivia Automation Suite 1.0');
console.log('What would you like to do?\n');
choices.forEach(({ title }, index) => console.log(`${index + 1}) ${title}`));
console.log();
const response = await prompts({
type: 'number',
name: 'choice',
message: 'Choice:',
});
const { choice } = response;
if (choice < 1 || choice > choices.length) {
console.log('Invalid selection!');
return null;
}
return choice;
};
const main = async () => {
const choice = await getChoice();
if (!choice) return;
choices[choice - 1].select();
};
main();

2801
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "tasjs",
"version": "1.0.0",
"description": "Trivia Automation Suite",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "babel-node index.js --presets env,es2016,stage-2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/eTr0nic/tas.js.git"
},
"author": "eTronic",
"license": "ISC",
"bugs": {
"url": "https://github.com/eTr0nic/tas.js/issues"
},
"homepage": "https://github.com/eTr0nic/tas.js#readme",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2016": "^6.24.1",
"babel-preset-stage-2": "^6.24.1"
},
"dependencies": {
"async-request": "^1.2.0",
"chalk": "^2.4.1",
"moment": "^2.22.2",
"prompt": "^1.0.0",
"prompts": "^0.1.10",
"read": "^1.0.7",
"ws": "^5.2.1"
}
}

348
trivia.js Normal file
View File

@@ -0,0 +1,348 @@
import moment from 'moment';
import read from 'read';
import request from 'async-request';
import WebSocket from 'ws';
import Player from './Player';
import { accounts } from './accounts.json';
import { LOG_TYPES } from './constants';
import Logger, { toUSD } from './helper';
const { log } = console;
import {
AUTOPLAY,
MASTER_BEARER_TOKEN,
MAX_TEST_PLAYERS,
MAX_TEST_QUESTIONS,
MIN_AMOUNT_THRESHOLD,
NEXT_GAME_TIME_BUFFER,
SAFE_WINNINGS_MODE,
SERVER_URL,
SPLIT_MODE,
SPLIT_MODES,
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;
}
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.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,
startTime: new Date().toJSON().toString(),
};
return;
}
try {
const response = await request(`https://${SERVER_URL}/shows/now`);
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));
}
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(broadcast));
await this.play();
}
handleClose() {
this.socket = null;
clearInterval(this.masterPingInterval);
clearTimeout(this.masterPingTimeout);
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.logger.logSilent(`Game Status: ${data}`);
break;
case 'question':
this.game.questionNumber = parsed.questionNumber;
this.showQuestion(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}`);
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) {
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);
return;
}
const choices = result.trim().split(/,| |;/);
switch (choices.length) {
case 1:
this.sendAnswers(questionId, answers, [choices[0]]);
break;
case 2:
this.sendAnswers(questionId, answers, [choices[0], choices[1]]);
break;
case 3:
this.sendAnswers(questionId, answers, [choices[0], choices[1], choices[2]]);
break;
default:
this.logger.log('You sent too many answer choices!', LOG_TYPES.ERROR);
this.autoSend(questionId, answers);
break;
}
});
}
}
checkAnswers({ answerCounts, advancingPlayersCount, id: questionId }) {
const answerIndex = answerCounts.findIndex(answer => answer.correct);
const correctAnswer = answerCounts[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) {
if (player.hasExtraLife && this.game.prize >= USE_EXTRA_LIFE_MIN_PRIZE) {
player.sendExtraLife(questionId);
} else {
wrongPlayers.push(player.userId);
}
}
});
if (wrongPlayers.length === this.players.length) {
this.logger.log(`All remaining ${wrongPlayers.length} players have been eliminated!`, LOG_TYPES.ERROR);
} else if (wrongPlayers.length > 0) {
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) {
this.prizePerPlayer = (this.game.prize / advancingPlayersCount);
const prizePerPlayer = toUSD(this.prizePerPlayer);
const total = toUSD(this.prizePerPlayer * this.players.length);
if (this.game.questionNumber >= this.game.questionCount) {
if (this.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)`);
}
}
}
autoSend(questionId, answers) {
const potentialPot = this.prizePerPlayer * this.players.length;
let splitMode = SPLIT_MODE;
if (SAFE_WINNINGS_MODE && potentialPot > MIN_AMOUNT_THRESHOLD) {
splitMode = SPLIT_MODE.EVENLY;
}
switch (splitMode) {
case SPLIT_MODES.WEIGHTS:
case SPLIT_MODES.GOOGLE:
case SPLIT_MODES.SUGGESTION:
case SPLIT_MODES.EVENLY:
default:
this.players.forEach((player, index) => {
const choice = (index % 3) + 1;
player.sendAnswer(questionId, answers[choice - 1], choice);
});
break;
}
}
sendAnswers(questionId, answers, choices) {
this.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);
player.removePlayer();
this.players = this.players.filter(player => player.userId !== userId);
}
endGame() {
this.socket.close();
}
async play() {
this.logger.log('Ready to play!');
}
}