fertig-classic-games/server/words/boggleEngine.js

118 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Boggle engine: dictionary trie, dice rolling, and board solving.
// Pure logic — no Express. Initialized once at server start from the ENABLE list.
// Standard 16-die Boggle set. The Q die face is treated as the token "Qu".
const DICE = [
'AAEEGN', 'ABBJOO', 'ACHOPS', 'AFFKPS',
'AOOTTW', 'CIMOTU', 'DEILRX', 'DELRVY',
'DISTTY', 'EEGHNW', 'EEINSU', 'EHRTVW',
'EIOSST', 'ELRTTY', 'HIMNQU', 'HLNNRZ',
];
const MIN_LEN = 3; // minimum scoring word length
// ── Trie ──────────────────────────────────────────────────────────────────────
// Node: { word: boolean, kids: Map<char, node> }. Used for fast prefix pruning
// during the board DFS while keeping memory far below a full prefix Set.
let root = null;
function makeNode() {
return { word: false, kids: new Map() };
}
export function initBoggleDictionary(words) {
root = makeNode();
for (const raw of words) {
const w = String(raw).toUpperCase();
if (w.length < MIN_LEN || w.length > 16 || !/^[A-Z]+$/.test(w)) continue;
let node = root;
for (const ch of w) {
let next = node.kids.get(ch);
if (!next) { next = makeNode(); node.kids.set(ch, next); }
node = next;
}
node.word = true;
}
return root;
}
// ── Board rolling ───────────────────────────────────────────────────────────────
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// Returns a 4×4 grid (array of 4 rows, each 4 face tokens). Each cell is a single
// uppercase letter, except the Q die which yields the token "Qu".
export function rollBoard() {
const dice = shuffle([...DICE]);
const faces = dice.map((die) => {
const ch = die[Math.floor(Math.random() * die.length)];
return ch === 'Q' ? 'Qu' : ch;
});
const grid = [];
for (let r = 0; r < 4; r++) grid.push(faces.slice(r * 4, r * 4 + 4));
return grid;
}
// ── Solving ───────────────────────────────────────────────────────────────────
const NEIGHBORS = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1],
];
// Walk a trie down every character of a face token (handles the two-letter "Qu").
// Returns the resulting node, or null if the prefix leaves the trie.
function stepNode(node, token) {
let n = node;
for (const ch of token.toUpperCase()) {
n = n.kids.get(ch);
if (!n) return null;
}
return n;
}
// Returns every valid word (length >= 3) found on the board, each with one
// representative path: [{ word, path: [[r,c], ...] }]. De-duped by word.
export function solveBoard(grid) {
if (!root) return [];
const size = grid.length;
const found = new Map(); // word -> path
const visited = Array.from({ length: size }, () => new Array(size).fill(false));
const dfs = (r, c, node, word, path) => {
const next = stepNode(node, grid[r][c]);
if (!next) return;
const nextWord = word + grid[r][c].toUpperCase();
const nextPath = [...path, [r, c]];
if (next.word && nextWord.length >= MIN_LEN && !found.has(nextWord)) {
found.set(nextWord, nextPath);
}
visited[r][c] = true;
for (const [dr, dc] of NEIGHBORS) {
const nr = r + dr, nc = c + dc;
if (nr < 0 || nc < 0 || nr >= size || nc >= size) continue;
if (visited[nr][nc]) continue;
dfs(nr, nc, next, nextWord, nextPath);
}
visited[r][c] = false;
};
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
dfs(r, c, root, '', []);
}
}
return [...found.entries()].map(([word, path]) => ({ word, path }));
}