wgsrobot / solver.js
hansaka01's picture
Upload 12 files
075e68c verified
Raw
History Blame Contribute Delete
6.87 kB
/**
* 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 };