WIP saving games

This commit is contained in:
Simon Cambier 2022-02-22 22:52:50 +01:00
parent d31e17e3be
commit 632351031f
12 changed files with 168 additions and 14 deletions

View File

@ -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",

View File

@ -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

View File

@ -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>

View File

@ -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 {

View 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)
// }

View File

@ -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 = {

View File

@ -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
View 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
}
}

View File

@ -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 } }
}

View File

@ -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)
} }

View File

@ -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()

View File

@ -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>