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)
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ export function createActionBar({ onShuffle, onSortAlpha, onToggleLock, onGetHin
|
||||
<span>Sort A-Z</span>
|
||||
</button>
|
||||
|
||||
<button class="btn-action ${isLocked ? 'active' : ''}" id="btn-action-lock" title="${isLocked ? 'Unlock Quartiles to shuffle freely' : 'Lock found Quartiles at top rows'}">
|
||||
<span class="action-icon">${isLocked ? '🔒' : '🔓'}</span>
|
||||
<span>${isLocked ? 'Locked' : 'Unlocked'}</span>
|
||||
<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>${isLocked ? 'Pinned' : 'Free'}</span>
|
||||
</button>
|
||||
|
||||
<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>
|
||||
<div style="font-weight:600; color:var(--text-primary);">Auto-Lock Quartiles</div>
|
||||
<div style="font-size:0.78rem; color:var(--text-secondary);">Lock completed Quartiles at top rows</div>
|
||||
<div style="font-weight:600; color:var(--text-primary);">Pin Solved Quartiles</div>
|
||||
<div style="font-size:0.78rem; color:var(--text-secondary);">Keep completed Quartiles organized at top rows of the board</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 = `
|
||||
<div class="score-rank-card">
|
||||
@@ -43,17 +44,28 @@ export function createScoreRankBar({ onOpenWordsDrawer }) {
|
||||
|
||||
<div class="words-found-strip">
|
||||
<button class="btn-words-drawer" id="btn-words-found-toggle">
|
||||
<span>Words Found</span>
|
||||
<span class="badge-count">${foundWordsCount} ${rankInfo.isExpert ? `/ ${totalWords}` : ''}</span>
|
||||
<span>${isRevealed ? 'All Words & Solutions' : 'Words Found'}</span>
|
||||
<span class="badge-count">${isRevealed ? `👁️ ${foundWordsCount}/${totalWords}` : `${foundWordsCount} ${rankInfo.isExpert ? `/ ${totalWords}` : ''}`}</span>
|
||||
<span style="font-size: 0.75rem;">▾</span>
|
||||
</button>
|
||||
|
||||
<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 rank in <strong>${rankInfo.pointsToNext}</strong> pts` : '🌟 Expert Reached!'}
|
||||
${rankInfo.next ? `Next in <strong>${rankInfo.pointsToNext}</strong> pts` : '🌟 Expert!'}
|
||||
</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 };
|
||||
|
||||
@@ -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 = `
|
||||
<div class="modal-sheet" id="words-drawer-sheet">
|
||||
<div class="sheet-handle"></div>
|
||||
<div class="sheet-header">
|
||||
<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;">
|
||||
${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>
|
||||
<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>
|
||||
|
||||
${
|
||||
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">
|
||||
<button class="tab-btn ${currentTab === 'all' ? 'active' : ''}" data-tab="all">
|
||||
All (${foundWordsList.length})
|
||||
All (${countingSource.length})
|
||||
</button>
|
||||
<button class="tab-btn ${currentTab === 'quartiles' ? 'active' : ''}" data-tab="quartiles">
|
||||
Quartiles (${quartilesCount}/5)
|
||||
Quartiles (${quartilesCount}${activeView === 'all' ? '' : '/5'})
|
||||
</button>
|
||||
<button class="tab-btn ${currentTab === '3-tiles' ? 'active' : ''}" data-tab="3-tiles">
|
||||
3-Tiles (${threeTilesCount})
|
||||
@@ -70,28 +106,55 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
|
||||
<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="font-size: 2rem; margin-bottom: 8px;">🔍</div>
|
||||
<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>
|
||||
${!isRevealed ? `
|
||||
<button class="btn-reveal-all" id="btn-inline-reveal" style="margin-top:16px;">
|
||||
💡 Reveal All Word Solutions
|
||||
</button>
|
||||
` : ''}
|
||||
</div>`
|
||||
: displayWords
|
||||
.map(w => {
|
||||
const isFound = state.foundWords.has(w.word.toLowerCase());
|
||||
const isQuartile = w.tileCount === 4;
|
||||
const clue = quartileDetailsMap.get(w.word.toLowerCase());
|
||||
|
||||
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-word">
|
||||
${w.word.toUpperCase()}
|
||||
${isQuartile ? ' ⭐' : ''}
|
||||
${!isFound ? ' <span style="font-size:0.75rem; color:var(--text-tertiary);">(Unfound)</span>' : ''}
|
||||
${isQuartile ? '<span title="Target Quartile (8 pts)"> ⭐</span>' : ''}
|
||||
${
|
||||
isFound
|
||||
? '<span class="status-chip found">Found</span>'
|
||||
: '<span class="status-chip revealed">Revealed</span>'
|
||||
}
|
||||
</div>
|
||||
<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>
|
||||
${clue ? `<div class="quartile-clue">💡 ${clue}</div>` : ''}
|
||||
</div>
|
||||
<div class="word-card-points">+${w.points}</div>
|
||||
</div>
|
||||
@@ -103,12 +166,18 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
|
||||
<div class="sheet-footer">
|
||||
<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>
|
||||
${
|
||||
!isRevealed
|
||||
!isRevealed && !isConfirmingReveal
|
||||
? `<button class="btn-reveal-all" id="btn-trigger-reveal">
|
||||
Reveal All Words
|
||||
💡 Reveal All Words
|
||||
</button>`
|
||||
: ''
|
||||
}
|
||||
@@ -127,6 +196,7 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
}
|
||||
});
|
||||
|
||||
// Tab buttons
|
||||
const tabBtns = backdrop.querySelectorAll('.tab-btn');
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -135,37 +205,54 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
});
|
||||
});
|
||||
|
||||
const revealBtn = backdrop.querySelector('#btn-trigger-reveal');
|
||||
if (revealBtn) {
|
||||
let isConfirming = false;
|
||||
let timer = null;
|
||||
revealBtn.addEventListener('click', () => {
|
||||
if (!isConfirming) {
|
||||
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);
|
||||
// View mode buttons (All Solutions vs Words You Found)
|
||||
const viewBtns = backdrop.querySelectorAll('.view-mode-btn');
|
||||
viewBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
viewMode = btn.getAttribute('data-view');
|
||||
render(state);
|
||||
});
|
||||
});
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
function close() {
|
||||
isConfirmingReveal = false;
|
||||
backdrop.classList.remove('open');
|
||||
}
|
||||
|
||||
|
||||
@@ -389,21 +389,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "gettable",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
6,
|
||||
7
|
||||
],
|
||||
"tileSegments": [
|
||||
"get",
|
||||
"table"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "grain",
|
||||
"points": 2,
|
||||
@@ -449,21 +434,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "kain",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
2,
|
||||
16
|
||||
],
|
||||
"tileSegments": [
|
||||
"ka",
|
||||
"in"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "marble",
|
||||
"points": 2,
|
||||
@@ -479,21 +449,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "marka",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"tileSegments": [
|
||||
"mar",
|
||||
"ka"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "photo",
|
||||
"points": 2,
|
||||
@@ -539,21 +494,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "ratable",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
18,
|
||||
7
|
||||
],
|
||||
"tileSegments": [
|
||||
"ra",
|
||||
"table"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "ration",
|
||||
"points": 2,
|
||||
@@ -614,21 +554,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "retable",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
0,
|
||||
7
|
||||
],
|
||||
"tileSegments": [
|
||||
"re",
|
||||
"table"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "spire",
|
||||
"points": 2,
|
||||
@@ -688,131 +613,10 @@ export const CURATED_PUZZLES = [
|
||||
],
|
||||
"isTargetQuartile": 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,
|
||||
"maxPossibleScore": 181
|
||||
"wordCount": 30,
|
||||
"maxPossibleScore": 160
|
||||
},
|
||||
{
|
||||
"id": "2026-10-24",
|
||||
@@ -1188,36 +992,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": 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",
|
||||
"points": 2,
|
||||
@@ -1263,21 +1037,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "ester",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
17,
|
||||
13
|
||||
],
|
||||
"tileSegments": [
|
||||
"es",
|
||||
"ter"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "grain",
|
||||
"points": 2,
|
||||
@@ -1323,21 +1082,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "moas",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
9,
|
||||
0
|
||||
],
|
||||
"tileSegments": [
|
||||
"mo",
|
||||
"as"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "moat",
|
||||
"points": 2,
|
||||
@@ -1353,21 +1097,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "mono",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
9,
|
||||
2
|
||||
],
|
||||
"tileSegments": [
|
||||
"mo",
|
||||
"no"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "motel",
|
||||
"points": 2,
|
||||
@@ -1443,21 +1172,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "stein",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
14,
|
||||
12
|
||||
],
|
||||
"tileSegments": [
|
||||
"ste",
|
||||
"in"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "stellar",
|
||||
"points": 2,
|
||||
@@ -1473,21 +1187,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "steno",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
14,
|
||||
2
|
||||
],
|
||||
"tileSegments": [
|
||||
"ste",
|
||||
"no"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "taco",
|
||||
"points": 2,
|
||||
@@ -1503,21 +1202,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "tain",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
6,
|
||||
12
|
||||
],
|
||||
"tileSegments": [
|
||||
"ta",
|
||||
"in"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "taste",
|
||||
"points": 2,
|
||||
@@ -1533,51 +1217,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": 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",
|
||||
"points": 2,
|
||||
@@ -1592,131 +1231,10 @@ export const CURATED_PUZZLES = [
|
||||
],
|
||||
"isTargetQuartile": 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,
|
||||
"maxPossibleScore": 193
|
||||
"wordCount": 30,
|
||||
"maxPossibleScore": 160
|
||||
},
|
||||
{
|
||||
"id": "2026-10-25",
|
||||
@@ -3780,21 +3298,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "noma",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
13,
|
||||
10
|
||||
],
|
||||
"tileSegments": [
|
||||
"no",
|
||||
"ma"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "notion",
|
||||
"points": 2,
|
||||
@@ -3840,51 +3343,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": 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",
|
||||
"points": 2,
|
||||
@@ -3944,49 +3402,10 @@ export const CURATED_PUZZLES = [
|
||||
],
|
||||
"isTargetQuartile": 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,
|
||||
"maxPossibleScore": 151
|
||||
"wordCount": 30,
|
||||
"maxPossibleScore": 140
|
||||
},
|
||||
{
|
||||
"id": "2026-10-29",
|
||||
@@ -5082,21 +4501,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "stover",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
18,
|
||||
2
|
||||
],
|
||||
"tileSegments": [
|
||||
"sto",
|
||||
"ver"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "thunder",
|
||||
"points": 2,
|
||||
@@ -5201,36 +4605,10 @@ export const CURATED_PUZZLES = [
|
||||
],
|
||||
"isTargetQuartile": 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,
|
||||
"maxPossibleScore": 158
|
||||
"wordCount": 30,
|
||||
"maxPossibleScore": 154
|
||||
},
|
||||
{
|
||||
"id": "2026-10-31",
|
||||
@@ -5810,45 +5188,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": 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",
|
||||
"points": 1,
|
||||
@@ -5874,23 +5213,10 @@ export const CURATED_PUZZLES = [
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "us",
|
||||
"points": 1,
|
||||
"tileCount": 1,
|
||||
"tileIndices": [
|
||||
3
|
||||
],
|
||||
"tileSegments": [
|
||||
"us"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
}
|
||||
],
|
||||
"wordCount": 34,
|
||||
"maxPossibleScore": 152
|
||||
"wordCount": 30,
|
||||
"maxPossibleScore": 148
|
||||
},
|
||||
{
|
||||
"id": "2026-10-22",
|
||||
@@ -7516,21 +6842,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "vina",
|
||||
"points": 2,
|
||||
"tileCount": 2,
|
||||
"tileIndices": [
|
||||
5,
|
||||
2
|
||||
],
|
||||
"tileSegments": [
|
||||
"vi",
|
||||
"na"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "vital",
|
||||
"points": 2,
|
||||
@@ -7545,23 +6856,10 @@ export const CURATED_PUZZLES = [
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "con",
|
||||
"points": 1,
|
||||
"tileCount": 1,
|
||||
"tileIndices": [
|
||||
8
|
||||
],
|
||||
"tileSegments": [
|
||||
"con"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
}
|
||||
],
|
||||
"wordCount": 32,
|
||||
"maxPossibleScore": 161
|
||||
"wordCount": 30,
|
||||
"maxPossibleScore": 158
|
||||
},
|
||||
{
|
||||
"id": "2026-10-19",
|
||||
@@ -8980,19 +8278,6 @@ export const CURATED_PUZZLES = [
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "dis",
|
||||
"points": 1,
|
||||
"tileCount": 1,
|
||||
"tileIndices": [
|
||||
16
|
||||
],
|
||||
"tileSegments": [
|
||||
"dis"
|
||||
],
|
||||
"isTargetQuartile": false,
|
||||
"isQuartile": false
|
||||
},
|
||||
{
|
||||
"word": "nation",
|
||||
"points": 1,
|
||||
@@ -9018,36 +8303,10 @@ export const CURATED_PUZZLES = [
|
||||
],
|
||||
"isTargetQuartile": 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,
|
||||
"maxPossibleScore": 149
|
||||
"wordCount": 30,
|
||||
"maxPossibleScore": 146
|
||||
},
|
||||
{
|
||||
"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)
|
||||
* 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 foundMap = new Map();
|
||||
|
||||
@@ -37,8 +37,7 @@ export function solveBoard(quartets) {
|
||||
const quartilesList = quartets.map(q => q.join('').toLowerCase());
|
||||
const quartileSet = new Set(quartilesList);
|
||||
|
||||
const words = [];
|
||||
let totalBaseScore = 0;
|
||||
let words = [];
|
||||
|
||||
for (const [word, path] of foundMap.entries()) {
|
||||
const tileCount = path.length;
|
||||
@@ -46,8 +45,6 @@ export function solveBoard(quartets) {
|
||||
const isTargetQuartile = quartileSet.has(word);
|
||||
const isQuartile = tileCount === 4;
|
||||
|
||||
totalBaseScore += points;
|
||||
|
||||
words.push({
|
||||
word,
|
||||
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) => {
|
||||
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);
|
||||
|
||||
return {
|
||||
tiles,
|
||||
quartets,
|
||||
|
||||
48
src/main.js
48
src/main.js
@@ -71,8 +71,9 @@ async function initApp() {
|
||||
},
|
||||
onRevealAll: () => {
|
||||
gameState.revealAllWords();
|
||||
gameState.notifyToast('All words revealed!', 'info');
|
||||
wordsDrawer.open();
|
||||
const count = gameState.currentPuzzle?.words?.length || 0;
|
||||
gameState.notifyToast(`All ${count} word solutions revealed!`, 'info');
|
||||
wordsDrawer.open(false);
|
||||
}
|
||||
});
|
||||
document.body.appendChild(modals.element);
|
||||
@@ -81,7 +82,8 @@ async function initApp() {
|
||||
const wordsDrawer = createWordsDrawer({
|
||||
onRevealAll: () => {
|
||||
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);
|
||||
@@ -111,7 +113,10 @@ async function initApp() {
|
||||
// Create Score & Rank Bar
|
||||
const scoreRankBar = createScoreRankBar({
|
||||
onOpenWordsDrawer: () => {
|
||||
wordsDrawer.open();
|
||||
wordsDrawer.open(false);
|
||||
},
|
||||
onOpenReveal: () => {
|
||||
wordsDrawer.open(true);
|
||||
}
|
||||
});
|
||||
appContainer.appendChild(scoreRankBar.element);
|
||||
@@ -306,6 +311,12 @@ async function initApp() {
|
||||
modals.render(state);
|
||||
});
|
||||
|
||||
// Save state on unload/refresh
|
||||
window.addEventListener('beforeunload', () => {
|
||||
gameState.saveActivePuzzle();
|
||||
gameState.saveProgress();
|
||||
});
|
||||
|
||||
// Check URL hash for shared custom puzzle
|
||||
const hash = window.location.hash;
|
||||
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 todayPuzzle = CURATED_PUZZLES.find(p => p.date === todayStr) || CURATED_PUZZLES[0];
|
||||
gameState.loadPuzzle(todayPuzzle);
|
||||
|
||||
@@ -46,6 +46,7 @@ class GameStateManager {
|
||||
}
|
||||
|
||||
this.currentPuzzle = puzzle;
|
||||
this.saveActivePuzzle();
|
||||
this.activeTileIds = [];
|
||||
this.foundWords = new Map();
|
||||
this.score = 0;
|
||||
@@ -107,15 +108,38 @@ class GameStateManager {
|
||||
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
|
||||
saveProgress() {
|
||||
if (!this.currentPuzzle) return;
|
||||
this.saveActivePuzzle();
|
||||
const saveKey = `quartiles_save_${this.currentPuzzle.id}`;
|
||||
const payload = {
|
||||
puzzleId: this.currentPuzzle.id,
|
||||
foundWords: Array.from(this.foundWords.entries()),
|
||||
score: this.score,
|
||||
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 {
|
||||
localStorage.setItem(saveKey, JSON.stringify(payload));
|
||||
@@ -132,22 +156,45 @@ class GameStateManager {
|
||||
const saved = localStorage.getItem(saveKey);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (parsed && Array.isArray(parsed.foundWords)) {
|
||||
this.foundWords = new Map(parsed.foundWords);
|
||||
if (parsed) {
|
||||
if (Array.isArray(parsed.foundWords)) {
|
||||
const validWordsMap = new Map(this.currentPuzzle.words.map(w => [w.word.toLowerCase(), w]));
|
||||
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.quartilesBonusAwarded = !!parsed.bonus;
|
||||
}
|
||||
this.isRevealed = !!parsed.isRevealed;
|
||||
this.hintIndex = parsed.hintIndex || 0;
|
||||
|
||||
// Re-mark solved quartiles
|
||||
this.updateSolvedQuartileTiles();
|
||||
|
||||
// Restore tile order if saved
|
||||
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);
|
||||
if (isSequential && this.foundWords.size === 0) {
|
||||
return false;
|
||||
// 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
|
||||
if (Array.isArray(parsed.tileOrder) && parsed.tileOrder.length === 20) {
|
||||
const isSequential = parsed.tileOrder.every((id, idx) => id === idx);
|
||||
if (!isSequential || this.foundWords.size > 0) {
|
||||
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) {
|
||||
@@ -156,6 +203,8 @@ class GameStateManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.foundWords.size > 0;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to restore progress:', err);
|
||||
@@ -228,6 +277,7 @@ class GameStateManager {
|
||||
this.lockQuartiles = !this.lockQuartiles;
|
||||
localStorage.setItem('quartiles_lock_setting', this.lockQuartiles ? 'true' : 'false');
|
||||
this.reorganizeBoard();
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
this.notifyToast(
|
||||
this.lockQuartiles ? 'Quartiles locked at top' : 'Quartiles unlocked for free shuffle',
|
||||
@@ -250,6 +300,7 @@ class GameStateManager {
|
||||
}
|
||||
|
||||
this.activeTileIds.push(tileId);
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
|
||||
@@ -257,6 +308,7 @@ class GameStateManager {
|
||||
removeActiveTile(index) {
|
||||
if (index >= 0 && index < this.activeTileIds.length) {
|
||||
this.activeTileIds.splice(index, 1);
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
@@ -272,6 +324,7 @@ class GameStateManager {
|
||||
) {
|
||||
const [item] = this.activeTileIds.splice(fromIndex, 1);
|
||||
this.activeTileIds.splice(toIndex, 0, item);
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
@@ -280,6 +333,7 @@ class GameStateManager {
|
||||
backspace() {
|
||||
if (this.activeTileIds.length > 0) {
|
||||
this.activeTileIds.pop();
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
@@ -288,6 +342,7 @@ class GameStateManager {
|
||||
clearActive() {
|
||||
if (this.activeTileIds.length > 0) {
|
||||
this.activeTileIds = [];
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
@@ -470,6 +525,7 @@ class GameStateManager {
|
||||
// Reveal all words (when expert reached or explicitly requested)
|
||||
revealAllWords() {
|
||||
this.isRevealed = true;
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -517,16 +517,199 @@
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-danger);
|
||||
padding: 6px 12px;
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
transition: all var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-reveal-all:hover {
|
||||
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
|
||||
========================================================================== */
|
||||
|
||||
Reference in New Issue
Block a user