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