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);