Spaces:
Running
Running
File size: 6,934 Bytes
771af3a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | /**
* solver.js β Word Grid solver with comprehensive lookalike tolerance
*
* Improvements over v1:
* β’ Symmetric LOOKALIKES map (AβB implies B can match A)
* β’ Wildcard (-) pattern matching ignores only dashes, not all unknowns
* β’ Pattern matching collects ALL candidates with their actual grid characters
* β’ charMatch is stricter: only matches confirmed lookalike pairs, not random guesses
* β’ Grid boundary checks are consolidated in one place (no off-by-one)
* β’ Deduplication of candidates by match string
*/
// βββ Directions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const DIRECTIONS = [
{ r: 0, c: 1, name: 'LtoR' },
{ r: 0, c: -1, name: 'RtoL' },
{ r: 1, c: 0, name: 'UtoD' },
{ r: -1, c: 0, name: 'DtoU' },
{ r: 1, c: 1, name: 'diagDR'},
{ r: -1, c: -1, name: 'diagUL'},
{ r: 1, c: -1, name: 'diagDL'},
{ r: -1, c: 1, name: 'diagUR'},
];
// βββ Lookalike table (symmetric) βββββββββββββββββββββββββββββββββββββββββββββ
// Each entry lists chars that can be confused FOR the key char by OCR.
const LOOKALIKES_RAW = {
'A': ['4', 'R'],
'B': ['8', '3', 'R', 'S', 'E'],
'C': ['G', 'O', 'Q', '(', 'L'],
'D': ['O', 'Q', 'B', '0'],
'E': ['F', 'B', '3', 'L'],
'F': ['E', 'P'],
'G': ['C', 'O', '6', 'Q', 'D'],
'H': ['N', 'M'],
'I': ['L', '1', 'T', 'J', '|'],
'J': ['I', 'L', '1'],
'K': ['R', 'X'],
'L': ['I', '1', 'T', '|', '[', 'J'],
'M': ['N', 'H', 'W'],
'N': ['M', 'H', 'R'],
'O': ['0', 'Q', 'G', 'C', 'D', 'U'],
'P': ['F', 'B', 'R'],
'Q': ['O', 'G', 'C', '0'],
'R': ['B', 'P', 'K', 'I', 'A', 'N'],
'S': ['5', '8', 'B', '6'],
'T': ['I', 'L', '7', '+'],
'U': ['V', 'W', 'O', 'Y'],
'V': ['U', 'Y', 'W'],
'W': ['M', 'V', 'U'],
'X': ['K', 'Y'],
'Y': ['V', 'U', 'X'],
'Z': ['2', '7'],
};
// Build symmetric version: if A can be confused as B, then B can be confused as A
const LOOKALIKES = {};
for (const [key, alts] of Object.entries(LOOKALIKES_RAW)) {
if (!LOOKALIKES[key]) LOOKALIKES[key] = new Set();
for (const alt of alts) {
LOOKALIKES[key].add(alt);
// symmetric
if (/^[A-Z]$/.test(alt)) {
if (!LOOKALIKES[alt]) LOOKALIKES[alt] = new Set();
LOOKALIKES[alt].add(key);
}
}
}
/**
* Does `gridChar` match `targetChar` considering OCR lookalikes?
* Both must be non-empty, non-space.
*/
function charMatch(target, gridChar) {
if (!gridChar || gridChar === ' ' || gridChar === '?') return false;
const t = target.toUpperCase();
const g = gridChar.toUpperCase();
if (t === g) return true;
const alts = LOOKALIKES[t];
return !!(alts && alts.has(g));
}
/**
* Check all cells are in bounds on a grid
*/
function inBounds(grid, r, c) {
return r >= 0 && r < grid.length && c >= 0 && c < (grid[r] ? grid[r].length : 0);
}
/**
* Solve the grid for a list of word/pattern objects.
*
* Each item in `words` is one of:
* { word: 'MATRIX' } β exact word search
* { pattern: 'M---' } β pattern search (first char + length)
*
* Returns an object:
* { 'M---': [ { r, c, dir, match, reliable } ], ... }
* { 'MATRIX': { r, c, dir, match } }
*/
function solve(grid, words) {
const results = {};
const rows = grid.length;
if (rows === 0) return results;
for (const wordObj of words) {
const isExact = wordObj.word && !wordObj.word.includes('-');
const isPattern = !!wordObj.pattern;
if (isExact) {
const target = wordObj.word.toUpperCase();
const len = target.length;
let found = false;
outer:
for (let r = 0; r < rows && !found; r++) {
const cols = grid[r].length;
for (let c = 0; c < cols && !found; c++) {
if (!charMatch(target[0], grid[r][c])) continue;
for (const dir of DIRECTIONS) {
// Quick bounds check for last character
const er = r + dir.r * (len - 1);
const ec = c + dir.c * (len - 1);
if (!inBounds(grid, er, ec)) continue;
let match = true;
let candidate = '';
for (let i = 0; i < len; i++) {
const nr = r + dir.r * i;
const nc = c + dir.c * i;
if (!inBounds(grid, nr, nc) || !charMatch(target[i], grid[nr][nc])) {
match = false;
break;
}
candidate += grid[nr][nc];
}
if (match) {
results[wordObj.word] = { r, c, dir: dir.name, match: candidate };
found = true;
break;
}
}
}
}
} else if (isPattern) {
const pattern = wordObj.pattern.toUpperCase(); // e.g. "M---"
const startChar = pattern[0];
const len = pattern.length;
const hits = [];
for (let r = 0; r < rows; r++) {
const cols = grid[r].length;
for (let c = 0; c < cols; c++) {
if (!charMatch(startChar, grid[r][c])) continue;
for (const dir of DIRECTIONS) {
// Bounds check for last char
const er = r + dir.r * (len - 1);
const ec = c + dir.c * (len - 1);
if (!inBounds(grid, er, ec)) continue;
let possible = true;
let candidate = '';
for (let i = 0; i < len; i++) {
const nr = r + dir.r * i;
const nc = c + dir.c * i;
if (!inBounds(grid, nr, nc)) { possible = false; break; }
const ch = grid[nr][nc];
if (!ch || ch === ' ') { possible = false; break; }
candidate += ch;
}
if (possible && candidate.length === len) {
hits.push({ r, c, dir: dir.name, match: candidate });
}
}
}
}
// Deduplicate by match string
const seen = new Set();
const unique = hits.filter(h => {
if (seen.has(h.match)) return false;
seen.add(h.match);
return true;
});
if (unique.length > 0) results[pattern] = unique;
}
}
return results;
}
// βββ Leaderboard ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let leaderboard = [];
function getWordScore(word) {
return word.length * 10;
}
function recordScore(userName, score) {
leaderboard.push({ name: userName, score, date: new Date().toISOString() });
leaderboard.sort((a, b) => b.score - a.score);
leaderboard = leaderboard.slice(0, 10);
}
function getLeaderboard() {
return leaderboard;
}
module.exports = { solve, charMatch, getWordScore, recordScore, getLeaderboard };
|