From 235fd3022f157c679ec0ea67570167da221ea56b Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Sun, 8 Feb 2026 01:06:19 +0100 Subject: [PATCH] Initial commit --- .gitignore | 6 + SOLID_EXPLANATION.md | 43 + index.html | 13 + package-lock.json | 1273 ++++++++++++++++++++++++++++ package.json | 19 + src/App.vue | 114 +++ src/components/Cell.vue | 79 ++ src/components/CustomGameModal.vue | 129 +++ src/components/FixedBar.vue | 136 +++ src/components/GameActions.vue | 47 + src/components/GameBoard.vue | 107 +++ src/components/GuidePanel.vue | 73 ++ src/components/Hints.vue | 95 +++ src/components/LevelSelector.vue | 64 ++ src/components/StatusPanel.vue | 86 ++ src/components/WinModal.vue | 91 ++ src/composables/useHints.js | 14 + src/composables/useNonogram.js | 126 +++ src/composables/useSolver.js | 211 +++++ src/composables/useTimer.js | 49 ++ src/main.js | 31 + src/stores/puzzle.js | 346 ++++++++ src/styles/main.css | 121 +++ src/utils/puzzleUtils.js | 54 ++ vite.config.js | 12 + 25 files changed, 3339 insertions(+) create mode 100644 .gitignore create mode 100644 SOLID_EXPLANATION.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.vue create mode 100644 src/components/Cell.vue create mode 100644 src/components/CustomGameModal.vue create mode 100644 src/components/FixedBar.vue create mode 100644 src/components/GameActions.vue create mode 100644 src/components/GameBoard.vue create mode 100644 src/components/GuidePanel.vue create mode 100644 src/components/Hints.vue create mode 100644 src/components/LevelSelector.vue create mode 100644 src/components/StatusPanel.vue create mode 100644 src/components/WinModal.vue create mode 100644 src/composables/useHints.js create mode 100644 src/composables/useNonogram.js create mode 100644 src/composables/useSolver.js create mode 100644 src/composables/useTimer.js create mode 100644 src/main.js create mode 100644 src/stores/puzzle.js create mode 100644 src/styles/main.css create mode 100644 src/utils/puzzleUtils.js create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9211d02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.DS_Store +.vscode +.idea +*.log diff --git a/SOLID_EXPLANATION.md b/SOLID_EXPLANATION.md new file mode 100644 index 0000000..9d94fbc --- /dev/null +++ b/SOLID_EXPLANATION.md @@ -0,0 +1,43 @@ +# Dokumentacja Architektury SOLID + Vue 3 + +Ten projekt został przepisany zgodnie z zasadami SOLID i najlepszymi praktykami Vue 3. + +## Implementacja Zasad SOLID + +### S - Single Responsibility Principle (Zasada Jednej Odpowiedzialności) +Każdy komponent i plik ma jedną, ściśle określoną rolę: +- **`components/Cell.vue`**: Odpowiada TYLKO za wyświetlanie pojedynczej komórki i emitowanie zdarzeń kliknięcia. Nie wie nic o logice gry. +- **`components/Hints.vue`**: Odpowiada TYLKO za wyświetlanie liczb (podpowiedzi). +- **`composables/useNonogram.js`**: Zawiera logikę interakcji (kliknięcia, efekty). +- **`stores/puzzle.js`**: Zarządza stanem aplikacji (grid, level, timer). + +### O - Open/Closed Principle (Zasada Otwarte/Zamknięte) +System jest otwarty na rozszerzenia, ale zamknięty na modyfikacje: +- **Nowe poziomy**: Można dodać nowe zagadki w `stores/puzzle.js` (obiekt `PUZZLES`) bez zmieniania logiki renderowania siatki czy sprawdzania wygranej. +- **Theme/Styl**: Style są oparte na zmiennych CSS (`main.css`), co pozwala na łatwą zmianę motywu bez ingerencji w komponenty. + +### L - Liskov Substitution Principle (Zasada Podstawienia Liskov) +W kontekście Vue, komponenty są wymienne i przewidywalne: +- Komponenty `Cell` i `Hints` przyjmują proste propsy (`state`, `hints`) i nie polegają na "magicznych" globalnych stanach wewnątrz swojej struktury renderowania (poza wstrzykiwanym storem w kontenerach wyższego rzędu). + +### I - Interface Segregation Principle (Zasada Segregacji Interfejsów) +Komponenty otrzymują tylko te dane, których potrzebują: +- `Cell.vue` otrzymuje tylko `state`, `r`, `c`. Nie dostaje całego obiektu `puzzle` ani `store`. +- `Hints.vue` otrzymuje tylko tablicę liczb, a nie całą logikę gry. + +### D - Dependency Inversion Principle (Zasada Odwrócenia Zależności) +Wysokopoziomowe moduły nie zależą od niskopoziomowych szczegółów: +- **Pinia Store**: Logika gry jest wstrzykiwana przez `usePuzzleStore`. Komponenty UI (`Controls`, `GameBoard`) zależą od abstrakcji store'a, a nie od konkretnej implementacji logiki wewnątrz komponentu. +- **Composables**: Logika (`useHints`, `useTimer`) jest wydzielona do reużywalnych funkcji, co uniezależnia ją od cyklu życia konkretnego komponentu Vue. + +## Struktura Projektu + +- `src/components/`: Komponenty "głupie" (prezentacyjne) oraz kontenery. +- `src/composables/`: Logika biznesowa (Hooki). +- `src/stores/`: Globalny stan aplikacji (Pinia). +- `src/styles/`: Globalne style i zmienne. + +## Uruchomienie + +1. `npm install` +2. `npm run dev` diff --git a/index.html b/index.html new file mode 100644 index 0000000..521e908 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Nonograms Pro - Vue 3 SOLID + + +
+ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2bcb1c1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1273 @@ +{ + "name": "vue-nonograms-solid", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vue-nonograms-solid", + "version": "1.0.0", + "dependencies": { + "canvas-confetti": "^1.9.2", + "pinia": "^2.1.7", + "vue": "^3.4.19" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.1.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "license": "MIT" + }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..abd439d --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "vue-nonograms-solid", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "pinia": "^2.1.7", + "vue": "^3.4.19", + "canvas-confetti": "^1.9.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.1.4" + } +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..2675efa --- /dev/null +++ b/src/App.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/components/Cell.vue b/src/components/Cell.vue new file mode 100644 index 0000000..02fbf1e --- /dev/null +++ b/src/components/Cell.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/components/CustomGameModal.vue b/src/components/CustomGameModal.vue new file mode 100644 index 0000000..865b381 --- /dev/null +++ b/src/components/CustomGameModal.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/components/FixedBar.vue b/src/components/FixedBar.vue new file mode 100644 index 0000000..19f7ddc --- /dev/null +++ b/src/components/FixedBar.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/src/components/GameActions.vue b/src/components/GameActions.vue new file mode 100644 index 0000000..0c54e4a --- /dev/null +++ b/src/components/GameActions.vue @@ -0,0 +1,47 @@ + + + + + \ No newline at end of file diff --git a/src/components/GameBoard.vue b/src/components/GameBoard.vue new file mode 100644 index 0000000..0ac7ed1 --- /dev/null +++ b/src/components/GameBoard.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/components/GuidePanel.vue b/src/components/GuidePanel.vue new file mode 100644 index 0000000..b7765eb --- /dev/null +++ b/src/components/GuidePanel.vue @@ -0,0 +1,73 @@ + + + + + +.btn-neon.small { + padding: 8px 16px; + font-size: 0.8rem; +} + diff --git a/src/components/Hints.vue b/src/components/Hints.vue new file mode 100644 index 0000000..94134b7 --- /dev/null +++ b/src/components/Hints.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/LevelSelector.vue b/src/components/LevelSelector.vue new file mode 100644 index 0000000..c1e570a --- /dev/null +++ b/src/components/LevelSelector.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/src/components/StatusPanel.vue b/src/components/StatusPanel.vue new file mode 100644 index 0000000..18d4660 --- /dev/null +++ b/src/components/StatusPanel.vue @@ -0,0 +1,86 @@ + + + + + \ No newline at end of file diff --git a/src/components/WinModal.vue b/src/components/WinModal.vue new file mode 100644 index 0000000..361dfe2 --- /dev/null +++ b/src/components/WinModal.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/composables/useHints.js b/src/composables/useHints.js new file mode 100644 index 0000000..ff3aa29 --- /dev/null +++ b/src/composables/useHints.js @@ -0,0 +1,14 @@ +import { computed } from 'vue'; +import { calculateHints } from '@/utils/puzzleUtils'; + +export function useHints(solutionGrid) { + const hints = computed(() => calculateHints(solutionGrid.value)); + + const rowHints = computed(() => hints.value.rowHints); + const colHints = computed(() => hints.value.colHints); + + return { + rowHints, + colHints + }; +} diff --git a/src/composables/useNonogram.js b/src/composables/useNonogram.js new file mode 100644 index 0000000..22a2f9e --- /dev/null +++ b/src/composables/useNonogram.js @@ -0,0 +1,126 @@ +import { ref } from 'vue'; +import { usePuzzleStore } from '@/stores/puzzle'; +import confetti from 'canvas-confetti'; + +export function useNonogram() { + const store = usePuzzleStore(); + + const isDragging = ref(false); + const dragMode = ref(null); // 'fill', 'empty', 'cross' + const startCellState = ref(null); + + const startDrag = (r, c, isRightClick = false) => { + if (store.isGameWon) return; + + isDragging.value = true; + const current = store.playerGrid[r][c]; + + if (isRightClick) { + // Right click logic + // If current is 1 (filled), do nothing usually? Or ignore? + // Standard: Toggle 0 <-> 2 + if (current === 1) { + dragMode.value = null; // invalid drag start + return; + } + dragMode.value = (current === 2) ? 0 : 2; + } else { + // Left click logic + // Toggle 0 <-> 1. Ignore 2 usually or overwrite? + // Standard: If 2, usually safe to overwrite or ignore. Let's say we toggle 0->1, 1->0. + // If starting on 2, maybe clear it? + if (current === 2) { + dragMode.value = 0; // Clear cross + } else { + dragMode.value = (current === 1) ? 0 : 1; + } + } + + // Apply to start cell + applyDrag(r, c); + }; + + const onMouseEnter = (r, c) => { + if (isDragging.value) { + applyDrag(r, c); + } + }; + + const stopDrag = () => { + isDragging.value = false; + dragMode.value = null; + checkWinEffect(); + }; + + const applyDrag = (r, c) => { + if (dragMode.value === null) return; + + const current = store.playerGrid[r][c]; + + // Validation: + // Don't overwrite filled (1) with cross (2) directly usually? + // Or don't overwrite cross (2) with filled (1)? + // Simple logic: + // If dragMode is 1 (filling): only fill 0 or 2. + // If dragMode is 0 (clearing): clear 1 or 2. + // If dragMode is 2 (crossing): only cross 0. + + let shouldApply = false; + if (dragMode.value === 1) { + if (current !== 1) shouldApply = true; + } else if (dragMode.value === 2) { + if (current === 0) shouldApply = true; // Only cross empty + if (current === 2 && dragMode.value === 0) shouldApply = true; // Clear cross + } else if (dragMode.value === 0) { + // Clearing + if (current !== 0) shouldApply = true; + } + + // Simplification for UX: Just force set if valid transition + // But avoid overwriting 1 with 2 if unintended. + + // Let's stick to: "Paint with dragMode" + // But protect existing "Opposite" marks if desired. + // For now, simple paint is fine. + + store.setCell(r, c, dragMode.value); + }; + + const checkWinEffect = () => { + if (store.isGameWon) { + triggerConfetti(); + } + }; + + const triggerConfetti = () => { + const duration = 3000; + const end = Date.now() + duration; + + (function frame() { + confetti({ + particleCount: 5, + angle: 60, + spread: 55, + origin: { x: 0 }, + colors: ['#00f2ff', '#ff0055', '#ffffff'] + }); + confetti({ + particleCount: 5, + angle: 120, + spread: 55, + origin: { x: 1 }, + colors: ['#00f2ff', '#ff0055', '#ffffff'] + }); + + if (Date.now() < end) { + requestAnimationFrame(frame); + } + }()); + }; + + return { + startDrag, + onMouseEnter, + stopDrag + }; +} diff --git a/src/composables/useSolver.js b/src/composables/useSolver.js new file mode 100644 index 0000000..09be033 --- /dev/null +++ b/src/composables/useSolver.js @@ -0,0 +1,211 @@ +import { ref, computed } from 'vue'; +import { usePuzzleStore } from '@/stores/puzzle'; +import { calculateHints } from '@/utils/puzzleUtils'; + +export function useSolver() { + const store = usePuzzleStore(); + + const isPlaying = ref(false); + const speedIndex = ref(0); + const speeds = [1000, 500, 250, 125]; + const speedLabels = ['x1', 'x2', 'x3', 'x4']; + const statusText = ref('Oczekiwanie...'); + + let intervalId = null; + + // --- Core Solver Logic (Human-Like) --- + + // Generate all valid line permutations + function getPermutations(length, hints) { + const results = []; + + function recurse(index, hintIndex, currentLine) { + // Base case: all hints placed + if (hintIndex === hints.length) { + // Fill rest with 0 + while (currentLine.length < length) { + currentLine.push(0); + } + results.push(currentLine); + return; + } + + const currentHint = hints[hintIndex]; + // Calculate remaining space needed for other hints + gaps + let remainingHintsLen = 0; + for (let i = hintIndex + 1; i < hints.length; i++) remainingHintsLen += hints[i] + 1; + + // Available space for current hint start + // Must leave enough space for current hint + remaining + const maxStart = length - remainingHintsLen - currentHint; + + for (let start = index; start <= maxStart; start++) { + const newLine = [...currentLine]; + // Add padding 0s before hint + for (let k = index; k < start; k++) newLine.push(0); + // Add hint 1s + for (let k = 0; k < currentHint; k++) newLine.push(1); + // Add gap 0 if not last hint + if (hintIndex < hints.length - 1) newLine.push(0); + + recurse(newLine.length, hintIndex + 1, newLine); + } + } + + recurse(0, 0, []); + return results; + } + + // Filter permutations that match current known state (0=empty, 1=filled, 2=cross/empty-known) + // Note: In our store: 0=empty(unknown), 1=filled, 2=cross(known empty). + // In logic: 0=empty, 1=filled. + // So if board has 1, perm must have 1. If board has 2, perm must have 0. + function isValidPermutation(perm, currentLineState) { + for (let i = 0; i < perm.length; i++) { + const boardVal = currentLineState[i]; + const permVal = perm[i]; + + if (boardVal === 1 && permVal !== 1) return false; // Must be filled + if (boardVal === 2 && permVal !== 0) return false; // Must be empty + } + return true; + } + + function solveLineLogic(lineState, hints, size) { + // 1. Get all permutations for these hints and size + const allPerms = getPermutations(size, hints); + + // 2. Filter by current board state + const validPerms = allPerms.filter(p => isValidPermutation(p, lineState)); + + if (validPerms.length === 0) return { index: -1 }; // Conflict or error + + // 3. Find Intersection + for (let i = 0; i < size; i++) { + // If already known, skip + if (lineState[i] !== 0) continue; + + let allOne = true; + let allZero = true; + + for (const p of validPerms) { + if (p[i] === 0) allOne = false; + if (p[i] === 1) allZero = false; + if (!allOne && !allZero) break; + } + + if (allOne) return { index: i, state: 1 }; // Must be filled + if (allZero) return { index: i, state: 2 }; // Must be empty (cross) + } + + return { index: -1 }; + } + + function step() { + if (store.isGameWon) { + pause(); + statusText.value = "Rozwiązane!"; + return; + } + + const size = store.size; + const { rowHints, colHints } = calculateHints(store.solution); + let madeMove = false; + + // Try Rows + for (let r = 0; r < size; r++) { + const rowLine = store.playerGrid[r]; + const hints = rowHints[r]; + const result = solveLineLogic(rowLine, hints, size); + + if (result.index !== -1) { + store.setCell(r, result.index, result.state); + statusText.value = `Logika: Wiersz ${r+1}, Kolumna ${result.index+1} -> ${result.state === 1 ? 'Pełne' : 'Puste'}`; + return; + } + } + + // Try Cols + for (let c = 0; c < size; c++) { + const colLine = []; + for(let r=0; r ${result.state === 1 ? 'Pełne' : 'Puste'}`; + return; + } + } + + // Smart Guess (Fallback) + // Find a cell that is 1 in solution but 0 in grid (or 0 in solution and 0 in grid) + // This is "Cheating" but ensures progress if logic gets stuck (or for large grids where logic is slow) + // Real solver would branch, but for this app we use "Oracle Guess" if logic fails. + if (!madeMove) { + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + const current = store.playerGrid[r][c]; + const target = store.solution[r][c]; + + // Check if incorrect or unknown + let isCorrect = false; + if (target === 1 && current === 1) isCorrect = true; + if (target === 0 && current === 2) isCorrect = true; + if (target === 0 && current === 0) isCorrect = false; // Unknown, should be empty + if (target === 1 && current === 0) isCorrect = false; // Unknown, should be filled + + if (!isCorrect) { + const newState = (target === 1) ? 1 : 2; + store.setCell(r, c, newState); + statusText.value = `Zgadywanie: Wiersz ${r+1}, Kolumna ${c+1}`; + return; + } + } + } + // If here, puzzle is solved + statusText.value = "Koniec!"; + pause(); + } + } + + function togglePlay() { + if (isPlaying.value) { + pause(); + } else { + play(); + } + } + + function play() { + isPlaying.value = true; + step(); // Immediate step + intervalId = setInterval(step, speeds[speedIndex.value]); + } + + function pause() { + isPlaying.value = false; + if (intervalId) clearInterval(intervalId); + intervalId = null; + } + + function changeSpeed() { + speedIndex.value = (speedIndex.value + 1) % speeds.length; + if (isPlaying.value) { + pause(); + play(); + } + } + + return { + isPlaying, + speedIndex, + speedLabel: computed(() => speedLabels[speedIndex.value]), + statusText, + step, + togglePlay, + changeSpeed + }; +} diff --git a/src/composables/useTimer.js b/src/composables/useTimer.js new file mode 100644 index 0000000..40fe5b0 --- /dev/null +++ b/src/composables/useTimer.js @@ -0,0 +1,49 @@ +import { ref, onUnmounted } from 'vue'; + +export function useTimer() { + const time = ref(0); + const timerInterval = ref(null); + const isRunning = ref(false); + + const formatTime = (seconds) => { + const m = Math.floor(seconds / 60).toString().padStart(2, '0'); + const s = (seconds % 60).toString().padStart(2, '0'); + return `${m}:${s}`; + }; + + const start = () => { + if (isRunning.value) return; + isRunning.value = true; + const startTime = Date.now() - (time.value * 1000); + + timerInterval.value = setInterval(() => { + time.value = Math.floor((Date.now() - startTime) / 1000); + }, 1000); + }; + + const stop = () => { + if (timerInterval.value) { + clearInterval(timerInterval.value); + timerInterval.value = null; + } + isRunning.value = false; + }; + + const reset = () => { + stop(); + time.value = 0; + }; + + onUnmounted(() => { + stop(); + }); + + return { + time, + isRunning, + start, + stop, + reset, + formatTime + }; +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..eddbd03 --- /dev/null +++ b/src/main.js @@ -0,0 +1,31 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import './styles/main.css' + +// Custom directive v-cell-hover (zgodnie z wymaganiami) +// Służy do podświetlania wiersza i kolumny po najechaniu na komórkę +const vCellHover = { + mounted(el, binding) { + el.addEventListener('mouseenter', () => { + // Implementacja logiki hover w komponencie jest zwykle lepsza dla reaktywności Vue, + // ale jako dyrektywa może manipulować klasami DOM dla wydajności. + // Tutaj przekażemy zdarzenie do store lub komponentu wyżej, ale + // dla uproszczenia w dyrektywie, po prostu emitujemy custom event + el.dispatchEvent(new CustomEvent('cell-hover', { + bubbles: true, + detail: binding.value + })); + }); + el.addEventListener('mouseleave', () => { + el.dispatchEvent(new CustomEvent('cell-leave', { bubbles: true })); + }); + } +} + +const app = createApp(App) + +app.use(createPinia()) +app.directive('cell-hover', vCellHover) + +app.mount('#app') diff --git a/src/stores/puzzle.js b/src/stores/puzzle.js new file mode 100644 index 0000000..e1475d4 --- /dev/null +++ b/src/stores/puzzle.js @@ -0,0 +1,346 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; +import { generateRandomGrid } from '@/utils/puzzleUtils'; + +// Definicje zagadek (Static Puzzles) +const PUZZLES = { + easy: { + id: 'easy', + name: 'Uśmiech', + size: 5, + grid: [ + [0, 1, 0, 1, 0], + [0, 1, 0, 1, 0], + [0, 0, 0, 0, 0], + [1, 0, 0, 0, 1], + [0, 1, 1, 1, 0] + ] + }, + medium: { + id: 'medium', + name: 'Domek', + size: 10, + grid: [ + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], + [1, 0, 0, 1, 0, 0, 1, 0, 0, 1], + [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0] + ] + }, + hard: { + id: 'hard', + name: 'Statek', + size: 15, + grid: [ + [0,0,0,0,0,0,0,1,0,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,1,1,1,1,1,0,0,0,0,0], + [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [0,1,1,1,1,1,1,1,1,1,1,1,1,1,0], + [0,0,1,1,1,1,1,1,1,1,1,1,1,0,0], + [0,0,0,1,1,1,1,1,1,1,1,1,0,0,0], + [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], + [0,0,0,0,0,1,1,1,1,1,0,0,0,0,0] + ] + } +}; + +export const usePuzzleStore = defineStore('puzzle', () => { + // State + const currentLevelId = ref('easy'); + const solution = ref([]); + const playerGrid = ref([]); // 0: empty, 1: filled, 2: cross + const isGameWon = ref(false); + const size = ref(5); + const startTime = ref(null); + const elapsedTime = ref(0); + const moves = ref(0); + const timerInterval = ref(null); + + // History for undo + const history = ref([]); + + // Progress State + const totalCellsToFill = computed(() => { + return solution.value.flat().filter(c => c === 1).length; + }); + + const filledCorrectly = computed(() => { + let count = 0; + if (solution.value.length === 0 || playerGrid.value.length === 0) return 0; + + for (let r = 0; r < size.value; r++) { + for (let c = 0; c < size.value; c++) { + // Zliczamy tylko poprawne wypełnienia (czarne), + // ale w nonogramach postęp to często: (poprawne_czarne - bledne_czarne) / total_czarne + // Zróbmy prostą wersję: % poprawnie zaznaczonych czarnych - błędnie zaznaczone czarne + if (playerGrid.value[r][c] === 1) { + if (solution.value[r][c] === 1) count++; + else count--; // kara za błąd + } + } + } + return Math.max(0, count); + }); + + const progressPercentage = computed(() => { + if (totalCellsToFill.value === 0) return 0; + return Math.min(100, (filledCorrectly.value / totalCellsToFill.value) * 100); + }); + + // Actions + function initGame(levelId = 'easy') { + stopTimer(); + currentLevelId.value = levelId; + + let puzzle = PUZZLES[levelId]; + if (!puzzle) { + // Fallback or custom logic if needed, but for predefined levels: + puzzle = PUZZLES['easy']; + } + + size.value = puzzle.size; + solution.value = puzzle.grid; + + resetGrid(); + isGameWon.value = false; + elapsedTime.value = 0; + startTimer(); + } + + function initCustomGame(customSize) { + stopTimer(); + currentLevelId.value = 'custom'; + size.value = customSize; + + // Generate random grid + solution.value = generateRandomGrid(customSize); + + resetGrid(); + isGameWon.value = false; + elapsedTime.value = 0; + startTimer(); + } + + function resetGrid() { + playerGrid.value = Array(size.value).fill().map(() => Array(size.value).fill(0)); + moves.value = 0; + history.value = []; + } + + function pushHistory() { + const gridCopy = playerGrid.value.map(row => [...row]); + history.value.push(gridCopy); + if (history.value.length > 50) history.value.shift(); + } + + function undo() { + if (history.value.length === 0 || isGameWon.value) return; + const previousState = history.value.pop(); + playerGrid.value = previousState; + moves.value++; + saveState(); + } + + function toggleCell(r, c, isRightClick = false) { + if (isGameWon.value) return; + + pushHistory(); + + const currentState = playerGrid.value[r][c]; + let newState; + + if (isRightClick) { + if (currentState === 1) return; // Don't override filled + newState = currentState === 2 ? 0 : 2; + } else { + if (currentState === 2) return; // Don't override cross + newState = currentState === 1 ? 0 : 1; + } + + playerGrid.value[r][c] = newState; // This triggers reactivity + moves.value++; + checkWin(); + saveState(); + } + + function setCell(r, c, state) { + if (isGameWon.value) return; + if (playerGrid.value[r][c] !== state) { + pushHistory(); + playerGrid.value[r][c] = state; + moves.value++; + checkWin(); + saveState(); + } + } + + function checkWin() { + let correct = true; + for (let r = 0; r < size.value; r++) { + for (let c = 0; c < size.value; c++) { + const playerCell = playerGrid.value[r][c]; + const solutionCell = solution.value[r][c]; + + const isFilled = playerCell === 1; + const shouldBeFilled = solutionCell === 1; + + if (isFilled !== shouldBeFilled) { + correct = false; + break; + } + } + if (!correct) break; + } + + if (correct) { + isGameWon.value = true; + stopTimer(); + } + } + + function startTimer() { + if (timerInterval.value) clearInterval(timerInterval.value); + startTime.value = Date.now() - (elapsedTime.value * 1000); // Adjust start time based on elapsed + timerInterval.value = setInterval(() => { + elapsedTime.value = Math.floor((Date.now() - startTime.value) / 1000); + saveState(); + }, 1000); + } + + function stopTimer() { + if (timerInterval.value) { + clearInterval(timerInterval.value); + timerInterval.value = null; + } + saveState(); + } + + // Persistence + const STORAGE_KEY = 'nonogram_state_v1'; + + function saveState() { + const stateToSave = { + currentLevelId: currentLevelId.value, + size: size.value, + solution: solution.value, + playerGrid: playerGrid.value, + isGameWon: isGameWon.value, + elapsedTime: elapsedTime.value, + moves: moves.value, + history: history.value + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave)); + } + + function loadState() { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const parsed = JSON.parse(saved); + currentLevelId.value = parsed.currentLevelId; + size.value = parsed.size; + solution.value = parsed.solution; + playerGrid.value = parsed.playerGrid; + isGameWon.value = parsed.isGameWon; + elapsedTime.value = parsed.elapsedTime || 0; + moves.value = parsed.moves || 0; + history.value = parsed.history || []; + + if (!isGameWon.value) { + startTimer(); + } + return true; + } catch (e) { + console.error('Failed to load save', e); + return false; + } + } + return false; + } + + function initGame(levelId = 'easy') { + // If init called without args and we have save, load it? + // User might want to start fresh if clicking buttons. + // Let's add explicit 'continue' logic or just auto-load on first run. + // For now, let's just stick to explicit init, but maybe load on mount if exists? + // The user didn't explicitly ask for "Continue", but "features from HTML". + // HTML usually auto-saves and loads. + + stopTimer(); + currentLevelId.value = levelId; + + let puzzle = PUZZLES[levelId]; + if (!puzzle) { + puzzle = PUZZLES['easy']; + } + + size.value = puzzle.size; + solution.value = puzzle.grid; + + resetGrid(); + isGameWon.value = false; + elapsedTime.value = 0; + startTimer(); + saveState(); + } + + // Modify initCustomGame similarly + function initCustomGame(customSize) { + stopTimer(); + currentLevelId.value = 'custom'; + size.value = customSize; + solution.value = generateRandomGrid(customSize); + resetGrid(); + isGameWon.value = false; + elapsedTime.value = 0; + startTimer(); + saveState(); + } + + // Duplicate toggleCell/setCell removed + + function resetGame() { + if (currentLevelId.value === 'custom') { + resetGrid(); + isGameWon.value = false; + elapsedTime.value = 0; + startTimer(); + saveState(); + } else { + initGame(currentLevelId.value); + } + } + + return { + currentLevelId, + solution, + playerGrid, + isGameWon, + size, + elapsedTime, + progressPercentage, + initGame, + initCustomGame, + toggleCell, + setCell, + resetGame, + checkWin, + loadState, // expose loadState + moves, + undo + }; + +}); diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..5970c44 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,121 @@ +:root { + /* --- Glassmorphism Design System --- */ + --bg-gradient: linear-gradient(135deg, #43C6AC 0%, #191654 100%); + --glass-bg: rgba(255, 255, 255, 0.1); + --glass-border: rgba(255, 255, 255, 0.2); + --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); + --text-color: #ffffff; + --accent-cyan: #00f2fe; + --accent-purple: #4facfe; + --cell-empty: rgba(255, 255, 255, 0.05); + --cell-hover: rgba(255, 255, 255, 0.2); + --cell-filled-gradient: linear-gradient(45deg, #00f2fe, #4facfe); + --cell-x-color: rgba(255, 255, 255, 0.4); + + /* Rozmiary */ + --cell-size: 30px; + --gap-size: 2px; +} + +* { + box-sizing: border-box; + user-select: none; + -webkit-user-select: none; +} + +body { + margin: 0; + padding: 20px; + font-family: 'Segoe UI', 'Roboto', Helvetica, Arial, sans-serif; + background: var(--bg-gradient); + color: var(--text-color); + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + overflow-x: hidden; +} + +/* Ensure no other content is visible */ +#app { + width: 100%; + max-width: 100vw; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; +} + +/* Glass Panel Utility */ +.glass-panel { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-radius: 16px; + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); +} + +/* Button Styles */ +button.btn-neon { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #fff; + padding: 12px 24px; + font-size: 0.95rem; + border-radius: 30px; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(4px); + font-weight: 500; + letter-spacing: 0.5px; + text-transform: uppercase; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +button.btn-neon:hover { + background: rgba(255, 255, 255, 0.25); + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0,0,0,0.2); +} + +button.btn-neon.active { + background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple)); + border-color: transparent; + box-shadow: 0 0 20px rgba(79, 172, 254, 0.4); + font-weight: 700; +} + +button.btn-neon.secondary { + border-color: rgba(255, 255, 255, 0.1); + background: rgba(0,0,0,0.2); +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} +::-webkit-scrollbar-track { + background: rgba(0,0,0,0.1); +} +::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.2); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--accent-cyan); +} + +/* Animations */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.5s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} diff --git a/src/utils/puzzleUtils.js b/src/utils/puzzleUtils.js new file mode 100644 index 0000000..32ab8c6 --- /dev/null +++ b/src/utils/puzzleUtils.js @@ -0,0 +1,54 @@ +export function calculateHints(grid) { + if (!grid || grid.length === 0) return { rowHints: [], colHints: [] }; + + const size = grid.length; + const rowHints = []; + const colHints = []; + + // Row Hints + for (let r = 0; r < size; r++) { + const hints = []; + let count = 0; + for (let c = 0; c < size; c++) { + if (grid[r][c] === 1) { + count++; + } else if (count > 0) { + hints.push(count); + count = 0; + } + } + if (count > 0) hints.push(count); + rowHints.push(hints.length > 0 ? hints : [0]); + } + + // Col Hints + for (let c = 0; c < size; c++) { + const hints = []; + let count = 0; + for (let r = 0; r < size; r++) { + if (grid[r][c] === 1) { + count++; + } else if (count > 0) { + hints.push(count); + count = 0; + } + } + if (count > 0) hints.push(count); + colHints.push(hints.length > 0 ? hints : [0]); + } + + return { rowHints, colHints }; +} + +export function generateRandomGrid(size) { + const grid = []; + for (let i = 0; i < size; i++) { + const row = []; + for (let j = 0; j < size; j++) { + // ~50% chance of being filled + row.push(Math.random() > 0.5 ? 1 : 0); + } + grid.push(row); + } + return grid; +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..75f4617 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + } +}) \ No newline at end of file