solvers
This commit is contained in:
@@ -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)
|
// Build prefix set for super fast solver pruning (up to 6 letters)
|
||||||
const prefixSet = new Set();
|
const prefixSet = new Set();
|
||||||
for (const word of cleanWordSet) {
|
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
|
// 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 tiles = quartets.flat();
|
||||||
const foundMap = new Map();
|
const foundMap = new Map();
|
||||||
|
|
||||||
@@ -55,12 +69,10 @@ export function solveBoard(quartets) {
|
|||||||
|
|
||||||
search('', []);
|
search('', []);
|
||||||
|
|
||||||
const quartilesList = quartets.map(q => q.join(''));
|
const quartilesList = quartets.map(q => q.join('').toLowerCase());
|
||||||
const quartileSet = new Set(quartilesList);
|
const quartileSet = new Set(quartilesList);
|
||||||
|
|
||||||
const words = [];
|
let words = [];
|
||||||
let totalScore = 0;
|
|
||||||
let quartileCount = 0;
|
|
||||||
|
|
||||||
for (const [word, path] of foundMap.entries()) {
|
for (const [word, path] of foundMap.entries()) {
|
||||||
const tileSegments = path.map(idx => tiles[idx]);
|
const tileSegments = path.map(idx => tiles[idx]);
|
||||||
@@ -69,9 +81,6 @@ export function solveBoard(quartets) {
|
|||||||
const isTargetQuartile = quartileSet.has(word);
|
const isTargetQuartile = quartileSet.has(word);
|
||||||
const isQuartile = tileCount === 4;
|
const isQuartile = tileCount === 4;
|
||||||
|
|
||||||
if (isQuartile) quartileCount++;
|
|
||||||
totalScore += points;
|
|
||||||
|
|
||||||
words.push({
|
words.push({
|
||||||
word,
|
word,
|
||||||
points,
|
points,
|
||||||
@@ -83,16 +92,37 @@ export function solveBoard(quartets) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 40 bonus points for finding all 5 quartiles
|
// Sort candidate words:
|
||||||
const maxPossibleScore = totalScore + 40;
|
// 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) => {
|
words.sort((a, b) => {
|
||||||
if (a.isTargetQuartile !== b.isTargetQuartile) return a.isTargetQuartile ? -1 : 1;
|
if (a.isTargetQuartile !== b.isTargetQuartile) return a.isTargetQuartile ? -1 : 1;
|
||||||
if (b.points !== a.points) return b.points - a.points;
|
if (b.points !== a.points) return b.points - a.points;
|
||||||
return a.word.localeCompare(b.word);
|
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 {
|
return {
|
||||||
tiles,
|
tiles,
|
||||||
quartets,
|
quartets,
|
||||||
@@ -100,7 +130,7 @@ export function solveBoard(quartets) {
|
|||||||
words,
|
words,
|
||||||
wordCount: words.length,
|
wordCount: words.length,
|
||||||
quartileCount: 5,
|
quartileCount: 5,
|
||||||
totalBaseScore: totalScore,
|
totalBaseScore,
|
||||||
maxPossibleScore
|
maxPossibleScore
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ export function createActionBar({ onShuffle, onSortAlpha, onToggleLock, onGetHin
|
|||||||
<span>Sort A-Z</span>
|
<span>Sort A-Z</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn-action ${isLocked ? 'active' : ''}" id="btn-action-lock" title="${isLocked ? 'Unlock Quartiles to shuffle freely' : 'Lock found Quartiles at top rows'}">
|
<button class="btn-action ${isLocked ? 'active' : ''}" id="btn-action-lock" title="${isLocked ? 'Solved Quartiles are pinned to top rows. Tap to allow free shuffle' : 'Pin solved Quartiles to top rows'}">
|
||||||
<span class="action-icon">${isLocked ? '🔒' : '🔓'}</span>
|
<span class="action-icon">${isLocked ? '📌' : '🔓'}</span>
|
||||||
<span>${isLocked ? 'Locked' : 'Unlocked'}</span>
|
<span>${isLocked ? 'Pinned' : 'Free'}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn-action" id="btn-action-hint" title="Get a hint for an unfound word">
|
<button class="btn-action" id="btn-action-hint" title="Get a hint for an unfound word">
|
||||||
|
|||||||
@@ -262,11 +262,11 @@ export function createModals({ onSelectPuzzle, onCustomPuzzleCreated, onResetPuz
|
|||||||
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:10px 0; border-bottom:1px solid var(--border-subtle);">
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:10px 0; border-bottom:1px solid var(--border-subtle);">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:600; color:var(--text-primary);">Auto-Lock Quartiles</div>
|
<div style="font-weight:600; color:var(--text-primary);">Pin Solved Quartiles</div>
|
||||||
<div style="font-size:0.78rem; color:var(--text-secondary);">Lock completed Quartiles at top rows</div>
|
<div style="font-size:0.78rem; color:var(--text-secondary);">Keep completed Quartiles organized at top rows of the board</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="setting-toggle-lock" style="padding:6px 14px; border-radius:var(--radius-full); background:var(--bg-tertiary); font-weight:600;">
|
<button id="setting-toggle-lock" style="padding:6px 14px; border-radius:var(--radius-full); background:var(--bg-tertiary); font-weight:600;">
|
||||||
${lockSetting ? '🔒 Locked' : '🔓 Off'}
|
${lockSetting ? '📌 Pinned' : '🔓 Free'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
export function createScoreRankBar({ onOpenWordsDrawer }) {
|
export function createScoreRankBar({ onOpenWordsDrawer, onOpenReveal }) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'score-rank-container';
|
container.className = 'score-rank-container';
|
||||||
|
|
||||||
function render(state) {
|
function render(state) {
|
||||||
const rankInfo = state.getRank();
|
const rankInfo = state.getRank();
|
||||||
const quartilesSolved = state.getTargetQuartilesSolvedCount();
|
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 foundWordsCount = state.foundWords.size;
|
||||||
|
const isRevealed = !!state.isRevealed;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="score-rank-card">
|
<div class="score-rank-card">
|
||||||
@@ -43,17 +44,28 @@ export function createScoreRankBar({ onOpenWordsDrawer }) {
|
|||||||
|
|
||||||
<div class="words-found-strip">
|
<div class="words-found-strip">
|
||||||
<button class="btn-words-drawer" id="btn-words-found-toggle">
|
<button class="btn-words-drawer" id="btn-words-found-toggle">
|
||||||
<span>Words Found</span>
|
<span>${isRevealed ? 'All Words & Solutions' : 'Words Found'}</span>
|
||||||
<span class="badge-count">${foundWordsCount} ${rankInfo.isExpert ? `/ ${totalWords}` : ''}</span>
|
<span class="badge-count">${isRevealed ? `👁️ ${foundWordsCount}/${totalWords}` : `${foundWordsCount} ${rankInfo.isExpert ? `/ ${totalWords}` : ''}`}</span>
|
||||||
<span style="font-size: 0.75rem;">▾</span>
|
<span style="font-size: 0.75rem;">▾</span>
|
||||||
</button>
|
</button>
|
||||||
<div style="font-size: 0.8rem; color: var(--text-secondary); font-weight: 500;">
|
|
||||||
${rankInfo.next ? `Next rank in <strong>${rankInfo.pointsToNext}</strong> pts` : '🌟 Expert Reached!'}
|
<div style="display:flex; align-items:center; gap:8px;">
|
||||||
|
${
|
||||||
|
!isRevealed
|
||||||
|
? `<button class="btn-strip-reveal" id="btn-strip-reveal" title="Reveal all words and solutions for this puzzle">
|
||||||
|
💡 Reveal
|
||||||
|
</button>`
|
||||||
|
: `<span class="strip-revealed-tag" title="Solutions revealed">👁️ Revealed</span>`
|
||||||
|
}
|
||||||
|
<div style="font-size: 0.8rem; color: var(--text-secondary); font-weight: 500;">
|
||||||
|
${rankInfo.next ? `Next in <strong>${rankInfo.pointsToNext}</strong> pts` : '🌟 Expert!'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
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 };
|
return { element: container, render };
|
||||||
|
|||||||
@@ -4,17 +4,25 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
backdrop.id = 'words-drawer-backdrop';
|
backdrop.id = 'words-drawer-backdrop';
|
||||||
|
|
||||||
let currentTab = 'all'; // 'all', 'quartiles', '3-tiles', '2-tiles', '1-tiles'
|
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) {
|
function render(state) {
|
||||||
if (!state.currentPuzzle) return;
|
if (!state || !state.currentPuzzle) return;
|
||||||
|
lastState = state;
|
||||||
|
|
||||||
const foundWordsList = Array.from(state.foundWords.values());
|
const foundWordsList = Array.from(state.foundWords.values());
|
||||||
const allPuzzleWords = state.currentPuzzle.words;
|
const allPuzzleWords = state.currentPuzzle.words || [];
|
||||||
const isRevealed = state.isRevealed;
|
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
|
// Filter words according to current tab
|
||||||
let displayWords = isRevealed ? allPuzzleWords : foundWordsList;
|
let displayWords = [...sourceWords];
|
||||||
|
|
||||||
if (currentTab === 'quartiles') {
|
if (currentTab === 'quartiles') {
|
||||||
displayWords = displayWords.filter(w => w.tileCount === 4);
|
displayWords = displayWords.filter(w => w.tileCount === 4);
|
||||||
} else if (currentTab === '3-tiles') {
|
} else if (currentTab === '3-tiles') {
|
||||||
@@ -32,30 +40,58 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
return a.word.localeCompare(b.word);
|
return a.word.localeCompare(b.word);
|
||||||
});
|
});
|
||||||
|
|
||||||
const quartilesCount = foundWordsList.filter(w => w.tileCount === 4).length;
|
const countingSource = (activeView === 'all' && isRevealed) ? allPuzzleWords : foundWordsList;
|
||||||
const threeTilesCount = foundWordsList.filter(w => w.tileCount === 3).length;
|
const quartilesCount = countingSource.filter(w => w.tileCount === 4).length;
|
||||||
const twoTilesCount = foundWordsList.filter(w => w.tileCount === 2).length;
|
const threeTilesCount = countingSource.filter(w => w.tileCount === 3).length;
|
||||||
const oneTilesCount = foundWordsList.filter(w => w.tileCount === 1).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 = `
|
backdrop.innerHTML = `
|
||||||
<div class="modal-sheet" id="words-drawer-sheet">
|
<div class="modal-sheet" id="words-drawer-sheet">
|
||||||
<div class="sheet-handle"></div>
|
<div class="sheet-handle"></div>
|
||||||
<div class="sheet-header">
|
<div class="sheet-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="sheet-title">Words Found</div>
|
<div style="display:flex; align-items:center; gap:8px;">
|
||||||
|
<div class="sheet-title">${isRevealed ? 'Words & Solutions' : 'Words Found'}</div>
|
||||||
|
${isRevealed ? '<span class="badge-revealed">👁️ Revealed</span>' : ''}
|
||||||
|
</div>
|
||||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 2px;">
|
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 2px;">
|
||||||
${foundWordsList.length} of ${allPuzzleWords.length} words found (${state.score} pts)
|
${
|
||||||
|
isRevealed
|
||||||
|
? `<strong>${foundWordsList.length}</strong> of ${allPuzzleWords.length} found (${state.score} pts) • ${allPuzzleWords.length - foundWordsList.length} revealed`
|
||||||
|
: `<strong>${foundWordsList.length}</strong> of ${allPuzzleWords.length} words found (${state.score} pts)`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-close-sheet" id="btn-close-words-drawer">✕</button>
|
<button class="btn-close-sheet" id="btn-close-words-drawer" aria-label="Close words drawer">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${
|
||||||
|
isRevealed ? `
|
||||||
|
<div class="drawer-view-mode-bar">
|
||||||
|
<button class="view-mode-btn ${activeView === 'all' ? 'active' : ''}" data-view="all">
|
||||||
|
👁️ All Solutions (${allPuzzleWords.length})
|
||||||
|
</button>
|
||||||
|
<button class="view-mode-btn ${activeView === 'found' ? 'active' : ''}" data-view="found">
|
||||||
|
✓ Words You Found (${foundWordsList.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''
|
||||||
|
}
|
||||||
|
|
||||||
<div class="sheet-tabs">
|
<div class="sheet-tabs">
|
||||||
<button class="tab-btn ${currentTab === 'all' ? 'active' : ''}" data-tab="all">
|
<button class="tab-btn ${currentTab === 'all' ? 'active' : ''}" data-tab="all">
|
||||||
All (${foundWordsList.length})
|
All (${countingSource.length})
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn ${currentTab === 'quartiles' ? 'active' : ''}" data-tab="quartiles">
|
<button class="tab-btn ${currentTab === 'quartiles' ? 'active' : ''}" data-tab="quartiles">
|
||||||
Quartiles (${quartilesCount}/5)
|
Quartiles (${quartilesCount}${activeView === 'all' ? '' : '/5'})
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn ${currentTab === '3-tiles' ? 'active' : ''}" data-tab="3-tiles">
|
<button class="tab-btn ${currentTab === '3-tiles' ? 'active' : ''}" data-tab="3-tiles">
|
||||||
3-Tiles (${threeTilesCount})
|
3-Tiles (${threeTilesCount})
|
||||||
@@ -70,28 +106,55 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
|
|
||||||
<div class="sheet-body">
|
<div class="sheet-body">
|
||||||
${
|
${
|
||||||
displayWords.length === 0
|
isConfirmingReveal ? `
|
||||||
|
<div class="reveal-confirm-box">
|
||||||
|
<div class="reveal-confirm-icon">💡</div>
|
||||||
|
<h3 class="reveal-confirm-title">Reveal All Word Solutions?</h3>
|
||||||
|
<p class="reveal-confirm-desc">
|
||||||
|
This will reveal all <strong>${allPuzzleWords.length}</strong> words and their tile combinations for today's puzzle.
|
||||||
|
</p>
|
||||||
|
<div class="reveal-confirm-actions">
|
||||||
|
<button class="btn-confirm-reveal-yes" id="btn-confirm-reveal-yes">
|
||||||
|
Reveal All Solutions
|
||||||
|
</button>
|
||||||
|
<button class="btn-confirm-reveal-cancel" id="btn-confirm-reveal-cancel">
|
||||||
|
Keep Playing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : displayWords.length === 0
|
||||||
? `<div style="text-align:center; padding: 32px 16px; color: var(--text-secondary);">
|
? `<div style="text-align:center; padding: 32px 16px; color: var(--text-secondary);">
|
||||||
<div style="font-size: 2rem; margin-bottom: 8px;">🔍</div>
|
<div style="font-size: 2rem; margin-bottom: 8px;">🔍</div>
|
||||||
<strong>No words found in this category yet</strong>
|
<strong>No words found in this category yet</strong>
|
||||||
<div style="font-size: 0.85rem; margin-top: 4px;">Combine tiles on the board to discover valid words!</div>
|
<div style="font-size: 0.85rem; margin-top: 4px;">Combine tiles on the board to discover valid words!</div>
|
||||||
|
${!isRevealed ? `
|
||||||
|
<button class="btn-reveal-all" id="btn-inline-reveal" style="margin-top:16px;">
|
||||||
|
💡 Reveal All Word Solutions
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
</div>`
|
</div>`
|
||||||
: displayWords
|
: displayWords
|
||||||
.map(w => {
|
.map(w => {
|
||||||
const isFound = state.foundWords.has(w.word.toLowerCase());
|
const isFound = state.foundWords.has(w.word.toLowerCase());
|
||||||
const isQuartile = w.tileCount === 4;
|
const isQuartile = w.tileCount === 4;
|
||||||
|
const clue = quartileDetailsMap.get(w.word.toLowerCase());
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="word-card ${isQuartile ? 'quartile-card' : ''}" style="${!isFound ? 'opacity: 0.6; border-style: dashed;' : ''}">
|
<div class="word-card ${isQuartile ? 'quartile-card' : ''} ${!isFound ? 'revealed-card' : ''}">
|
||||||
<div class="word-card-left">
|
<div class="word-card-left">
|
||||||
<div class="word-card-word">
|
<div class="word-card-word">
|
||||||
${w.word.toUpperCase()}
|
${w.word.toUpperCase()}
|
||||||
${isQuartile ? ' ⭐' : ''}
|
${isQuartile ? '<span title="Target Quartile (8 pts)"> ⭐</span>' : ''}
|
||||||
${!isFound ? ' <span style="font-size:0.75rem; color:var(--text-tertiary);">(Unfound)</span>' : ''}
|
${
|
||||||
|
isFound
|
||||||
|
? '<span class="status-chip found">Found</span>'
|
||||||
|
: '<span class="status-chip revealed">Revealed</span>'
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="word-card-tiles">
|
<div class="word-card-tiles">
|
||||||
${w.tileSegments.map(s => `<span class="tile-chip-mini">${s.toUpperCase()}</span>`).join('+')}
|
${(w.tileSegments || []).map(s => `<span class="tile-chip-mini">${s.toUpperCase()}</span>`).join('+')}
|
||||||
</div>
|
</div>
|
||||||
|
${clue ? `<div class="quartile-clue">💡 ${clue}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="word-card-points">+${w.points}</div>
|
<div class="word-card-points">+${w.points}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,12 +166,18 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
|
|
||||||
<div class="sheet-footer">
|
<div class="sheet-footer">
|
||||||
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
<div style="font-size: 0.8rem; color: var(--text-secondary);">
|
||||||
${isRevealed ? 'All solutions revealed' : state.getRank().isExpert ? '🏆 Expert reached! You can keep hunting or reveal solutions.' : 'Reach 100 points for Expert rank!'}
|
${
|
||||||
|
isRevealed
|
||||||
|
? 'All solutions revealed • You can keep testing words'
|
||||||
|
: state.getRank().isExpert
|
||||||
|
? '🏆 Expert reached! You can keep hunting or reveal solutions.'
|
||||||
|
: 'Reach 100 points for Expert rank!'
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
${
|
${
|
||||||
!isRevealed
|
!isRevealed && !isConfirmingReveal
|
||||||
? `<button class="btn-reveal-all" id="btn-trigger-reveal">
|
? `<button class="btn-reveal-all" id="btn-trigger-reveal">
|
||||||
Reveal All Words
|
💡 Reveal All Words
|
||||||
</button>`
|
</button>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
@@ -127,6 +196,7 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tab buttons
|
||||||
const tabBtns = backdrop.querySelectorAll('.tab-btn');
|
const tabBtns = backdrop.querySelectorAll('.tab-btn');
|
||||||
tabBtns.forEach(btn => {
|
tabBtns.forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
@@ -135,37 +205,54 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const revealBtn = backdrop.querySelector('#btn-trigger-reveal');
|
// View mode buttons (All Solutions vs Words You Found)
|
||||||
if (revealBtn) {
|
const viewBtns = backdrop.querySelectorAll('.view-mode-btn');
|
||||||
let isConfirming = false;
|
viewBtns.forEach(btn => {
|
||||||
let timer = null;
|
btn.addEventListener('click', () => {
|
||||||
revealBtn.addEventListener('click', () => {
|
viewMode = btn.getAttribute('data-view');
|
||||||
if (!isConfirming) {
|
render(state);
|
||||||
isConfirming = true;
|
|
||||||
revealBtn.innerText = 'Tap again to reveal all words';
|
|
||||||
revealBtn.style.background = 'var(--accent-danger)';
|
|
||||||
revealBtn.style.color = '#ffffff';
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
isConfirming = false;
|
|
||||||
if (revealBtn) {
|
|
||||||
revealBtn.innerText = 'Reveal All Words';
|
|
||||||
revealBtn.style.background = 'rgba(239, 68, 68, 0.1)';
|
|
||||||
revealBtn.style.color = 'var(--accent-danger)';
|
|
||||||
}
|
|
||||||
}, 3500);
|
|
||||||
} else {
|
|
||||||
clearTimeout(timer);
|
|
||||||
onRevealAll();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// Trigger reveal prompt
|
||||||
|
const triggerBtn = backdrop.querySelector('#btn-trigger-reveal');
|
||||||
|
triggerBtn?.addEventListener('click', () => {
|
||||||
|
isConfirmingReveal = true;
|
||||||
|
render(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
const inlineRevealBtn = backdrop.querySelector('#btn-inline-reveal');
|
||||||
|
inlineRevealBtn?.addEventListener('click', () => {
|
||||||
|
isConfirmingReveal = true;
|
||||||
|
render(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirmation actions
|
||||||
|
backdrop.querySelector('#btn-confirm-reveal-yes')?.addEventListener('click', () => {
|
||||||
|
isConfirmingReveal = false;
|
||||||
|
viewMode = 'all';
|
||||||
|
onRevealAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
backdrop.querySelector('#btn-confirm-reveal-cancel')?.addEventListener('click', () => {
|
||||||
|
isConfirmingReveal = false;
|
||||||
|
render(state);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open(showRevealPrompt = false) {
|
||||||
|
if (showRevealPrompt && lastState && !lastState.isRevealed) {
|
||||||
|
isConfirmingReveal = true;
|
||||||
|
render(lastState);
|
||||||
|
} else {
|
||||||
|
isConfirmingReveal = false;
|
||||||
|
if (lastState) render(lastState);
|
||||||
|
}
|
||||||
backdrop.classList.add('open');
|
backdrop.classList.add('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
isConfirmingReveal = false;
|
||||||
backdrop.classList.remove('open');
|
backdrop.classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -389,21 +389,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "gettable",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
6,
|
|
||||||
7
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"get",
|
|
||||||
"table"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "grain",
|
"word": "grain",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -449,21 +434,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "kain",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
2,
|
|
||||||
16
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ka",
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "marble",
|
"word": "marble",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -479,21 +449,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "marka",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"mar",
|
|
||||||
"ka"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "photo",
|
"word": "photo",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -539,21 +494,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "ratable",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
18,
|
|
||||||
7
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ra",
|
|
||||||
"table"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "ration",
|
"word": "ration",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -614,21 +554,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "retable",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
0,
|
|
||||||
7
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"re",
|
|
||||||
"table"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "spire",
|
"word": "spire",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -688,131 +613,10 @@ export const CURATED_PUZZLES = [
|
|||||||
],
|
],
|
||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "venin",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
13,
|
|
||||||
16
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ven",
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "venous",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
13,
|
|
||||||
15
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ven",
|
|
||||||
"ous"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "for",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
5
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"for"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "get",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
6
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"get"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "in",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
16
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "mar",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
1
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"mar"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "pho",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
8
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"pho"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "table",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
7
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"table"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "to",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
9
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"to"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wordCount": 44,
|
"wordCount": 30,
|
||||||
"maxPossibleScore": 181
|
"maxPossibleScore": 160
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2026-10-24",
|
"id": "2026-10-24",
|
||||||
@@ -1188,36 +992,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "aspic",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
0,
|
|
||||||
19
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"as",
|
|
||||||
"pic"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "aster",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
0,
|
|
||||||
13
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"as",
|
|
||||||
"ter"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "coat",
|
"word": "coat",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1263,21 +1037,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "ester",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
17,
|
|
||||||
13
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"es",
|
|
||||||
"ter"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "grain",
|
"word": "grain",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1323,21 +1082,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "moas",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
9,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"mo",
|
|
||||||
"as"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "moat",
|
"word": "moat",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1353,21 +1097,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "mono",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
9,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"mo",
|
|
||||||
"no"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "motel",
|
"word": "motel",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1443,21 +1172,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "stein",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
14,
|
|
||||||
12
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ste",
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "stellar",
|
"word": "stellar",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1473,21 +1187,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "steno",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
14,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ste",
|
|
||||||
"no"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "taco",
|
"word": "taco",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1503,21 +1202,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "tain",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
6,
|
|
||||||
12
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ta",
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "taste",
|
"word": "taste",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1533,51 +1217,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "tater",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
6,
|
|
||||||
13
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ta",
|
|
||||||
"ter"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "troat",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
1,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"tro",
|
|
||||||
"at"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "tropic",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
1,
|
|
||||||
19
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"tro",
|
|
||||||
"pic"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "vies",
|
"word": "vies",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -1592,131 +1231,10 @@ export const CURATED_PUZZLES = [
|
|||||||
],
|
],
|
||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "vino",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
5,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"vi",
|
|
||||||
"no"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "vita",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
5,
|
|
||||||
6
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"vi",
|
|
||||||
"ta"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "as",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"as"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "at",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
8
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"at"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "ere",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
11
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ere"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "in",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
12
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "my",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
3
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"my"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "no",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"no"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "pic",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
19
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"pic"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wordCount": 50,
|
"wordCount": 30,
|
||||||
"maxPossibleScore": 193
|
"maxPossibleScore": 160
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2026-10-25",
|
"id": "2026-10-25",
|
||||||
@@ -3780,21 +3298,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "noma",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
13,
|
|
||||||
10
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"no",
|
|
||||||
"ma"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "notion",
|
"word": "notion",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -3840,51 +3343,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "rota",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
6,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ro",
|
|
||||||
"ta"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "tael",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
18,
|
|
||||||
4
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ta",
|
|
||||||
"el"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "tain",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
18,
|
|
||||||
12
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"ta",
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "taro",
|
"word": "taro",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -3944,49 +3402,10 @@ export const CURATED_PUZZLES = [
|
|||||||
],
|
],
|
||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "in",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
12
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "no",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
13
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"no"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "to",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
9
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"to"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wordCount": 37,
|
"wordCount": 30,
|
||||||
"maxPossibleScore": 151
|
"maxPossibleScore": 140
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2026-10-29",
|
"id": "2026-10-29",
|
||||||
@@ -5082,21 +4501,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "stover",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
18,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"sto",
|
|
||||||
"ver"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "thunder",
|
"word": "thunder",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -5201,36 +4605,10 @@ export const CURATED_PUZZLES = [
|
|||||||
],
|
],
|
||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "cat",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
8
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"cat"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "lar",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
11
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"lar"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wordCount": 33,
|
"wordCount": 30,
|
||||||
"maxPossibleScore": 158
|
"maxPossibleScore": 154
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2026-10-31",
|
"id": "2026-10-31",
|
||||||
@@ -5810,45 +5188,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "an",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
16
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"an"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "in",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
12
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"in"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "mys",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"mys"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "nation",
|
"word": "nation",
|
||||||
"points": 1,
|
"points": 1,
|
||||||
@@ -5874,23 +5213,10 @@ export const CURATED_PUZZLES = [
|
|||||||
],
|
],
|
||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "us",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
3
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"us"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wordCount": 34,
|
"wordCount": 30,
|
||||||
"maxPossibleScore": 152
|
"maxPossibleScore": 148
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2026-10-22",
|
"id": "2026-10-22",
|
||||||
@@ -7516,21 +6842,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "vina",
|
|
||||||
"points": 2,
|
|
||||||
"tileCount": 2,
|
|
||||||
"tileIndices": [
|
|
||||||
5,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"vi",
|
|
||||||
"na"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "vital",
|
"word": "vital",
|
||||||
"points": 2,
|
"points": 2,
|
||||||
@@ -7545,23 +6856,10 @@ export const CURATED_PUZZLES = [
|
|||||||
],
|
],
|
||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "con",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
8
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"con"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wordCount": 32,
|
"wordCount": 30,
|
||||||
"maxPossibleScore": 161
|
"maxPossibleScore": 158
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2026-10-19",
|
"id": "2026-10-19",
|
||||||
@@ -8980,19 +8278,6 @@ export const CURATED_PUZZLES = [
|
|||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"word": "dis",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
16
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"dis"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"word": "nation",
|
"word": "nation",
|
||||||
"points": 1,
|
"points": 1,
|
||||||
@@ -9018,36 +8303,10 @@ export const CURATED_PUZZLES = [
|
|||||||
],
|
],
|
||||||
"isTargetQuartile": false,
|
"isTargetQuartile": false,
|
||||||
"isQuartile": false
|
"isQuartile": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "shed",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
11
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"shed"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"word": "tin",
|
|
||||||
"points": 1,
|
|
||||||
"tileCount": 1,
|
|
||||||
"tileIndices": [
|
|
||||||
17
|
|
||||||
],
|
|
||||||
"tileSegments": [
|
|
||||||
"tin"
|
|
||||||
],
|
|
||||||
"isTargetQuartile": false,
|
|
||||||
"isQuartile": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"wordCount": 33,
|
"wordCount": 30,
|
||||||
"maxPossibleScore": 149
|
"maxPossibleScore": 146
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2026-10-16",
|
"id": "2026-10-16",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export { POINTS_BY_TILE_COUNT, QUARTILE_COMPLETION_BONUS, EXPERT_RANK_POINTS, RA
|
|||||||
* Solves a 20-tile board (5 quartets)
|
* Solves a 20-tile board (5 quartets)
|
||||||
* Returns all constructible valid words, point values, and tile paths
|
* Returns all constructible valid words, point values, and tile paths
|
||||||
*/
|
*/
|
||||||
export function solveBoard(quartets) {
|
export function solveBoard(quartets, maxWords = 30) {
|
||||||
const tiles = quartets.flat();
|
const tiles = quartets.flat();
|
||||||
const foundMap = new Map();
|
const foundMap = new Map();
|
||||||
|
|
||||||
@@ -37,8 +37,7 @@ export function solveBoard(quartets) {
|
|||||||
const quartilesList = quartets.map(q => q.join('').toLowerCase());
|
const quartilesList = quartets.map(q => q.join('').toLowerCase());
|
||||||
const quartileSet = new Set(quartilesList);
|
const quartileSet = new Set(quartilesList);
|
||||||
|
|
||||||
const words = [];
|
let words = [];
|
||||||
let totalBaseScore = 0;
|
|
||||||
|
|
||||||
for (const [word, path] of foundMap.entries()) {
|
for (const [word, path] of foundMap.entries()) {
|
||||||
const tileCount = path.length;
|
const tileCount = path.length;
|
||||||
@@ -46,8 +45,6 @@ export function solveBoard(quartets) {
|
|||||||
const isTargetQuartile = quartileSet.has(word);
|
const isTargetQuartile = quartileSet.has(word);
|
||||||
const isQuartile = tileCount === 4;
|
const isQuartile = tileCount === 4;
|
||||||
|
|
||||||
totalBaseScore += points;
|
|
||||||
|
|
||||||
words.push({
|
words.push({
|
||||||
word,
|
word,
|
||||||
points,
|
points,
|
||||||
@@ -59,13 +56,32 @@ export function solveBoard(quartets) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort words: Quartiles first, then by points descending, then alphabetically
|
// Sort candidate words:
|
||||||
|
// 1. Target quartiles always first (all 5 guaranteed)
|
||||||
|
// 2. Tile count descending (4-tiles > 3-tiles > 2-tiles > 1-tile)
|
||||||
|
// 3. Word length descending
|
||||||
|
// 4. 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;
|
||||||
|
if (b.word.length !== a.word.length) return b.word.length - a.word.length;
|
||||||
|
return a.word.localeCompare(b.word);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cap at maxWords (default: 30)
|
||||||
|
if (maxWords && words.length > maxWords) {
|
||||||
|
words = words.slice(0, maxWords);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final display sort: Quartiles first, then by points descending, then alphabetically
|
||||||
words.sort((a, b) => {
|
words.sort((a, b) => {
|
||||||
if (a.isTargetQuartile !== b.isTargetQuartile) return a.isTargetQuartile ? -1 : 1;
|
if (a.isTargetQuartile !== b.isTargetQuartile) return a.isTargetQuartile ? -1 : 1;
|
||||||
if (b.points !== a.points) return b.points - a.points;
|
if (b.points !== a.points) return b.points - a.points;
|
||||||
return a.word.localeCompare(b.word);
|
return a.word.localeCompare(b.word);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const totalBaseScore = words.reduce((sum, w) => sum + w.points, 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tiles,
|
tiles,
|
||||||
quartets,
|
quartets,
|
||||||
|
|||||||
48
src/main.js
48
src/main.js
@@ -71,8 +71,9 @@ async function initApp() {
|
|||||||
},
|
},
|
||||||
onRevealAll: () => {
|
onRevealAll: () => {
|
||||||
gameState.revealAllWords();
|
gameState.revealAllWords();
|
||||||
gameState.notifyToast('All words revealed!', 'info');
|
const count = gameState.currentPuzzle?.words?.length || 0;
|
||||||
wordsDrawer.open();
|
gameState.notifyToast(`All ${count} word solutions revealed!`, 'info');
|
||||||
|
wordsDrawer.open(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(modals.element);
|
document.body.appendChild(modals.element);
|
||||||
@@ -81,7 +82,8 @@ async function initApp() {
|
|||||||
const wordsDrawer = createWordsDrawer({
|
const wordsDrawer = createWordsDrawer({
|
||||||
onRevealAll: () => {
|
onRevealAll: () => {
|
||||||
gameState.revealAllWords();
|
gameState.revealAllWords();
|
||||||
gameState.notifyToast('All words revealed!', 'info');
|
const count = gameState.currentPuzzle?.words?.length || 0;
|
||||||
|
gameState.notifyToast(`All ${count} word solutions revealed!`, 'info');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(wordsDrawer.element);
|
document.body.appendChild(wordsDrawer.element);
|
||||||
@@ -111,7 +113,10 @@ async function initApp() {
|
|||||||
// Create Score & Rank Bar
|
// Create Score & Rank Bar
|
||||||
const scoreRankBar = createScoreRankBar({
|
const scoreRankBar = createScoreRankBar({
|
||||||
onOpenWordsDrawer: () => {
|
onOpenWordsDrawer: () => {
|
||||||
wordsDrawer.open();
|
wordsDrawer.open(false);
|
||||||
|
},
|
||||||
|
onOpenReveal: () => {
|
||||||
|
wordsDrawer.open(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
appContainer.appendChild(scoreRankBar.element);
|
appContainer.appendChild(scoreRankBar.element);
|
||||||
@@ -306,6 +311,12 @@ async function initApp() {
|
|||||||
modals.render(state);
|
modals.render(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save state on unload/refresh
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
gameState.saveActivePuzzle();
|
||||||
|
gameState.saveProgress();
|
||||||
|
});
|
||||||
|
|
||||||
// Check URL hash for shared custom puzzle
|
// Check URL hash for shared custom puzzle
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
if (hash && hash.includes('puzzle=')) {
|
if (hash && hash.includes('puzzle=')) {
|
||||||
@@ -323,7 +334,34 @@ async function initApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load today's puzzle or default
|
// Restore previously active puzzle if available
|
||||||
|
const savedActivePuzzleId = localStorage.getItem('quartiles_active_puzzle_id');
|
||||||
|
const isCustomActive = localStorage.getItem('quartiles_active_is_custom') === 'true';
|
||||||
|
|
||||||
|
if (savedActivePuzzleId) {
|
||||||
|
if (isCustomActive) {
|
||||||
|
try {
|
||||||
|
const rawCustom = localStorage.getItem(`quartiles_custom_puzzle_${savedActivePuzzleId}`);
|
||||||
|
if (rawCustom) {
|
||||||
|
const parsedCustom = JSON.parse(rawCustom);
|
||||||
|
if (parsedCustom && parsedCustom.id) {
|
||||||
|
gameState.loadPuzzle(parsedCustom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to restore custom active puzzle:', e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const matched = CURATED_PUZZLES.find(p => p.id === savedActivePuzzleId || p.date === savedActivePuzzleId);
|
||||||
|
if (matched) {
|
||||||
|
gameState.loadPuzzle(matched);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Load today's puzzle or default
|
||||||
const todayStr = gameState.getTodayDateString();
|
const todayStr = gameState.getTodayDateString();
|
||||||
const todayPuzzle = CURATED_PUZZLES.find(p => p.date === todayStr) || CURATED_PUZZLES[0];
|
const todayPuzzle = CURATED_PUZZLES.find(p => p.date === todayStr) || CURATED_PUZZLES[0];
|
||||||
gameState.loadPuzzle(todayPuzzle);
|
gameState.loadPuzzle(todayPuzzle);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class GameStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.currentPuzzle = puzzle;
|
this.currentPuzzle = puzzle;
|
||||||
|
this.saveActivePuzzle();
|
||||||
this.activeTileIds = [];
|
this.activeTileIds = [];
|
||||||
this.foundWords = new Map();
|
this.foundWords = new Map();
|
||||||
this.score = 0;
|
this.score = 0;
|
||||||
@@ -107,15 +108,38 @@ class GameStateManager {
|
|||||||
return `${y}-${m}-${d}`;
|
return `${y}-${m}-${d}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save current active puzzle reference to LocalStorage
|
||||||
|
saveActivePuzzle() {
|
||||||
|
if (!this.currentPuzzle) return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('quartiles_active_puzzle_id', this.currentPuzzle.id);
|
||||||
|
const isCurated = CURATED_PUZZLES.some(p => p.id === this.currentPuzzle.id);
|
||||||
|
if (!isCurated) {
|
||||||
|
localStorage.setItem(`quartiles_custom_puzzle_${this.currentPuzzle.id}`, JSON.stringify(this.currentPuzzle));
|
||||||
|
localStorage.setItem('quartiles_active_is_custom', 'true');
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('quartiles_active_is_custom');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to save active puzzle reference:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save current puzzle progress to LocalStorage
|
// Save current puzzle progress to LocalStorage
|
||||||
saveProgress() {
|
saveProgress() {
|
||||||
if (!this.currentPuzzle) return;
|
if (!this.currentPuzzle) return;
|
||||||
|
this.saveActivePuzzle();
|
||||||
const saveKey = `quartiles_save_${this.currentPuzzle.id}`;
|
const saveKey = `quartiles_save_${this.currentPuzzle.id}`;
|
||||||
const payload = {
|
const payload = {
|
||||||
|
puzzleId: this.currentPuzzle.id,
|
||||||
foundWords: Array.from(this.foundWords.entries()),
|
foundWords: Array.from(this.foundWords.entries()),
|
||||||
score: this.score,
|
score: this.score,
|
||||||
bonus: this.quartilesBonusAwarded,
|
bonus: this.quartilesBonusAwarded,
|
||||||
tileOrder: this.boardTiles.map(t => t.id)
|
tileOrder: this.boardTiles.map(t => t.id),
|
||||||
|
activeTileIds: this.activeTileIds,
|
||||||
|
isRevealed: this.isRevealed,
|
||||||
|
hintIndex: this.hintIndex,
|
||||||
|
lastPlayed: Date.now()
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(saveKey, JSON.stringify(payload));
|
localStorage.setItem(saveKey, JSON.stringify(payload));
|
||||||
@@ -132,29 +156,54 @@ class GameStateManager {
|
|||||||
const saved = localStorage.getItem(saveKey);
|
const saved = localStorage.getItem(saveKey);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const parsed = JSON.parse(saved);
|
const parsed = JSON.parse(saved);
|
||||||
if (parsed && Array.isArray(parsed.foundWords)) {
|
if (parsed) {
|
||||||
this.foundWords = new Map(parsed.foundWords);
|
if (Array.isArray(parsed.foundWords)) {
|
||||||
this.score = parsed.score || 0;
|
const validWordsMap = new Map(this.currentPuzzle.words.map(w => [w.word.toLowerCase(), w]));
|
||||||
this.quartilesBonusAwarded = !!parsed.bonus;
|
const filteredEntries = [];
|
||||||
|
let recalculatedScore = 0;
|
||||||
|
for (const [w, entry] of parsed.foundWords) {
|
||||||
|
const matchedWord = validWordsMap.get(w.toLowerCase());
|
||||||
|
if (matchedWord) {
|
||||||
|
entry.points = matchedWord.points;
|
||||||
|
entry.tileCount = matchedWord.tileCount;
|
||||||
|
entry.tileSegments = matchedWord.tileSegments;
|
||||||
|
filteredEntries.push([w.toLowerCase(), entry]);
|
||||||
|
recalculatedScore += matchedWord.points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.foundWords = new Map(filteredEntries);
|
||||||
|
this.score = recalculatedScore;
|
||||||
|
if (parsed.bonus) {
|
||||||
|
this.quartilesBonusAwarded = true;
|
||||||
|
this.score += 40;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.score = parsed.score || 0;
|
||||||
|
}
|
||||||
|
this.isRevealed = !!parsed.isRevealed;
|
||||||
|
this.hintIndex = parsed.hintIndex || 0;
|
||||||
|
|
||||||
// Re-mark solved quartiles
|
// Re-mark solved quartiles
|
||||||
this.updateSolvedQuartileTiles();
|
this.updateSolvedQuartileTiles();
|
||||||
|
|
||||||
|
// Restore active tray tiles if any
|
||||||
|
if (Array.isArray(parsed.activeTileIds)) {
|
||||||
|
this.activeTileIds = parsed.activeTileIds.filter(id => id >= 0 && id < 20);
|
||||||
|
}
|
||||||
|
|
||||||
// Restore tile order if saved
|
// Restore tile order if saved
|
||||||
if (Array.isArray(parsed.tileOrder) && parsed.tileOrder.length === 20) {
|
if (Array.isArray(parsed.tileOrder) && parsed.tileOrder.length === 20) {
|
||||||
// Check if saved order was the trivial unscrambled [0..19] order
|
|
||||||
const isSequential = parsed.tileOrder.every((id, idx) => id === idx);
|
const isSequential = parsed.tileOrder.every((id, idx) => id === idx);
|
||||||
if (isSequential && this.foundWords.size === 0) {
|
if (!isSequential || this.foundWords.size > 0) {
|
||||||
return false;
|
const tileMap = new Map(this.boardTiles.map(t => [t.id, t]));
|
||||||
}
|
const reordered = parsed.tileOrder.map(id => tileMap.get(id)).filter(Boolean);
|
||||||
|
if (reordered.length === 20) {
|
||||||
const tileMap = new Map(this.boardTiles.map(t => [t.id, t]));
|
this.boardTiles = reordered;
|
||||||
const reordered = parsed.tileOrder.map(id => tileMap.get(id)).filter(Boolean);
|
return true;
|
||||||
if (reordered.length === 20) {
|
}
|
||||||
this.boardTiles = reordered;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return this.foundWords.size > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -228,6 +277,7 @@ class GameStateManager {
|
|||||||
this.lockQuartiles = !this.lockQuartiles;
|
this.lockQuartiles = !this.lockQuartiles;
|
||||||
localStorage.setItem('quartiles_lock_setting', this.lockQuartiles ? 'true' : 'false');
|
localStorage.setItem('quartiles_lock_setting', this.lockQuartiles ? 'true' : 'false');
|
||||||
this.reorganizeBoard();
|
this.reorganizeBoard();
|
||||||
|
this.saveProgress();
|
||||||
this.notify();
|
this.notify();
|
||||||
this.notifyToast(
|
this.notifyToast(
|
||||||
this.lockQuartiles ? 'Quartiles locked at top' : 'Quartiles unlocked for free shuffle',
|
this.lockQuartiles ? 'Quartiles locked at top' : 'Quartiles unlocked for free shuffle',
|
||||||
@@ -250,6 +300,7 @@ class GameStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.activeTileIds.push(tileId);
|
this.activeTileIds.push(tileId);
|
||||||
|
this.saveProgress();
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +308,7 @@ class GameStateManager {
|
|||||||
removeActiveTile(index) {
|
removeActiveTile(index) {
|
||||||
if (index >= 0 && index < this.activeTileIds.length) {
|
if (index >= 0 && index < this.activeTileIds.length) {
|
||||||
this.activeTileIds.splice(index, 1);
|
this.activeTileIds.splice(index, 1);
|
||||||
|
this.saveProgress();
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,6 +324,7 @@ class GameStateManager {
|
|||||||
) {
|
) {
|
||||||
const [item] = this.activeTileIds.splice(fromIndex, 1);
|
const [item] = this.activeTileIds.splice(fromIndex, 1);
|
||||||
this.activeTileIds.splice(toIndex, 0, item);
|
this.activeTileIds.splice(toIndex, 0, item);
|
||||||
|
this.saveProgress();
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,6 +333,7 @@ class GameStateManager {
|
|||||||
backspace() {
|
backspace() {
|
||||||
if (this.activeTileIds.length > 0) {
|
if (this.activeTileIds.length > 0) {
|
||||||
this.activeTileIds.pop();
|
this.activeTileIds.pop();
|
||||||
|
this.saveProgress();
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,6 +342,7 @@ class GameStateManager {
|
|||||||
clearActive() {
|
clearActive() {
|
||||||
if (this.activeTileIds.length > 0) {
|
if (this.activeTileIds.length > 0) {
|
||||||
this.activeTileIds = [];
|
this.activeTileIds = [];
|
||||||
|
this.saveProgress();
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,6 +525,7 @@ class GameStateManager {
|
|||||||
// Reveal all words (when expert reached or explicitly requested)
|
// Reveal all words (when expert reached or explicitly requested)
|
||||||
revealAllWords() {
|
revealAllWords() {
|
||||||
this.isRevealed = true;
|
this.isRevealed = true;
|
||||||
|
this.saveProgress();
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -517,16 +517,199 @@
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-danger);
|
color: var(--accent-danger);
|
||||||
padding: 6px 12px;
|
padding: 6px 14px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-reveal-all:hover {
|
.btn-reveal-all:hover {
|
||||||
background: rgba(239, 68, 68, 0.18);
|
background: rgba(239, 68, 68, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-revealed {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: rgba(245, 158, 11, 0.15);
|
||||||
|
color: var(--accent-quartile);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-view-mode-bar {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 16px;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: transparent;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-btn.active {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-card.revealed-card {
|
||||||
|
opacity: 0.9;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(99, 102, 241, 0.25);
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.04), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip.found {
|
||||||
|
background: rgba(16, 185, 129, 0.15);
|
||||||
|
color: var(--accent-submit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip.revealed {
|
||||||
|
background: rgba(245, 158, 11, 0.18);
|
||||||
|
color: var(--accent-quartile);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quartile-clue {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-confirm-box {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-confirm-icon {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-confirm-title {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-confirm-desc {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.45;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-reveal-yes {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--accent-danger);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-reveal-yes:hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-reveal-cancel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-reveal-cancel:hover {
|
||||||
|
background: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-strip-reveal {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: rgba(245, 158, 11, 0.12);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.35);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-quartile);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-strip-reveal:hover {
|
||||||
|
background: rgba(245, 158, 11, 0.22);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-revealed-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: rgba(245, 158, 11, 0.12);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-quartile);
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
TOAST NOTIFICATIONS
|
TOAST NOTIFICATIONS
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|||||||
Reference in New Issue
Block a user