Compare commits

...

10 Commits

Author SHA1 Message Date
ba0000af2a replaced plausible with umami 2023-11-26 11:10:59 +01:00
43198a382b balancing 2023-03-29 21:24:20 +02:00
38d77bcc7c Tweaked difficulty 2022-03-23 21:36:44 +01:00
bbb7460723 Cleaned up sharing 2022-03-21 22:15:58 +01:00
fe831a6004 Updated first game date 2022-03-21 19:04:48 +01:00
53261b719e Fixed bug 2022-03-21 19:04:36 +01:00
cada72746a Improved sharing 2022-03-04 12:51:56 +01:00
5ef3abb66e Improved sharing 2022-03-03 12:55:50 +01:00
dfe3caa7eb Translations 2022-03-02 17:58:35 +01:00
fb0b8c010f Practice mode 2022-03-02 13:18:35 +01:00
16 changed files with 135 additions and 103 deletions

View File

@ -12,6 +12,7 @@
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" sizes="180x180"> <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" sizes="180x180">
<link rel="mask-icon" href="/icons/favicon.svg" color="#1C1917"> <link rel="mask-icon" href="/icons/favicon.svg" color="#1C1917">
<meta name="theme-color" content="#1C1917"> <meta name="theme-color" content="#1C1917">
<script async src="https://stats.scambier.xyz/script.js" data-website-id="25b0bcf4-c4c1-4da4-a1bd-3251f7bc3878"></script>
</head> </head>
<body class="bg-stone-300 text-stone-900 dark:bg-stone-900 dark:text-stone-200 h-full relative"> <body class="bg-stone-300 text-stone-900 dark:bg-stone-900 dark:text-stone-200 h-full relative">

View File

@ -20,14 +20,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'
import AppHeader from '@/components/AppHeader.vue' import AppHeader from '@/components/AppHeader.vue'
import SideMenu from '@/components/SideMenu.vue' import SideMenu from '@/components/SideMenu.vue'
import ToastMessage from '@/components/ToastMessage.vue' import ToastMessage from '@/components/ToastMessage.vue'
import { plausible } from '@/analytics'
onMounted(() => {
plausible.enableAutoPageviews()
})
</script> </script>

View File

@ -5,7 +5,7 @@ import { shuffle } from './utils'
type HistoryType = { a: number; b: number; op: OperatorType }[] type HistoryType = { a: number; b: number; op: OperatorType }[]
export function isHardEnough(num: number): boolean { export function isHardEnough(num: number): boolean {
return num % 100 > 0 return num % 100 > 0 && num % 50 > 0
} }
export function isOperationResultValid(op: Operation): boolean { export function isOperationResultValid(op: Operation): boolean {
@ -63,7 +63,7 @@ export function isSolvable(result: number, plaquettes: number[]): boolean {
for (const item of history) { for (const item of history) {
if (item.a < item.b) [item.a, item.b] = [item.b, item.a] if (item.a < item.b) [item.a, item.b] = [item.b, item.a]
console.log( console.log(
`${item.a} ${item.op} ${item.b} = ${operate(item.op, item.a, item.b)}`, `${item.a} ${item.op === '*' ? 'x' : item.op} ${item.b} = ${operate(item.op, item.a, item.b)}`,
) )
} }
} }
@ -117,7 +117,7 @@ export function isSolvable(result: number, plaquettes: number[]): boolean {
histories.push(history) histories.push(history)
if (!found) { if (!found) {
found = true found = true
console.log(`1e solution trouvée en ${Date.now() - start}ms`) console.log(`1st solution found in ${Date.now() - start}ms`)
} }
return return
} }
@ -138,12 +138,12 @@ export function isSolvable(result: number, plaquettes: number[]): boolean {
loopOperations(plaquettes, []) loopOperations(plaquettes, [])
if (found) { if (found) {
console.log( console.log(
`Réussite : ${Date.now() - start}ms et ${numberOfIterations} iterations`, `Success : ${Date.now() - start}ms and ${numberOfIterations} iterations`,
) )
} }
else { else {
console.log( console.log(
`Echec : ${Date.now() - start}ms et ${numberOfIterations} iterations`, `Failure : ${Date.now() - start}ms and ${numberOfIterations} iterations`,
) )
} }

View File

@ -1,7 +0,0 @@
import Plausible from 'plausible-tracker'
// Setup tracking
export const plausible = Plausible({
domain: 'n0mbers.scambier.xyz',
apiHost: 'https://stats.scambier.xyz',
})

View File

@ -8,7 +8,7 @@
</RouterLink> </RouterLink>
<h1 <h1
class="z-20 py-2 font-mono text-3xl text-center bg-stone-300 dark:bg-stone-900"> class="z-20 py-2 font-mono text-3xl text-center bg-stone-300 dark:bg-stone-900">
N<span class="text-cyan-500">0</span>mbers<span class="text-xs">beta</span> N<span class="text-cyan-500">0</span>mbers<span class="text-xs">alpha</span>
</h1> </h1>
<button @click="isSideMenuVisible = true"> <button @click="isSideMenuVisible = true">
<IconMenu class="text-xl btn" /> <IconMenu class="text-xl btn" />

View File

@ -8,7 +8,7 @@
<IconClose class="absolute top-0 right-1 h-12 text-xl btn" /> <IconClose class="absolute top-0 right-1 h-12 text-xl btn" />
</button> </button>
</div> </div>
<div class="flex flex-col flex-1 content-center items-center"> <div class="flex flex-col flex-1 content-center items-center text-sm">
<div class="h-12" /> <div class="h-12" />
<InputSwitch <InputSwitch
class="mb-4" class="mb-4"
@ -24,15 +24,6 @@
v-model="isLocaleFrench" /> v-model="isLocaleFrench" />
</div> </div>
<div class="mb-8 text-xs text-stone-500">
Features to come:
<ul class="ml-4 list-disc">
<li>Keyboard input</li>
<li>Random number</li>
<li>Less bugs</li>
</ul>
</div>
<div class="text-xs text-stone-500 dark:text-stone-600"> <div class="text-xs text-stone-500 dark:text-stone-600">
Build {{ buildDate }} Build {{ buildDate }}
</div> </div>

View File

@ -17,7 +17,7 @@ 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 // #region Difficulty-related values
export const gameDifficulty = ref(GameDifficultyType.Normal) export const gameDifficulty = ref(GameDifficultyType.Normal)
@ -29,6 +29,10 @@ export const startingNumberOfPlaquettes = computed(() =>
gameDifficulty.value === GameDifficultyType.Hard ? 6 : 5, gameDifficulty.value === GameDifficultyType.Hard ? 6 : 5,
) )
export const isHardMode = computed(
() => gameDifficulty.value === GameDifficultyType.Hard,
)
// #endregion Difficulty-related constants // #endregion Difficulty-related constants
export const currentOperation = computed( export const currentOperation = computed(

View File

@ -1,34 +1,56 @@
import { i18n } from '@/i18n' import { i18n } from '@/i18n'
import { percentageDiff } from '@/utils'
import { numberOfGamesSinceStart, operations, result } from './game-state' import {
isHardMode,
numberOfGamesSinceStart,
operations,
result,
} from './game-state'
import { showToast } from './toast-manager' import { showToast } from './toast-manager'
function getSharingText(): string { function getSharingText(): string {
// × ÷ + - const endResult = operations[operations.length - 1].result?.value ?? 0
const emojis: string[] = [] const success = result.value === endResult
// const allSymbols = ['𝙰', '𝙱', '𝙲', '𝙳', '𝙴', '𝙵', '𝚅', '𝚆', '𝚇', '𝚈', '𝚉']
const abcSymbols = ['𝚊', '𝚋', '𝚌', '𝚍', '𝚎', '𝚏']
const xyzSymbols = ['𝚟', '𝚠', '𝚡', '𝚢', '𝚣'].slice(-(operations.length - 1))
// const xyzSymbols = ["𝚊'", "𝚋'", "𝚌'", "𝚍'", "𝚎'", "𝚏'"]
// const xyzSymbols = ['𝚅', '𝚆', '𝚇', '𝚈', '𝚉']
const squares = ['🟦', '🟩', '🟨', '🟧', '🟥']
const lines: string[] = []
for (const op of operations) { for (const op of operations) {
op.left!.symbol = op.left!.symbol ?? abcSymbols.shift()
op.right!.symbol = op.right!.symbol ?? abcSymbols.shift()
op.result!.symbol = op.result!.symbol ?? xyzSymbols.shift()
let s = ''
switch (op.operator) { switch (op.operator) {
case '+':
emojis.push('+')
break
case '-':
emojis.push('-')
break
case '*': case '*':
emojis.push('×') s = '×'
break break
case '/': case '/':
emojis.push('÷') s = '÷'
break
case '-':
s = ''
break
case '+':
s = '+'
break break
} }
lines.push(
`${squares.shift()} ${op.left?.symbol} ${s} ${op.right?.symbol} = ${
op.result?.value === endResult ? endResult : op.result?.symbol
}`,
)
} }
const endResult = operations[operations.length - 1].result?.value ?? 0
return `N0mbers #${numberOfGamesSinceStart()} return `N0mbers #${numberOfGamesSinceStart()} - ${
=========== isHardMode.value ? 'Advanced' : 'Normal'
${emojis.join(' ')} = ${endResult} }
Score: ${100 - percentageDiff(result.value, endResult) * 100}% ${lines.join('\n')} ${success ? '✔' : '❌'}
===========
https://n0mbers.scambier.xyz` https://n0mbers.scambier.xyz`
} }

View File

@ -1,7 +1,6 @@
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import { reactive, watch } from 'vue' import { reactive, watch } from 'vue'
// import { plausible } from '@/analytics'
import { LSK_STATS } from '@/globals' import { LSK_STATS } from '@/globals'
import * as storage from '@/storage' import * as storage from '@/storage'
import { GameStats } from '@/types' import { GameStats } from '@/types'
@ -62,8 +61,7 @@ function setScore(seed: string, won: boolean, score: number): void {
// Don't overwrite an existing score // Don't overwrite an existing score
if (!gameStats.games[seed]) { if (!gameStats.games[seed]) {
gameStats.games[seed] = { score, won } gameStats.games[seed] = { score, won }
// plausible.trackEvent(won ? 'win_game' : 'lose_game')
// plausible.trackEvent('end_game')
} }
} }

View File

@ -1,4 +1,4 @@
export const GAME_STARTING_DATE = '2022-02-18' export const GAME_STARTING_DATE = '2022-03-22'
export const BXL_TZ = 'Europe/Brussels' export const BXL_TZ = 'Europe/Brussels'
export const LSK_DARKMODE = 'n0_dark' export const LSK_DARKMODE = 'n0_dark'
@ -8,9 +8,9 @@ export const LSK_STATS = 'n0_stats'
export const operators = ['+', '-', '*', '/'] as const export const operators = ['+', '-', '*', '/'] 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, 50, 75, 100],
2: [2, 2, 3, 3, 5, 5, 7, 11, 13, 17, 19, 23], 2: [2, 2, 3, 3, 5, 5, 7, 7, 11, 13, 17, 19, 23],
3: [5, 5, 5, 5, 5, 2, 2, 2, 2, 2], 3: [1, 1, 2, 3, 5, 8, 13, 21],
} as const } as const
export enum GameStateType { export enum GameStateType {

View File

@ -12,10 +12,14 @@
"dailyDescription": "The daily challenge changes every day at midnight (CET), and is common to all players.", "dailyDescription": "The daily challenge changes every day at midnight (CET), and is common to all players.",
"share": "Share", "share": "Share",
"finishDailyToPlayRandom": "Finish the daily challenge to unlock.", "finishDailyToPlayRandom": "Finish the daily challenge to unlock.",
"gameDescription": "Combine the numbers to reach the result.", "gameDescription": "Combine the required numbers to reach the result.<br><strong>Game under development, some features are missing or changing.</strong>",
"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", "dailyHard": "Advanced Mode",
"dailyNormal": "Normal Mode" "dailyNormal": "Normal Mode",
"practiceMode": "Practice",
"practiceModeDescription": "Use the Practice mode to improve your skills.",
"hardModeDescription": "The Advanced mode is generally more challenging, with 6 starting numbers.",
"normalModeDescription": "The Normal mode has 5 starting numbers, for a generally easier challenge."
} }

View File

@ -9,7 +9,7 @@
"impossible": "☠", "impossible": "☠",
"dailyGame": "Défi quotidien", "dailyGame": "Défi quotidien",
"randomGame": "Nombre au hasard", "randomGame": "Nombre au hasard",
"gameDescription": "Combinez les nombres imposés afin d'atteindre le résultat.", "gameDescription": "Combinez les nombres imposés afin d'atteindre le résultat.<br><strong>Jeu en cours de développement, certaines fonctionnalités sont manquantes ou changeantes.</strong>",
"dailyDescription": "Le défi quotidien change chaque jour à minuit (CET), et est commun à tous les joueurs.", "dailyDescription": "Le défi quotidien change chaque jour à minuit (CET), 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",
@ -17,5 +17,9 @@
"copiedInClipboard": "Copié dans le presse-papier", "copiedInClipboard": "Copié dans le presse-papier",
"soon": "Bientôt", "soon": "Bientôt",
"dailyNormal": "Mode Normal", "dailyNormal": "Mode Normal",
"dailyHard": "Mode Difficile" "dailyHard": "Mode Avancé",
"practiceMode": "Entraînement",
"practiceModeDescription": "Profitez du mode Entraînement pour améliorer vos compétences.",
"hardModeDescription": "Le mode Avancé propose un challenge généralement plus relevé, avec 6 nombres de base.",
"normalModeDescription": "Le mode Normal propose un challenge généralement plus facile, avec 5 nombres de base."
} }

View File

@ -12,8 +12,8 @@ const router = createRouter({
meta: { transition: '' }, meta: { transition: '' },
}, },
{ {
path: '/hard', path: '/advanced',
name: 'hard', name: 'advanced',
component: () => import('../views/GameView.vue'), component: () => import('../views/GameView.vue'),
meta: { transition: 'route' }, meta: { transition: 'route' },
}, },
@ -23,6 +23,12 @@ const router = createRouter({
component: () => import('../views/GameView.vue'), component: () => import('../views/GameView.vue'),
meta: { transition: 'route' }, meta: { transition: 'route' },
}, },
{
path: '/practice',
name: 'practice',
component: () => import('../views/GameView.vue'),
meta: { transition: 'route' },
},
], ],
}) })

View File

@ -3,6 +3,12 @@ export type OperatorType = '+' | '-' | '*' | '/'
export type Plaquette = { export type Plaquette = {
value: number value: number
free: boolean free: boolean
/** If the plaquette is one of the pre-selected */
original: boolean
/** For sharing */
symbol?: string
} }
export type Operation = { export type Operation = {

View File

@ -12,12 +12,17 @@
class="text-center" class="text-center"
v-if="gameState <= GameStateType.Waiting"> v-if="gameState <= GameStateType.Waiting">
{{ t('dailyDescription') }}<br><br> {{ t('dailyDescription') }}<br><br>
<div v-if="isPracticeMode">
{{ t('practiceModeDescription') }}
</div>
<div v-else>
<div v-if="isHardMode"> <div v-if="isHardMode">
{{ t('hardModeDescription') }} {{ t('hardModeDescription') }}
</div> </div>
<div v-else> <div v-else>
{{ t('normalModeDescription') }} {{ t('normalModeDescription') }}
</div> </div>
</div>
<button <button
@click="startGame" @click="startGame"
@ -35,7 +40,7 @@
<Transition name="slide_up"> <Transition name="slide_up">
<div v-if="gameIsRunning"> <div v-if="gameIsRunning">
<!-- OPERATORS --> <!-- Operators -->
<OperatorsList @click="selectOperator" /> <OperatorsList @click="selectOperator" />
<!-- Divider --> <!-- Divider -->
@ -58,11 +63,19 @@
v-else v-else
v-html="t('endGame.failureLabel')" /> v-html="t('endGame.failureLabel')" />
<button <button
@click="toClipboard" v-if="!isPracticeMode"
@click="sharing.toClipboard"
class="inline-flex items-center btn-border"> class="inline-flex items-center btn-border">
<IconShare class="mr-2" /> <IconShare class="mr-2" />
{{ t('share') }} {{ t('share') }}
</button> </button>
<button
v-else
@click="reboot"
class="inline-flex items-center btn-border">
<IconReload class="mr-2" />
{{ t('playAgain') }}
</button>
</div> </div>
</Transition> </Transition>
</div> </div>
@ -91,23 +104,17 @@ import {
gameIsRunning, gameIsRunning,
gameState, gameState,
isEndGame, isEndGame,
isHardMode,
isResultPerfect, isResultPerfect,
operations, operations,
plaquettes, plaquettes,
result, result,
} from '@/composables/game-state' } from '@/composables/game-state'
import { toClipboard } from '@/composables/sharing' import * as sharing from '@/composables/sharing'
import { hasPlayed } from '@/composables/statistics'
import { GameDifficultyType, GameStateType, pools } from '@/globals' import { GameDifficultyType, GameStateType, pools } from '@/globals'
import { OperatorType, Plaquette } from '@/types' import { OperatorType, Plaquette } from '@/types'
import { import { randItem, random, randRange, setDailyPRNG, setMathPRNG } from '@/utils'
getCurrentSessionKey, import IconReload from '~icons/ph/arrow-clockwise'
randItem,
random,
randRange,
setDailyPRNG,
setMathPRNG,
} from '@/utils'
import IconShare from '~icons/ph/share-network' import IconShare from '~icons/ph/share-network'
const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning
@ -119,9 +126,7 @@ const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` return
const poolType = 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 isHardMode = computed( const isPracticeMode = ref(false)
() => gameDifficulty.value === GameDifficultyType.Hard,
)
const shownPlaquettes = computed(() => const shownPlaquettes = computed(() =>
gameIsRunning.value ? plaquettes.value : [], gameIsRunning.value ? plaquettes.value : [],
) )
@ -136,6 +141,7 @@ watch(
if (isOperationReady(op) && isOperationResultValid(op) && !op.result) { if (isOperationReady(op) && isOperationResultValid(op) && !op.result) {
op.result = { op.result = {
free: true, free: true,
original: false,
value: operate(op.operator!, op.left!.value, op.right!.value), value: operate(op.operator!, op.left!.value, op.right!.value),
} }
if (operations.length < 5 && !isEndGame.value) { if (operations.length < 5 && !isEndGame.value) {
@ -187,15 +193,12 @@ function selectOperator(o: OperatorType): void {
function reboot(): void { function reboot(): void {
gameState.value = GameStateType.Playing gameState.value = GameStateType.Playing
// The daily number is >= 500 to have a minimum challenge const minValue = 101
const minValue = isHardMode.value ? 500 : 101 const numPlaquettes = isHardMode.value || isPracticeMode.value ? 6 : 5
const numPlaquettes = isHardMode.value ? 6 : 5
do { do {
// Find a problem // Find a problem
// result.value = randRange(101, 1000)
poolType.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
// Reset Operations list // Reset Operations list
clearOperationsList() clearOperationsList()
@ -205,12 +208,9 @@ function reboot(): void {
for (let i = 0; i < numPlaquettes; ++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, original: true })
} }
// plaquettes.value = [4, 8, 10, 25, 50, 100].map(value => ({
// free: true,
// value,
// }))
// Solve it // Solve it
} while ( } while (
(isHardMode.value ? !isHardEnough(result.value) : false) || (isHardMode.value ? !isHardEnough(result.value) : false) ||
@ -228,11 +228,18 @@ function reboot(): void {
onMounted(() => { onMounted(() => {
gameDifficulty.value = gameDifficulty.value =
useRoute()?.name === 'hard' useRoute()?.name !== 'normal'
? GameDifficultyType.Hard ? GameDifficultyType.Hard
: GameDifficultyType.Normal : GameDifficultyType.Normal
isPracticeMode.value = useRoute().name === 'practice'
if (isPracticeMode.value) {
setMathPRNG()
}
else {
setDailyPRNG() setDailyPRNG()
}
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

View File

@ -1,15 +1,10 @@
<template> <template>
<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-8 justify-center my-4 md:my-16">
<div> <div v-html="t('gameDescription')" />
{{ t('gameDescription') }}<br><br>
<span v-html="t('dailyDescription')" />
<br>
</div>
<!-- Normal --> <!-- Normal -->
<div class="flex flex-col gap-4 items-center"> <div class="flex flex-col items-center">
<RouterLink <RouterLink
to="/normal" to="/normal"
class="text-2xl btn-border"> class="text-2xl btn-border">
@ -18,14 +13,22 @@
</div> </div>
<!-- Hard --> <!-- Hard -->
<div class="flex flex-col gap-4 items-center"> <div class="flex flex-col items-center">
<RouterLink <RouterLink
to="/hard" to="/advanced"
class="text-2xl btn-border"> class="text-2xl btn-border">
{{ t('dailyHard') }} {{ t('dailyHard') }}
</RouterLink> </RouterLink>
</div> </div>
<!-- Practice -->
<div class="flex flex-col items-center mt-4">
<RouterLink
to="/practice"
class="text-xl btn-border">
{{ t('practiceMode') }}
</RouterLink>
</div>
</div> </div>
</div> </div>
</template> </template>