Compare commits
4 Commits
2261f44b4a
...
v1.14.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
43c0290fac
|
|||
|
fa5fa12157
|
|||
|
29682c9a06
|
|||
|
2d30315ae6
|
@@ -82,7 +82,7 @@ define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict';
|
|||||||
*/
|
*/
|
||||||
workbox.precacheAndRoute([{
|
workbox.precacheAndRoute([{
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.geiftdl7j9o"
|
"revision": "0.kkc80cp3p5o"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.12.11",
|
"version": "1.14.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.12.11",
|
"version": "1.14.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fireworks-js": "^2.10.8",
|
"fireworks-js": "^2.10.8",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.13.1",
|
"version": "1.14.1",
|
||||||
"homepage": "https://nonograms.7u.pl/",
|
"homepage": "https://nonograms.7u.pl/",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const imageLoaded = ref(false);
|
|||||||
const processing = ref(false);
|
const processing = ref(false);
|
||||||
const processingProgress = ref(0);
|
const processingProgress = ref(0);
|
||||||
const isCameraOpen = ref(false);
|
const isCameraOpen = ref(false);
|
||||||
|
const hasMultipleCameras = ref(false);
|
||||||
const stream = ref(null);
|
const stream = ref(null);
|
||||||
const facingMode = ref('environment');
|
const facingMode = ref('environment');
|
||||||
|
|
||||||
@@ -319,6 +320,12 @@ const startCamera = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
stream.value = await navigator.mediaDevices.getUserMedia(constraints);
|
stream.value = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
|
||||||
|
// Check available devices
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
hasMultipleCameras.value = videoDevices.length > 1;
|
||||||
|
|
||||||
// Wait for next tick or ensure videoRef is available
|
// Wait for next tick or ensure videoRef is available
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (videoRef.value) {
|
if (videoRef.value) {
|
||||||
@@ -401,7 +408,7 @@ onUnmounted(() => {
|
|||||||
<button class="camera-btn capture" @click="capturePhoto">
|
<button class="camera-btn capture" @click="capturePhoto">
|
||||||
<div class="shutter"></div>
|
<div class="shutter"></div>
|
||||||
</button>
|
</button>
|
||||||
<button class="camera-btn secondary" @click="switchCamera">
|
<button v-if="hasMultipleCameras" class="camera-btn secondary" @click="switchCamera">
|
||||||
<RefreshCw :size="24" />
|
<RefreshCw :size="24" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,13 +118,15 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
elapsedTime.value = 0;
|
elapsedTime.value = 0;
|
||||||
startTimer();
|
startTimer();
|
||||||
saveState();
|
saveState();
|
||||||
|
}
|
||||||
|
|
||||||
function resetGrid() {
|
function resetGrid() {
|
||||||
const rows = solution.value.length;
|
const rows = solution.value.length;
|
||||||
const cols = solution.value[0].length;
|
const cols = solution.value[0].length;
|
||||||
playerGrid.value = Array(rows).fill().map(() => Array(cols).fill(0));
|
playerGrid.value = Array(rows).fill().map(() => Array(cols).fill(0));
|
||||||
history.value = [];
|
history.value = [];
|
||||||
moves.value = 0;
|
moves.value = 0;
|
||||||
} currentTransaction.value = null;
|
currentTransaction.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startInteraction() {
|
function startInteraction() {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const memo = new Map();
|
|||||||
* @param {number[]} hints - Array of block lengths
|
* @param {number[]} hints - Array of block lengths
|
||||||
* @returns {number[]} - Updated line (or null if contradiction/impossible - though shouldn't happen for valid puzzles)
|
* @returns {number[]} - Updated line (or null if contradiction/impossible - though shouldn't happen for valid puzzles)
|
||||||
*/
|
*/
|
||||||
function solveLine(currentLine, hints) {
|
export function solveLine(currentLine, hints) {
|
||||||
const length = currentLine.length;
|
const length = currentLine.length;
|
||||||
|
|
||||||
// If no hints, all must be empty
|
// If no hints, all must be empty
|
||||||
@@ -57,6 +57,8 @@ function solveLine(currentLine, hints) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Cannot skip a filled cell - if we pass a '1', it becomes uncovered
|
||||||
|
if (currentLine[currentIdx] === 1) return null;
|
||||||
currentIdx++;
|
currentIdx++;
|
||||||
}
|
}
|
||||||
if (leftPositions.length <= hIndex) return null; // Impossible
|
if (leftPositions.length <= hIndex) return null; // Impossible
|
||||||
@@ -81,6 +83,8 @@ function solveLine(currentLine, hints) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Cannot skip a filled cell
|
||||||
|
if (reversedLine[currentIdx] === 1) return null;
|
||||||
currentIdx++;
|
currentIdx++;
|
||||||
}
|
}
|
||||||
if (rightPositionsReversed.length <= hIndex) return null;
|
if (rightPositionsReversed.length <= hIndex) return null;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { calculateHints } from '../utils/puzzleUtils.js';
|
import { calculateHints } from '../utils/puzzleUtils.js';
|
||||||
|
import { solveLine } from '../utils/solver.js';
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
pl: {
|
pl: {
|
||||||
@@ -40,195 +41,33 @@ const t = (locale, key, params) => {
|
|||||||
return typeof value === 'string' ? format(value, params) : key;
|
return typeof value === 'string' ? format(value, params) : key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildPrefix = (lineState) => {
|
|
||||||
const n = lineState.length;
|
|
||||||
const filled = new Array(n + 1).fill(0);
|
|
||||||
const cross = new Array(n + 1).fill(0);
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
filled[i + 1] = filled[i] + (lineState[i] === 1 ? 1 : 0);
|
|
||||||
cross[i + 1] = cross[i] + (lineState[i] === 2 ? 1 : 0);
|
|
||||||
}
|
|
||||||
return { filled, cross };
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildSuffixMin = (hints) => {
|
|
||||||
const m = hints.length;
|
|
||||||
const suffixMin = new Array(m + 1).fill(0);
|
|
||||||
let sumHints = 0;
|
|
||||||
for (let i = m - 1; i >= 0; i--) {
|
|
||||||
sumHints += hints[i];
|
|
||||||
const separators = m - i - 1;
|
|
||||||
suffixMin[i] = sumHints + separators;
|
|
||||||
}
|
|
||||||
return suffixMin;
|
|
||||||
};
|
|
||||||
|
|
||||||
const solveLineLogic = (lineState, hints) => {
|
const solveLineLogic = (lineState, hints) => {
|
||||||
const n = lineState.length;
|
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
|
||||||
const m = hints.length;
|
const solverLine = lineState.map(cell => {
|
||||||
if (m === 0) {
|
if (cell === 0) return -1; // Unknown
|
||||||
for (let i = 0; i < n; i++) {
|
if (cell === 1) return 1; // Filled
|
||||||
if (lineState[i] === 0) return { index: i, state: 2 };
|
if (cell === 2) return 0; // Empty/Cross
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call robust solver
|
||||||
|
const resultLine = solveLine(solverLine, hints);
|
||||||
|
|
||||||
|
// Check for new info
|
||||||
|
if (!resultLine) return { index: -1 }; // Contradiction or error
|
||||||
|
|
||||||
|
for (let i = 0; i < lineState.length; i++) {
|
||||||
|
// We only care about cells that are currently 0 (Unknown) in Store
|
||||||
|
if (lineState[i] === 0) {
|
||||||
|
if (resultLine[i] === 1) {
|
||||||
|
return { index: i, state: 1 }; // Suggest Fill
|
||||||
}
|
}
|
||||||
return { index: -1 };
|
if (resultLine[i] === 0) {
|
||||||
}
|
return { index: i, state: 2 }; // Suggest Cross
|
||||||
|
|
||||||
const { filled, cross } = buildPrefix(lineState);
|
|
||||||
const suffixMin = buildSuffixMin(hints);
|
|
||||||
|
|
||||||
const hasFilled = (a, b) => filled[b] - filled[a] > 0;
|
|
||||||
const hasCross = (a, b) => cross[b] - cross[a] > 0;
|
|
||||||
|
|
||||||
const memoSuffix = Array.from({ length: n + 1 }, () => Array(m + 1).fill(null));
|
|
||||||
const memoPrefix = Array.from({ length: n + 1 }, () => Array(m + 1).fill(null));
|
|
||||||
|
|
||||||
const canPlaceSuffix = (pos, hintIndex) => {
|
|
||||||
const cached = memoSuffix[pos][hintIndex];
|
|
||||||
if (cached !== null) return cached;
|
|
||||||
if (hintIndex === m) {
|
|
||||||
const result = !hasFilled(pos, n);
|
|
||||||
memoSuffix[pos][hintIndex] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const len = hints[hintIndex];
|
|
||||||
// maxStart logic: we need enough space for this block (len) + subsequent blocks/gaps (suffixMin[hintIndex+1])
|
|
||||||
// suffixMin[hintIndex] = len + (m - hintIndex - 1) + suffixMin[hintIndex+1]
|
|
||||||
// Actually suffixMin[hintIndex] already includes everything needed from here to end.
|
|
||||||
// So if we place block at start, end is start + len.
|
|
||||||
// Total space needed is suffixMin[hintIndex].
|
|
||||||
// So start can go up to n - suffixMin[hintIndex].
|
|
||||||
const maxStart = n - suffixMin[hintIndex];
|
|
||||||
|
|
||||||
for (let start = pos; start <= maxStart; start++) {
|
|
||||||
if (hasFilled(pos, start)) continue; // Must be empty before this block
|
|
||||||
if (hasCross(start, start + len)) continue; // Block space must be free of crosses
|
|
||||||
|
|
||||||
// If not the last block, we need a gap after
|
|
||||||
if (hintIndex < m - 1) {
|
|
||||||
if (start + len < n && lineState[start + len] === 1) continue; // Gap must not be filled
|
|
||||||
// We can assume gap is at start + len. Next block starts at least at start + len + 1
|
|
||||||
const nextPos = start + len + 1;
|
|
||||||
if (canPlaceSuffix(nextPos, hintIndex + 1)) {
|
|
||||||
memoSuffix[pos][hintIndex] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Last block
|
|
||||||
// Check if we can fill the rest with empty
|
|
||||||
if (hasFilled(start + len, n)) continue;
|
|
||||||
memoSuffix[pos][hintIndex] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
memoSuffix[pos][hintIndex] = false;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canPlacePrefix = (pos, hintCount) => {
|
|
||||||
const cached = memoPrefix[pos][hintCount];
|
|
||||||
if (cached !== null) return cached;
|
|
||||||
if (hintCount === 0) {
|
|
||||||
const result = !hasFilled(0, pos);
|
|
||||||
memoPrefix[pos][hintCount] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const len = hints[hintCount - 1];
|
|
||||||
|
|
||||||
// Logic for prefix:
|
|
||||||
// We are placing the (hintCount-1)-th block ending at 'start + len' <= pos.
|
|
||||||
// So 'start' <= pos - len.
|
|
||||||
// But we also need to ensure there is space for previous blocks.
|
|
||||||
// However, the simple constraint is just iterating backwards.
|
|
||||||
|
|
||||||
// maxStart: if this is the only block, maxStart = pos - len.
|
|
||||||
// If there are previous blocks, we need a gap before this block.
|
|
||||||
// So previous block ended at start - 1.
|
|
||||||
// Actually the recursive call will handle space check.
|
|
||||||
// But for the gap check:
|
|
||||||
// If we place block at 'start', we need lineState[start-1] != 1 (if start > 0).
|
|
||||||
// And we recursively check canPlacePrefix(start-1, count-1).
|
|
||||||
// But if start=0 and count > 1, impossible.
|
|
||||||
|
|
||||||
const maxStart = pos - len; // Simplified, loop condition handles rest
|
|
||||||
|
|
||||||
for (let start = maxStart; start >= 0; start--) {
|
|
||||||
if (hasCross(start, start + len)) continue;
|
|
||||||
if (hasFilled(start + len, pos)) continue; // Must be empty after this block up to pos
|
|
||||||
|
|
||||||
// Check gap before
|
|
||||||
if (hintCount > 1) {
|
|
||||||
if (start === 0) continue; // No space for previous blocks
|
|
||||||
if (lineState[start - 1] === 1) continue; // Gap must not be filled
|
|
||||||
const prevPos = start - 1;
|
|
||||||
if (canPlacePrefix(prevPos, hintCount - 1)) {
|
|
||||||
memoPrefix[pos][hintCount] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// First block
|
|
||||||
if (hasFilled(0, start)) continue; // Before first block must be empty
|
|
||||||
memoPrefix[pos][hintCount] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
memoPrefix[pos][hintCount] = false;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const possibleStarts = [];
|
|
||||||
for (let i = 0; i < m; i++) {
|
|
||||||
const len = hints[i];
|
|
||||||
const starts = [];
|
|
||||||
for (let start = 0; start <= n - len; start++) {
|
|
||||||
if (i === 0) {
|
|
||||||
if (!canPlacePrefix(start, 0)) continue;
|
|
||||||
} else {
|
|
||||||
if (start === 0) continue;
|
|
||||||
if (lineState[start - 1] === 1) continue;
|
|
||||||
if (!canPlacePrefix(start - 1, i)) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCross(start, start + len)) continue;
|
|
||||||
if (start + len < n && lineState[start + len] === 1) continue;
|
|
||||||
const nextPos = start + len < n ? start + len + 1 : start + len;
|
|
||||||
if (!canPlaceSuffix(nextPos, i + 1)) continue;
|
|
||||||
starts.push(start);
|
|
||||||
}
|
|
||||||
possibleStarts.push(starts);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mustFill = new Array(n).fill(false);
|
|
||||||
const coverage = new Array(n).fill(false);
|
|
||||||
|
|
||||||
for (let i = 0; i < m; i++) {
|
|
||||||
const starts = possibleStarts[i];
|
|
||||||
const len = hints[i];
|
|
||||||
if (starts.length === 0) return { index: -1 };
|
|
||||||
let earliest = starts[0];
|
|
||||||
let latest = starts[0];
|
|
||||||
for (let j = 1; j < starts.length; j++) {
|
|
||||||
earliest = Math.min(earliest, starts[j]);
|
|
||||||
latest = Math.max(latest, starts[j]);
|
|
||||||
}
|
|
||||||
const startOverlap = Math.max(earliest, latest);
|
|
||||||
const endOverlap = Math.min(earliest + len - 1, latest + len - 1);
|
|
||||||
for (let k = startOverlap; k <= endOverlap; k++) {
|
|
||||||
if (k >= 0 && k < n) mustFill[k] = true;
|
|
||||||
}
|
|
||||||
for (let s = 0; s < starts.length; s++) {
|
|
||||||
const start = starts[s];
|
|
||||||
for (let k = start; k < start + len; k++) {
|
|
||||||
coverage[k] = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
if (lineState[i] === 0 && mustFill[i]) return { index: i, state: 1 };
|
|
||||||
}
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
if (lineState[i] === 0 && !coverage[i]) return { index: i, state: 2 };
|
|
||||||
}
|
|
||||||
return { index: -1 };
|
return { index: -1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user