more words
This commit is contained in:
@@ -6,7 +6,7 @@ export function createScoreRankBar({ onOpenWordsDrawer, onOpenReveal }) {
|
||||
const rankInfo = state.getRank();
|
||||
const quartilesSolved = state.getTargetQuartilesSolvedCount();
|
||||
const totalWords = state.currentPuzzle ? (state.currentPuzzle.wordCount || state.currentPuzzle.words?.length || 0) : 0;
|
||||
const foundWordsCount = state.foundWords.size;
|
||||
const foundWordsCount = state.getListedFoundCount();
|
||||
const isRevealed = !!state.isRevealed;
|
||||
|
||||
container.innerHTML = `
|
||||
|
||||
@@ -14,12 +14,14 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
|
||||
const foundWordsList = Array.from(state.foundWords.values());
|
||||
const allPuzzleWords = state.currentPuzzle.words || [];
|
||||
const bonusFoundList = foundWordsList.filter(w => w.isBonus);
|
||||
const listedFoundCount = foundWordsList.length - bonusFoundList.length;
|
||||
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;
|
||||
const sourceWords = (activeView === 'all' && isRevealed) ? [...allPuzzleWords, ...bonusFoundList] : foundWordsList;
|
||||
|
||||
// Filter words according to current tab
|
||||
let displayWords = [...sourceWords];
|
||||
@@ -40,7 +42,7 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
return a.word.localeCompare(b.word);
|
||||
});
|
||||
|
||||
const countingSource = (activeView === 'all' && isRevealed) ? allPuzzleWords : foundWordsList;
|
||||
const countingSource = sourceWords;
|
||||
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;
|
||||
@@ -65,8 +67,8 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 2px;">
|
||||
${
|
||||
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)`
|
||||
? `<strong>${listedFoundCount}</strong> of ${allPuzzleWords.length} found (${state.score} pts) • ${allPuzzleWords.length - listedFoundCount} revealed${bonusFoundList.length ? ` • +${bonusFoundList.length} bonus` : ''}`
|
||||
: `<strong>${listedFoundCount}</strong> of ${allPuzzleWords.length} words found${bonusFoundList.length ? ` +${bonusFoundList.length} bonus` : ''} (${state.score} pts)`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +149,9 @@ export function createWordsDrawer({ onRevealAll }) {
|
||||
${isQuartile ? '<span title="Target Quartile (8 pts)"> ⭐</span>' : ''}
|
||||
${
|
||||
isFound
|
||||
? '<span class="status-chip found">Found</span>'
|
||||
? (w.isBonus
|
||||
? '<span class="status-chip found" title="Valid word not on the main list">Bonus</span>'
|
||||
: '<span class="status-chip found">Found</span>')
|
||||
: '<span class="status-chip revealed">Revealed</span>'
|
||||
}
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
3435
src/data/puzzles.js
3435
src/data/puzzles.js
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { DICTIONARY_SET, PREFIX_SET } from '../data/lexicon.js';
|
||||
import { capWordList } from './word-cap.js';
|
||||
import { POINTS_BY_TILE_COUNT, QUARTILE_COMPLETION_BONUS, EXPERT_RANK_POINTS, RANK_TIERS, getRankInfo } from './constants.js';
|
||||
|
||||
export { POINTS_BY_TILE_COUNT, QUARTILE_COMPLETION_BONUS, EXPERT_RANK_POINTS, RANK_TIERS, getRankInfo };
|
||||
@@ -77,13 +78,9 @@ export function solveBoard(quartets, maxWords = 30) {
|
||||
return a.word.localeCompare(b.word);
|
||||
});
|
||||
|
||||
// Cap at maxWords (default: 30). Target quartiles and single-tile words
|
||||
// (tiles that are words on their own) always keep their spot.
|
||||
if (maxWords && words.length > maxWords) {
|
||||
const reserved = words.filter(w => w.isTargetQuartile || w.tileCount === 1);
|
||||
const rest = words.filter(w => !w.isTargetQuartile && w.tileCount !== 1);
|
||||
words = [...reserved, ...rest.slice(0, Math.max(0, maxWords - reserved.length))];
|
||||
}
|
||||
// Cap at maxWords (default: 30) while keeping a fair share of 2-tile words.
|
||||
// Valid words beyond the cap are still accepted in play as bonus words.
|
||||
words = capWordList(words, maxWords);
|
||||
|
||||
// Final display sort: Quartiles first, then by points descending, then alphabetically
|
||||
words.sort((a, b) => {
|
||||
|
||||
27
src/engine/word-cap.js
Normal file
27
src/engine/word-cap.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Caps a sorted candidate word list to `maxWords` entries.
|
||||
*
|
||||
* - Target quartiles and single-tile words always keep their spot.
|
||||
* - 2-tile words are guaranteed roughly half of the remaining slots (when
|
||||
* available) so long 3/4-tile combos can't crowd them out.
|
||||
* - Remaining slots are filled in the incoming sort order.
|
||||
*
|
||||
* Words cut by the cap are still accepted during play as bonus words.
|
||||
*/
|
||||
export function capWordList(words, maxWords = 30) {
|
||||
if (!maxWords || words.length <= maxWords) return words;
|
||||
|
||||
const reserved = words.filter(w => w.isTargetQuartile || w.tileCount === 1);
|
||||
const others = words.filter(w => !w.isTargetQuartile && w.tileCount !== 1);
|
||||
const slots = Math.max(0, maxWords - reserved.length);
|
||||
|
||||
const twoTile = others.filter(w => w.tileCount === 2);
|
||||
const quota = Math.min(twoTile.length, Math.ceil(slots / 2));
|
||||
const picked = new Set(twoTile.slice(0, quota));
|
||||
for (const w of others) {
|
||||
if (picked.size >= slots) break;
|
||||
picked.add(w);
|
||||
}
|
||||
|
||||
return [...reserved, ...others.filter(w => picked.has(w))];
|
||||
}
|
||||
@@ -209,7 +209,7 @@ async function initApp() {
|
||||
} else {
|
||||
// Standard word
|
||||
sound.playWordValid(result.points >= 4 ? 3 : result.points);
|
||||
gameState.notifyToast(`+${result.points} pts: ${result.word.toUpperCase()}`, 'success', 2200);
|
||||
gameState.notifyToast(`+${result.points} pts: ${result.word.toUpperCase()}${result.isBonus ? ' (bonus word)' : ''}`, 'success', 2200);
|
||||
}
|
||||
|
||||
// Check rank progression
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CURATED_PUZZLES } from '../data/puzzles.js';
|
||||
import { getRankInfo, QUARTILE_COMPLETION_BONUS } from '../engine/constants.js';
|
||||
import { getRankInfo, QUARTILE_COMPLETION_BONUS, POINTS_BY_TILE_COUNT } from '../engine/constants.js';
|
||||
import { DICTIONARY_SET } from '../data/lexicon.js';
|
||||
|
||||
class GameStateManager {
|
||||
constructor() {
|
||||
@@ -168,13 +169,21 @@ class GameStateManager {
|
||||
const filteredEntries = [];
|
||||
let recalculatedScore = 0;
|
||||
for (const [w, entry] of parsed.foundWords) {
|
||||
const matchedWord = validWordsMap.get(w.toLowerCase());
|
||||
const key = w.toLowerCase();
|
||||
const matchedWord = validWordsMap.get(key);
|
||||
if (matchedWord) {
|
||||
entry.points = matchedWord.points;
|
||||
entry.tileCount = matchedWord.tileCount;
|
||||
entry.tileSegments = matchedWord.tileSegments;
|
||||
filteredEntries.push([w.toLowerCase(), entry]);
|
||||
entry.isBonus = false;
|
||||
filteredEntries.push([key, entry]);
|
||||
recalculatedScore += matchedWord.points;
|
||||
} else if (entry && DICTIONARY_SET.has(key)) {
|
||||
// Valid word beyond the displayed 30-word list
|
||||
entry.isBonus = true;
|
||||
entry.points = POINTS_BY_TILE_COUNT[entry.tileCount] || 1;
|
||||
filteredEntries.push([key, entry]);
|
||||
recalculatedScore += entry.points;
|
||||
}
|
||||
}
|
||||
this.foundWords = new Map(filteredEntries);
|
||||
@@ -434,7 +443,13 @@ class GameStateManager {
|
||||
}
|
||||
|
||||
// Look up in current puzzle's valid words list
|
||||
const match = this.currentPuzzle.words.find(w => w.word.toLowerCase() === constructedWord);
|
||||
const listed = this.currentPuzzle.words.find(w => w.word.toLowerCase() === constructedWord);
|
||||
const tileCount = this.activeTileIds.length;
|
||||
|
||||
// Any real word built from the tiles counts, even if it was cut from the
|
||||
// displayed 30-word list (accepted as a bonus word)
|
||||
const isBonus = !listed && DICTIONARY_SET.has(constructedWord);
|
||||
const match = listed || (isBonus ? { points: POINTS_BY_TILE_COUNT[tileCount] || 1 } : null);
|
||||
|
||||
if (!match) {
|
||||
this.notifyToast(`"${constructedWord.toUpperCase()}" is not in the word list`, 'error');
|
||||
@@ -446,7 +461,6 @@ class GameStateManager {
|
||||
const tile = this.boardTiles.find(t => t.id === id);
|
||||
return tile ? tile.text : '';
|
||||
});
|
||||
const tileCount = this.activeTileIds.length;
|
||||
const isTargetQuartile = this.currentPuzzle.targetQuartiles.some(t => t.toLowerCase() === constructedWord);
|
||||
const isQuartile = tileCount === 4;
|
||||
|
||||
@@ -457,6 +471,7 @@ class GameStateManager {
|
||||
tileSegments,
|
||||
isTargetQuartile,
|
||||
isQuartile,
|
||||
isBonus,
|
||||
foundAt: Date.now()
|
||||
};
|
||||
|
||||
@@ -496,6 +511,7 @@ class GameStateManager {
|
||||
points: match.points,
|
||||
isQuartile,
|
||||
isTargetQuartile,
|
||||
isBonus,
|
||||
quartilesFound: quartilesCount,
|
||||
bonusUnlocked
|
||||
};
|
||||
@@ -511,11 +527,20 @@ class GameStateManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Number of found words that are on the puzzle's displayed list (excludes bonus words)
|
||||
getListedFoundCount() {
|
||||
let count = 0;
|
||||
for (const entry of this.foundWords.values()) {
|
||||
if (!entry.isBonus) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Get rank data
|
||||
getRank() {
|
||||
return getRankInfo(
|
||||
this.score,
|
||||
this.foundWords.size,
|
||||
this.getListedFoundCount(),
|
||||
this.currentPuzzle ? this.currentPuzzle.wordCount : 0
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user