Normal and Hard modes

This commit is contained in:
Simon Cambier 2022-03-01 23:16:40 +01:00
parent 1909b75663
commit 80064a98e5
12 changed files with 117 additions and 81 deletions

View File

@ -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]) printHistory(histories[0])
} }
return found return found

View File

@ -72,7 +72,7 @@ import {
result, result,
} from '@/composables/game-state' } from '@/composables/game-state'
import { operatorIcons } from '@/composables/operators' import { operatorIcons } from '@/composables/operators'
import { GameState } from '@/globals' import { GameStateType } from '@/globals'
import { Operation } from '@/types' import { Operation } from '@/types'
import IconEquals from '~icons/ph/equals-bold' import IconEquals from '~icons/ph/equals-bold'
import IconSad from '~icons/ph/smiley-sad' import IconSad from '~icons/ph/smiley-sad'
@ -85,7 +85,7 @@ function canOperationBeDeleted(op: Operation): boolean {
} }
function undoOperation(index: number): void { function undoOperation(index: number): void {
if (gameState.value !== GameState.Playing) return if (gameState.value !== GameStateType.Playing) return
const len = operations.length const len = operations.length
for (let i = len - 1; i >= index; --i) { for (let i = len - 1; i >= index; --i) {
let popped: Operation let popped: Operation

View File

@ -1,6 +1,10 @@
<template> <template>
<div <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"> <TransitionGroup name="slide_left">
<PlaquetteBox <PlaquetteBox
v-for="(item, i) in plaquettes" v-for="(item, i) in plaquettes"
@ -25,6 +29,10 @@
import { ref } from 'vue' import { ref } from 'vue'
import PlaquetteBox from '@/components/common/PlaquetteBox.vue' import PlaquetteBox from '@/components/common/PlaquetteBox.vue'
import {
maxNumberOfOperations,
startingNumberOfPlaquettes,
} from '@/composables/game-state'
import { Plaquette } from '@/types' import { Plaquette } from '@/types'
defineProps<{ defineProps<{

View File

@ -3,25 +3,45 @@ import { utcToZonedTime } from 'date-fns-tz'
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { getEmptyOperation, isOperationReady } from '@/algo' 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 { Operation, Plaquette } from '@/types'
import { getCurrentDate } from '@/utils' import { getCurrentDate } from '@/utils'
export const gameState = ref(GameState.Undefined) export const gameState = ref(GameStateType.Undefined)
export const operations = reactive<Operation[]>([getEmptyOperation()]) export const operations = reactive<Operation[]>([getEmptyOperation()])
export const plaquettes = ref<Plaquette[]>([]) export const plaquettes = ref<Plaquette[]>([])
export const result = ref(0) 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( export const currentOperation = computed(
() => operations[operations.length - 1], () => operations[operations.length - 1],
) )
export const gameIsRunning = computed(() => gameState.value > GameState.Loading) export const gameIsRunning = computed(
() => gameState.value > GameStateType.Loading,
)
export const isEndGame = computed( export const isEndGame = computed(
() => () =>
(operations.length === 5 && isOperationReady(currentOperation.value)) || (operations.length === maxNumberOfOperations.value &&
isOperationReady(currentOperation.value)) ||
isResultPerfect.value, isResultPerfect.value,
) )

View File

@ -13,9 +13,14 @@ export const pools = {
3: [5, 5, 5, 5, 5, 2, 2, 2, 2, 2], 3: [5, 5, 5, 5, 5, 2, 2, 2, 2, 2],
} as const } as const
export enum GameState { export enum GameStateType {
Undefined = 0, Undefined = 0,
Waiting = 1, Waiting = 1,
Loading = 2, Loading = 2,
Playing = 3, Playing = 3,
} }
export enum GameDifficultyType {
Normal = 1,
Hard = 2,
}

View File

@ -15,5 +15,7 @@
"gameDescription": "Combine the numbers to reach the result.", "gameDescription": "Combine the numbers to reach the result.",
"randomDescription": "A challenge at random, just for fun.", "randomDescription": "A challenge at random, just for fun.",
"copiedInClipboard": "Copied in clipboard", "copiedInClipboard": "Copied in clipboard",
"soon": "Soon" "soon": "Soon",
"dailyHard": "Hard Mode",
"dailyNormal": "Normal Mode"
} }

View File

@ -15,5 +15,7 @@
"share": "Partager", "share": "Partager",
"finishDailyToPlayRandom": "Terminez le défi quotidien pour débloquer.", "finishDailyToPlayRandom": "Terminez le défi quotidien pour débloquer.",
"copiedInClipboard": "Copié dans le presse-papier", "copiedInClipboard": "Copié dans le presse-papier",
"soon": "Bientôt" "soon": "Bientôt",
"dailyNormal": "Mode Normal",
"dailyHard": "Mode Difficile"
} }

View File

@ -12,14 +12,14 @@ const router = createRouter({
meta: { transition: '' }, meta: { transition: '' },
}, },
{ {
path: '/daily', path: '/hard',
name: 'daily', name: 'hard',
component: () => import('../views/GameView.vue'), component: () => import('../views/GameView.vue'),
meta: { transition: 'route' }, meta: { transition: 'route' },
}, },
{ {
path: '/random', path: '/normal',
name: 'random', name: 'normal',
component: () => import('../views/GameView.vue'), component: () => import('../views/GameView.vue'),
meta: { transition: 'route' }, meta: { transition: 'route' },
}, },

View File

@ -18,3 +18,23 @@ export type GameStats = {
nbGames: number nbGames: number
games: { [key: string]: { score: number; won: boolean } } 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[]
}

View File

@ -77,7 +77,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 + getCurrentSessionKey()) const hashed = hashStr(prefix + getCurrentSessionKey())
return mulberry32(hashed) return mulberry32(hashed)
} }

View File

@ -10,9 +10,13 @@
<Transition name="zero_height"> <Transition name="zero_height">
<div <div
class="text-center" class="text-center"
v-if="gameState <= GameState.Waiting"> v-if="gameState <= GameStateType.Waiting">
<div v-if="isDaily"> {{ t('dailyDescription') }}<br><br>
{{ t('dailyDescription') }}<br> <div v-if="isHardMode">
{{ t('hardModeDescription') }}
</div>
<div v-else>
{{ t('normalModeDescription') }}
</div> </div>
<button <button
@ -54,13 +58,6 @@
v-else v-else
v-html="t('endGame.failureLabel')" /> v-html="t('endGame.failureLabel')" />
<button <button
v-if="!isDaily"
class="btn-border"
@click="reboot">
{{ t('playAgain') }}
</button>
<button
v-else
@click="toClipboard" @click="toClipboard"
class="inline-flex items-center btn-border"> class="inline-flex items-center btn-border">
<IconShare class="mr-2" /> <IconShare class="mr-2" />
@ -74,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, useRouter } from 'vue-router' import { useRoute } from 'vue-router'
import { import {
getEmptyOperation, getEmptyOperation,
@ -90,6 +87,7 @@ import PlaquettesList from '@/components/PlaquettesList.vue'
import { import {
clearOperationsList, clearOperationsList,
currentOperation, currentOperation,
gameDifficulty,
gameIsRunning, gameIsRunning,
gameState, gameState,
isEndGame, isEndGame,
@ -100,7 +98,7 @@ import {
} from '@/composables/game-state' } from '@/composables/game-state'
import { toClipboard } from '@/composables/sharing' import { toClipboard } from '@/composables/sharing'
import { hasPlayed } from '@/composables/statistics' import { hasPlayed } from '@/composables/statistics'
import { GameState, pools } from '@/globals' import { GameDifficultyType, GameStateType, pools } from '@/globals'
import { OperatorType, Plaquette } from '@/types' import { OperatorType, Plaquette } from '@/types'
import { import {
getCurrentSessionKey, getCurrentSessionKey,
@ -118,10 +116,12 @@ const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` return
* Computed & refs * 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 cmpPlaquettes = ref<InstanceType<typeof PlaquettesList> | null>(null)
const isDaily = computed(() => useRoute()?.name === 'daily') const isHardMode = computed(
() => gameDifficulty.value === GameDifficultyType.Hard,
)
const shownPlaquettes = computed(() => const shownPlaquettes = computed(() =>
gameIsRunning.value ? plaquettes.value : [], gameIsRunning.value ? plaquettes.value : [],
) )
@ -148,8 +148,8 @@ watch(
) )
function startGame(): void { function startGame(): void {
gameState.value = GameState.Loading gameState.value = GameStateType.Loading
setTimeout(() => (gameState.value = GameState.Playing), 500) setTimeout(() => (gameState.value = GameStateType.Playing), 500)
setTimeout(() => { setTimeout(() => {
if (cmpPlaquettes.value) { if (cmpPlaquettes.value) {
// Remove the animation delay for the plaquettes, // Remove the animation delay for the plaquettes,
@ -185,15 +185,15 @@ function selectOperator(o: OperatorType): void {
} }
function reboot(): void { function reboot(): void {
gameState.value = GameState.Playing gameState.value = GameStateType.Playing
// The daily number is >= 500 to have a minimum challenge // 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 { do {
// Find a problem // Find a problem
// result.value = randRange(101, 1000) // 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 = randRange(minValue, 1000)
// result.value = 29 // result.value = 29
// Reset Operations list // Reset Operations list
@ -201,8 +201,8 @@ function reboot(): void {
// Generate result and plaquettes // Generate result and plaquettes
plaquettes.value = [] plaquettes.value = []
const poolCopy = [...pools[pool.value]] const poolCopy = [...pools[poolType.value]]
for (let i = 0; i < 6; ++i) { for (let i = 0; i < numPlaquettes; ++i) {
const rndItem = Math.floor(random() * poolCopy.length) const rndItem = Math.floor(random() * poolCopy.length)
const el = poolCopy.splice(rndItem, 1)[0] const el = poolCopy.splice(rndItem, 1)[0]
plaquettes.value.push({ value: el, free: true }) plaquettes.value.push({ value: el, free: true })
@ -213,7 +213,7 @@ function reboot(): void {
// })) // }))
// Solve it // Solve it
} while ( } while (
(isDaily.value ? !isHardEnough(result.value) : false) || (isHardMode.value ? !isHardEnough(result.value) : false) ||
!isSolvable( !isSolvable(
result.value, result.value,
plaquettes.value.map(p => p.value), plaquettes.value.map(p => p.value),
@ -227,33 +227,24 @@ function reboot(): void {
*/ */
onMounted(() => { onMounted(() => {
// if (!isDaily.value && !hasPlayed(getCurrentSessionKey())) { gameDifficulty.value =
// const router = useRouter() useRoute()?.name === 'hard'
// router.replace({ name: 'home' }) ? GameDifficultyType.Hard
// return : GameDifficultyType.Normal
// }
if (isDaily.value) {
console.log('daily rng')
setDailyPRNG() setDailyPRNG()
}
else {
console.log('normal rng')
setMathPRNG()
}
result.value = 0 result.value = 0
// Wait until after the page transion to generate the number // Wait until after the page transion to generate the number
setTimeout(() => { setTimeout(() => {
reboot() reboot()
gameState.value = GameState.Waiting gameState.value = GameStateType.Waiting
}, 800) }, 800)
// But make sure the operations list is empty asap // But make sure the operations list is empty asap
clearOperationsList() clearOperationsList()
}) })
onUnmounted(() => { onUnmounted(() => {
gameState.value = GameState.Waiting gameState.value = GameStateType.Waiting
}) })
</script> </script>

View File

@ -2,47 +2,35 @@
<div class="flex flex-col grow p-1 w-full text-center"> <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 class="flex flex-col grow gap-16 justify-center md:mt-16">
<div> <div>
{{ t('gameDescription') }}<br> {{ t('gameDescription') }}<br><br>
<span v-html="t('dailyDescription')" />
<br> <br>
</div> </div>
<!-- Daily -->
<!-- Normal -->
<div class="flex flex-col gap-4 items-center"> <div class="flex flex-col gap-4 items-center">
<RouterLink <RouterLink
to="/daily" to="/normal"
class="text-2xl btn-border"> class="text-2xl btn-border">
{{ t('dailyGame') }} {{ t('dailyNormal') }}
</RouterLink> </RouterLink>
<span v-html="t('dailyDescription')" />
</div> </div>
<!-- Random --> <!-- Hard -->
<div class="flex flex-col gap-4 items-center"> <div class="flex flex-col gap-4 items-center">
<RouterLink <RouterLink
:to="canPlayRandom ? '/random' : ''" to="/hard"
class="text-2xl btn-border" class="text-2xl btn-border">
:class="{ 'btn-disabled': !canPlayRandom }"> {{ t('dailyHard') }}
{{ t('randomGame') }}
</RouterLink> </RouterLink>
{{ t('soon') }}
<!-- {{ 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>