diff --git a/package.json b/package.json index 075aa8f..d3218aa 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "date-fns": "^2.28.0", "date-fns-tz": "^1.2.2", + "lodash-es": "^4.17.21", "vue": "^3.2.31", "vue-i18n": "^9.1.9", "vue-router": "^4.0.12" @@ -21,6 +22,7 @@ "@iconify-json/ph": "^1.1.0", "@intlify/vite-plugin-vue-i18n": "^3.3.0", "@rushstack/eslint-patch": "^1.1.0", + "@types/lodash-es": "^4.17.6", "@types/node": "^16.11.25", "@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/parser": "5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dde484b..ea28e7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ specifiers: '@iconify-json/ph': ^1.1.0 '@intlify/vite-plugin-vue-i18n': ^3.3.0 '@rushstack/eslint-patch': ^1.1.0 + '@types/lodash-es': ^4.17.6 '@types/node': ^16.11.25 '@typescript-eslint/eslint-plugin': ^5.12.0 '@typescript-eslint/parser': 5.0.0 @@ -23,6 +24,7 @@ specifiers: eslint-plugin-tailwindcss: ^3.4.4 eslint-plugin-vue: ^8.4.1 jsdom: ^19.0.0 + lodash-es: ^4.17.21 postcss: ^8.4.6 prettier: ^2.5.1 prettier-eslint: ^13.0.0 @@ -41,6 +43,7 @@ specifiers: dependencies: date-fns: 2.28.0 date-fns-tz: 1.2.2_date-fns@2.28.0 + lodash-es: 4.17.21 vue: 3.2.31 vue-i18n: 9.1.9_vue@3.2.31 vue-router: 4.0.12_vue@3.2.31 @@ -49,6 +52,7 @@ devDependencies: '@iconify-json/ph': 1.1.0 '@intlify/vite-plugin-vue-i18n': 3.3.0_vite@2.8.4+vue-i18n@9.1.9 '@rushstack/eslint-patch': 1.1.0 + '@types/lodash-es': 4.17.6 '@types/node': 16.11.25 '@typescript-eslint/eslint-plugin': 5.12.0_ae020354c3da76ce329e71c9084ef5bf '@typescript-eslint/parser': 5.0.0_eslint@7.32.0+typescript@4.5.5 @@ -1574,6 +1578,16 @@ packages: resolution: {integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4=} dev: true + /@types/lodash-es/4.17.6: + resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} + dependencies: + '@types/lodash': 4.14.178 + dev: true + + /@types/lodash/4.14.178: + resolution: {integrity: sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==} + dev: true + /@types/node/16.11.25: resolution: {integrity: sha512-NrTwfD7L1RTc2qrHQD4RTTy4p0CO2LatKBEKEds3CaVuhoM/+DJzmWZl5f+ikR8cm8F5mfJxK+9rQq07gRiSjQ==} dev: true @@ -4056,6 +4070,10 @@ packages: p-locate: 5.0.0 dev: true + /lodash-es/4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash.debounce/4.0.8: resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=} dev: true diff --git a/src/App.vue b/src/App.vue index 69d2808..bec75e1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -21,14 +21,13 @@ diff --git a/src/composables/settings.ts b/src/composables/settings.ts index 8bc30f7..1e5e15b 100644 --- a/src/composables/settings.ts +++ b/src/composables/settings.ts @@ -1,5 +1,8 @@ import { ref, watch } from 'vue' +import { LSK_DARKMODE } from '@/globals' +import { setItem } from '@/storage' + export const darkMode = ref(true) watch(darkMode, val => { @@ -9,7 +12,7 @@ watch(darkMode, val => { else { document.documentElement.classList.remove('dark') } - localStorage.setItem('n0_dark', val ? 'true' : 'false') + setItem(LSK_DARKMODE, val ? 'true' : 'false') }) export function isDarkModeDefault(): boolean { diff --git a/src/composables/statistics.ts b/src/composables/statistics.ts new file mode 100644 index 0000000..e13e5e2 --- /dev/null +++ b/src/composables/statistics.ts @@ -0,0 +1,76 @@ +import { merge } from 'lodash-es' +import { reactive, watch } from 'vue' + +// import { plausible } from '@/analytics' +import { LSK_STATS } from '@/globals' +import * as storage from '@/storage' +import { GameStats } from '@/types' +// import { getCurrentSessionKey } from '@/utils' + +// import { countTotalGuesses, isWinner } from './game-state' + +export const gameStats = reactive(loadStats()) + +// Triggered when the list of played games has changed +watch( + () => gameStats.games, + games => { + const keys = Object.keys(games).sort() + + // Recompute games count + gameStats.bestStreak = 0 + gameStats.currentStreak = 0 + gameStats.nbGames = keys.length + for (const key of keys) { + if (gameStats.games[key].won) { + if (++gameStats.currentStreak > gameStats.bestStreak) { + gameStats.bestStreak = gameStats.currentStreak + } + } + else { + gameStats.currentStreak = 0 + } + } + + // Automatically save stats in storage when updated + storage.setItem(LSK_STATS, JSON.stringify(gameStats)) + }, + { deep: true, immediate: true }, +) + +function loadStats(): GameStats { + const stats: GameStats = { + bestStreak: 0, + currentStreak: 0, + nbGames: 0, + games: {}, + } + const loaded = (() => { + try { + return JSON.parse(storage.getItem(LSK_STATS)!) + } + catch (e) { + return {} + } + })() + merge(stats, loaded) + + return stats +} + +function setScore(seed: string, won: boolean, score: number): void { + // Don't overwrite an existing score + if (!gameStats.games[seed]) { + gameStats.games[seed] = { score, won } + // plausible.trackEvent(won ? 'win_game' : 'lose_game') + // plausible.trackEvent('end_game') + } +} + +export function hasPlayed(seed:string): boolean { + return !!gameStats.games[seed] +} + +// export function saveScore(): void { +// setScore(getCurrentSessionKey(), isWinner.value, countTotalGuesses.value) +// } diff --git a/src/globals.ts b/src/globals.ts index c01e8c3..8307564 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -1,6 +1,9 @@ export const GAME_STARTING_DATE = '2022-02-18' export const BXL_TZ = 'Europe/Brussels' +export const LSK_DARKMODE = 'n0_dark' +export const LSK_STATS = 'n0_stats' + export const operators = ['+', '-', '*', '/'] as const export const pools = { diff --git a/src/locales/fr.json b/src/locales/fr.json index 5ffccd2..d5695ca 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -12,5 +12,6 @@ "gameDescription": "Combinez les nombres imposés afin d'atteindre le résultat, ou de vous en approcher le plus possible.", "dailyDescription": "Le défi quotidien change chaque jour à minuit, et est commun à tous les joueurs.", "randomDescription": "Une partie au hasard, pour le plaisir.", - "share": "Partager" + "share": "Partager", + "finishDailyToPlayRandom": "Terminez le défi quotidien pour débloquer." } diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..3519970 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,16 @@ +import { hasPlayed } from './composables/statistics' +import { getCurrentSessionKey } from './utils' + +export function setItem(k: string, v: string): void { + return localStorage.setItem(k, v) +} +export function getItem(k: string): string | null +export function getItem(k: string, defaultValue: any): string +export function getItem(k: string, defaultValue?: any): string | null { + try { + return localStorage.getItem(k) ?? null + } + catch (e) { + return defaultValue ?? null + } +} diff --git a/src/types.ts b/src/types.ts index 5413f88..a6bce34 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,3 +11,10 @@ export type Operation = { operator: OperatorType | null result: Plaquette | null } + +export type GameStats = { + bestStreak: number + currentStreak: number + nbGames: number + games: { [key: string]: { score: number; won: boolean } } +} diff --git a/src/utils.ts b/src/utils.ts index 6a5ea75..a7d0bbe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,6 +6,10 @@ export function getCurrentDate(): Date { return utcToZonedTime(new Date(), BXL_TZ) } +export function getCurrentSessionKey(): string { + return getCurrentDate().toISOString().slice(0, 10) +} + // #region RNG export let random = Math.random @@ -60,7 +64,7 @@ export function shuffle(array: T[]): T[] { function initDailyPRNG(): () => number { // Prefix the seed when in dev to avoid spoiling myself while working on it const prefix = import.meta.env.DEV ? 'dev-' : '' - const hashed = hashStr(prefix + getCurrentDate().toISOString().slice(0, 10)) + const hashed = hashStr(prefix + getCurrentSessionKey()) return mulberry32(hashed) } diff --git a/src/views/GameView.vue b/src/views/GameView.vue index f93b68c..e06cbdb 100644 --- a/src/views/GameView.vue +++ b/src/views/GameView.vue @@ -71,7 +71,7 @@