more words2
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import fs from 'node:fs';
|
||||
import { buildWordSet } from './lexicon-sources.mjs';
|
||||
import { buildWordSet, loadComboWords } from './lexicon-sources.mjs';
|
||||
|
||||
// Build a clean, high-frequency recognized vocabulary for client-side unlimited/custom puzzles
|
||||
// (includes regional spellings and the proper-word allowlist)
|
||||
const vocabulary = buildWordSet();
|
||||
|
||||
// Accept-only words (scripts/combo-words.txt): valid in play, never shown in solution lists
|
||||
const comboWords = [...new Set(loadComboWords())].filter(w => !vocabulary.has(w)).sort();
|
||||
|
||||
console.log('Total vocabulary words:', vocabulary.size);
|
||||
console.log('Accept-only combo words:', comboWords.length);
|
||||
|
||||
// Pack into space-separated string or array
|
||||
const sortedWords = [...vocabulary].sort();
|
||||
@@ -15,6 +19,11 @@ const RAW_LEXICON = ${JSON.stringify(sortedWords)};
|
||||
|
||||
export const DICTIONARY_SET = new Set(RAW_LEXICON);
|
||||
|
||||
// Accept-only words buildable from pool tiles (generated by
|
||||
// scripts/find-missing-combo-words.mjs). Space-packed to keep the bundle small.
|
||||
const RAW_COMBO_WORDS = ${JSON.stringify(comboWords.join(' '))};
|
||||
export const COMBO_WORD_SET = new Set(RAW_COMBO_WORDS ? RAW_COMBO_WORDS.split(' ') : []);
|
||||
|
||||
// Prefix set for lightning fast tree-pruning search (1 to 6 chars)
|
||||
export const PREFIX_SET = new Set();
|
||||
for (const word of RAW_LEXICON) {
|
||||
@@ -26,6 +35,12 @@ for (const word of RAW_LEXICON) {
|
||||
export function isWordValid(word) {
|
||||
return DICTIONARY_SET.has(word.toLowerCase());
|
||||
}
|
||||
|
||||
// Any word a player may submit: main dictionary + accept-only combo words
|
||||
export function isAcceptedWord(word) {
|
||||
const w = word.toLowerCase();
|
||||
return DICTIONARY_SET.has(w) || COMBO_WORD_SET.has(w);
|
||||
}
|
||||
`;
|
||||
|
||||
fs.writeFileSync('src/data/lexicon.js', lexiconContent, 'utf8');
|
||||
|
||||
66464
scripts/combo-words.txt
Normal file
66464
scripts/combo-words.txt
Normal file
File diff suppressed because it is too large
Load Diff
67
scripts/find-missing-combo-words.mjs
Normal file
67
scripts/find-missing-combo-words.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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, CURATED_PUZZLES } 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 (random + daily puzzles)
|
||||
const chunks = new Set();
|
||||
TARGET_WORDS_POOL.forEach(t => t.tiles.forEach(c => chunks.add(c.toLowerCase())));
|
||||
CURATED_PUZZLES.forEach(p => p.quartets.flat().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}`);
|
||||
}
|
||||
@@ -1,5 +1,18 @@
|
||||
import fs from 'node:fs';
|
||||
import wordlistEnglish from 'wordlist-english';
|
||||
|
||||
const COMBO_WORDS_FILE = new URL('./combo-words.txt', import.meta.url);
|
||||
|
||||
// Extra words (from scripts/find-missing-combo-words.mjs) that are buildable
|
||||
// from pool tiles but missing from wordlist-english. Accepted in play only.
|
||||
export function loadComboWords() {
|
||||
if (!fs.existsSync(COMBO_WORDS_FILE)) return [];
|
||||
return fs.readFileSync(COMBO_WORDS_FILE, 'utf8')
|
||||
.split(/\r?\n/)
|
||||
.map(l => l.trim().toLowerCase())
|
||||
.filter(l => l && !l.startsWith('#') && /^[a-z]+$/.test(l));
|
||||
}
|
||||
|
||||
// Shared dictionary sources for the client lexicon and the build-time solver.
|
||||
|
||||
// Valid 2-letter English words accepted in standard games
|
||||
@@ -40,7 +53,7 @@ function isAcceptable(w) {
|
||||
return w.length >= 3 && w.length <= 16 && /^[a-z]+$/.test(w);
|
||||
}
|
||||
|
||||
export function buildWordSet() {
|
||||
export function buildWordSet({ includeComboWords = false } = {}) {
|
||||
const set = new Set();
|
||||
for (const key of SOURCE_LISTS) {
|
||||
for (const raw of wordlistEnglish[key] || []) {
|
||||
@@ -51,6 +64,11 @@ export function buildWordSet() {
|
||||
for (const w of PROPER_WORD_ALLOWLIST) {
|
||||
if (isAcceptable(w)) set.add(w);
|
||||
}
|
||||
if (includeComboWords) {
|
||||
for (const w of loadComboWords()) {
|
||||
if (isAcceptable(w)) set.add(w);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user