more words
This commit is contained in:
@@ -53,7 +53,9 @@ export function solveBoard(quartets, maxWords = 30) {
|
||||
}
|
||||
|
||||
if (prefix.length >= 2 && cleanWordSet.has(prefix)) {
|
||||
if (!foundMap.has(prefix)) {
|
||||
const existing = foundMap.get(prefix);
|
||||
// Keep first path found, but a single tile that is a word always wins
|
||||
if (!existing || (usedIndices.length === 1 && existing.length > 1)) {
|
||||
foundMap.set(prefix, [...usedIndices]);
|
||||
}
|
||||
}
|
||||
@@ -115,9 +117,12 @@ export function solveBoard(quartets, maxWords = 30) {
|
||||
return a.word.localeCompare(b.word);
|
||||
});
|
||||
|
||||
// Cap words at maxWords (default: 30)
|
||||
// Cap words 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) {
|
||||
words = words.slice(0, 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))];
|
||||
}
|
||||
|
||||
// Final display sorting: Target quartiles first, then points desc, then alphabetical
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sound } from '../engine/audio.js';
|
||||
|
||||
export function createTileGrid({ onTileSelected, onTogglePin }) {
|
||||
export function createTileGrid({ onTileSelected }) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'board-container';
|
||||
|
||||
@@ -13,27 +13,17 @@ export function createTileGrid({ onTileSelected, onTogglePin }) {
|
||||
.map(tile => {
|
||||
const isSelected = activeSet.has(tile.id);
|
||||
const isLocked = tile.isLocked;
|
||||
const isPinned = !!tile.isPinned;
|
||||
|
||||
return `
|
||||
<div
|
||||
class="board-tile ${isSelected ? 'selected' : ''} ${isLocked ? 'locked' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
data-id="${tile.id}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Tile ${tile.text}${isPinned ? ', pinned' : ''}"
|
||||
>
|
||||
<span class="tile-letter-text">${tile.text}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="tile-pin-btn ${isPinned ? 'is-pinned' : ''}"
|
||||
data-pin-id="${tile.id}"
|
||||
aria-label="${isPinned ? 'Unpin tile' : 'Pin tile in place'}"
|
||||
title="${isPinned ? 'Unpin tile (Click or right-click to pin/unpin)' : 'Pin tile to current spot (keeps it from moving on shuffle)'}"
|
||||
class="board-tile ${isSelected ? 'selected' : ''} ${isLocked ? 'locked' : ''}"
|
||||
data-id="${tile.id}"
|
||||
aria-label="Tile ${tile.text}"
|
||||
aria-pressed="${isSelected}"
|
||||
>
|
||||
<span class="pin-icon">${isPinned ? '📌' : '📍'}</span>
|
||||
${tile.text}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
@@ -41,48 +31,12 @@ export function createTileGrid({ onTileSelected, onTogglePin }) {
|
||||
`;
|
||||
|
||||
const tiles = container.querySelectorAll('.board-tile');
|
||||
tiles.forEach(tileEl => {
|
||||
const id = parseInt(tileEl.getAttribute('data-id'), 10);
|
||||
|
||||
// Main tile click to add/remove from tray
|
||||
tileEl.addEventListener('click', e => {
|
||||
// If clicking pin button, don't trigger tile selection
|
||||
if (e.target.closest('.tile-pin-btn')) return;
|
||||
tiles.forEach(tileBtn => {
|
||||
const id = parseInt(tileBtn.getAttribute('data-id'), 10);
|
||||
tileBtn.addEventListener('click', () => {
|
||||
sound.playTileClick();
|
||||
onTileSelected(id);
|
||||
});
|
||||
|
||||
// Keyboard accessibility
|
||||
tileEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
sound.playTileClick();
|
||||
onTileSelected(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Right-click to toggle pin
|
||||
tileEl.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
if (onTogglePin) {
|
||||
sound.playTileClick();
|
||||
onTogglePin(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Pin button click listener
|
||||
const pinBtns = container.querySelectorAll('.tile-pin-btn');
|
||||
pinBtns.forEach(btn => {
|
||||
const pinId = parseInt(btn.getAttribute('data-pin-id'), 10);
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (onTogglePin) {
|
||||
sound.playTileClick();
|
||||
onTogglePin(pinId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,9 @@ export function solveBoard(quartets, maxWords = 30) {
|
||||
}
|
||||
|
||||
if (prefix.length >= 2 && DICTIONARY_SET.has(prefix)) {
|
||||
if (!foundMap.has(prefix)) {
|
||||
const existing = foundMap.get(prefix);
|
||||
// Keep first path found, but a single tile that is a word always wins
|
||||
if (!existing || (usedIndices.length === 1 && existing.length > 1)) {
|
||||
foundMap.set(prefix, [...usedIndices]);
|
||||
}
|
||||
}
|
||||
@@ -75,9 +77,12 @@ export function solveBoard(quartets, maxWords = 30) {
|
||||
return a.word.localeCompare(b.word);
|
||||
});
|
||||
|
||||
// Cap at maxWords (default: 30)
|
||||
// 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) {
|
||||
words = words.slice(0, 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))];
|
||||
}
|
||||
|
||||
// Final display sort: Quartiles first, then by points descending, then alphabetically
|
||||
|
||||
14
src/main.js
14
src/main.js
@@ -145,9 +145,6 @@ async function initApp() {
|
||||
const tileGrid = createTileGrid({
|
||||
onTileSelected: tileId => {
|
||||
gameState.onTileClicked(tileId);
|
||||
},
|
||||
onTogglePin: tileId => {
|
||||
gameState.togglePinTile(tileId);
|
||||
}
|
||||
});
|
||||
appContainer.appendChild(tileGrid.element);
|
||||
@@ -348,6 +345,17 @@ async function initApp() {
|
||||
if (rawCustom) {
|
||||
const parsedCustom = JSON.parse(rawCustom);
|
||||
if (parsedCustom && parsedCustom.id) {
|
||||
try {
|
||||
const { solveBoard } = await import('./engine/solver.js');
|
||||
if (Array.isArray(parsedCustom.quartets) && parsedCustom.quartets.length === 5) {
|
||||
const solved = solveBoard(parsedCustom.quartets, 30);
|
||||
parsedCustom.words = solved.words;
|
||||
parsedCustom.wordCount = solved.wordCount;
|
||||
parsedCustom.maxPossibleScore = solved.maxPossibleScore;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not re-solve saved puzzle, using stored word list:', e);
|
||||
}
|
||||
gameState.loadPuzzle(parsedCustom);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import { getRankInfo, QUARTILE_COMPLETION_BONUS } from '../engine/constants.js';
|
||||
class GameStateManager {
|
||||
constructor() {
|
||||
this.currentPuzzle = null;
|
||||
this.boardTiles = []; // Array of { id, text, originalText, isQuartileSolved, isLocked, isPinned }
|
||||
this.boardTiles = []; // Array of { id, text, originalText, isQuartileSolved, isLocked }
|
||||
this.activeTileIds = []; // Array of tile IDs in construction tray
|
||||
this.userPinnedTileIds = new Set(); // Set of tile IDs manually pinned by user
|
||||
this.foundWords = new Map(); // word -> wordData
|
||||
this.score = 0;
|
||||
this.quartilesBonusAwarded = false;
|
||||
@@ -49,7 +48,6 @@ class GameStateManager {
|
||||
this.currentPuzzle = puzzle;
|
||||
this.saveActivePuzzle();
|
||||
this.activeTileIds = [];
|
||||
this.userPinnedTileIds = new Set();
|
||||
this.foundWords = new Map();
|
||||
this.score = 0;
|
||||
this.quartilesBonusAwarded = false;
|
||||
@@ -62,8 +60,7 @@ class GameStateManager {
|
||||
text: text.toUpperCase(),
|
||||
originalText: text,
|
||||
isQuartileSolved: false,
|
||||
isLocked: false,
|
||||
isPinned: false
|
||||
isLocked: false
|
||||
}));
|
||||
|
||||
// Restore saved state from LocalStorage if available
|
||||
@@ -139,7 +136,6 @@ class GameStateManager {
|
||||
score: this.score,
|
||||
bonus: this.quartilesBonusAwarded,
|
||||
tileOrder: this.boardTiles.map(t => t.id),
|
||||
pinnedTileIds: Array.from(this.userPinnedTileIds),
|
||||
lockQuartiles: this.lockQuartiles,
|
||||
activeTileIds: this.activeTileIds,
|
||||
isRevealed: this.isRevealed,
|
||||
@@ -196,13 +192,6 @@ class GameStateManager {
|
||||
this.lockQuartiles = parsed.lockQuartiles;
|
||||
}
|
||||
|
||||
// Restore pinned tiles
|
||||
if (Array.isArray(parsed.pinnedTileIds)) {
|
||||
this.userPinnedTileIds = new Set(parsed.pinnedTileIds.filter(id => id >= 0 && id < 20));
|
||||
} else {
|
||||
this.userPinnedTileIds = new Set();
|
||||
}
|
||||
|
||||
// Restore active tray tiles if any
|
||||
if (Array.isArray(parsed.activeTileIds)) {
|
||||
this.activeTileIds = parsed.activeTileIds.filter(id => id >= 0 && id < 20);
|
||||
@@ -211,18 +200,20 @@ class GameStateManager {
|
||||
// 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 || this.userPinnedTileIds.size > 0) {
|
||||
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) {
|
||||
this.boardTiles = reordered;
|
||||
this.updateSolvedQuartileTiles();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-mark solved quartiles and user-pinned tiles
|
||||
// Re-mark solved quartiles
|
||||
this.updateSolvedQuartileTiles();
|
||||
return this.foundWords.size > 0 || this.userPinnedTileIds.size > 0;
|
||||
return this.foundWords.size > 0;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -231,7 +222,7 @@ class GameStateManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if a tile belongs to an already found Quartile or is user-pinned
|
||||
// Check if a tile belongs to an already found Quartile
|
||||
updateSolvedQuartileTiles() {
|
||||
const solvedQuartileWords = new Set();
|
||||
for (const [w, data] of this.foundWords.entries()) {
|
||||
@@ -240,11 +231,9 @@ class GameStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all tiles with their pin state
|
||||
this.boardTiles.forEach(tile => {
|
||||
tile.isQuartileSolved = false;
|
||||
tile.isPinned = this.userPinnedTileIds.has(tile.id);
|
||||
tile.isLocked = tile.isPinned;
|
||||
tile.isLocked = false;
|
||||
});
|
||||
|
||||
for (let qIdx = 0; qIdx < this.currentPuzzle.quartets.length; qIdx++) {
|
||||
@@ -257,7 +246,7 @@ class GameStateManager {
|
||||
const tile = this.boardTiles.find(t => t.id === i);
|
||||
if (tile) {
|
||||
tile.isQuartileSolved = true;
|
||||
if (this.lockQuartiles || tile.isPinned) {
|
||||
if (this.lockQuartiles) {
|
||||
tile.isLocked = true;
|
||||
}
|
||||
}
|
||||
@@ -266,25 +255,6 @@ class GameStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle user pin on an individual tile
|
||||
togglePinTile(tileId) {
|
||||
if (this.userPinnedTileIds.has(tileId)) {
|
||||
this.userPinnedTileIds.delete(tileId);
|
||||
} else {
|
||||
this.userPinnedTileIds.add(tileId);
|
||||
}
|
||||
this.updateSolvedQuartileTiles();
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
const tile = this.boardTiles.find(t => t.id === tileId);
|
||||
const isPinned = this.userPinnedTileIds.has(tileId);
|
||||
this.notifyToast(
|
||||
isPinned ? `📌 Pinned tile "${tile?.text || ''}" in place` : `🔓 Unpinned tile "${tile?.text || ''}"`,
|
||||
'info',
|
||||
1600
|
||||
);
|
||||
}
|
||||
|
||||
// Reorganizes the board: locked solved quartiles go to top rows
|
||||
reorganizeBoard() {
|
||||
this.updateSolvedQuartileTiles();
|
||||
@@ -392,7 +362,7 @@ class GameStateManager {
|
||||
const unpinnedTiles = [];
|
||||
|
||||
this.boardTiles.forEach((tile, idx) => {
|
||||
const isFixed = tile.isPinned || (this.lockQuartiles && tile.isLocked);
|
||||
const isFixed = this.lockQuartiles && tile.isLocked;
|
||||
if (!isFixed) {
|
||||
unpinnedPositions.push(idx);
|
||||
unpinnedTiles.push(tile);
|
||||
@@ -421,7 +391,7 @@ class GameStateManager {
|
||||
const unpinnedTiles = [];
|
||||
|
||||
this.boardTiles.forEach((tile, idx) => {
|
||||
const isFixed = tile.isPinned || (this.lockQuartiles && tile.isLocked);
|
||||
const isFixed = this.lockQuartiles && tile.isLocked;
|
||||
if (!isFixed) {
|
||||
unpinnedPositions.push(idx);
|
||||
unpinnedTiles.push(tile);
|
||||
|
||||
@@ -329,55 +329,6 @@
|
||||
border-color: var(--accent-quartile);
|
||||
}
|
||||
|
||||
.board-tile.locked::before {
|
||||
content: '🔒';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 6px;
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* User-pinned tile state */
|
||||
.board-tile.pinned {
|
||||
border-color: #f59e0b !important;
|
||||
box-shadow: 0 0 0 1.5px rgba(245, 158, 11, 0.45), var(--tile-shadow) !important;
|
||||
}
|
||||
|
||||
.tile-pin-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
opacity: 0.22;
|
||||
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.board-tile:hover .tile-pin-btn,
|
||||
.tile-pin-btn.is-pinned {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.tile-pin-btn .pin-icon {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tile-pin-btn.is-pinned {
|
||||
transform: scale(1.12);
|
||||
filter: drop-shadow(0 1px 2px rgba(245, 158, 11, 0.4));
|
||||
}
|
||||
|
||||
/* Board tile shuffle animation */
|
||||
.board-tile.flipping {
|
||||
animation: tileFlip 0.4s var(--ease-smooth);
|
||||
|
||||
Reference in New Issue
Block a user