From 076e70992d78280ec63f098d37a546e373ea0514 Mon Sep 17 00:00:00 2001 From: Tommy Parnell Date: Sat, 24 Oct 2026 13:26:24 -0400 Subject: [PATCH] more words --- scripts/solver.mjs | 11 +- src/components/TileGrid.js | 68 +-- src/data/puzzles.js | 864 +++++++++++++++++-------------------- src/engine/solver.js | 11 +- src/main.js | 14 +- src/state/game-state.js | 54 +-- src/styles/board.css | 49 --- 7 files changed, 446 insertions(+), 625 deletions(-) diff --git a/scripts/solver.mjs b/scripts/solver.mjs index a55d997..4b32fe0 100644 --- a/scripts/solver.mjs +++ b/scripts/solver.mjs @@ -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 diff --git a/src/components/TileGrid.js b/src/components/TileGrid.js index 702f400..fc32abd 100644 --- a/src/components/TileGrid.js +++ b/src/components/TileGrid.js @@ -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 ` -
- ${tile.text} - -
+ ${tile.text} + `; }) .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); - } - }); }); } diff --git a/src/data/puzzles.js b/src/data/puzzles.js index f7e0ea7..0bc523a 100644 --- a/src/data/puzzles.js +++ b/src/data/puzzles.js @@ -404,21 +404,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "graven", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 10, - 13 - ], - "tileSegments": [ - "gra", - "ven" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "into", "points": 2, @@ -434,21 +419,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "marble", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 1, - 3 - ], - "tileSegments": [ - "mar", - "ble" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "photo", "points": 2, @@ -494,36 +464,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "ration", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 18, - 19 - ], - "tileSegments": [ - "ra", - "tion" - ], - "isTargetQuartile": false, - "isQuartile": false - }, - { - "word": "raven", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 18, - 13 - ], - "tileSegments": [ - "ra", - "ven" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "read", "points": 2, @@ -539,36 +479,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "rein", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 0, - 16 - ], - "tileSegments": [ - "re", - "in" - ], - "isTargetQuartile": false, - "isQuartile": false - }, - { - "word": "spire", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 17, - 0 - ], - "tileSegments": [ - "spi", - "re" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "toad", "points": 2, @@ -600,15 +510,91 @@ export const CURATED_PUZZLES = [ "isQuartile": false }, { - "word": "unto", - "points": 2, - "tileCount": 2, + "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": [ - 4, 9 ], "tileSegments": [ - "un", "to" ], "isTargetQuartile": false, @@ -616,7 +602,7 @@ export const CURATED_PUZZLES = [ } ], "wordCount": 30, - "maxPossibleScore": 160 + "maxPossibleScore": 153 }, { "id": "2026-10-24", @@ -1052,21 +1038,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "grater", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 4, - 13 - ], - "tileSegments": [ - "gra", - "ter" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "inter", "points": 2, @@ -1082,36 +1053,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "moat", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 9, - 8 - ], - "tileSegments": [ - "mo", - "at" - ], - "isTargetQuartile": false, - "isQuartile": false - }, - { - "word": "motel", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 9, - 16 - ], - "tileSegments": [ - "mo", - "tel" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "motion", "points": 2, @@ -1127,21 +1068,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "noes", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 2, - 17 - ], - "tileSegments": [ - "no", - "es" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "notion", "points": 2, @@ -1172,36 +1098,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "stellar", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 14, - 15 - ], - "tileSegments": [ - "ste", - "llar" - ], - "isTargetQuartile": false, - "isQuartile": false - }, - { - "word": "taco", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 6, - 18 - ], - "tileSegments": [ - "ta", - "co" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "taste", "points": 2, @@ -1218,23 +1114,99 @@ export const CURATED_PUZZLES = [ "isQuartile": false }, { - "word": "vies", - "points": 2, - "tileCount": 2, + "word": "as", + "points": 1, + "tileCount": 1, "tileIndices": [ - 5, - 17 + 0 ], "tileSegments": [ - "vi", - "es" + "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": 30, - "maxPossibleScore": 160 + "maxPossibleScore": 153 }, { "id": "2026-10-25", @@ -4265,21 +4237,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "dime", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 1, - 17 - ], - "tileSegments": [ - "di", - "me" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "dine", "points": 2, @@ -4295,36 +4252,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "diver", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 1, - 2 - ], - "tileSegments": [ - "di", - "ver" - ], - "isTargetQuartile": false, - "isQuartile": false - }, - { - "word": "leer", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 18, - 9 - ], - "tileSegments": [ - "le", - "er" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "less", "points": 2, @@ -4340,21 +4267,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "lever", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 18, - 2 - ], - "tileSegments": [ - "le", - "ver" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "mess", "points": 2, @@ -4416,23 +4328,73 @@ export const CURATED_PUZZLES = [ "isQuartile": false }, { - "word": "wilder", - "points": 2, - "tileCount": 2, + "word": "cat", + "points": 1, + "tileCount": 1, "tileIndices": [ - 12, - 13 + 8 ], "tileSegments": [ - "wil", - "der" + "cat" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "cha", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 16 + ], + "tileSegments": [ + "cha" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "lar", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 11 + ], + "tileSegments": [ + "lar" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "me", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 17 + ], + "tileSegments": [ + "me" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "on", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 19 + ], + "tileSegments": [ + "on" ], "isTargetQuartile": false, "isQuartile": false } ], "wordCount": 30, - "maxPossibleScore": 164 + "maxPossibleScore": 159 }, { "id": "2026-10-31", @@ -4910,21 +4872,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "edit", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 15, - 10 - ], - "tileSegments": [ - "ed", - "it" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "enter", "points": 2, @@ -4940,21 +4887,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "mint", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 16, - 14 - ], - "tileSegments": [ - "mi", - "nt" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "nation", "points": 2, @@ -4986,83 +4918,99 @@ export const CURATED_PUZZLES = [ "isQuartile": false }, { - "word": "raced", - "points": 2, - "tileCount": 2, + "word": "cha", + "points": 1, + "tileCount": 1, "tileIndices": [ - 17, - 15 + 13 ], "tileSegments": [ - "rac", - "ed" + "cha" ], "isTargetQuartile": false, "isQuartile": false }, { - "word": "sued", - "points": 2, - "tileCount": 2, + "word": "ion", + "points": 1, + "tileCount": 1, "tileIndices": [ - 4, - 15 + 11 ], "tileSegments": [ - "su", - "ed" + "ion" ], "isTargetQuartile": false, "isQuartile": false }, { - "word": "suit", - "points": 2, - "tileCount": 2, + "word": "it", + "points": 1, + "tileCount": 1, "tileIndices": [ - 4, 10 ], "tileSegments": [ - "su", "it" ], "isTargetQuartile": false, "isQuartile": false }, { - "word": "super", - "points": 2, - "tileCount": 2, + "word": "mys", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 0 + ], + "tileSegments": [ + "mys" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "par", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 9 + ], + "tileSegments": [ + "par" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "per", + "points": 1, + "tileCount": 1, "tileIndices": [ - 4, 5 ], "tileSegments": [ - "su", "per" ], "isTargetQuartile": false, "isQuartile": false }, { - "word": "used", - "points": 2, - "tileCount": 2, + "word": "us", + "points": 1, + "tileCount": 1, "tileIndices": [ - 3, - 15 + 3 ], "tileSegments": [ - "us", - "ed" + "us" ], "isTargetQuartile": false, "isQuartile": false } ], "wordCount": 30, - "maxPossibleScore": 172 + "maxPossibleScore": 165 }, { "id": "2026-10-22", @@ -5424,21 +5372,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "hype", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 4, - 15 - ], - "tileSegments": [ - "hy", - "pe" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "micro", "points": 2, @@ -5469,36 +5402,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "miry", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 12, - 11 - ], - "tileSegments": [ - "mi", - "ry" - ], - "isTargetQuartile": false, - "isQuartile": false - }, - { - "word": "nomen", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 17, - 18 - ], - "tileSegments": [ - "nom", - "en" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "onto", "points": 2, @@ -5529,21 +5432,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "peen", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 15, - 18 - ], - "tileSegments": [ - "pe", - "en" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "peon", "points": 2, @@ -5574,21 +5462,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "pheon", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 16, - 19 - ], - "tileSegments": [ - "phe", - "on" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "potent", "points": 2, @@ -5604,21 +5477,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "potto", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 5, - 10 - ], - "tileSegments": [ - "pot", - "to" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "scope", "points": 2, @@ -5650,23 +5508,99 @@ export const CURATED_PUZZLES = [ "isQuartile": false }, { - "word": "tope", - "points": 2, - "tileCount": 2, + "word": "he", + "points": 1, + "tileCount": 1, "tileIndices": [ - 10, - 15 + 6 ], "tileSegments": [ - "to", - "pe" + "he" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "lab", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 8 + ], + "tileSegments": [ + "lab" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "on", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 19 + ], + "tileSegments": [ + "on" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "ora", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 9 + ], + "tileSegments": [ + "ora" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "pot", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 5 + ], + "tileSegments": [ + "pot" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "sis", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 7 + ], + "tileSegments": [ + "sis" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "to", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 10 + ], + "tileSegments": [ + "to" ], "isTargetQuartile": false, "isQuartile": false } ], "wordCount": 30, - "maxPossibleScore": 142 + "maxPossibleScore": 135 }, { "id": "2026-10-21", @@ -6753,21 +6687,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "mock", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 6, - 2 - ], - "tileSegments": [ - "mo", - "ck" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "motion", "points": 2, @@ -6783,21 +6702,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "motive", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 6, - 7 - ], - "tileSegments": [ - "mo", - "tive" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "nation", "points": 2, @@ -6813,21 +6717,6 @@ export const CURATED_PUZZLES = [ "isTargetQuartile": false, "isQuartile": false }, - { - "word": "native", - "points": 2, - "tileCount": 2, - "tileIndices": [ - 12, - 7 - ], - "tileSegments": [ - "na", - "tive" - ], - "isTargetQuartile": false, - "isQuartile": false - }, { "word": "pack", "points": 2, @@ -6872,10 +6761,49 @@ export const CURATED_PUZZLES = [ ], "isTargetQuartile": false, "isQuartile": false + }, + { + "word": "back", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 0 + ], + "tileSegments": [ + "back" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "con", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 8 + ], + "tileSegments": [ + "con" + ], + "isTargetQuartile": false, + "isQuartile": false + }, + { + "word": "tin", + "points": 1, + "tileCount": 1, + "tileIndices": [ + 9 + ], + "tileSegments": [ + "tin" + ], + "isTargetQuartile": false, + "isQuartile": false } ], "wordCount": 30, - "maxPossibleScore": 166 + "maxPossibleScore": 163 }, { "id": "2026-10-19", diff --git a/src/engine/solver.js b/src/engine/solver.js index e591a57..c8f4134 100644 --- a/src/engine/solver.js +++ b/src/engine/solver.js @@ -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 diff --git a/src/main.js b/src/main.js index a8ed3cf..6a46488 100644 --- a/src/main.js +++ b/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; } diff --git a/src/state/game-state.js b/src/state/game-state.js index 7273aa4..ac29424 100644 --- a/src/state/game-state.js +++ b/src/state/game-state.js @@ -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); diff --git a/src/styles/board.css b/src/styles/board.css index de366a6..861e4e2 100644 --- a/src/styles/board.css +++ b/src/styles/board.css @@ -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);