Huge rework

This commit is contained in:
Simon Cambier 2022-02-17 22:20:09 +01:00
parent 5e203ca70d
commit 9467186907
17 changed files with 276 additions and 185 deletions

View File

@ -16,7 +16,6 @@
"vue-router": "^4.0.12" "vue-router": "^4.0.12"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/bx": "^1.0.3",
"@iconify-json/ph": "^1.0.4", "@iconify-json/ph": "^1.0.4",
"@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",

View File

@ -1,7 +1,6 @@
lockfileVersion: 5.3 lockfileVersion: 5.3
specifiers: specifiers:
'@iconify-json/bx': ^1.0.3
'@iconify-json/ph': ^1.0.4 '@iconify-json/ph': ^1.0.4
'@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
@ -41,7 +40,6 @@ dependencies:
vue-router: 4.0.12_vue@3.2.30 vue-router: 4.0.12_vue@3.2.30
devDependencies: devDependencies:
'@iconify-json/bx': 1.0.3
'@iconify-json/ph': 1.0.4 '@iconify-json/ph': 1.0.4
'@intlify/vite-plugin-vue-i18n': 3.3.0_vite@2.8.1+vue-i18n@9.1.9 '@intlify/vite-plugin-vue-i18n': 3.3.0_vite@2.8.1+vue-i18n@9.1.9
'@rushstack/eslint-patch': 1.1.0 '@rushstack/eslint-patch': 1.1.0
@ -179,12 +177,6 @@ packages:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true dev: true
/@iconify-json/bx/1.0.3:
resolution: {integrity: sha512-nwUxwOwocTp5u+KcBUraqEXiC7VG6niL6RQIdbLsRjZwouxayyVXPIkBPwMEmxpcTk1SA8Jh52MI+Scex1wJSA==}
dependencies:
'@iconify/types': 1.0.12
dev: true
/@iconify-json/ph/1.0.4: /@iconify-json/ph/1.0.4:
resolution: {integrity: sha512-hcxC2k25/Lh/bgXgbwAD4WvnC8BeunSqafFwIOyL1dCu3QGBgKmPFIBUv4W2kBm+rbrv7F3WHPFBAJDVrjpunA==} resolution: {integrity: sha512-hcxC2k25/Lh/bgXgbwAD4WvnC8BeunSqafFwIOyL1dCu3QGBgKmPFIBUv4W2kBm+rbrv7F3WHPFBAJDVrjpunA==}
dependencies: dependencies:

View File

@ -1,5 +1,7 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template> <template>
<RouterView class="container px-2 mx-auto h-full" /> <div class="container px-2 mx-auto h-full">
<RouterView />
</div>
</template> </template>

View File

@ -0,0 +1,7 @@
<template>
<h1 class="pt-2 pb-4 text-3xl font-bold text-center">
Numbers
</h1>
</template>
<script setup lang="ts"></script>

View File

@ -11,24 +11,30 @@
<div class="inline-block relative text-xl"> <div class="inline-block relative text-xl">
<!-- OPERATION --> <!-- OPERATION -->
<div <div
class="flex items-center" class="flex items-center border-b border-stone-600"
:class="{ 'text-red-600': isOperationInvalid(op) }"> :class="{ 'text-red-400': isOperationInvalid(op) }">
<!-- LEFT --> <!-- LEFT -->
<div class="w-[2.5em] border-b border-stone-600 transition-all"> <div class="w-[2.5em] transition-all">
{{ op.left?.value ?? '&nbsp;' }} {{ op.left?.value ?? '&nbsp;' }}
</div> </div>
<!-- RIGHT -->
<div class="w-[2.5em] border-b border-stone-600 transition-all"> <!-- OPERATOR -->
{{ op.operator ?? '&nbsp;' }} <div class="flex justify-center w-[2.5em] transition-all">
<Component
class="text-lg"
v-if="op.operator"
:is="operatorIcons[op.operator]" />
<span v-else>&nbsp;</span>
</div> </div>
<div class="w-[2.5em] border-b border-stone-600 transition-all"> <!-- RIGHT -->
<div class="w-[2.5em] transition-all">
{{ op.right?.value ?? '&nbsp;' }} {{ op.right?.value ?? '&nbsp;' }}
</div> </div>
<!-- EQUALS --> <!-- EQUALS -->
<div class="mx-4 border-none"> <div class="mx-4 border-none">
= <IconEquals />
</div> </div>
<!-- RESULT --> <!-- RESULT -->
@ -72,13 +78,14 @@ import {
isOperationResultValid, isOperationResultValid,
operate, operate,
} from '@/algo' } from '@/algo'
import { operations, plaquettes } from '@/game-state' import PlaquetteBox from '@/components/common/PlaquetteBox.vue'
import { GameState, gameState } from '@/globals' import { gameState, operations, plaquettes } from '@/composables/game-state'
import { operatorIcons } from '@/composables/operators'
import { GameState } from '@/globals'
import { Operation } from '@/types' import { Operation } from '@/types'
import IconUndo from '~icons/bx/bx-undo' import IconEquals from '~icons/ph/equals-bold'
import IconSad from '~icons/ph/smiley-sad' import IconSad from '~icons/ph/smiley-sad'
import IconUndo from '~icons/ph/x-circle-fill'
import PlaquetteBox from './common/PlaquetteBox.vue'
const transDelay = 100 const transDelay = 100

View File

@ -0,0 +1,23 @@
<template>
<div class="flex gap-2 justify-center my-4">
<PlaquetteBox
is="button"
class="aspect-square w-[1.5em] text-2xl border hover:border-white"
@click="emit('click', item)"
v-for="(item, i) in operators"
:key="i">
<Component :is="operatorIcons[item]" />
</PlaquetteBox>
</div>
</template>
<script setup lang="ts">
import PlaquetteBox from '@/components/common/PlaquetteBox.vue'
import { operatorIcons } from '@/composables/operators'
import { operators } from '@/globals'
import { OperatorType } from '@/types'
const emit = defineEmits<{
(e: 'click', operator: OperatorType): void
}>()
</script>

View File

@ -0,0 +1,43 @@
<template>
<div
class="grid grid-cols-6 grid-rows-2 gap-2 justify-center px-2 mx-auto max-w-sm">
<TransitionGroup name="slide_left">
<PlaquetteBox
v-for="(item, i) in plaquettes"
:key="i"
is="button"
@click="click(item)"
class="h-11 border"
:class="{
'text-stone-600 border-stone-600': !item.free,
'hover:border-white': item.free,
}"
:style="{ transitionDelay: `${initDelay * i}ms` }"
:dynamic-size="true">
{{ item.value }}
</PlaquetteBox>
</TransitionGroup>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import PlaquetteBox from '@/components/common/PlaquetteBox.vue'
import { Plaquette } from '@/types'
defineProps<{
plaquettes: Plaquette[]
}>()
const emit = defineEmits<{
(e: 'clickNumber', item: Plaquette): void
}>()
const initDelay = ref(100)
function click(item: Plaquette): void {
initDelay.value = 0
emit('clickNumber', item)
}
</script>

View File

@ -1,6 +0,0 @@
<template>
<div
class="flex overflow-hidden justify-center font-bold rounded border border-stone-600 transition-all">
<span class="self-center text-center"><slot /></span>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template> <template>
<Component <Component
:is="is" :is="is"
class="flex overflow-hidden justify-center font-bold rounded border border-stone-600 transition-all" class="flex overflow-hidden justify-center font-bold rounded border-stone-600 transition-opacity"
:class="[textSize]"> :class="[textSize]">
<span <span
class="self-center text-center" class="self-center text-center"

View File

@ -1,7 +1,10 @@
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { getEmptyOperation, isOperationReady } from './algo' import { getEmptyOperation, isOperationReady } from '../algo'
import { Operation, Plaquette } from './types' import { GameState } from '../globals'
import { Operation, Plaquette } from '../types'
export const gameState = ref(GameState.Undefined)
export const operations = reactive<Operation[]>([getEmptyOperation()]) export const operations = reactive<Operation[]>([getEmptyOperation()])
export const plaquettes = ref<Plaquette[]>([]) export const plaquettes = ref<Plaquette[]>([])
@ -12,6 +15,7 @@ export const currentOperation = computed(
() => operations[operations.length - 1], () => operations[operations.length - 1],
) )
export const gameIsRunning = computed(() => gameState.value > GameState.Loading)
export const isEndGame = computed( export const isEndGame = computed(
() => () =>
(operations.length === 5 && isOperationReady(currentOperation.value)) || (operations.length === 5 && isOperationReady(currentOperation.value)) ||

View File

@ -0,0 +1,11 @@
import IconDivide from '~icons/ph/divide-bold'
import IconMinus from '~icons/ph/minus-bold'
import IconPlus from '~icons/ph/plus-bold'
import IconMultiply from '~icons/ph/x-bold'
export const operatorIcons = {
'+': IconPlus,
'-': IconMinus,
x: IconMultiply,
'/': IconDivide,
} as const

View File

@ -1,15 +1,8 @@
import { ref } from 'vue'
export const operators = ['+', '-', 'x', '/'] as const export const operators = ['+', '-', 'x', '/'] as const
export const pools = { export const pools = {
1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25], 1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25],
2: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25, 50, 75], 2: [2, 2, 3, 3, 5, 5, 7, 11, 13, 17, 19, 23],
3: [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25, 50, 75,
100,
],
4: [2, 2, 3, 3, 5, 5, 7, 11, 13, 17, 19, 23],
} as const } as const
export enum GameState { export enum GameState {
@ -18,5 +11,3 @@ export enum GameState {
Loading = 2, Loading = 2,
Playing = 3, Playing = 3,
} }
export const gameState = ref(GameState.Undefined)

View File

@ -75,4 +75,11 @@
.zero_height-leave-to { .zero_height-leave-to {
height: 0; height: 0;
opacity: 0; opacity: 0;
}
@layer components {
.btn {
@apply w-fit p-2;
@apply rounded border border-stone-600 transition-opacity;
}
} }

View File

@ -6,5 +6,7 @@
"easy": "facile", "easy": "facile",
"medium": "moyen", "medium": "moyen",
"hard": "difficile", "hard": "difficile",
"impossible": "☠" "impossible": "☠",
"dailyGame": "Défi quotidien",
"randomGame": "Nombre au hasard"
} }

View File

@ -1,14 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import GameView from '../views/GameView.vue' import HomeView from '../views/HomeView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',
name: 'game', name: 'home',
component: GameView, component: HomeView,
},
{
path: '/daily',
name: 'daily',
component: () => import('../views/GameView.vue'),
},
{
path: '/random',
name: 'random',
component: () => import('../views/GameView.vue'),
}, },
], ],
}) })

View File

@ -1,120 +1,78 @@
<template> <template>
<div class=""> <AppHeader />
<h1 class="pt-2 pb-4 text-3xl font-bold text-center">
Le compte est bon
</h1>
<!-- Number to find --> <!-- Number to find -->
<NumberBox <PlaquetteBox
class="aspect-square mx-auto mb-8 w-[3em] text-4xl" class="p-1 mx-auto mb-4 w-fit"
:class="{ :class="{
'text-green-400': difficultyLevel === 1, 'text-blue-400': difficultyLevel === 1,
'text-amber-400': difficultyLevel === 2, 'text-purple-500': difficultyLevel === 2,
'text-orange-400': difficultyLevel === 3, }">
'text-red-500': difficultyLevel === 4, <span class="text-4xl">{{ result }}</span>
}"> </PlaquetteBox>
{{ result }}
</NumberBox>
<!-- Start button --> <!-- Start button -->
<!-- TODO: fix animation --> <!-- TODO: fix animation -->
<Transition name="zero_height"> <Transition name="zero_height">
<div
class="text-center"
v-if="gameState === GameState.Waiting">
<div>
Combinez les nombres imposés afin de trouver le résultat ci-dessus, ou
de vous en approcher le plus possible.<br>
Le compteur démarre quand vous cliquez sur le bouton.<br>Partagez
vos meilleurs temps !
</div>
<div
class="my-4 font-bold"
:class="{
'text-green-400': difficultyLevel === 1,
'text-amber-400': difficultyLevel === 2,
'text-orange-400': difficultyLevel === 3,
'text-red-500': difficultyLevel === 4,
}">
Niveau de difficulté :
<span v-if="difficultyLevel < 4">{{ difficultyLabel }}</span>
<IconSkull
class="inline"
v-else />
</div>
<button
@click="startGame"
class="py-2 px-4 mt-4 bg-stone-800 rounded border">
{{ t('startGame') }}
</button>
</div>
</Transition>
<!-- PLAQUETTES -->
<div <div
class="grid grid-cols-6 grid-rows-2 gap-2 justify-center px-2 mx-auto max-w-sm"> class="text-center"
<TransitionGroup name="slide_left"> v-if="gameState === GameState.Waiting">
<PlaquetteBox <div>
v-for="(item, i) in shownPlaquettes" Combinez les nombres imposés afin de trouver le résultat ci-dessus, ou
:key="i" de vous en approcher le plus possible.<br>
is="button" Le compteur démarre quand vous cliquez sur le bouton.<br>Partagez vos
@click="selectNumber(item)" meilleurs temps !
class="h-11" </div>
:class="{ 'text-stone-600 border-stone-600': !item.free }"
:style="{ transitionDelay: `${initDelay * i}ms` }" <button
:dynamic-size="true"> @click="startGame"
{{ item.value }} class="py-2 px-4 mt-4 btn">
</PlaquetteBox> {{ t('startGame') }}
</TransitionGroup> </button>
</div> </div>
</Transition>
<!-- OPERATORS --> <!-- PLAQUETTES -->
<Transition name="slide_up"> <PlaquettesList
<div v-if="gameIsRunning"> :plaquettes="shownPlaquettes"
<div class="flex gap-2 justify-center my-4"> @click-number="selectNumber" />
<PlaquetteBox
is="button"
class="aspect-square w-[1.5em] text-2xl"
@click="selectOperator(item)"
v-for="(item, i) in operators"
:key="i">
{{ item }}
</PlaquetteBox>
</div>
<!-- Divider --> <Transition name="slide_up">
<div class="my-4 mx-auto max-w-sm border-b" /> <div v-if="gameIsRunning">
<!-- OPERATORS -->
<OperatorsList @click="selectOperator" />
<!-- List of Operations --> <!-- Divider -->
<OperationsList v-show="gameIsRunning" /> <div class="my-4 mx-auto max-w-sm border-b" />
</div>
</Transition>
<Transition name="slide_up"> <!-- List of Operations -->
<div <OperationsList v-show="gameIsRunning" />
v-if="isEndGame" </div>
class="flex flex-row justify-evenly items-center mx-auto mt-8 max-w-sm"> </Transition>
<span
v-if="isResultPerfect" <Transition name="slide_up">
v-html="t('endGame.victoryLabel')" /> <div
<span v-if="isEndGame"
v-else class="flex flex-row justify-evenly items-center mx-auto mt-8 max-w-sm">
v-html="t('endGame.failureLabel')" /> <span
<button v-if="isResultPerfect"
class="p-2 rounded border" v-html="t('endGame.victoryLabel')" />
@click="reboot"> <span
{{ t('playAgain') }} v-else
</button> v-html="t('endGame.failureLabel')" />
</div> <button
</Transition> class="p-2 rounded border"
</div> @click="reboot">
{{ t('playAgain') }}
</button>
</div>
</Transition>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, 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 { import {
getEmptyOperation, getEmptyOperation,
@ -123,39 +81,41 @@ import {
isSolvable, isSolvable,
operate, operate,
} from '@/algo' } from '@/algo'
import NumberBox from '@/components/common/NumberBox.vue' import AppHeader from '@/components/AppHeader.vue'
import PlaquetteBox from '@/components/common/PlaquetteBox.vue' import PlaquetteBox from '@/components/common/PlaquetteBox.vue'
import OperationsList from '@/components/OperationsList.vue' import OperationsList from '@/components/OperationsList.vue'
import OperatorsList from '@/components/OperatorsList.vue'
import PlaquettesList from '@/components/PlaquettesList.vue'
import { import {
currentOperation, currentOperation,
gameIsRunning,
gameState,
isEndGame, isEndGame,
isResultPerfect, isResultPerfect,
operations, operations,
plaquettes, plaquettes,
result, result,
} from '@/game-state' } from '@/composables/game-state'
import { GameState, gameState, operators, pools } from '@/globals' import { GameState, pools } from '@/globals'
import { OperatorType, Plaquette } from '@/types' import { OperatorType, Plaquette } from '@/types'
import { randItem, randRange } from '@/utils' import { randItem, randRange } from '@/utils'
import IconSkull from '~icons/ph/skull'
const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning
const initDelay = ref(100)
/* /*
* Computed * Computed & refs
*/ */
const gameIsRunning = computed(() => gameState.value > GameState.Loading) const difficultyLevel = ref<1 | 2>(1)
const isDaily = computed(() => useRoute().name === 'daily')
const shownPlaquettes = computed(() => const shownPlaquettes = computed(() =>
gameIsRunning.value ? plaquettes.value : [], gameIsRunning.value ? plaquettes.value : [],
) )
onMounted(() => { /*
reboot() * Watchers
gameState.value = GameState.Waiting */
})
watch( watch(
currentOperation, currentOperation,
@ -181,7 +141,6 @@ function startGame(): void {
function selectNumber(p: Plaquette): void { function selectNumber(p: Plaquette): void {
if (isEndGame.value) return if (isEndGame.value) return
initDelay.value = 0
const op = currentOperation.value const op = currentOperation.value
if (!p.free) return if (!p.free) return
@ -196,42 +155,27 @@ function selectNumber(p: Plaquette): void {
} }
} }
/*
* Functions
*/
function selectOperator(o: OperatorType): void { function selectOperator(o: OperatorType): void {
if (isEndGame.value) return if (isEndGame.value) return
currentOperation.value.operator = o currentOperation.value.operator = o
} }
const difficultyLevel = ref<1 | 2 | 3 | 4>(1)
const difficultyLabel = computed(() => {
switch (difficultyLevel.value) {
case 1:
return t('easy')
case 2:
return t('medium')
case 3:
return t('hard')
default:
return t('impossible')
}
})
function reboot(): void { function reboot(): void {
gameState.value = GameState.Playing gameState.value = GameState.Playing
do { do {
// Find a problem // Find a problem
// result.value = randRange(101, 1000) // result.value = randRange(101, 1000)
difficultyLevel.value = randItem([1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4]) difficultyLevel.value = randItem([1, 1, 1, 1, 2])
result.value = (() => { result.value = (() => {
switch (difficultyLevel.value) { switch (difficultyLevel.value) {
case 1: case 1:
return randRange(80, 200) return randRange(101, 1000)
case 2: case 2:
return randRange(201, 400) return randRange(101, 1000)
case 3:
return randRange(401, 1000)
case 4:
return randRange(50, 1000)
} }
})() })()
// result.value = 29 // result.value = 29
@ -260,4 +204,17 @@ function reboot(): void {
) )
plaquettes.value.sort((a, b) => a.value - b.value) plaquettes.value.sort((a, b) => a.value - b.value)
} }
/*
* Hooks
*/
onMounted(() => {
reboot()
gameState.value = GameState.Waiting
})
onUnmounted(() => {
gameState.value = GameState.Waiting
})
</script> </script>

42
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<div class="flex flex-col h-full text-center">
<AppHeader />
<div>
Combinez les nombres imposés afin d'atteindre le résultat, ou de vous en
approcher le plus possible.<br>
<br>
</div>
<div class="flex flex-col grow gap-16 justify-center md:flex-row md:mt-16">
<!-- Daily -->
<div class="flex flex-col gap-4 items-center">
<RouterLink
to="/daily"
class="text-3xl btn">
{{ t('dailyGame') }}
</RouterLink>
<span>
Un nombre à atteindre, unique pour la journée,<br>commun à tous les
joueurs
</span>
</div>
<!-- Random -->
<div class="flex flex-col gap-4 items-center">
<RouterLink
to="/random"
class="text-3xl btn">
{{ t('randomGame') }}
</RouterLink>
Un nombre au hasard, pour le plaisir
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import AppHeader from '@/components/AppHeader.vue'
const { t } = useI18n()
</script>