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="mask-icon" href="/icons/favicon.svg" color="#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>
<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>
<script setup lang="ts">
import { onMounted } from 'vue'
import AppHeader from '@/components/AppHeader.vue'
import SideMenu from '@/components/SideMenu.vue'
import ToastMessage from '@/components/ToastMessage.vue'
import { plausible } from '@/analytics'
onMounted(() => {
plausible.enableAutoPageviews()
})
</script>

View File

@ -5,7 +5,7 @@ import { shuffle } from './utils'
type HistoryType = { a: number; b: number; op: OperatorType }[]
export function isHardEnough(num: number): boolean {
return num % 100 > 0
return num % 100 > 0 && num % 50 > 0
}
export function isOperationResultValid(op: Operation): boolean {
@ -63,7 +63,7 @@ export function isSolvable(result: number, plaquettes: number[]): boolean {
for (const item of history) {
if (item.a < item.b) [item.a, item.b] = [item.b, item.a]
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)
if (!found) {
found = true
console.log(`1e solution trouvée en ${Date.now() - start}ms`)
console.log(`1st solution found in ${Date.now() - start}ms`)
}
return
}
@ -138,12 +138,12 @@ export function isSolvable(result: number, plaquettes: number[]): boolean {
loopOperations(plaquettes, [])
if (found) {
console.log(
`Réussite : ${Date.now() - start}ms et ${numberOfIterations} iterations`,
`Success : ${Date.now() - start}ms and ${numberOfIterations} iterations`,
)
}
else {
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>
<h1
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>
<button @click="isSideMenuVisible = true">
<IconMenu class="text-xl btn" />

View File

@ -8,7 +8,7 @@
<IconClose class="absolute top-0 right-1 h-12 text-xl btn" />
</button>
</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" />
<InputSwitch
class="mb-4"
@ -24,15 +24,6 @@
v-model="isLocaleFrench" />
</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">
Build {{ buildDate }}
</div>

View File

@ -17,7 +17,7 @@ export const operations = reactive<Operation[]>([getEmptyOperation()])
export const plaquettes = ref<Plaquette[]>([])
export const result = ref(0)
// #region Difficulty-related constants
// #region Difficulty-related values
export const gameDifficulty = ref(GameDifficultyType.Normal)
@ -29,6 +29,10 @@ export const startingNumberOfPlaquettes = computed(() =>
gameDifficulty.value === GameDifficultyType.Hard ? 6 : 5,
)
export const isHardMode = computed(
() => gameDifficulty.value === GameDifficultyType.Hard,
)
// #endregion Difficulty-related constants
export const currentOperation = computed(

View File

@ -1,34 +1,56 @@
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'
function getSharingText(): string {
// × ÷ + -
const emojis: string[] = []
const endResult = operations[operations.length - 1].result?.value ?? 0
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) {
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) {
case '+':
emojis.push('+')
break
case '-':
emojis.push('-')
break
case '*':
emojis.push('×')
s = '×'
break
case '/':
emojis.push('÷')
s = '÷'
break
case '-':
s = ''
break
case '+':
s = '+'
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()}
===========
${emojis.join(' ')} = ${endResult}
Score: ${100 - percentageDiff(result.value, endResult) * 100}%
===========
return `N0mbers #${numberOfGamesSinceStart()} - ${
isHardMode.value ? 'Advanced' : 'Normal'
}
${lines.join('\n')} ${success ? '✔' : '❌'}
https://n0mbers.scambier.xyz`
}

View File

@ -1,7 +1,6 @@
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'
@ -62,8 +61,7 @@ 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')
}
}

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 LSK_DARKMODE = 'n0_dark'
@ -8,9 +8,9 @@ export const LSK_STATS = 'n0_stats'
export const operators = ['+', '-', '*', '/'] as const
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],
2: [2, 2, 3, 3, 5, 5, 7, 11, 13, 17, 19, 23],
3: [5, 5, 5, 5, 5, 2, 2, 2, 2, 2],
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, 7, 11, 13, 17, 19, 23],
3: [1, 1, 2, 3, 5, 8, 13, 21],
} as const
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.",
"share": "Share",
"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.",
"copiedInClipboard": "Copied in clipboard",
"soon": "Soon",
"dailyHard": "Hard Mode",
"dailyNormal": "Normal Mode"
"dailyHard": "Advanced 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": "☠",
"dailyGame": "Défi quotidien",
"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.",
"randomDescription": "Une partie au hasard, pour le plaisir.",
"share": "Partager",
@ -17,5 +17,9 @@
"copiedInClipboard": "Copié dans le presse-papier",
"soon": "Bientôt",
"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: '' },
},
{
path: '/hard',
name: 'hard',
path: '/advanced',
name: 'advanced',
component: () => import('../views/GameView.vue'),
meta: { transition: 'route' },
},
@ -23,6 +23,12 @@ const router = createRouter({
component: () => import('../views/GameView.vue'),
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 = {
value: number
free: boolean
/** If the plaquette is one of the pre-selected */
original: boolean
/** For sharing */
symbol?: string
}
export type Operation = {

View File

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

View File

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