WIP saving games
This commit is contained in:
parent
d31e17e3be
commit
632351031f
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
11
src/App.vue
11
src/App.vue
|
@ -21,14 +21,13 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import SideMenu from '@/components/SideMenu.vue'
|
||||
import { darkMode } from '@/composables/settings'
|
||||
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import SideMenu from './components/SideMenu.vue'
|
||||
import { LSK_DARKMODE } from '@/globals'
|
||||
import { getItem } from '@/storage'
|
||||
|
||||
onMounted(() => {
|
||||
darkMode.value = localStorage.getItem('n0_dark')
|
||||
? localStorage.getItem('n0_dark') === 'true'
|
||||
: true
|
||||
darkMode.value = JSON.parse(getItem(LSK_DARKMODE, 'true'))
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -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 {
|
||||
|
|
76
src/composables/statistics.ts
Normal file
76
src/composables/statistics.ts
Normal file
|
@ -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<GameStats>(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)
|
||||
// }
|
|
@ -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 = {
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
16
src/storage.ts
Normal file
16
src/storage.ts
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 } }
|
||||
}
|
||||
|
|
|
@ -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<T>(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)
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
getEmptyOperation,
|
||||
|
@ -84,7 +84,7 @@ import OperationsList from '@/components/OperationsList.vue'
|
|||
import OperatorsList from '@/components/OperatorsList.vue'
|
||||
import PlaquettesList from '@/components/PlaquettesList.vue'
|
||||
import {
|
||||
clearOperationsList,
|
||||
clearOperationsList,
|
||||
currentOperation,
|
||||
gameIsRunning,
|
||||
gameState,
|
||||
|
@ -94,9 +94,17 @@ clearOperationsList,
|
|||
plaquettes,
|
||||
result,
|
||||
} from '@/composables/game-state'
|
||||
import { hasPlayed } from '@/composables/statistics'
|
||||
import { GameState, pools } from '@/globals'
|
||||
import { OperatorType, Plaquette } from '@/types'
|
||||
import { randItem, random, randRange, setDailyPRNG, setMathPRNG } from '@/utils'
|
||||
import {
|
||||
getCurrentSessionKey,
|
||||
randItem,
|
||||
random,
|
||||
randRange,
|
||||
setDailyPRNG,
|
||||
setMathPRNG,
|
||||
} from '@/utils'
|
||||
|
||||
const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning
|
||||
|
||||
|
@ -219,6 +227,12 @@ function reboot(): void {
|
|||
*/
|
||||
|
||||
onMounted(() => {
|
||||
if (!isDaily.value && !hasPlayed(getCurrentSessionKey())) {
|
||||
const router = useRouter()
|
||||
router.replace({ name: 'home' })
|
||||
return
|
||||
}
|
||||
|
||||
if (isDaily.value) {
|
||||
console.log('daily rng')
|
||||
setDailyPRNG()
|
||||
|
|
|
@ -18,18 +18,29 @@
|
|||
<!-- Random -->
|
||||
<div class="flex flex-col gap-4 items-center">
|
||||
<RouterLink
|
||||
to="/random"
|
||||
class="text-2xl btn-border">
|
||||
:to="canPlayRandom ? '/random' : ''"
|
||||
class="text-2xl btn-border"
|
||||
:class="{ 'btn-disabled': !canPlayRandom }">
|
||||
{{ t('randomGame') }}
|
||||
</RouterLink>
|
||||
{{ t('randomDescription') }}
|
||||
<span
|
||||
class="italic"
|
||||
v-if="!canPlayRandom">{{
|
||||
t('finishDailyToPlayRandom')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { hasPlayed } from '@/composables/statistics'
|
||||
import { getCurrentSessionKey } from '@/utils'
|
||||
|
||||
const { t } = useI18n()
|
||||
const canPlayRandom = computed(() => hasPlayed(getCurrentSessionKey()))
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue
Block a user