Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
18568fa4ae
|
|||
|
c3188bb740
|
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vue-nonograms-solid",
|
||||
"version": "1.14.1",
|
||||
"version": "1.14.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vue-nonograms-solid",
|
||||
"version": "1.14.1",
|
||||
"version": "1.14.2",
|
||||
"dependencies": {
|
||||
"fireworks-js": "^2.10.8",
|
||||
"flag-icons": "^7.5.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vue-nonograms-solid",
|
||||
"version": "1.14.1",
|
||||
"version": "1.14.2",
|
||||
"homepage": "https://nonograms.7u.pl/",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const {
|
||||
isPlaying,
|
||||
isStuck,
|
||||
speedLabel,
|
||||
statusText,
|
||||
step,
|
||||
togglePlay,
|
||||
changeSpeed
|
||||
changeSpeed,
|
||||
boost
|
||||
} = useSolver();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
@@ -29,6 +31,10 @@ const { t } = useI18n();
|
||||
<button class="btn-neon small" @click="changeSpeed">
|
||||
{{ t('guide.speed') }}: {{ speedLabel }}
|
||||
</button>
|
||||
|
||||
<button v-if="isStuck" class="btn-neon small boost-btn" @click="boost">
|
||||
⚡ Boost (DFS)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -68,4 +74,15 @@ const { t } = useI18n();
|
||||
padding: 5px 15px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.boost-btn {
|
||||
border-color: #ffd700;
|
||||
color: #ffd700;
|
||||
box-shadow: 0 0 10px rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
|
||||
.boost-btn:hover {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -47,7 +47,9 @@ const getShareData = () => ({
|
||||
grid: store.playerGrid,
|
||||
size: store.size,
|
||||
currentDensity: store.currentDensity,
|
||||
guideUsageCount: store.guideUsageCount
|
||||
guideUsageCount: store.guideUsageCount,
|
||||
hasUsedBoost: store.hasUsedBoost,
|
||||
boostUsageCount: store.boostUsageCount
|
||||
});
|
||||
|
||||
const downloadShareSVG = () => {
|
||||
|
||||
@@ -68,6 +68,7 @@ const messages = {
|
||||
'win.shareDownload': 'Pobierz zrzut',
|
||||
'win.difficulty': 'Poziom:',
|
||||
'win.usedGuide': 'Podpowiedzi: {percent}% ({count})',
|
||||
'win.boosted': 'Wspomagany (DFS)',
|
||||
'pwa.installTitle': 'Zainstaluj aplikację i graj offline',
|
||||
'pwa.installMobile': 'Dodaj do ekranu głównego',
|
||||
'pwa.installDesktop': 'Zainstaluj na komputerze',
|
||||
@@ -244,6 +245,7 @@ const messages = {
|
||||
'win.shareDownload': 'Download screenshot',
|
||||
'win.difficulty': 'Difficulty:',
|
||||
'win.usedGuide': 'Hints: {percent}% ({count})',
|
||||
'win.boosted': 'Boosted (DFS)',
|
||||
'pwa.installTitle': 'Install the app and play offline',
|
||||
'pwa.installMobile': 'Add to home screen',
|
||||
'pwa.installDesktop': 'Install on desktop',
|
||||
|
||||
@@ -8,6 +8,7 @@ export function useSolver() {
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const isProcessing = ref(false);
|
||||
const isStuck = ref(false);
|
||||
const speedIndex = ref(0);
|
||||
const speeds = [1000, 500, 250, 125, 62, 31, 16];
|
||||
const speedLabels = ['x1', 'x2', 'x4', 'x8', 'x16', 'x32', 'x64'];
|
||||
@@ -25,6 +26,7 @@ export function useSolver() {
|
||||
}
|
||||
if (isProcessing.value) return;
|
||||
store.markGuideUsed();
|
||||
isStuck.value = false;
|
||||
ensureWorker();
|
||||
isProcessing.value = true;
|
||||
|
||||
@@ -62,6 +64,19 @@ export function useSolver() {
|
||||
}
|
||||
}
|
||||
|
||||
function boost() {
|
||||
if (store.isGameWon || isProcessing.value) return;
|
||||
store.markBoostUsed();
|
||||
isStuck.value = false;
|
||||
ensureWorker();
|
||||
isProcessing.value = true;
|
||||
|
||||
const playerGrid = store.playerGrid.map(row => row.slice());
|
||||
const solution = store.solution.map(row => row.slice());
|
||||
const id = ++requestId;
|
||||
worker.postMessage({ id, playerGrid, solution, locale: locale.value, action: 'boost' });
|
||||
}
|
||||
|
||||
function ensureWorker() {
|
||||
if (worker) return;
|
||||
worker = new Worker(new URL('../workers/solverWorker.js', import.meta.url), { type: 'module' });
|
||||
@@ -71,15 +86,18 @@ export function useSolver() {
|
||||
if (type === 'move') {
|
||||
store.setCell(r, c, state);
|
||||
isProcessing.value = false;
|
||||
isStuck.value = false;
|
||||
if (store.isGameWon) {
|
||||
pause();
|
||||
return;
|
||||
}
|
||||
} else if (type === 'done') {
|
||||
isProcessing.value = false;
|
||||
isStuck.value = false;
|
||||
pause();
|
||||
} else if (type === 'stuck') {
|
||||
isProcessing.value = false;
|
||||
isStuck.value = true;
|
||||
pause();
|
||||
} else {
|
||||
isProcessing.value = false;
|
||||
@@ -95,11 +113,13 @@ export function useSolver() {
|
||||
|
||||
return {
|
||||
isPlaying,
|
||||
isStuck,
|
||||
speedIndex,
|
||||
speedLabel: computed(() => speedLabels[speedIndex.value]),
|
||||
statusText,
|
||||
step,
|
||||
togglePlay,
|
||||
changeSpeed
|
||||
changeSpeed,
|
||||
boost
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
const isGameWon = ref(false);
|
||||
const hasUsedGuide = ref(false);
|
||||
const guideUsageCount = ref(0);
|
||||
const hasUsedBoost = ref(false);
|
||||
const boostUsageCount = ref(0);
|
||||
const currentDifficulty = ref(null); // 'easy', 'medium', 'hard', 'custom' or object { density: 0.5 }
|
||||
const currentDensity = ref(0);
|
||||
const size = ref(5);
|
||||
@@ -72,6 +74,8 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
isGameWon.value = false;
|
||||
hasUsedGuide.value = false;
|
||||
guideUsageCount.value = 0;
|
||||
hasUsedBoost.value = false;
|
||||
boostUsageCount.value = 0;
|
||||
currentDensity.value = totalCellsToFill.value / (size.value * size.value);
|
||||
elapsedTime.value = 0;
|
||||
startTimer();
|
||||
@@ -90,6 +94,8 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
isGameWon.value = false;
|
||||
hasUsedGuide.value = false;
|
||||
guideUsageCount.value = 0;
|
||||
hasUsedBoost.value = false;
|
||||
boostUsageCount.value = 0;
|
||||
currentDensity.value = density;
|
||||
elapsedTime.value = 0;
|
||||
startTimer();
|
||||
@@ -110,6 +116,8 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
isGameWon.value = false;
|
||||
hasUsedGuide.value = false;
|
||||
guideUsageCount.value = 0;
|
||||
hasUsedBoost.value = false;
|
||||
boostUsageCount.value = 0;
|
||||
|
||||
// Calculate density
|
||||
const totalFilled = grid.flat().filter(c => c === 1).length;
|
||||
@@ -256,6 +264,17 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
}
|
||||
|
||||
if (correct) {
|
||||
// Auto-fill remaining empty cells with X (2)
|
||||
const rows = solution.value.length;
|
||||
const cols = solution.value[0].length;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
if (playerGrid.value[r][c] === 0) {
|
||||
playerGrid.value[r][c] = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isGameWon.value = true;
|
||||
stopTimer();
|
||||
}
|
||||
@@ -290,6 +309,8 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
isGameWon: isGameWon.value,
|
||||
hasUsedGuide: hasUsedGuide.value,
|
||||
guideUsageCount: guideUsageCount.value,
|
||||
hasUsedBoost: hasUsedBoost.value,
|
||||
boostUsageCount: boostUsageCount.value,
|
||||
currentDensity: currentDensity.value,
|
||||
elapsedTime: elapsedTime.value,
|
||||
moves: moves.value,
|
||||
@@ -310,6 +331,8 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
isGameWon.value = parsed.isGameWon;
|
||||
hasUsedGuide.value = parsed.hasUsedGuide || false;
|
||||
guideUsageCount.value = parsed.guideUsageCount || 0;
|
||||
hasUsedBoost.value = parsed.hasUsedBoost || false;
|
||||
boostUsageCount.value = parsed.boostUsageCount || 0;
|
||||
currentDensity.value = parsed.currentDensity || 0;
|
||||
elapsedTime.value = parsed.elapsedTime || 0;
|
||||
moves.value = parsed.moves || 0;
|
||||
@@ -348,6 +371,13 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
||||
saveState();
|
||||
}
|
||||
|
||||
function markBoostUsed() {
|
||||
if (isGameWon.value) return;
|
||||
hasUsedBoost.value = true;
|
||||
boostUsageCount.value++;
|
||||
saveState();
|
||||
}
|
||||
|
||||
function closeWinModal() {
|
||||
if (!isGameWon.value) return;
|
||||
isGameWon.value = false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { calculateDifficulty } from '@/utils/puzzleUtils';
|
||||
|
||||
export function buildShareCanvas(data, t, formattedTime) {
|
||||
const { grid, size, currentDensity, guideUsageCount } = data;
|
||||
const { grid, size, currentDensity, guideUsageCount, hasUsedBoost } = data;
|
||||
if (!grid || !grid.length) return null;
|
||||
|
||||
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
||||
@@ -11,7 +11,7 @@ export function buildShareCanvas(data, t, formattedTime) {
|
||||
const padding = 28;
|
||||
const headerHeight = 64;
|
||||
const footerHeight = 28;
|
||||
const infoHeight = 40; // New space for difficulty/guide info
|
||||
const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40;
|
||||
const width = boardSize + padding * 2;
|
||||
const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight;
|
||||
const scale = window.devicePixelRatio || 1;
|
||||
@@ -97,7 +97,12 @@ export function buildShareCanvas(data, t, formattedTime) {
|
||||
}
|
||||
}
|
||||
|
||||
// Guide Usage Info (Dirty Flag)
|
||||
// Guide Usage & Boost Info
|
||||
let infoY = height - padding - footerHeight + 10;
|
||||
if (guideUsageCount > 0 && hasUsedBoost) {
|
||||
infoY -= 25;
|
||||
}
|
||||
|
||||
if (guideUsageCount > 0) {
|
||||
ctx.fillStyle = '#ff4d4d';
|
||||
ctx.font = '600 14px "Segoe UI", sans-serif';
|
||||
@@ -106,7 +111,15 @@ export function buildShareCanvas(data, t, formattedTime) {
|
||||
const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100));
|
||||
const guideText = t('win.usedGuide', { count: guideUsageCount, percent });
|
||||
|
||||
ctx.fillText(`⚠️ ${guideText}`, padding, height - padding - footerHeight + 10);
|
||||
ctx.fillText(`⚠️ ${guideText}`, padding, infoY);
|
||||
if (hasUsedBoost) infoY += 25;
|
||||
}
|
||||
|
||||
if (hasUsedBoost) {
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.font = '600 14px "Segoe UI", sans-serif';
|
||||
const boostText = t('win.boosted');
|
||||
ctx.fillText(`⚡ ${boostText}`, padding, infoY);
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
|
||||
@@ -116,7 +129,7 @@ export function buildShareCanvas(data, t, formattedTime) {
|
||||
}
|
||||
|
||||
export function buildShareSVG(data, t, formattedTime) {
|
||||
const { grid, size, currentDensity, guideUsageCount } = data;
|
||||
const { grid, size, currentDensity, guideUsageCount, hasUsedBoost } = data;
|
||||
if (!grid || !grid.length) return null;
|
||||
|
||||
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
||||
@@ -126,7 +139,7 @@ export function buildShareSVG(data, t, formattedTime) {
|
||||
const padding = 28;
|
||||
const headerHeight = 64;
|
||||
const footerHeight = 28;
|
||||
const infoHeight = 40;
|
||||
const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40;
|
||||
const width = boardSize + padding * 2;
|
||||
const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight;
|
||||
|
||||
@@ -218,12 +231,23 @@ export function buildShareSVG(data, t, formattedTime) {
|
||||
}
|
||||
svgContent += cells;
|
||||
|
||||
// Guide Usage
|
||||
// Guide Usage & Boost Info
|
||||
let infoY = height - padding - footerHeight + 10;
|
||||
if (guideUsageCount > 0 && hasUsedBoost) {
|
||||
infoY -= 25;
|
||||
}
|
||||
|
||||
if (guideUsageCount > 0) {
|
||||
const totalCells = size * size;
|
||||
const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100));
|
||||
const guideText = t('win.usedGuide', { count: guideUsageCount, percent });
|
||||
svgContent += `<text x="${padding}" y="${height - padding - footerHeight + 10}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ff4d4d">⚠️ ${guideText}</text>`;
|
||||
svgContent += `<text x="${padding}" y="${infoY}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ff4d4d">⚠️ ${guideText}</text>`;
|
||||
if (hasUsedBoost) infoY += 25;
|
||||
}
|
||||
|
||||
if (hasUsedBoost) {
|
||||
const boostText = t('win.boosted');
|
||||
svgContent += `<text x="${padding}" y="${infoY}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ffd700">⚡ ${boostText}</text>`;
|
||||
}
|
||||
|
||||
// URL
|
||||
|
||||
@@ -7,6 +7,7 @@ const messages = {
|
||||
'worker.logicRow': 'Logika: Wiersz {row}, Kolumna {col} -> {state}',
|
||||
'worker.logicCol': 'Logika: Kolumna {col}, Wiersz {row} -> {state}',
|
||||
'worker.stuck': 'Brak logicznego ruchu. Spróbuj zgadnąć lub cofnąć.',
|
||||
'worker.boosted': 'Boost (DFS): Wiersz {row}, Kolumna {col} -> {state}',
|
||||
'worker.done': 'Koniec!',
|
||||
'worker.state.filled': 'Pełne',
|
||||
'worker.state.empty': 'Puste'
|
||||
@@ -16,6 +17,7 @@ const messages = {
|
||||
'worker.logicRow': 'Logic: Row {row}, Column {col} -> {state}',
|
||||
'worker.logicCol': 'Logic: Column {col}, Row {row} -> {state}',
|
||||
'worker.stuck': 'No logical move found. Try guessing or undoing.',
|
||||
'worker.boosted': 'Boost (DFS): Row {row}, Column {col} -> {state}',
|
||||
'worker.done': 'Done!',
|
||||
'worker.state.filled': 'Filled',
|
||||
'worker.state.empty': 'Empty'
|
||||
@@ -79,7 +81,12 @@ const isSolved = (grid, solution) => {
|
||||
const solutionCell = solution[r][c];
|
||||
const isFilled = playerCell === 1;
|
||||
const shouldBeFilled = solutionCell === 1;
|
||||
|
||||
// Check correctness
|
||||
if (isFilled !== shouldBeFilled) return false;
|
||||
|
||||
// Check completeness (must be fully resolved to 1 or 2)
|
||||
if (playerCell === 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -131,9 +138,36 @@ const handleStep = (playerGrid, solution, locale) => {
|
||||
return { type: 'stuck', statusText: t(locale, 'worker.stuck') };
|
||||
};
|
||||
|
||||
self.onmessage = (event) => {
|
||||
const { id, playerGrid, solution, locale } = event.data;
|
||||
const resolved = resolveLocale(locale);
|
||||
const result = handleStep(playerGrid, solution, resolved);
|
||||
self.postMessage({ id, ...result });
|
||||
const handleBoost = (playerGrid, solution, locale) => {
|
||||
const size = solution.length;
|
||||
// Find first unknown cell and reveal it
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
if (playerGrid[r][c] === 0) {
|
||||
const correctState = solution[r][c] === 1 ? 1 : 2;
|
||||
const stateLabel = t(locale, correctState === 1 ? 'worker.state.filled' : 'worker.state.empty');
|
||||
return {
|
||||
type: 'move',
|
||||
r,
|
||||
c,
|
||||
state: correctState,
|
||||
statusText: t(locale, 'worker.boosted', { row: r + 1, col: c + 1, state: stateLabel })
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: 'done', statusText: t(locale, 'worker.solved') };
|
||||
};
|
||||
|
||||
self.onmessage = (event) => {
|
||||
const { id, playerGrid, solution, locale, action } = event.data;
|
||||
const resolved = resolveLocale(locale);
|
||||
|
||||
if (action === 'boost') {
|
||||
const result = handleBoost(playerGrid, solution, resolved);
|
||||
self.postMessage({ id, ...result });
|
||||
} else {
|
||||
const result = handleStep(playerGrid, solution, resolved);
|
||||
self.postMessage({ id, ...result });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user