diff --git a/src/components/CustomGameModal.vue b/src/components/CustomGameModal.vue
index f59b4af..1b4061c 100644
--- a/src/components/CustomGameModal.vue
+++ b/src/components/CustomGameModal.vue
@@ -10,6 +10,15 @@ const { t } = useI18n();
const customSize = ref(10);
const errorMsg = ref('');
+const snapToStep = (value, step) => {
+ const rounded = Math.round(value / step) * step;
+ return Math.max(5, Math.min(80, rounded));
+};
+
+const handleSnap = () => {
+ customSize.value = snapToStep(Number(customSize.value), 5);
+};
+
const confirm = () => {
const size = parseInt(customSize.value);
if (isNaN(size) || size < 5 || size > 80) {
@@ -36,6 +45,7 @@ const confirm = () => {
min="5"
max="80"
step="1"
+ @change="handleSnap"
/>
5
diff --git a/src/workers/solverWorker.js b/src/workers/solverWorker.js
index d31b85f..8bbcce9 100644
--- a/src/workers/solverWorker.js
+++ b/src/workers/solverWorker.js
@@ -40,61 +40,143 @@ const t = (locale, key, params) => {
return typeof value === 'string' ? format(value, params) : key;
};
-const getPermutations = (length, hints) => {
- const results = [];
- const recurse = (index, hintIndex, currentLine) => {
- if (hintIndex === hints.length) {
- while (currentLine.length < length) {
- currentLine.push(0);
+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 n = lineState.length;
+ const m = hints.length;
+ if (m === 0) {
+ for (let i = 0; i < n; i++) {
+ if (lineState[i] === 0) return { index: i, state: 2 };
+ }
+ return { index: -1 };
+ }
+
+ 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];
+ const maxStart = n - suffixMin[hintIndex];
+ for (let start = pos; start <= maxStart; start++) {
+ if (hasFilled(pos, start)) 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, hintIndex + 1)) {
+ memoSuffix[pos][hintIndex] = true;
+ return true;
}
- results.push(currentLine);
- return;
- }
-
- const currentHint = hints[hintIndex];
- let remainingHintsLen = 0;
- for (let i = hintIndex + 1; i < hints.length; i++) remainingHintsLen += hints[i] + 1;
- const maxStart = length - remainingHintsLen - currentHint;
-
- for (let start = index; start <= maxStart; start++) {
- const newLine = [...currentLine];
- for (let k = index; k < start; k++) newLine.push(0);
- for (let k = 0; k < currentHint; k++) newLine.push(1);
- if (hintIndex < hints.length - 1) newLine.push(0);
- recurse(newLine.length, hintIndex + 1, newLine);
}
+ memoSuffix[pos][hintIndex] = false;
+ return false;
};
- recurse(0, 0, []);
- return results;
-};
-
-const 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;
- if (boardVal === 2 && permVal !== 0) return false;
- }
- return true;
-};
-
-const solveLineLogic = (lineState, hints, size) => {
- const allPerms = getPermutations(size, hints);
- const validPerms = allPerms.filter(p => isValidPermutation(p, lineState));
- if (validPerms.length === 0) return { index: -1 };
-
- for (let i = 0; i < size; i++) {
- 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;
+ 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;
}
- if (allOne) return { index: i, state: 1 };
- if (allZero) return { index: i, state: 2 };
+ const len = hints[hintCount - 1];
+ const maxStart = pos - len;
+ for (let start = maxStart; start >= 0; start--) {
+ if (hasCross(start, start + len)) continue;
+ if (start + len < pos && lineState[start + len] === 1) continue;
+ if (hasFilled(start + len, pos)) continue;
+ if (start > 0 && lineState[start - 1] === 1) continue;
+ const prevPos = start > 0 ? start - 1 : 0;
+ if (canPlacePrefix(prevPos, hintCount - 1)) {
+ 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 (!canPlacePrefix(start, 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 };
};
@@ -124,7 +206,7 @@ const handleStep = (playerGrid, solution, locale) => {
for (let r = 0; r < size; r++) {
const rowLine = playerGrid[r];
const hints = rowHints[r];
- const result = solveLineLogic(rowLine, hints, size);
+ const result = solveLineLogic(rowLine, hints);
if (result.index !== -1) {
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
return {
@@ -141,7 +223,7 @@ const handleStep = (playerGrid, solution, locale) => {
const colLine = [];
for (let r = 0; r < size; r++) colLine.push(playerGrid[r][c]);
const hints = colHints[c];
- const result = solveLineLogic(colLine, hints, size);
+ const result = solveLineLogic(colLine, hints);
if (result.index !== -1) {
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
return {