File size: 6,872 Bytes
bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 1424dd1 075e68c 1424dd1 bb2cf03 075e68c bb2cf03 bb7daea 075e68c bb2cf03 075e68c bb2cf03 075e68c 1424dd1 bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 1424dd1 bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 1424dd1 075e68c bb2cf03 075e68c 1424dd1 bb2cf03 1424dd1 bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 075e68c bb2cf03 1424dd1 bb2cf03 1424dd1 bb2cf03 1424dd1 075e68c 1424dd1 bb2cf03 1424dd1 bb2cf03 | 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 | /**
* solver.js β Word Grid solver
*
* Key design decisions:
* β’ LOOKALIKES are ONE-WAY only: "OCR may read char X as char Y" means
* when we're looking for X, we also accept Y in the grid.
* It does NOT mean when looking for Y we accept X β that caused false matches.
* β’ '?' wildcard: OCR-uncertain cells match any target letter.
* β’ Pattern search collects every N-char path starting at cells that
* match the pattern's first letter (exact or lookalike).
* β’ Deduplication by match string before returning.
*/
'use strict';
// βββ 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 (ONE-WAY) ββββββββββββββββββββββββββββββββββββββββββββββββ
// "OCR may misread target letter X as one of these grid characters."
// When searching for X, we also accept any char listed here.
// This is intentionally NOT symmetric β visual similarity is asymmetric.
// e.g. OCR misreads I as L (thin vertical), but does NOT misread L as I.
const LOOKALIKES = {
'A': new Set(['4']),
'B': new Set(['8', '3']),
'C': new Set(['G', 'O', 'Q']),
'D': new Set(['O', 'Q', '0']),
'E': new Set(['F']),
'F': new Set(['E']),
'G': new Set(['C', '6', 'Q']),
'H': new Set([]),
'I': new Set(['L', '1', '|', 'J']),
'J': new Set(['I']),
'K': new Set(['X']),
'L': new Set(['I', '1', '|']),
'M': new Set(['N']),
'N': new Set(['M']),
'O': new Set(['0', 'Q', 'D']),
'P': new Set([]),
'Q': new Set(['O', 'G', '0']),
'R': new Set([]),
'S': new Set(['5', '8']),
'T': new Set(['7']),
'U': new Set(['V']),
'V': new Set(['U']),
'W': new Set([]),
'X': new Set(['K']),
'Y': new Set([]),
'Z': new Set(['2', '7']),
};
/**
* Does gridChar match targetChar?
* - Exact match always wins.
* - '?' in grid = OCR-uncertain = wildcard, matches any target.
* - One-way lookalike: target's known OCR substitutes are checked.
*/
function charMatch(target, gridChar) {
if (!gridChar || gridChar === ' ') return false;
if (gridChar === '?') return true; // OCR uncertain β wildcard
const t = target.toUpperCase();
const g = gridChar.toUpperCase();
if (t === g) return true; // exact match
const alts = LOOKALIKES[t];
return !!(alts && alts.has(g)); // known OCR substitute
}
function inBounds(grid, r, c) {
return r >= 0 && r < grid.length &&
c >= 0 && c < (grid[r] ? grid[r].length : 0);
}
// βββ Solver βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* @param {string[][]} grid
* @param {{ pattern?: string, word?: string }[]} words
* @returns {Object}
*/
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) {
// ββ Exact word search ββββββββββββββββββββββββββββββββββββββββββββββββ
const target = wordObj.word.toUpperCase();
const len = target.length;
let found = false;
outer:
for (let r = 0; r < rows && !found; r++) {
for (let c = 0; c < grid[r].length && !found; c++) {
if (!charMatch(target[0], grid[r][c])) continue;
for (const dir of DIRECTIONS) {
const er = r + dir.r * (len - 1);
const ec = c + dir.c * (len - 1);
if (!inBounds(grid, er, ec)) continue;
let ok = true, candidate = '';
for (let i = 0; i < len; i++) {
const nr = r + dir.r * i, nc = c + dir.c * i;
if (!inBounds(grid, nr, nc) || !charMatch(target[i], grid[nr][nc])) {
ok = false; break;
}
candidate += grid[nr][nc];
}
if (ok) {
results[wordObj.word] = { r, c, dir: dir.name, match: candidate };
found = true; break;
}
}
}
}
} else if (isPattern) {
// ββ Pattern search βββββββββββββββββββββββββββββββββββββββββββββββββββ
const pattern = wordObj.pattern.toUpperCase();
const startChar = pattern[0];
const len = pattern.length;
const hits = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < grid[r].length; c++) {
if (!charMatch(startChar, grid[r][c])) continue;
for (const dir of DIRECTIONS) {
const er = r + dir.r * (len - 1);
const ec = c + dir.c * (len - 1);
if (!inBounds(grid, er, ec)) continue;
let ok = true, candidate = '';
for (let i = 0; i < len; i++) {
const nr = r + dir.r * i, nc = c + dir.c * i;
if (!inBounds(grid, nr, nc)) { ok = false; break; }
const ch = grid[nr][nc];
if (!ch || ch === ' ') { ok = false; break; }
candidate += ch;
}
if (ok && 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 => !seen.has(h.match) && seen.add(h.match));
if (unique.length > 0) results[pattern] = unique;
}
}
return results;
}
// βββ Leaderboard ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let leaderboard = [];
const getWordScore = word => word.length * 10;
const getLeaderboard = () => leaderboard;
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);
}
module.exports = { solve, charMatch, getWordScore, recordScore, getLeaderboard };
|