Browse Source

Change to bonus slots being X available per tier

master
Brandon Cornejo 7 months ago
parent
commit
07fbaeebef
7 changed files with 240 additions and 157 deletions
  1. +0
    -3
      pg_setup.sql
  2. +2
    -0
      public/landing.html
  3. +1
    -4
      src/db.js
  4. +82
    -44
      src/quiz.js
  5. +95
    -63
      src/redis-brain.js
  6. +52
    -35
      templates/index.html
  7. +8
    -8
      templates/leaderboard.html

+ 0
- 3
pg_setup.sql View File

@@ -9,9 +9,6 @@ CREATE TABLE questions (
total_correct int,
question text UNIQUE,
answer text,
bonus_slot_1 text,
bonus_slot_2 text,
bonus_slot_3 text,
answer_a_total int,
answer_b_total int,
answer_c_total int,


+ 2
- 0
public/landing.html View File

@@ -100,6 +100,7 @@
tr.insertCell().appendChild(document.createTextNode(data.active.active_question));

// Bonus Slots
/*
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("First to Answer"));
tr.insertCell().appendChild(document.createTextNode(data.active.bonus_slot_1 || "AVAILABLE!"));
@@ -109,6 +110,7 @@
tr = tbody.insertRow();
tr.insertCell().appendChild(document.createTextNode("Third to Answer"));
tr.insertCell().appendChild(document.createTextNode(data.active.bonus_slot_3 || "AVAILABLE!"));
*/

// Percents
if(data.active.answer_percents) {


+ 1
- 4
src/db.js View File

@@ -14,7 +14,7 @@ pool.on('error', (err, client) => {
});

const config = {
query_create_question: "INSERT INTO questions(question, answer, total_responses, total_valid, total_correct, bonus_slot_1, bonus_slot_2, bonus_slot_3, answer_a_total, answer_b_total, answer_c_total, answer_d_total, answer_a, answer_b, answer_c, answer_d, ended_at) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id",
query_create_question: "INSERT INTO questions(question, answer, total_responses, total_valid, total_correct, answer_a_total, answer_b_total, answer_c_total, answer_d_total, answer_a, answer_b, answer_c, answer_d, ended_at) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id",
query_create_viewer: "INSERT INTO viewers(username, points) VALUES ($1, $2) ON CONFLICT (username) DO UPDATE SET points = viewers.points + EXCLUDED.points RETURNING id, username",
query_create_answer: "INSERT INTO answers(viewer_id, question_id, answer, points) VALUES ($1, $2, $3, $4) RETURNING id",

@@ -95,9 +95,6 @@ var self = module.exports = {
q.total_responses,
q.total_valid,
q.total_correct,
q.bonus_slot_1,
q.bonus_slot_2,
q.bonus_slot_3,
q.answer_totals['A'] || null,
q.answer_totals['B'] || null,
q.answer_totals['C'] || null,


+ 82
- 44
src/quiz.js View File

@@ -1,7 +1,48 @@

let config = {
valid_answers: ['A', 'B', 'C', 'D'],
standard_points: 20,
bonus_slots: {
one: {
max: 5,
points: 15,
viewers: [],
},
two: {
max: 5,
points: 14,
viewers: [],
},
three: {
max: 10,
points: 13,
viewers: [],
},
four: {
max: 15,
points: 12,
viewers: [],
},
five: {
max: 20,
points: 11,
viewers: [],
},
order: ['one', 'two', 'three', 'four', 'five'],
},
leaderboard_max: 10,
personal_leaderboard_max: 10,
question_timeout_minutes: 3,
}

var quiz = {
bonus_slot_1: null,
bonus_slot_2: null,
bonus_slot_3: null,
bonus_viewers: {
one: [],
two: [],
three: [],
four: [],
five: [],
}
};

module.exports = quiz;
@@ -11,18 +52,11 @@ const db = require('./db');
const redis = require('./redis-brain');
const twitch = require('./twitch');

const valid_answers = ['A', 'B', 'C', 'D'];
const bonus_slot_points = [100, 50, 25];
const standard_points = [20];
const leaderboard_max = 10;
const personal_leaderboard_max = 10;
const question_timer_minutes = 3;

quiz.setCurrentQuestion = function(data) {
return new Promise((resolve) => {
redis.setActiveQuestion(...data).then(() => {
twitch.sendNewQuestionMessage(...data);
setTimeout(() => { quiz.endCurrentQuestion(); }, (1000 * (60 * question_timer_minutes)));
setTimeout(() => { quiz.endCurrentQuestion(); }, (1000 * (60 * config.question_timeout_minutes)));
resolve();
}).catch((error) => {
// errlog
@@ -44,10 +78,14 @@ quiz.endCurrentQuestion = function() {
console.error("* [Quiz] Error writing question data to Postgres");
});

// TODO: Send bonus slot data to redis if we need to? dont lose it

redis.clearActiveQuestion().then(() => {
quiz.bonus_slot_1 = null;
quiz.bonus_slot_2 = null;
quiz.bonus_slot_3 = null;
quiz.bonus_viewers.one = [];
quiz.bonus_viewers.two = [];
quiz.bonus_viewers.three = [];
quiz.bonus_viewers.four = [];
quiz.bonus_viewers.five = [];

twitch.sendEndQuestionMessage();

@@ -66,13 +104,11 @@ quiz.handleViewerVote = function(username, answer) {
redis.getActiveQuestion().then((aq_data) => {
// If there is no active question, or there is no valid answer, nothing to do
answer = answer.trim().toUpperCase();
if(!aq_data.question || !(answer) || !(valid_answers.includes(answer))) {
if(!aq_data.question || !(answer) || !(config.valid_answers.includes(answer))) {
redis.incrementResponseCounts(username, false, false);
return resolve();
}

console.log('PAST IT!!!');

// We have an (A|B|C|D), check it against the actual active answer
redis.checkActiveAnswer(answer, username).then((is_correct_answer) => {
if(is_correct_answer) {
@@ -100,9 +136,9 @@ quiz.handleViewerVote = function(username, answer) {

quiz.addViewerScore = function(username, nopoints=false) {
if(nopoints) {
// If it was the wrong answer
// Wrong answer, set zero points in case we haven't seen user before
redis.addViewerPoints(username, 0, null).then(() => {
console.info(`* [quiz] Assigning 0 points to ${username} for an incorrect answer`);
console.info(`* [quiz] Repeat or wrong answer from ${username}. Setting 0 points if not already present.`);
}).catch((error) => {
console.error(`* [quiz] Error adding no-points for ${username}`, error);
});
@@ -110,28 +146,28 @@ quiz.addViewerScore = function(username, nopoints=false) {
return;
}

// Check if any of our bonuses are still available
let points = standard_points;
let used_slot = null;

if(!Boolean(quiz.bonus_slot_1 && quiz.bonus_slot_2 && quiz.bonus_slot_3)) {
if(!quiz.bonus_slot_1) {
points = bonus_slot_points[0];
used_slot = 1;
quiz.bonus_slot_1 = username;
} else if(!quiz.bonus_slot_2) {
points = bonus_slot_points[1];
used_slot = 2;
quiz.bonus_slot_2 = username;
} else if(!quiz.bonus_slot_3) {
points = bonus_slot_points[2];
used_slot = 3;
quiz.bonus_slot_3 = username;
function findAvailableBonusSlot() {
for(let slot_key of config.bonus_slots.order) {
let bs = quiz.bonus_viewers[slot_key];
console.log('***', slot_key, bs);
if(bs.length < config.bonus_slots[slot_key].max) {
return slot_key;
}
}

return null;
}

let points = config.standard_points;
let slot = findAvailableBonusSlot();
console.log('SLOT RETURNED: ', slot);
if(slot) {
quiz.bonus_viewers[slot].push(username);
points = config.bonus_slots[slot].points;
}

redis.addViewerPoints(username, points, used_slot).then(() => {
console.info(`* [quiz] Awarding ${points} points to ${username} for a correct first answer`);
redis.addViewerPoints(username, points, slot).then(() => {
console.info(`* [quiz] Correct answer for ${username}. Bonus slot ${slot}. Setting ${points} if not already present.`);
}).catch((error) => {
console.error(`* [quiz] Error adding ${points} for ${username}`, error);
});
@@ -281,9 +317,11 @@ quiz.generateFakeAnswerData = function() {
quiz.set_bot_state = function() {
return new Promise((resolve) => {
redis.checkBonusSlotAvailability().then((bs_data) => {
quiz.bonus_slot_1 = bs_data[0];
quiz.bonus_slot_2 = bs_data[1];
quiz.bonus_slot_3 = bs_data[2];
quiz.bonus_viewers.one = bs_data[0] || [];
quiz.bonus_viewers.two = bs_data[1] || [];
quiz.bonus_viewers.three = bs_data[2] || [];
quiz.bonus_viewers.four = bs_data[3] || [];
quiz.bonus_viewers.five = bs_data[4] || [];
});
});
};
@@ -295,13 +333,13 @@ function cleanLeaderboard(lb, username=null) {
});

if(username) {
for(let i = (personal_leaderboard_max - 1); i < lb.length; i++) {
for(let i = (config.personal_leaderboard_max - 1); i < lb.length; i++) {
if(lb[i].username === username) {
lb[personal_leaderboard_max - 1] = lb[i];
lb[config.personal_leaderboard_max - 1] = lb[i];
}
}
}

// Trim it back down to 10 folks total
return lb.slice(0, username ? personal_leaderboard_max : leaderboard_max);
return lb.slice(0, username ? config.personal_leaderboard_max : config.leaderboard_max);
}

+ 95
- 63
src/redis-brain.js View File

@@ -15,13 +15,17 @@ const config = {
answer_d_key: "twitch_bot_answer_d",
votes_hash: "twitch_bot_votes_hash",
points_hash: "twitch_bot_points_hash",
bonus_slot_1_key: "twitch_bot_bonus_1",
bonus_slot_2_key: "twitch_bot_bonus_2",
bonus_slot_3_key: "twitch_bot_bonus_3",
total_responses_key: "twitch_bot_total_responses",
total_valid_key: "twitch_bot_total_valid_answers",
total_correct_key: "twitch_bot_total_correct_answers",
answer_totals: "twitch_bot_answer_totals_hash",

/* Bonus Slots Keys */
bonus_slot_1_set: "twitch_bot_bonus_1",
bonus_slot_2_set: "twitch_bot_bonus_2",
bonus_slot_3_set: "twitch_bot_bonus_3",
bonus_slot_4_set: "twitch_bot_bonus_4",
bonus_slot_5_set: "twitch_bot_bonus_5",
};

client.on("error", function(error) {
@@ -54,9 +58,11 @@ var self = module.exports = {
.del(config.active_answer_key, config.output)
.del(config.votes_hash, config.output)
.del(config.points_hash, config.output)
.del(config.bonus_slot_1_key, config.output)
.del(config.bonus_slot_2_key, config.output)
.del(config.bonus_slot_3_key, config.output)
.del(config.bonus_slot_1_set, config.output)
.del(config.bonus_slot_2_set, config.output)
.del(config.bonus_slot_3_set, config.output)
.del(config.bonus_slot_4_set, config.output)
.del(config.bonus_slot_5_set, config.output)
.del(config.total_responses_key, config.output)
.del(config.total_valid_key, config.output)
.del(config.total_correct_key, config.output)
@@ -93,9 +99,11 @@ var self = module.exports = {
client.multi()
.get(config.active_question_key)
.get(config.active_answer_key)
.get(config.bonus_slot_1_key)
.get(config.bonus_slot_2_key)
.get(config.bonus_slot_3_key)
.smembers(config.bonus_slot_1_set)
.smembers(config.bonus_slot_2_set)
.smembers(config.bonus_slot_3_set)
.smembers(config.bonus_slot_4_set)
.smembers(config.bonus_slot_5_set)
.hgetall(config.votes_hash)
.hgetall(config.points_hash)
.get(config.total_responses_key)
@@ -109,48 +117,56 @@ var self = module.exports = {
.get(config.overall_interactions)
.scard(config.overall_viewers_set)
.exec(function(err, replies) {
var answer_totals = {};
var answer_percents = {};

if(replies[10]) {
answer_totals = Object.fromEntries(Object.entries(replies[10]).map(([k, v]) => [k, Number(v)]));
answer_percents = calculateAnswerPercents(answer_totals)
}
var answer_percents = replies[10] ? calculateAnswerPercents(replies[10]) : null;

// Turn array into a dict for easier visual parsing
let data = {
reply_map = {
active_question: replies[0],
active_answer: replies[1],
bonus_slots: {
one: replies[2],
two: replies[3],
three: replies[4],
four: replies[5],
five: replies[6],
},
active_votes: replies[7],
active_points: replies[8],
total_responses: intOrZero(replies[9]),
total_valid: intOrZero(replies[10]),
total_correct: intOrZero(replies[11]),
total_incorrect: 0,
answer_totals: replies[12],
answers: {
a: replies[11],
b: replies[12],
c: replies[13],
d: replies[14],
a: replies[13],
b: replies[14],
c: replies[15],
d: replies[16],
},
bonus_slot_1: replies[2],
bonus_slot_2: replies[3],
bonus_slot_3: replies[4],
total_responses: replies[7] || 0,
total_valid: replies[8] || 0,
total_correct: replies[9] || 0,
total_incorrect: (replies[8] - replies[9]).toString(),
answer_totals: answer_totals,
answer_percents: answer_percents,
overall: {
interactions: parseInt(replies[15]) || 0,
unique_viewers: replies[16],
}
interactions: intOrZero(replies[17]),
unique_viewers: replies[18],
},
};

if(data.overall.interactions == NaN) { data.overall.interactions = 0; }
reply_map.total_incorrect = reply_map.total_valid - reply_map.total_correct;

if(reply_map.answer_totals) {
reply_map.answer_totals = Object.fromEntries(Object.entries(reply_map.answer_totals).map(([k, v]) => [k, Number(v)]));
reply_map.answer_percents = calculateAnswerPercents(reply_map.answer_totals)
}

if(trimmed) {
// replace bonus-slot lists with used-slot count
/*
for([key, value] of Object.entries(reply_map.bonus_slots)) {
reply_map.bonus_slots[key] = value ? value.length : 0;
}
*/

if(!trimmed) {
data.active_votes = replies[5];
data.active_points = replies[6];
// delete vote and points maps (lengthy)
delete reply_map.active_votes;
delete reply_map.active_points;
}

resolve(data);
resolve(reply_map);
});
});
},
@@ -177,28 +193,37 @@ var self = module.exports = {
points = points.toString();

if(slot_used) {
// If we used a slot, we need to invalidate it
// If we used a slot, we need to add the viewer to redis set
let slot_key = null;
if(slot_used === 1) {
slot_key = config.bonus_slot_1_key;
} else if (slot_used === 2) {
slot_key = config.bonus_slot_2_key;
} else if (slot_used === 3) {
slot_key = config.bonus_slot_3_key;
switch (slot_used) {
case 'one':
slot_key = config.bonus_slot_1_set;
break;
case 'two':
slot_key = config.bonus_slot_2_set;
break;
case 'three':
slot_key = config.bonus_slot_3_set;
break;
case 'four':
slot_key = config.bonus_slot_4_set;
break;
case 'five':
slot_key = config.bonus_slot_5_set;
break;
}

client.multi()
.set(slot_key, username, config.output)
.hsetnx(config.points_hash, username, points)
.exec(function(err, replies) {
return resolve();
});
} else {
// Otherwise just set the points
client.hsetnx(config.points_hash, username, points, function(err) {
return resolve();
});
client.sadd(slot_key, username, config.output);
}

// Add the user and their points to the active data
client.hsetnx(config.points_hash, username, points, function(err) {
if(err) {
console.error("* [redis] Error setting user point value to points hash.");
}

return resolve();
});
});
},
checkActiveAnswer: function(submitted_answer, username) {
@@ -226,9 +251,11 @@ var self = module.exports = {
checkBonusSlotAvailability: function() {
return new Promise((resolve) => {
client.multi()
.get(config.bonus_slot_1_key, config.output)
.get(config.bonus_slot_2_key, config.output)
.get(config.bonus_slot_3_key, config.output)
.get(config.bonus_slot_1_set, config.output)
.get(config.bonus_slot_2_set, config.output)
.get(config.bonus_slot_3_set, config.output)
.get(config.bonus_slot_4_set, config.output)
.get(config.bonus_slot_5_set, config.output)
.exec(function(err, replies) {
resolve(replies);
});
@@ -305,8 +332,13 @@ function calculateAnswerPercents(answer_totals) {

for([key, value] of Object.entries(answer_totals)) {
// percents[key] = Number.parseFloat(((Number(value)/total) * 100).toFixed(2));
percents[key] = Math.trunc((Number(value)/total) * 100);
percents[key] = intOrZero(Math.trunc((Number(value)/total) * 100));
}

return percents;
}

function intOrZero(value) {
let v = parseInt(value);
return v || 0;
}

+ 52
- 35
templates/index.html View File

@@ -18,49 +18,66 @@
</div>
</form>
<hr class="uk-divider-icon" />
<div class="uk-margin uk-flex uk-flex-around">
<div>
<div class="uk-text-bold">Total Responses</div>
<div class="uk-text-center">{{ total_responses }}</div>
</div>
<div>
<div class="uk-text-bold">Total Valid</div>
<div class="uk-text-center">{{ total_valid }}</div>
</div>
<div>
<div class="uk-text-bold">Total Correct</div>
<div class="uk-text-center">{{ total_correct }}</div>
</div>
<div>
<div class="uk-text-bold">Total Incorrect</div>
<div class="uk-text-center">{{ total_incorrect }}</div>
<div class="uk-margin uk-text-center">
<h3>Question Totals</h3>
<div class="uk-flex uk-flex-around">
<div>
<div class="uk-text-bold">Total Responses</div>
<div class="uk-text-center">{{ total_responses }}</div>
</div>
<div>
<div class="uk-text-bold">Total Valid</div>
<div class="uk-text-center">{{ total_valid }}</div>
</div>
<div>
<div class="uk-text-bold">Total Correct</div>
<div class="uk-text-center">{{ total_correct }}</div>
</div>
<div>
<div class="uk-text-bold">Total Incorrect</div>
<div class="uk-text-center">{{ total_incorrect }}</div>
</div>
</div>
</div>
<hr class="uk-divider-icon" />
{% if answer_percents %}
<div class="uk-margin uk-flex uk-flex-around">
{% for ap in answer_percents | dictsort %}
<div class="uk-text-center">
<div class="uk-text-bold">{{ ap[0] }}</div>
<div class="{% if ap[0] == active_answer %}uk-text-warning{% endif %}">{{ ap[1] }}% ({{ answer_totals[ap[0]] }})</div>
<div class="uk-margin uk-text-center">
<h3>Answer Trends</h3>
<div class="uk-flex uk-flex-around">
{% for ap in answer_percents | dictsort %}
<div class="uk-text-center">
<div class="uk-text-bold">{{ ap[0] }}</div>
<div class="{% if ap[0] == active_answer %}uk-text-warning{% endif %}">{{ ap[1] }}% ({{ answer_totals[ap[0]] }})</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<hr class="uk-divider-icon" />
{% endif %}
<div class="uk-margin uk-flex uk-flex-around">
<div class="uk-text-center">
<div class="uk-text-bold">1st</div>
<div>{{ bonus_slot_1 or "Open" }}</div>
</div>
<div class="uk-text-center">
<div class="uk-text-bold">2nd</div>
<div>{{ bonus_slot_2 or "Open" }}</div>
</div>
<div class="uk-text-center">
<div class="uk-text-bold">3rd</div>
<div>{{ bonus_slot_3 or "Open"}}</div>
</div>
<div class="uk-margin uk-text-center">
<h3>Bonus Point Earners</h3>
<ul class="uk-subnav uk-subnav-pill uk-flex uk-flex-around" uk-switcher>
<li><a href="#">First</a></li>
<li><a href="#">Second</a></li>
<li><a href="#">Third</a></li>
<li><a href="#">Fourth</a></li>
<li><a href="#">Fifth</a></li>
</ul>
<ul class="uk-switcher uk-margin uk-text-left">
{% for slot in ['one', 'two', 'three', 'four', 'five'] %}
<li>
<ul class="uk-list uk-list-primary uk-list-striped uk-list-disc uk-width-1-2 uk-align-center">
{% if bonus_slots[slot].length %}
{% for viewer in bonus_slots[slot] %}
<li>{{ viewer }}</li>
{% endfor %}
{% else %}
<span>Bonus slots are all available!</span>
{% endif %}
</ul>
</li>
{% endfor %}
</ul>
</div>
<hr class="uk-divider-icon" />



+ 8
- 8
templates/leaderboard.html View File

@@ -8,16 +8,16 @@
<h2 class="uk-text-muted uk-margin-small-top">{{ username }}</h2>
{% endif %}

<div class="uk-margin-xlarge">
<h2 class="uk-text-center">Overall</h2>
<div class="uk-margin-large">
<h3 class="uk-text-center">Overall</h3>
<div class="uk-margin uk-flex uk-flex-around">
<div>
<div class="uk-text-bold uk-text-large">Total Interactions</div>
<div class="uk-text-center uk-text-large">{{ overall.interactions }}</div>
<div class="uk-text-bold">Total Interactions</div>
<div class="uk-text-center uk-text-primary uk-text-large">{{ overall.interactions }}</div>
</div>
<div>
<div class="uk-text-bold uk-text-large">Total Unique Users</div>
<div class="uk-text-center uk-text-large">{{ overall.unique_viewers }}</div>
<div class="uk-text-bold">Total Unique Users</div>
<div class="uk-text-center uk-text-primary uk-text-large">{{ overall.unique_viewers }}</div>
</div>
</div>
</div>
@@ -46,7 +46,7 @@
<hr class="uk-divider-icon" />

{% if active %}
<div class="uk-margin">
<div class="uk-margin" hidden>
<h3 class="uk-text-center">Current Question</h3>
<div class="uk-margin uk-flex uk-flex-around">
<div>
@@ -67,7 +67,7 @@
</div>
</div>
</div>
<hr class="uk-divider-icon" />
<hr class="uk-divider-icon" hidden/>
{% endif %}

<div class="uk-margin" hidden>


Loading…
Cancel
Save