mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-11-10 08:55:37 +08:00
Implement streaks (#3385) typermonkeyuser, bruception, miodec
* Implement streaks * Improve code * Update package.json * Store streak on the user * Pretty-fix * Ensure that streak is up to date * Fix issue in streak calculation * Revert prettier changes * Update polish.json * Update db.ts * Remove initial calculation * Write mandatory test * using strict equality * moved functions to the utils file * importing utils * adding streak to breakdown, rounding and parsing streak modifier * renamed variable * renamed fields * using correct field name * added streaks to configuration * showing streak during xp breakdown * incrementing streak earlier checking configuration before applying modifier * returning streak to the client * setting local streak with the number returned from the backedn * only reading streak when updating profile instead of updating * sending streak information in profile * only showing streak if greater than 0 * setting to empty if no streak * renamed config property * updated streak calculation * refactored isYesterday * refactored streak update * only displaying if streak larger than 1 * merged configuration properties into 1 * added configuration for max streak bonus * added isToday check back (derp) * reverted streaks back to multiplier approach * using better maprange functin * removed import * moved test to dal spec * clamping * removed imports * fixed test * increased coverage * removed angry console log * typo * using date now instead of dates * mocking date now awaiting expect * not using date Co-authored-by: Anonymous <110769200+fasttyperdog@users.noreply.github.com> Co-authored-by: Rizwan Mustafa <rizwanmustafa0000@gmail.com> Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
parent
20a5cac5e6
commit
b08c194c3d
18 changed files with 192 additions and 7 deletions
|
@ -1,5 +1,6 @@
|
|||
import _ from "lodash";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { updateStreak } from "../../src/api/controllers/user";
|
||||
import * as UserDAL from "../../src/dal/user";
|
||||
|
||||
const mockPersonalBest = {
|
||||
|
@ -488,6 +489,10 @@ describe("UserDal", () => {
|
|||
|
||||
expect(resetUser.bananas).toStrictEqual(0);
|
||||
expect(resetUser.xp).toStrictEqual(0);
|
||||
expect(resetUser.streak).toStrictEqual({
|
||||
length: 0,
|
||||
lastResultTimestamp: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("getInbox should return the user's inbox", async () => {
|
||||
|
@ -600,4 +605,26 @@ describe("UserDal", () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("updateStreak should update streak", async () => {
|
||||
await UserDAL.addUser("testStack", "test email", "TestID");
|
||||
|
||||
Date.now = jest.fn(() => 1662372000000);
|
||||
|
||||
const streak1 = await updateStreak("TestID", 1662372000000);
|
||||
|
||||
await expect(streak1).toBe(1);
|
||||
|
||||
Date.now = jest.fn(() => 1662458400000);
|
||||
|
||||
const streak2 = await updateStreak("TestID", 1662458400000);
|
||||
|
||||
await expect(streak2).toBe(2);
|
||||
|
||||
Date.now = jest.fn(() => 1999969721000000);
|
||||
|
||||
const streak3 = await updateStreak("TestID", 1999969721000);
|
||||
|
||||
await expect(streak3).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
|||
// These percentages should never decrease
|
||||
statements: 40,
|
||||
branches: 40,
|
||||
functions: 24,
|
||||
functions: 25,
|
||||
lines: 43,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@ import * as PublicStatsDAL from "../../dal/public-stats";
|
|||
import {
|
||||
getCurrentDayTimestamp,
|
||||
getStartOfDayTimestamp,
|
||||
mapRange,
|
||||
roundTo2,
|
||||
stdDev,
|
||||
} from "../../utils/misc";
|
||||
|
@ -35,6 +36,7 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards";
|
|||
import AutoRoleList from "../../constants/auto-roles";
|
||||
import * as UserDAL from "../../dal/user";
|
||||
import { buildMonkeyMail } from "../../utils/monkey-mail";
|
||||
import { updateStreak } from "./user";
|
||||
|
||||
try {
|
||||
if (anticheatImplemented() === false) throw new Error("undefined");
|
||||
|
@ -96,6 +98,7 @@ interface AddResultData {
|
|||
xp: number;
|
||||
dailyXpBonus: boolean;
|
||||
xpBreakdown: Record<string, number>;
|
||||
streak: number;
|
||||
}
|
||||
|
||||
export async function addResult(
|
||||
|
@ -368,11 +371,14 @@ export async function addResult(
|
|||
);
|
||||
}
|
||||
|
||||
const streak = await updateStreak(uid, result.timestamp);
|
||||
|
||||
const xpGained = await calculateXp(
|
||||
result,
|
||||
req.ctx.configuration.users.xp,
|
||||
uid,
|
||||
user.xp ?? 0
|
||||
user.xp ?? 0,
|
||||
streak
|
||||
);
|
||||
|
||||
if (result.bailedOut === false) delete result.bailedOut;
|
||||
|
@ -410,6 +416,7 @@ export async function addResult(
|
|||
xp: xpGained.xp,
|
||||
dailyXpBonus: xpGained.dailyBonus ?? false,
|
||||
xpBreakdown: xpGained.breakdown ?? {},
|
||||
streak,
|
||||
};
|
||||
|
||||
if (dailyLeaderboardRank !== -1) {
|
||||
|
@ -430,7 +437,8 @@ async function calculateXp(
|
|||
result,
|
||||
xpConfiguration: MonkeyTypes.Configuration["users"]["xp"],
|
||||
uid: string,
|
||||
currentTotalXp: number
|
||||
currentTotalXp: number,
|
||||
streak: number
|
||||
): Promise<XpResult> {
|
||||
const {
|
||||
mode,
|
||||
|
@ -488,6 +496,24 @@ async function calculateXp(
|
|||
}
|
||||
}
|
||||
|
||||
if (xpConfiguration.streak.enabled) {
|
||||
const streakModifier = parseFloat(
|
||||
mapRange(
|
||||
streak,
|
||||
0,
|
||||
xpConfiguration.streak.maxStreakDays,
|
||||
0,
|
||||
xpConfiguration.streak.maxStreakMultiplier,
|
||||
true
|
||||
).toFixed(1)
|
||||
);
|
||||
|
||||
if (streakModifier > 0) {
|
||||
modifier += streakModifier;
|
||||
breakdown["streak"] = Math.round(baseXp * streakModifier);
|
||||
}
|
||||
}
|
||||
|
||||
const incompleteXp = Math.round(incompleteTestSeconds);
|
||||
breakdown["incomplete"] = incompleteXp;
|
||||
|
||||
|
|
|
@ -4,7 +4,12 @@ import MonkeyError from "../../utils/error";
|
|||
import Logger from "../../utils/logger";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import { getDiscordUser } from "../../utils/discord";
|
||||
import { buildAgentLog, sanitizeString } from "../../utils/misc";
|
||||
import {
|
||||
buildAgentLog,
|
||||
isToday,
|
||||
isYesterday,
|
||||
sanitizeString,
|
||||
} from "../../utils/misc";
|
||||
import * as George from "../../tasks/george";
|
||||
import admin from "firebase-admin";
|
||||
import { deleteAllApeKeys } from "../../dal/ape-keys";
|
||||
|
@ -435,6 +440,7 @@ export async function getProfile(
|
|||
discordId,
|
||||
discordAvatar,
|
||||
xp,
|
||||
streak,
|
||||
} = await UserDAL.getUser(uid, "get user profile");
|
||||
|
||||
const validTimePbs = _.pick(personalBests?.time, "15", "30", "60", "120");
|
||||
|
@ -460,6 +466,7 @@ export async function getProfile(
|
|||
discordId,
|
||||
discordAvatar,
|
||||
xp,
|
||||
streak: streak?.length ?? 0,
|
||||
};
|
||||
|
||||
if (banned) {
|
||||
|
@ -526,3 +533,22 @@ export async function updateInbox(
|
|||
|
||||
return new MonkeyResponse("Inbox updated");
|
||||
}
|
||||
|
||||
export async function updateStreak(uid, timestamp): Promise<number> {
|
||||
const user = await UserDAL.getUser(uid, "calculate streak");
|
||||
const streak: MonkeyTypes.UserStreak = {
|
||||
lastResultTimestamp: user.streak?.lastResultTimestamp ?? 0,
|
||||
length: user.streak?.length ?? 0,
|
||||
};
|
||||
|
||||
if (isYesterday(streak.lastResultTimestamp)) {
|
||||
streak.length += 1;
|
||||
} else if (!isToday(streak.lastResultTimestamp)) {
|
||||
streak.length = 1;
|
||||
}
|
||||
|
||||
streak.lastResultTimestamp = timestamp;
|
||||
await UserDAL.getUsersCollection().updateOne({ uid }, { $set: { streak } });
|
||||
|
||||
return streak.length;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,11 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
gainMultiplier: 0,
|
||||
maxDailyBonus: 0,
|
||||
minDailyBonus: 0,
|
||||
streak: {
|
||||
enabled: false,
|
||||
maxStreakDays: 0,
|
||||
maxStreakMultiplier: 0,
|
||||
},
|
||||
},
|
||||
inbox: {
|
||||
enabled: false,
|
||||
|
@ -229,6 +234,24 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = {
|
|||
type: "number",
|
||||
label: "Min Daily Bonus",
|
||||
},
|
||||
streak: {
|
||||
type: "object",
|
||||
label: "Streak",
|
||||
fields: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
},
|
||||
maxStreakDays: {
|
||||
type: "number",
|
||||
label: "Max Streak Days",
|
||||
},
|
||||
maxStreakMultiplier: {
|
||||
type: "number",
|
||||
label: "Max Streak Multiplier",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discordIntegration: {
|
||||
|
|
|
@ -70,6 +70,10 @@ export async function resetUser(uid: string): Promise<void> {
|
|||
customThemes: [],
|
||||
tags: [],
|
||||
xp: 0,
|
||||
streak: {
|
||||
length: 0,
|
||||
lastResultTimestamp: 0,
|
||||
},
|
||||
},
|
||||
$unset: {
|
||||
discordAvatar: "",
|
||||
|
|
11
backend/src/types/types.d.ts
vendored
11
backend/src/types/types.d.ts
vendored
|
@ -45,6 +45,11 @@ declare namespace MonkeyTypes {
|
|||
gainMultiplier: number;
|
||||
maxDailyBonus: number;
|
||||
minDailyBonus: number;
|
||||
streak: {
|
||||
enabled: boolean;
|
||||
maxStreakDays: number;
|
||||
maxStreakMultiplier: number;
|
||||
};
|
||||
};
|
||||
inbox: {
|
||||
enabled: boolean;
|
||||
|
@ -161,6 +166,12 @@ declare namespace MonkeyTypes {
|
|||
inventory?: UserInventory;
|
||||
xp?: number;
|
||||
inbox?: MonkeyMail[];
|
||||
streak?: UserStreak;
|
||||
}
|
||||
|
||||
interface UserStreak {
|
||||
lastResultTimestamp: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface UserInventory {
|
||||
|
|
|
@ -165,12 +165,38 @@ export function getOrdinalNumberString(number: number): string {
|
|||
return `${number}${suffix}`;
|
||||
}
|
||||
|
||||
export function isYesterday(timestamp: number): boolean {
|
||||
const yesterday = getStartOfDayTimestamp(Date.now() - MILLISECONDS_IN_DAY);
|
||||
const date = getStartOfDayTimestamp(timestamp);
|
||||
|
||||
return yesterday === date;
|
||||
}
|
||||
|
||||
export function isToday(timestamp: number): boolean {
|
||||
const today = getStartOfDayTimestamp(Date.now());
|
||||
const date = getStartOfDayTimestamp(timestamp);
|
||||
|
||||
return today === date;
|
||||
}
|
||||
|
||||
export function mapRange(
|
||||
value: number,
|
||||
inMin: number,
|
||||
inMax: number,
|
||||
outMin: number,
|
||||
outMax: number
|
||||
outMax: number,
|
||||
clamp = false
|
||||
): number {
|
||||
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
||||
const result =
|
||||
((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
||||
|
||||
if (clamp) {
|
||||
if (outMin < outMax) {
|
||||
return Math.min(Math.max(result, outMin), outMax);
|
||||
} else {
|
||||
return Math.max(Math.min(result, outMin), outMax);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -292,8 +292,14 @@
|
|||
font-size: 0.7em;
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
.streak {
|
||||
color: var(--sub-color);
|
||||
font-size: 0.7em;
|
||||
line-height: 0.7rem;
|
||||
}
|
||||
.badge,
|
||||
.joined {
|
||||
.joined,
|
||||
.streak {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
.levelAndBar {
|
||||
|
|
|
@ -27,4 +27,5 @@ export const defaultSnap: MonkeyTypes.Snapshot = {
|
|||
addedAt: 0,
|
||||
filterPresets: [],
|
||||
xp: 0,
|
||||
streak: 0,
|
||||
};
|
||||
|
|
|
@ -96,6 +96,7 @@ export async function initSnapshot(): Promise<
|
|||
snap.addedAt = userData.addedAt;
|
||||
snap.inventory = userData.inventory;
|
||||
snap.xp = userData.xp ?? 0;
|
||||
snap.streak = userData?.streak?.length ?? 0;
|
||||
|
||||
if (userData.lbMemory?.time15 || userData.lbMemory?.time60) {
|
||||
//old memory format
|
||||
|
@ -814,6 +815,12 @@ export function addXp(xp: number): void {
|
|||
setSnapshot(snapshot);
|
||||
}
|
||||
|
||||
export function setStreak(streak: number): void {
|
||||
const snapshot = getSnapshot();
|
||||
snapshot.streak = streak;
|
||||
setSnapshot(snapshot);
|
||||
}
|
||||
|
||||
// export async function DB.getLocalTagPB(tagId) {
|
||||
// function cont() {
|
||||
// let ret = 0;
|
||||
|
|
|
@ -276,6 +276,14 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skipBreakdown) return;
|
||||
|
||||
if (breakdown["streak"]) {
|
||||
await Misc.sleep(delay);
|
||||
await append(`streak +${breakdown["streak"]}`);
|
||||
total += breakdown["streak"];
|
||||
}
|
||||
|
||||
if (skipBreakdown) return;
|
||||
|
||||
if (breakdown["accPenalty"]) {
|
||||
await Misc.sleep(delay);
|
||||
await append(`accuracy penalty -${breakdown["accPenalty"]}`);
|
||||
|
|
|
@ -72,6 +72,18 @@ export async function update(
|
|||
const balloonText = `${diffDays} day${diffDays != 1 ? "s" : ""} ago`;
|
||||
details.find(".joined").text(joinedText).attr("aria-label", balloonText);
|
||||
|
||||
if (profile.streak && profile?.streak > 1) {
|
||||
details
|
||||
.find(".streak")
|
||||
.text(
|
||||
`Current streak: ${profile.streak} ${
|
||||
profile.streak === 1 ? "day" : "days"
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
details.find(".streak").text("");
|
||||
}
|
||||
|
||||
const typingStatsEl = details.find(".typingStats");
|
||||
typingStatsEl
|
||||
.find(".started .value")
|
||||
|
|
|
@ -20,6 +20,7 @@ function reset(): void {
|
|||
<div class="badges"></div>
|
||||
<div class="allBadges"></div>
|
||||
<div class="joined" data-balloon-pos="up">-</div>
|
||||
<div class="streak">-</div>
|
||||
</div>
|
||||
<div class="levelAndBar">
|
||||
<div class="level">-</div>
|
||||
|
|
|
@ -1698,6 +1698,10 @@ async function saveResult(
|
|||
DB.addXp(response.data.xp);
|
||||
}
|
||||
|
||||
if (response.data.streak) {
|
||||
DB.setStreak(response.data.streak);
|
||||
}
|
||||
|
||||
completedEvent._id = response.data.insertedId;
|
||||
if (response.data.isPb) {
|
||||
completedEvent.isPb = true;
|
||||
|
|
1
frontend/src/ts/types/types.d.ts
vendored
1
frontend/src/ts/types/types.d.ts
vendored
|
@ -473,6 +473,7 @@ declare namespace MonkeyTypes {
|
|||
addedAt: number;
|
||||
filterPresets: ResultFilters[];
|
||||
xp: number;
|
||||
streak: number;
|
||||
}
|
||||
|
||||
interface UserDetails {
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
<div class="badges"></div>
|
||||
<div class="allBadges"></div>
|
||||
<div class="joined" data-balloon-pos="up">-</div>
|
||||
<div class="streak">-</div>
|
||||
</div>
|
||||
<div class="levelAndBar">
|
||||
<div class="level" data-balloon-pos="up">-</div>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<div class="badges"></div>
|
||||
<div class="allBadges"></div>
|
||||
<div class="joined" data-balloon-pos="up">-</div>
|
||||
<div class="streak">-</div>
|
||||
</div>
|
||||
<div class="levelAndBar">
|
||||
<div class="level">-</div>
|
||||
|
|
Loading…
Reference in a new issue