From cf18b5a99ebb275373d554c0598aff6d5be2486a Mon Sep 17 00:00:00 2001 From: Tommy Parnell Date: Sat, 24 Oct 2026 00:38:55 -0400 Subject: [PATCH] solvers --- scripts/solver.mjs | 54 ++- src/components/ActionBar.js | 6 +- src/components/Modals.js | 6 +- src/components/ScoreRankBar.js | 26 +- src/components/WordsDrawer.js | 179 ++++++-- src/data/puzzles.js | 769 +-------------------------------- src/engine/solver.js | 28 +- src/main.js | 48 +- src/state/game-state.js | 86 +++- src/styles/components.css | 185 +++++++- 10 files changed, 534 insertions(+), 853 deletions(-) diff --git a/scripts/solver.mjs b/scripts/solver.mjs index 3d8611f..af06e4d 100644 --- a/scripts/solver.mjs +++ b/scripts/solver.mjs @@ -19,6 +19,20 @@ for (const raw of wordlistEnglish.english) { } } +// Commonness tiers for intelligent word quality ranking +const t10 = new Set(wordlistEnglish['english/10'] || []); +const t20 = new Set(wordlistEnglish['english/20'] || []); +const t35 = new Set(wordlistEnglish['english/35'] || []); +const t50 = new Set(wordlistEnglish['english/50'] || []); + +function getCommonnessScore(word) { + if (t10.has(word)) return 4; + if (t20.has(word)) return 3; + if (t35.has(word)) return 2; + if (t50.has(word)) return 1; + return 0; +} + // Build prefix set for super fast solver pruning (up to 6 letters) const prefixSet = new Set(); for (const word of cleanWordSet) { @@ -28,7 +42,7 @@ for (const word of cleanWordSet) { } // Function to solve a 20-tile board and return all valid words and paths -export function solveBoard(quartets) { +export function solveBoard(quartets, maxWords = 30) { const tiles = quartets.flat(); const foundMap = new Map(); @@ -55,12 +69,10 @@ export function solveBoard(quartets) { search('', []); - const quartilesList = quartets.map(q => q.join('')); + const quartilesList = quartets.map(q => q.join('').toLowerCase()); const quartileSet = new Set(quartilesList); - const words = []; - let totalScore = 0; - let quartileCount = 0; + let words = []; for (const [word, path] of foundMap.entries()) { const tileSegments = path.map(idx => tiles[idx]); @@ -69,9 +81,6 @@ export function solveBoard(quartets) { const isTargetQuartile = quartileSet.has(word); const isQuartile = tileCount === 4; - if (isQuartile) quartileCount++; - totalScore += points; - words.push({ word, points, @@ -83,16 +92,37 @@ export function solveBoard(quartets) { }); } - // Add 40 bonus points for finding all 5 quartiles - const maxPossibleScore = totalScore + 40; + // Sort candidate words: + // 1. Target quartiles always first (all 5 guaranteed) + // 2. Tile count descending (4-tiles > 3-tiles > 2-tiles > 1-tile) + // 3. Commonness score descending (familiar words first) + // 4. Word length descending + // 5. Alphabetical + words.sort((a, b) => { + if (a.isTargetQuartile !== b.isTargetQuartile) return a.isTargetQuartile ? -1 : 1; + if (a.tileCount !== b.tileCount) return b.tileCount - a.tileCount; + const ca = getCommonnessScore(a.word); + const cb = getCommonnessScore(b.word); + if (ca !== cb) return cb - ca; + if (b.word.length !== a.word.length) return b.word.length - a.word.length; + return a.word.localeCompare(b.word); + }); - // Sort words: Quartiles first, then by points descending, then alphabetically + // Cap words at maxWords (default: 30) + if (maxWords && words.length > maxWords) { + words = words.slice(0, maxWords); + } + + // Final display sorting: Target quartiles first, then points desc, then alphabetical words.sort((a, b) => { if (a.isTargetQuartile !== b.isTargetQuartile) return a.isTargetQuartile ? -1 : 1; if (b.points !== a.points) return b.points - a.points; return a.word.localeCompare(b.word); }); + const totalBaseScore = words.reduce((sum, w) => sum + w.points, 0); + const maxPossibleScore = totalBaseScore + 40; // 40 bonus points for all 5 quartiles + return { tiles, quartets, @@ -100,7 +130,7 @@ export function solveBoard(quartets) { words, wordCount: words.length, quartileCount: 5, - totalBaseScore: totalScore, + totalBaseScore, maxPossibleScore }; } diff --git a/src/components/ActionBar.js b/src/components/ActionBar.js index 6616bd0..1ba8e7d 100644 --- a/src/components/ActionBar.js +++ b/src/components/ActionBar.js @@ -18,9 +18,9 @@ export function createActionBar({ onShuffle, onSortAlpha, onToggleLock, onGetHin Sort A-Z - diff --git a/src/components/ScoreRankBar.js b/src/components/ScoreRankBar.js index 4a6357a..94ae316 100644 --- a/src/components/ScoreRankBar.js +++ b/src/components/ScoreRankBar.js @@ -1,12 +1,13 @@ -export function createScoreRankBar({ onOpenWordsDrawer }) { +export function createScoreRankBar({ onOpenWordsDrawer, onOpenReveal }) { const container = document.createElement('div'); container.className = 'score-rank-container'; function render(state) { const rankInfo = state.getRank(); const quartilesSolved = state.getTargetQuartilesSolvedCount(); - const totalWords = state.currentPuzzle ? state.currentPuzzle.wordCount : 0; + const totalWords = state.currentPuzzle ? (state.currentPuzzle.wordCount || state.currentPuzzle.words?.length || 0) : 0; const foundWordsCount = state.foundWords.size; + const isRevealed = !!state.isRevealed; container.innerHTML = `
@@ -43,17 +44,28 @@ export function createScoreRankBar({ onOpenWordsDrawer }) {
-
- ${rankInfo.next ? `Next rank in ${rankInfo.pointsToNext} pts` : '🌟 Expert Reached!'} + +
+ ${ + !isRevealed + ? `` + : `👁️ Revealed` + } +
+ ${rankInfo.next ? `Next in ${rankInfo.pointsToNext} pts` : '🌟 Expert!'} +
`; - container.querySelector('#btn-words-found-toggle')?.addEventListener('click', onOpenWordsDrawer); + container.querySelector('#btn-words-found-toggle')?.addEventListener('click', () => onOpenWordsDrawer()); + container.querySelector('#btn-strip-reveal')?.addEventListener('click', () => onOpenReveal?.()); } return { element: container, render }; diff --git a/src/components/WordsDrawer.js b/src/components/WordsDrawer.js index 6a56d80..c8b6b0d 100644 --- a/src/components/WordsDrawer.js +++ b/src/components/WordsDrawer.js @@ -4,17 +4,25 @@ export function createWordsDrawer({ onRevealAll }) { backdrop.id = 'words-drawer-backdrop'; let currentTab = 'all'; // 'all', 'quartiles', '3-tiles', '2-tiles', '1-tiles' + let viewMode = null; // 'all' (all solutions) or 'found' (words found) + let isConfirmingReveal = false; + let lastState = null; function render(state) { - if (!state.currentPuzzle) return; + if (!state || !state.currentPuzzle) return; + lastState = state; const foundWordsList = Array.from(state.foundWords.values()); - const allPuzzleWords = state.currentPuzzle.words; - const isRevealed = state.isRevealed; + const allPuzzleWords = state.currentPuzzle.words || []; + const isRevealed = !!state.isRevealed; + + // Active view: if user picked one use it, otherwise default to 'all' if revealed, 'found' if not + const activeView = viewMode !== null ? viewMode : (isRevealed ? 'all' : 'found'); + + const sourceWords = (activeView === 'all' && isRevealed) ? allPuzzleWords : foundWordsList; // Filter words according to current tab - let displayWords = isRevealed ? allPuzzleWords : foundWordsList; - + let displayWords = [...sourceWords]; if (currentTab === 'quartiles') { displayWords = displayWords.filter(w => w.tileCount === 4); } else if (currentTab === '3-tiles') { @@ -32,30 +40,58 @@ export function createWordsDrawer({ onRevealAll }) { return a.word.localeCompare(b.word); }); - const quartilesCount = foundWordsList.filter(w => w.tileCount === 4).length; - const threeTilesCount = foundWordsList.filter(w => w.tileCount === 3).length; - const twoTilesCount = foundWordsList.filter(w => w.tileCount === 2).length; - const oneTilesCount = foundWordsList.filter(w => w.tileCount === 1).length; + const countingSource = (activeView === 'all' && isRevealed) ? allPuzzleWords : foundWordsList; + const quartilesCount = countingSource.filter(w => w.tileCount === 4).length; + const threeTilesCount = countingSource.filter(w => w.tileCount === 3).length; + const twoTilesCount = countingSource.filter(w => w.tileCount === 2).length; + const oneTilesCount = countingSource.filter(w => w.tileCount === 1).length; + + const quartileDetailsMap = new Map(); + if (state.currentPuzzle.quartileDetails) { + state.currentPuzzle.quartileDetails.forEach(q => { + quartileDetailsMap.set(q.word.toLowerCase(), q.clue); + }); + } backdrop.innerHTML = ` + ` : displayWords.length === 0 ? `
🔍
No words found in this category yet
Combine tiles on the board to discover valid words!
+ ${!isRevealed ? ` + + ` : ''}
` : displayWords .map(w => { const isFound = state.foundWords.has(w.word.toLowerCase()); const isQuartile = w.tileCount === 4; + const clue = quartileDetailsMap.get(w.word.toLowerCase()); return ` -
+
${w.word.toUpperCase()} - ${isQuartile ? ' ⭐' : ''} - ${!isFound ? ' (Unfound)' : ''} + ${isQuartile ? ' ⭐' : ''} + ${ + isFound + ? 'Found' + : 'Revealed' + }
- ${w.tileSegments.map(s => `${s.toUpperCase()}`).join('+')} + ${(w.tileSegments || []).map(s => `${s.toUpperCase()}`).join('+')}
+ ${clue ? `
💡 ${clue}
` : ''}
+${w.points}
@@ -103,12 +166,18 @@ export function createWordsDrawer({ onRevealAll }) {