Normal and Hard modes
This commit is contained in:
		
							parent
							
								
									1909b75663
								
							
						
					
					
						commit
						80064a98e5
					
				| 
						 | 
				
			
			@ -147,7 +147,7 @@ export function isSolvable(result: number, plaquettes: number[]): boolean {
 | 
			
		|||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (histories.length) {
 | 
			
		||||
  if (histories.length && import.meta.env.DEV) {
 | 
			
		||||
    printHistory(histories[0])
 | 
			
		||||
  }
 | 
			
		||||
  return found
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,7 +72,7 @@ import {
 | 
			
		|||
  result,
 | 
			
		||||
} from '@/composables/game-state'
 | 
			
		||||
import { operatorIcons } from '@/composables/operators'
 | 
			
		||||
import { GameState } from '@/globals'
 | 
			
		||||
import { GameStateType } from '@/globals'
 | 
			
		||||
import { Operation } from '@/types'
 | 
			
		||||
import IconEquals from '~icons/ph/equals-bold'
 | 
			
		||||
import IconSad from '~icons/ph/smiley-sad'
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +85,7 @@ function canOperationBeDeleted(op: Operation): boolean {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function undoOperation(index: number): void {
 | 
			
		||||
  if (gameState.value !== GameState.Playing) return
 | 
			
		||||
  if (gameState.value !== GameStateType.Playing) return
 | 
			
		||||
  const len = operations.length
 | 
			
		||||
  for (let i = len - 1; i >= index; --i) {
 | 
			
		||||
    let popped: Operation
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,10 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="grid grid-cols-6 grid-rows-2 gap-2 justify-center px-2 mx-auto max-w-sm font-mono">
 | 
			
		||||
    class="grid grid-rows-2 gap-2 justify-center px-2 mx-auto max-w-sm font-mono"
 | 
			
		||||
    :class="{
 | 
			
		||||
      'grid-cols-6': startingNumberOfPlaquettes === 6,
 | 
			
		||||
      'grid-cols-5': startingNumberOfPlaquettes === 5,
 | 
			
		||||
    }">
 | 
			
		||||
    <TransitionGroup name="slide_left">
 | 
			
		||||
      <PlaquetteBox
 | 
			
		||||
        v-for="(item, i) in plaquettes"
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +29,10 @@
 | 
			
		|||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
import PlaquetteBox from '@/components/common/PlaquetteBox.vue'
 | 
			
		||||
import {
 | 
			
		||||
  maxNumberOfOperations,
 | 
			
		||||
  startingNumberOfPlaquettes,
 | 
			
		||||
} from '@/composables/game-state'
 | 
			
		||||
import { Plaquette } from '@/types'
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,25 +3,45 @@ import { utcToZonedTime } from 'date-fns-tz'
 | 
			
		|||
import { computed, reactive, ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
import { getEmptyOperation, isOperationReady } from '@/algo'
 | 
			
		||||
import { BXL_TZ, GAME_STARTING_DATE, GameState } from '@/globals'
 | 
			
		||||
import {
 | 
			
		||||
  BXL_TZ,
 | 
			
		||||
  GAME_STARTING_DATE,
 | 
			
		||||
  GameDifficultyType,
 | 
			
		||||
  GameStateType,
 | 
			
		||||
} from '@/globals'
 | 
			
		||||
import { Operation, Plaquette } from '@/types'
 | 
			
		||||
import { getCurrentDate } from '@/utils'
 | 
			
		||||
 | 
			
		||||
export const gameState = ref(GameState.Undefined)
 | 
			
		||||
 | 
			
		||||
export const gameState = ref(GameStateType.Undefined)
 | 
			
		||||
export const operations = reactive<Operation[]>([getEmptyOperation()])
 | 
			
		||||
export const plaquettes = ref<Plaquette[]>([])
 | 
			
		||||
 | 
			
		||||
export const result = ref(0)
 | 
			
		||||
 | 
			
		||||
// #region Difficulty-related constants
 | 
			
		||||
 | 
			
		||||
export const gameDifficulty = ref(GameDifficultyType.Normal)
 | 
			
		||||
 | 
			
		||||
export const maxNumberOfOperations = computed(() =>
 | 
			
		||||
  gameDifficulty.value === GameDifficultyType.Hard ? 5 : 4,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export const startingNumberOfPlaquettes = computed(() =>
 | 
			
		||||
  gameDifficulty.value === GameDifficultyType.Hard ? 6 : 5,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// #endregion Difficulty-related constants
 | 
			
		||||
 | 
			
		||||
export const currentOperation = computed(
 | 
			
		||||
  () => operations[operations.length - 1],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export const gameIsRunning = computed(() => gameState.value > GameState.Loading)
 | 
			
		||||
export const gameIsRunning = computed(
 | 
			
		||||
  () => gameState.value > GameStateType.Loading,
 | 
			
		||||
)
 | 
			
		||||
export const isEndGame = computed(
 | 
			
		||||
  () =>
 | 
			
		||||
    (operations.length === 5 && isOperationReady(currentOperation.value)) ||
 | 
			
		||||
    (operations.length === maxNumberOfOperations.value &&
 | 
			
		||||
      isOperationReady(currentOperation.value)) ||
 | 
			
		||||
    isResultPerfect.value,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,9 +13,14 @@ export const pools = {
 | 
			
		|||
  3: [5, 5, 5, 5, 5, 2, 2, 2, 2, 2],
 | 
			
		||||
} as const
 | 
			
		||||
 | 
			
		||||
export enum GameState {
 | 
			
		||||
export enum GameStateType {
 | 
			
		||||
  Undefined = 0,
 | 
			
		||||
  Waiting = 1,
 | 
			
		||||
  Loading = 2,
 | 
			
		||||
  Playing = 3,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum GameDifficultyType {
 | 
			
		||||
  Normal = 1,
 | 
			
		||||
  Hard = 2,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,5 +15,7 @@
 | 
			
		|||
  "gameDescription": "Combine the numbers to reach the result.",
 | 
			
		||||
  "randomDescription": "A challenge at random, just for fun.",
 | 
			
		||||
  "copiedInClipboard": "Copied in clipboard",
 | 
			
		||||
  "soon": "Soon"
 | 
			
		||||
  "soon": "Soon",
 | 
			
		||||
  "dailyHard": "Hard Mode",
 | 
			
		||||
  "dailyNormal": "Normal Mode"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,5 +15,7 @@
 | 
			
		|||
  "share": "Partager",
 | 
			
		||||
  "finishDailyToPlayRandom": "Terminez le défi quotidien pour débloquer.",
 | 
			
		||||
  "copiedInClipboard": "Copié dans le presse-papier",
 | 
			
		||||
  "soon": "Bientôt"
 | 
			
		||||
  "soon": "Bientôt",
 | 
			
		||||
  "dailyNormal": "Mode Normal",
 | 
			
		||||
  "dailyHard": "Mode Difficile"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,14 +12,14 @@ const router = createRouter({
 | 
			
		|||
      meta: { transition: '' },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/daily',
 | 
			
		||||
      name: 'daily',
 | 
			
		||||
      path: '/hard',
 | 
			
		||||
      name: 'hard',
 | 
			
		||||
      component: () => import('../views/GameView.vue'),
 | 
			
		||||
      meta: { transition: 'route' },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/random',
 | 
			
		||||
      name: 'random',
 | 
			
		||||
      path: '/normal',
 | 
			
		||||
      name: 'normal',
 | 
			
		||||
      component: () => import('../views/GameView.vue'),
 | 
			
		||||
      meta: { transition: 'route' },
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								src/types.ts
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								src/types.ts
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -18,3 +18,23 @@ export type GameStats = {
 | 
			
		|||
  nbGames: number
 | 
			
		||||
  games: { [key: string]: { score: number; won: boolean } }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
type SerializableOperation = {
 | 
			
		||||
  left: number
 | 
			
		||||
  right: number
 | 
			
		||||
  operator: OperatorType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SrGame = {
 | 
			
		||||
  type: 'daily' | 'random'
 | 
			
		||||
  over: boolean
 | 
			
		||||
  won: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type GameState = {
 | 
			
		||||
  games: SrGame[]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,7 +77,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 prefix = ''// import.meta.env.DEV ? 'dev-' : ''
 | 
			
		||||
  const hashed = hashStr(prefix + getCurrentSessionKey())
 | 
			
		||||
  return mulberry32(hashed)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,13 @@
 | 
			
		|||
    <Transition name="zero_height">
 | 
			
		||||
      <div
 | 
			
		||||
        class="text-center"
 | 
			
		||||
        v-if="gameState <= GameState.Waiting">
 | 
			
		||||
        <div v-if="isDaily">
 | 
			
		||||
          {{ t('dailyDescription') }}<br>
 | 
			
		||||
        v-if="gameState <= GameStateType.Waiting">
 | 
			
		||||
        {{ t('dailyDescription') }}<br><br>
 | 
			
		||||
        <div v-if="isHardMode">
 | 
			
		||||
          {{ t('hardModeDescription') }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-else>
 | 
			
		||||
          {{ t('normalModeDescription') }}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <button
 | 
			
		||||
| 
						 | 
				
			
			@ -54,13 +58,6 @@
 | 
			
		|||
          v-else
 | 
			
		||||
          v-html="t('endGame.failureLabel')" />
 | 
			
		||||
        <button
 | 
			
		||||
          v-if="!isDaily"
 | 
			
		||||
          class="btn-border"
 | 
			
		||||
          @click="reboot">
 | 
			
		||||
          {{ t('playAgain') }}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
          v-else
 | 
			
		||||
          @click="toClipboard"
 | 
			
		||||
          class="inline-flex items-center btn-border">
 | 
			
		||||
          <IconShare class="mr-2" />
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +71,7 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
 | 
			
		||||
import { useI18n } from 'vue-i18n'
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  getEmptyOperation,
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +87,7 @@ import PlaquettesList from '@/components/PlaquettesList.vue'
 | 
			
		|||
import {
 | 
			
		||||
  clearOperationsList,
 | 
			
		||||
  currentOperation,
 | 
			
		||||
  gameDifficulty,
 | 
			
		||||
  gameIsRunning,
 | 
			
		||||
  gameState,
 | 
			
		||||
  isEndGame,
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +98,7 @@ import {
 | 
			
		|||
} from '@/composables/game-state'
 | 
			
		||||
import { toClipboard } from '@/composables/sharing'
 | 
			
		||||
import { hasPlayed } from '@/composables/statistics'
 | 
			
		||||
import { GameState, pools } from '@/globals'
 | 
			
		||||
import { GameDifficultyType, GameStateType, pools } from '@/globals'
 | 
			
		||||
import { OperatorType, Plaquette } from '@/types'
 | 
			
		||||
import {
 | 
			
		||||
  getCurrentSessionKey,
 | 
			
		||||
| 
						 | 
				
			
			@ -118,10 +116,12 @@ const { t } = useI18n() // call `useI18n`, and spread `t` from  `useI18n` return
 | 
			
		|||
 * Computed & refs
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const pool = ref<1 | 2 | 3>(1)
 | 
			
		||||
const poolType = ref<1 | 2 | 3>(1)
 | 
			
		||||
const cmpPlaquettes = ref<InstanceType<typeof PlaquettesList> | null>(null)
 | 
			
		||||
 | 
			
		||||
const isDaily = computed(() => useRoute()?.name === 'daily')
 | 
			
		||||
const isHardMode = computed(
 | 
			
		||||
  () => gameDifficulty.value === GameDifficultyType.Hard,
 | 
			
		||||
)
 | 
			
		||||
const shownPlaquettes = computed(() =>
 | 
			
		||||
  gameIsRunning.value ? plaquettes.value : [],
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -148,8 +148,8 @@ watch(
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
function startGame(): void {
 | 
			
		||||
  gameState.value = GameState.Loading
 | 
			
		||||
  setTimeout(() => (gameState.value = GameState.Playing), 500)
 | 
			
		||||
  gameState.value = GameStateType.Loading
 | 
			
		||||
  setTimeout(() => (gameState.value = GameStateType.Playing), 500)
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    if (cmpPlaquettes.value) {
 | 
			
		||||
      // Remove the animation delay for the plaquettes,
 | 
			
		||||
| 
						 | 
				
			
			@ -185,15 +185,15 @@ function selectOperator(o: OperatorType): void {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function reboot(): void {
 | 
			
		||||
  gameState.value = GameState.Playing
 | 
			
		||||
  gameState.value = GameStateType.Playing
 | 
			
		||||
 | 
			
		||||
  // The daily number is >= 500 to have a minimum challenge
 | 
			
		||||
  const minValue = isDaily.value ? 500 : 101
 | 
			
		||||
 | 
			
		||||
  const minValue = isHardMode.value ? 500 : 101
 | 
			
		||||
  const numPlaquettes = isHardMode.value ? 6 : 5
 | 
			
		||||
  do {
 | 
			
		||||
    // Find a problem
 | 
			
		||||
    // result.value = randRange(101, 1000)
 | 
			
		||||
    pool.value = randItem([1, 1, 1, 1, 1, 2, 3])
 | 
			
		||||
    poolType.value = randItem([1, 1, 1, 1, 1, 2, 3])
 | 
			
		||||
    result.value = randRange(minValue, 1000)
 | 
			
		||||
    // result.value = 29
 | 
			
		||||
    // Reset Operations list
 | 
			
		||||
| 
						 | 
				
			
			@ -201,8 +201,8 @@ function reboot(): void {
 | 
			
		|||
 | 
			
		||||
    // Generate result and plaquettes
 | 
			
		||||
    plaquettes.value = []
 | 
			
		||||
    const poolCopy = [...pools[pool.value]]
 | 
			
		||||
    for (let i = 0; i < 6; ++i) {
 | 
			
		||||
    const poolCopy = [...pools[poolType.value]]
 | 
			
		||||
    for (let i = 0; i < numPlaquettes; ++i) {
 | 
			
		||||
      const rndItem = Math.floor(random() * poolCopy.length)
 | 
			
		||||
      const el = poolCopy.splice(rndItem, 1)[0]
 | 
			
		||||
      plaquettes.value.push({ value: el, free: true })
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +213,7 @@ function reboot(): void {
 | 
			
		|||
    // }))
 | 
			
		||||
    // Solve it
 | 
			
		||||
  } while (
 | 
			
		||||
    (isDaily.value ? !isHardEnough(result.value) : false) ||
 | 
			
		||||
    (isHardMode.value ? !isHardEnough(result.value) : false) ||
 | 
			
		||||
    !isSolvable(
 | 
			
		||||
      result.value,
 | 
			
		||||
      plaquettes.value.map(p => p.value),
 | 
			
		||||
| 
						 | 
				
			
			@ -227,33 +227,24 @@ 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()
 | 
			
		||||
  }
 | 
			
		||||
  else {
 | 
			
		||||
    console.log('normal rng')
 | 
			
		||||
    setMathPRNG()
 | 
			
		||||
  }
 | 
			
		||||
  gameDifficulty.value =
 | 
			
		||||
    useRoute()?.name === 'hard'
 | 
			
		||||
      ? GameDifficultyType.Hard
 | 
			
		||||
      : GameDifficultyType.Normal
 | 
			
		||||
 | 
			
		||||
  setDailyPRNG()
 | 
			
		||||
  result.value = 0
 | 
			
		||||
 | 
			
		||||
  // Wait until after the page transion to generate the number
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    reboot()
 | 
			
		||||
    gameState.value = GameState.Waiting
 | 
			
		||||
    gameState.value = GameStateType.Waiting
 | 
			
		||||
  }, 800)
 | 
			
		||||
  // But make sure the operations list is empty asap
 | 
			
		||||
  clearOperationsList()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  gameState.value = GameState.Waiting
 | 
			
		||||
  gameState.value = GameStateType.Waiting
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,47 +2,35 @@
 | 
			
		|||
  <div class="flex flex-col grow p-1 w-full text-center">
 | 
			
		||||
    <div class="flex flex-col grow gap-16 justify-center md:mt-16">
 | 
			
		||||
      <div>
 | 
			
		||||
        {{ t('gameDescription') }}<br>
 | 
			
		||||
        {{ t('gameDescription') }}<br><br>
 | 
			
		||||
        <span v-html="t('dailyDescription')" />
 | 
			
		||||
 | 
			
		||||
        <br>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- Daily -->
 | 
			
		||||
 | 
			
		||||
      <!-- Normal -->
 | 
			
		||||
      <div class="flex flex-col gap-4 items-center">
 | 
			
		||||
        <RouterLink
 | 
			
		||||
          to="/daily"
 | 
			
		||||
          to="/normal"
 | 
			
		||||
          class="text-2xl btn-border">
 | 
			
		||||
          {{ t('dailyGame') }}
 | 
			
		||||
          {{ t('dailyNormal') }}
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
        <span v-html="t('dailyDescription')" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Random -->
 | 
			
		||||
      <!-- Hard -->
 | 
			
		||||
      <div class="flex flex-col gap-4 items-center">
 | 
			
		||||
        <RouterLink
 | 
			
		||||
          :to="canPlayRandom ? '/random' : ''"
 | 
			
		||||
          class="text-2xl btn-border"
 | 
			
		||||
          :class="{ 'btn-disabled': !canPlayRandom }">
 | 
			
		||||
          {{ t('randomGame') }}
 | 
			
		||||
          to="/hard"
 | 
			
		||||
          class="text-2xl btn-border">
 | 
			
		||||
          {{ t('dailyHard') }}
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
        {{ t('soon') }} ™
 | 
			
		||||
        <!-- {{ 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