67 lines
2.9 KiB
JavaScript
67 lines
2.9 KiB
JavaScript
// Finds words that can be built from 1-4 distinct tiles in the puzzle pool
|
|
// but are missing from our dictionary, using a large reference word list
|
|
// (an-array-of-english-words, ~275k words) as the source of truth.
|
|
//
|
|
// Usage:
|
|
// node scripts/find-missing-combo-words.mjs # report only
|
|
// node scripts/find-missing-combo-words.mjs --write # save scripts/combo-words.txt
|
|
import fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { buildWordSet } from './lexicon-sources.mjs';
|
|
import { TARGET_WORDS_POOL } from '../src/data/puzzles.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const REFERENCE = require('an-array-of-english-words');
|
|
const OUT_FILE = 'scripts/combo-words.txt';
|
|
|
|
// Every tile that can appear on a board (all puzzles are generated from the pool)
|
|
const chunks = new Set();
|
|
TARGET_WORDS_POOL.forEach(t => t.tiles.forEach(c => chunks.add(c.toLowerCase())));
|
|
const maxChunkLen = Math.max(...[...chunks].map(c => c.length));
|
|
|
|
// Base dictionary WITHOUT the previously generated combo list, so re-runs are stable
|
|
const dictionary = buildWordSet({ includeComboWords: false });
|
|
|
|
// Can `word` be split into 1-4 distinct pool tiles? Returns the split or null.
|
|
function segment(word, start = 0, used = []) {
|
|
if (start === word.length) return used.length ? [...used] : null;
|
|
if (used.length === 4) return null;
|
|
for (let len = 1; len <= maxChunkLen && start + len <= word.length; len++) {
|
|
const piece = word.slice(start, start + len);
|
|
if (!chunks.has(piece) || used.includes(piece)) continue;
|
|
used.push(piece);
|
|
const result = segment(word, start + len, used);
|
|
used.pop();
|
|
if (result) return result;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const found = [];
|
|
for (const raw of REFERENCE) {
|
|
const w = raw.toLowerCase();
|
|
if (w.length < 3 || w.length > 16 || !/^[a-z]+$/.test(w)) continue; // 2-letter words use the fixed allowlist
|
|
if (dictionary.has(w)) continue;
|
|
const split = segment(w);
|
|
if (split) found.push({ word: w, split });
|
|
}
|
|
found.sort((a, b) => a.word.localeCompare(b.word));
|
|
|
|
console.log(`Pool tiles: ${chunks.size}`);
|
|
console.log(`Missing words buildable from pool tiles: ${found.length}`);
|
|
const byTiles = { 1: 0, 2: 0, 3: 0, 4: 0 };
|
|
found.forEach(f => byTiles[f.split.length]++);
|
|
console.log('By tile count:', byTiles);
|
|
console.log('Sample:', found.slice(0, 40).map(f => `${f.word}(${f.split.join('+')})`).join(' '));
|
|
|
|
if (process.argv.includes('--write')) {
|
|
const header = [
|
|
'# Words buildable from pool tiles that are missing from wordlist-english.',
|
|
'# Generated by scripts/find-missing-combo-words.mjs (source: an-array-of-english-words).',
|
|
'# These are accepted in play but never shown in the 30-word solution lists.',
|
|
'# Delete a line to stop accepting that word.'
|
|
];
|
|
fs.writeFileSync(OUT_FILE, [...header, ...found.map(f => f.word)].join('\n') + '\n', 'utf8');
|
|
console.log(`Saved ${found.length} words to ${OUT_FILE}`);
|
|
}
|