Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streaks #34

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const path = require('path');
const functions = require('firebase-functions');
const express = require('express');
const user = require('./utilities/user');
const { getStreak } = require('./utilities/streak');

const BADGES = require('./build/badges.json');
const PAGES = require('./build/pages.json');
Expand Down Expand Up @@ -85,6 +86,8 @@ app.use((req, res, next) => {
PAGES[l].filter(p => (res.locals.now >= p.available));
}

res.locals.streak = getStreak(req, PAGES, new Date(res.locals.now));

next();
});

Expand Down
138 changes: 138 additions & 0 deletions functions/utilities/streak.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const isNextStreak = (currentPage, nextPage, answers) => {
const answer = answers[currentPage?.url];

if (answer && answer.submitted && answer.time) {
if (
answer.time > new Date(currentPage.available).getTime() &&
answer.time < new Date(nextPage ? nextPage.available : currentPage.deadline).getTime()
) {
return true;
}
}

return false;
};

const getStreakMultiplier = (streak) => {
if (typeof streak !== "number" || streak < 2) return 0;
return (streak - 1) * 5;
};

const millisecondsInDay = 1000*60*60*24;

const getNextReadableTime = (target, todayDate) => {
if(!target){
return undefined;
}

const prefix = 'Next parallelogram: '
const targetDate = new Date(target);

if(isNaN(targetDate.getTime())) return undefined;

if (todayDate > targetDate) return undefined;

const differenceMillisecond = targetDate.getTime() - todayDate.getTime();

const daysAway = differenceMillisecond / millisecondsInDay;

const lessThan24HoursAway = (daysAway) < 1;

if (todayDate.getDate() === targetDate.getDate() && lessThan24HoursAway) {
const hours = targetDate.getHours();

if (hours === 12) return prefix + `${hours}pm today`;
if (hours < 12) return prefix + `${hours}am today`;
if (hours > 12) return prefix + `${hours - 12}pm today`;
}

if (lessThan24HoursAway) {
return prefix + `tomorrow`;
}

return prefix + `in ${Math.floor(daysAway)} days`;
};

const getStreakValue = (activePageIndex, levels, answers) => {
const userHasCompletedCurrentlyActive = !!answers[levels[activePageIndex]?.url]

const adjustedActivePageIndex = userHasCompletedCurrentlyActive ? activePageIndex : activePageIndex + 1;

const activePage = activePageIndex > 0 ? levels[adjustedActivePageIndex] : undefined;
const nextPage = levels[adjustedActivePageIndex - 1];

let streak = isNextStreak(activePage, nextPage, answers) ? 1 : 0;


while (
isNextStreak(
levels[adjustedActivePageIndex + streak],
levels[adjustedActivePageIndex - 1 + streak],
answers
)
) {
streak++;
}

return {streak, userHasCompletedCurrentlyActive};
}

const getStreak = (req, PAGES, now) => {
const levelName = req?.user?.level;

if (!levelName || req?.user?.code) {
return undefined;
}

const activePageIndex = PAGES[levelName].findIndex(({ available }) => now > new Date(available));

const yearHasNotStartedYet = activePageIndex == -1;

const userHasCompletedLastOfYear = !!req?.user?.answers[PAGES[levelName][activePageIndex]?.url];
const yearHasEnded = activePageIndex === 0 && (new Date(PAGES[levelName][activePageIndex].deadline) < now || userHasCompletedLastOfYear);

const { streak, userHasCompletedCurrentlyActive } = getStreakValue(activePageIndex, PAGES[levelName], req?.user?.answers)

if (yearHasNotStartedYet) {
return {
value: 0,
multiplier: 0,
next: getNextReadableTime(PAGES[levelName][PAGES[levelName].length - 1]?.available, now),
motivator: 'Join us for a new year of parallelograms!'
}
}

if (yearHasEnded) {
return {
value: streak,
multiplier: getStreakMultiplier(streak),
next: undefined,
motivator: streak ? 'Great final streak, come back next year to see if you can beat it!' : 'Come back in August to build a streak and earn a score multiplier!'
}
}

if(streak && userHasCompletedCurrentlyActive){
return {
value: streak,
multiplier: getStreakMultiplier(streak),
next: getNextReadableTime(PAGES[levelName][activePageIndex - 1]?.available, now),
motivator: 'Come back for the next parallelogram to continue your streak!'
}
} else if(streak) {
return {
value: streak,
multiplier: getStreakMultiplier(streak),
next: `Next parallelogram is out!`,
motivator: 'Complete the latest parallelogram to build up your multiplier!'
}
}

return {
value: 0,
multiplier: 0,
next: undefined,
motivator: 'Answer the latest parallelogram to start a score streak and earn a score multiplier!'
};
};

exports.getStreak = getStreak;
196 changes: 196 additions & 0 deletions functions/utilities/streak.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
const { getStreak } = require("./streak");

const AprilFirstDate = "2022-04-01T11:00:00.000Z";
const MayFirstDate = "2022-05-01T11:00:00.000Z";
const JuneFirstDate = "2022-06-01T11:00:00.000Z";
const JuneFifteenth = "2022-06-15T11:00:00.000Z";

const MarchTenthUnix = 1646906400000;
const MarchTwentiethUnix = 1647770400000;

const AprilTenthUnix = 1649581200000;
const AprilTwentiethUnix = 1650445200000;

const MayTenthUnix = 1652173200000;
const MayTwentiethUnix = 1653037200000;

const JuneTenthUnix = 1654851600000;
const JuneTwentiethUnix = 1655715600000;

const AprilQuestion = "7-33-aluminium-can-engineering";
const MayQuestion = "7-34-pet-puzzle";
const JuneQuestion = "7-35-summer-sums";

const PAGES = {
year7: [
{
url: JuneQuestion,
available: JuneFirstDate,
deadline: JuneFifteenth,
},
{
url: MayQuestion,
available: MayFirstDate,
},
{
url: AprilQuestion,
available: AprilFirstDate,
},
],
};

describe("streak", () => {
it("when student has running streak and next parallelogram not out yet", () => {
const now = new Date(AprilTwentiethUnix);

const req = {
user: {
level: "year7",
answers: {
[AprilQuestion]: {
submitted: true,
time: AprilTenthUnix,
},
},
},
};

expect(getStreak(req, PAGES, now)).toEqual({
motivator: "Come back for the next parallelogram to continue your streak!",
multiplier: 0,
next: "Next parallelogram: in 11 days",
value: 1,
});
});

it("when student has running streak and next parallelogram is out & not completed", () => {
const now = new Date(MayTenthUnix);

const req = {
user: {
level: "year7",
answers: {
[AprilQuestion]: {
submitted: true,
time: AprilTenthUnix,
},
},
},
};

expect(getStreak(req, PAGES, now)).toEqual({
motivator: "Complete the latest parallelogram to build up your multiplier!",
multiplier: 0,
next: "Next parallelogram is out!",
value: 1,
});
});

it("when student has no running streak", () => {
const now = new Date(MayTenthUnix);

const req = {
user: {
level: "year7",
answers: {},
},
};

expect(getStreak(req, PAGES, now)).toEqual({
motivator: "Answer the latest parallelogram to start a score streak and earn a score multiplier!",
multiplier: 0,
next: undefined,
value: 0,
});
});

it("when student has no running streak and year hasnt started yet", () => {
const now = new Date(MarchTenthUnix);

const req = {
user: {
level: "year7",
answers: {},
},
};

expect(getStreak(req, PAGES, now)).toEqual({
motivator: "Join us for a new year of parallelograms!",
multiplier: 0,
next: "Next parallelogram: in 22 days",
value: 0,
});
});

it("when student has a running streak and year has ended", () => {
const now = new Date(JuneTwentiethUnix);

const req = {
user: {
level: "year7",
answers: {
[JuneQuestion]: {
submitted: true,
time: JuneTenthUnix,
},
[MayQuestion]: {
submitted: true,
time: MayTenthUnix,
},
},
},
};

expect(getStreak(req, PAGES, now)).toEqual({
motivator: "Great final streak, come back next year to see if you can beat it!",
multiplier: 5,
next: undefined,
value: 2,
});
});

it("when student has no running streak and year has ended", () => {
const now = new Date(JuneTwentiethUnix);

const req = {
user: {
level: "year7",
answers: {},
},
};

expect(getStreak(req, PAGES, now)).toEqual({
motivator: "Come back in August to build a streak and earn a score multiplier!",
multiplier: 0,
next: undefined,
value: 0,
});
});

it("when student has a running streak, has done the current parallelogram, and year is about to end", () => {
const now = new Date(JuneTenthUnix);

const req = {
user: {
level: "year7",
answers: {
[JuneQuestion]: {
submitted: true,
time: JuneTenthUnix,
},
[MayQuestion]: {
submitted: true,
time: MayTenthUnix,
},
},
},
};

expect(getStreak(req, PAGES, now)).toEqual({
motivator: "Great final streak, come back next year to see if you can beat it!",
multiplier: 5,
next: undefined,
value: 2,
});
});
});
13 changes: 13 additions & 0 deletions functions/views/_layout.pug
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ html
.badge(style='background-image: url(/images/badges/' + b.id + '.jpg); color: #' + b.color)
if user.visibleBadges.length > 4
.badge.more +
if streak
.sidebar-field.streak
if !streak.value
.intro #{streak.motivator}
if streak.next
.next #{streak.next}
if streak.value
div Current streak: #{streak.value}
if(streak.multiplier)
.multiplier + #{streak.multiplier}%
script!= 'window.__STREAK_MULTIPLIER__ = ' + JSON.stringify(streak.multiplier) + ';'
.motivator #{streak.motivator}
.next #{streak.next}

if !user || user.code || user.level !== 'year7'
.toggle
Expand Down
Loading