WIP saving games
This commit is contained in:
parent
d31e17e3be
commit
632351031f
|
@ -13,6 +13,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"date-fns-tz": "^1.2.2",
|
"date-fns-tz": "^1.2.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"vue": "^3.2.31",
|
"vue": "^3.2.31",
|
||||||
"vue-i18n": "^9.1.9",
|
"vue-i18n": "^9.1.9",
|
||||||
"vue-router": "^4.0.12"
|
"vue-router": "^4.0.12"
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
"@iconify-json/ph": "^1.1.0",
|
"@iconify-json/ph": "^1.1.0",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^3.3.0",
|
"@intlify/vite-plugin-vue-i18n": "^3.3.0",
|
||||||
"@rushstack/eslint-patch": "^1.1.0",
|
"@rushstack/eslint-patch": "^1.1.0",
|
||||||
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/node": "^16.11.25",
|
"@types/node": "^16.11.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||||
"@typescript-eslint/parser": "5.0.0",
|
"@typescript-eslint/parser": "5.0.0",
|
||||||
|
|
|
@ -4,6 +4,7 @@ specifiers:
|
||||||
'@iconify-json/ph': ^1.1.0
|
'@iconify-json/ph': ^1.1.0
|
||||||
'@intlify/vite-plugin-vue-i18n': ^3.3.0
|
'@intlify/vite-plugin-vue-i18n': ^3.3.0
|
||||||
'@rushstack/eslint-patch': ^1.1.0
|
'@rushstack/eslint-patch': ^1.1.0
|
||||||
|
'@types/lodash-es': ^4.17.6
|
||||||
'@types/node': ^16.11.25
|
'@types/node': ^16.11.25
|
||||||
'@typescript-eslint/eslint-plugin': ^5.12.0
|
'@typescript-eslint/eslint-plugin': ^5.12.0
|
||||||
'@typescript-eslint/parser': 5.0.0
|
'@typescript-eslint/parser': 5.0.0
|
||||||
|
@ -23,6 +24,7 @@ specifiers:
|
||||||
eslint-plugin-tailwindcss: ^3.4.4
|
eslint-plugin-tailwindcss: ^3.4.4
|
||||||
eslint-plugin-vue: ^8.4.1
|
eslint-plugin-vue: ^8.4.1
|
||||||
jsdom: ^19.0.0
|
jsdom: ^19.0.0
|
||||||
|
lodash-es: ^4.17.21
|
||||||
postcss: ^8.4.6
|
postcss: ^8.4.6
|
||||||
prettier: ^2.5.1
|
prettier: ^2.5.1
|
||||||
prettier-eslint: ^13.0.0
|
prettier-eslint: ^13.0.0
|
||||||
|
@ -41,6 +43,7 @@ specifiers:
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns: 2.28.0
|
date-fns: 2.28.0
|
||||||
date-fns-tz: 1.2.2_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: 3.2.31
|
||||||
vue-i18n: 9.1.9_vue@3.2.31
|
vue-i18n: 9.1.9_vue@3.2.31
|
||||||
vue-router: 4.0.12_vue@3.2.31
|
vue-router: 4.0.12_vue@3.2.31
|
||||||
|
@ -49,6 +52,7 @@ devDependencies:
|
||||||
'@iconify-json/ph': 1.1.0
|
'@iconify-json/ph': 1.1.0
|
||||||
'@intlify/vite-plugin-vue-i18n': 3.3.0_vite@2.8.4+vue-i18n@9.1.9
|
'@intlify/vite-plugin-vue-i18n': 3.3.0_vite@2.8.4+vue-i18n@9.1.9
|
||||||
'@rushstack/eslint-patch': 1.1.0
|
'@rushstack/eslint-patch': 1.1.0
|
||||||
|
'@types/lodash-es': 4.17.6
|
||||||
'@types/node': 16.11.25
|
'@types/node': 16.11.25
|
||||||
'@typescript-eslint/eslint-plugin': 5.12.0_ae020354c3da76ce329e71c9084ef5bf
|
'@typescript-eslint/eslint-plugin': 5.12.0_ae020354c3da76ce329e71c9084ef5bf
|
||||||
'@typescript-eslint/parser': 5.0.0_eslint@7.32.0+typescript@4.5.5
|
'@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=}
|
resolution: {integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4=}
|
||||||
dev: true
|
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:
|
/@types/node/16.11.25:
|
||||||
resolution: {integrity: sha512-NrTwfD7L1RTc2qrHQD4RTTy4p0CO2LatKBEKEds3CaVuhoM/+DJzmWZl5f+ikR8cm8F5mfJxK+9rQq07gRiSjQ==}
|
resolution: {integrity: sha512-NrTwfD7L1RTc2qrHQD4RTTy4p0CO2LatKBEKEds3CaVuhoM/+DJzmWZl5f+ikR8cm8F5mfJxK+9rQq07gRiSjQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4056,6 +4070,10 @@ packages:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash-es/4.17.21:
|
||||||
|
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.debounce/4.0.8:
|
/lodash.debounce/4.0.8:
|
||||||
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
|
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
11
src/App.vue
11
src/App.vue
|
@ -21,14 +21,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
|
import SideMenu from '@/components/SideMenu.vue'
|
||||||
import { darkMode } from '@/composables/settings'
|
import { darkMode } from '@/composables/settings'
|
||||||
|
import { LSK_DARKMODE } from '@/globals'
|
||||||
import AppHeader from './components/AppHeader.vue'
|
import { getItem } from '@/storage'
|
||||||
import SideMenu from './components/SideMenu.vue'
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
darkMode.value = localStorage.getItem('n0_dark')
|
darkMode.value = JSON.parse(getItem(LSK_DARKMODE, 'true'))
|
||||||
? localStorage.getItem('n0_dark') === 'true'
|
|
||||||
: true
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { LSK_DARKMODE } from '@/globals'
|
||||||
|
import { setItem } from '@/storage'
|
||||||
|
|
||||||
export const darkMode = ref(true)
|
export const darkMode = ref(true)
|
||||||
|
|
||||||
watch(darkMode, val => {
|
watch(darkMode, val => {
|
||||||
|
@ -9,7 +12,7 @@ watch(darkMode, val => {
|
||||||
else {
|
else {
|
||||||
document.documentElement.classList.remove('dark')
|
document.documentElement.classList.remove('dark')
|
||||||
}
|
}
|
||||||
localStorage.setItem('n0_dark', val ? 'true' : 'false')
|
setItem(LSK_DARKMODE, val ? 'true' : 'false')
|
||||||
})
|
})
|
||||||
|
|
||||||
export function isDarkModeDefault(): boolean {
|
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 GAME_STARTING_DATE = '2022-02-18'
|
||||||
export const BXL_TZ = 'Europe/Brussels'
|
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 operators = ['+', '-', '*', '/'] as const
|
||||||
|
|
||||||
export const pools = {
|
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.",
|
"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.",
|
"dailyDescription": "Le défi quotidien change chaque jour à minuit, et est commun à tous les joueurs.",
|
||||||
"randomDescription": "Une partie au hasard, pour le plaisir.",
|
"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
|
operator: OperatorType | null
|
||||||
result: Plaquette | 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)
|
return utcToZonedTime(new Date(), BXL_TZ)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCurrentSessionKey(): string {
|
||||||
|
return getCurrentDate().toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
// #region RNG
|
// #region RNG
|
||||||
|
|
||||||
export let random = Math.random
|
export let random = Math.random
|
||||||
|
@ -60,7 +64,7 @@ export function shuffle<T>(array: T[]): T[] {
|
||||||
function initDailyPRNG(): () => number {
|
function initDailyPRNG(): () => number {
|
||||||
// Prefix the seed when in dev to avoid spoiling myself while working on it
|
// Prefix the seed when in dev to avoid spoiling myself while working on it
|
||||||
const prefix = import.meta.env.DEV ? 'dev-' : ''
|
const prefix = import.meta.env.DEV ? 'dev-' : ''
|
||||||
const hashed = hashStr(prefix + getCurrentDate().toISOString().slice(0, 10))
|
const hashed = hashStr(prefix + getCurrentSessionKey())
|
||||||
return mulberry32(hashed)
|
return mulberry32(hashed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getEmptyOperation,
|
getEmptyOperation,
|
||||||
|
@ -84,7 +84,7 @@ import OperationsList from '@/components/OperationsList.vue'
|
||||||
import OperatorsList from '@/components/OperatorsList.vue'
|
import OperatorsList from '@/components/OperatorsList.vue'
|
||||||
import PlaquettesList from '@/components/PlaquettesList.vue'
|
import PlaquettesList from '@/components/PlaquettesList.vue'
|
||||||
import {
|
import {
|
||||||
clearOperationsList,
|
clearOperationsList,
|
||||||
currentOperation,
|
currentOperation,
|
||||||
gameIsRunning,
|
gameIsRunning,
|
||||||
gameState,
|
gameState,
|
||||||
|
@ -94,9 +94,17 @@ clearOperationsList,
|
||||||
plaquettes,
|
plaquettes,
|
||||||
result,
|
result,
|
||||||
} from '@/composables/game-state'
|
} from '@/composables/game-state'
|
||||||
|
import { hasPlayed } from '@/composables/statistics'
|
||||||
import { GameState, pools } from '@/globals'
|
import { GameState, pools } from '@/globals'
|
||||||
import { OperatorType, Plaquette } from '@/types'
|
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
|
const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning
|
||||||
|
|
||||||
|
@ -219,6 +227,12 @@ function reboot(): void {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (!isDaily.value && !hasPlayed(getCurrentSessionKey())) {
|
||||||
|
const router = useRouter()
|
||||||
|
router.replace({ name: 'home' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isDaily.value) {
|
if (isDaily.value) {
|
||||||
console.log('daily rng')
|
console.log('daily rng')
|
||||||
setDailyPRNG()
|
setDailyPRNG()
|
||||||
|
|
|
@ -18,18 +18,29 @@
|
||||||
<!-- Random -->
|
<!-- Random -->
|
||||||
<div class="flex flex-col gap-4 items-center">
|
<div class="flex flex-col gap-4 items-center">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/random"
|
:to="canPlayRandom ? '/random' : ''"
|
||||||
class="text-2xl btn-border">
|
class="text-2xl btn-border"
|
||||||
|
:class="{ 'btn-disabled': !canPlayRandom }">
|
||||||
{{ t('randomGame') }}
|
{{ t('randomGame') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
{{ t('randomDescription') }}
|
{{ t('randomDescription') }}
|
||||||
|
<span
|
||||||
|
class="italic"
|
||||||
|
v-if="!canPlayRandom">{{
|
||||||
|
t('finishDailyToPlayRandom')
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { hasPlayed } from '@/composables/statistics'
|
||||||
|
import { getCurrentSessionKey } from '@/utils'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const canPlayRandom = computed(() => hasPlayed(getCurrentSessionKey()))
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user