many many fixes
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { solveBoard } from './solver.mjs';
|
||||
import { extraWords } from './extra-words.mjs';
|
||||
import { DICTIONARY_SET } from '../src/data/lexicon.js';
|
||||
|
||||
@@ -228,210 +227,9 @@ for (const item of targetWordsPool) {
|
||||
|
||||
console.log('Target words pool verified! Total words:', targetWordsPool.length);
|
||||
|
||||
const dailyPuzzlesDefs = [
|
||||
{
|
||||
id: '2026-10-23',
|
||||
date: '2026-10-23',
|
||||
title: 'Daily Puzzle — Oct 23, 2026',
|
||||
theme: 'Discovery & Wonder',
|
||||
difficulty: 'Normal',
|
||||
words: ['remarkable', 'unforgettable', 'photography', 'adventurous', 'inspiration']
|
||||
},
|
||||
{
|
||||
id: '2026-10-24',
|
||||
date: '2026-10-24',
|
||||
title: 'Daily Puzzle — Oct 24, 2026',
|
||||
theme: 'Cosmic Horizons',
|
||||
difficulty: 'Challenging',
|
||||
words: ['astronomy', 'gravitation', 'atmosphere', 'interstellar', 'telescopic']
|
||||
},
|
||||
{
|
||||
id: '2026-10-25',
|
||||
date: '2026-10-25',
|
||||
title: 'Daily Puzzle — Oct 25, 2026',
|
||||
theme: 'Mind & Memory',
|
||||
difficulty: 'Normal',
|
||||
words: ['imagination', 'understanding', 'psychology', 'intelligence', 'consciousness']
|
||||
},
|
||||
{
|
||||
id: '2026-10-26',
|
||||
date: '2026-10-26',
|
||||
title: 'Daily Puzzle — Oct 26, 2026',
|
||||
theme: 'Urban Pulse',
|
||||
difficulty: 'Challenging',
|
||||
words: ['architecture', 'metropolitan', 'construction', 'neighborhood', 'skyscraper']
|
||||
},
|
||||
{
|
||||
id: '2026-10-27',
|
||||
date: '2026-10-27',
|
||||
title: 'Daily Puzzle — Oct 27, 2026',
|
||||
theme: 'Harmony & Arts',
|
||||
difficulty: 'Normal',
|
||||
words: ['saxophone', 'masterpiece', 'calligraphy', 'exhibition', 'creativity']
|
||||
},
|
||||
{
|
||||
id: '2026-10-28',
|
||||
date: '2026-10-28',
|
||||
title: 'Daily Puzzle — Oct 28, 2026',
|
||||
theme: 'Digital Future',
|
||||
difficulty: 'Normal',
|
||||
words: ['technology', 'electronic', 'automation', 'computer', 'cybersecurity']
|
||||
},
|
||||
{
|
||||
id: '2026-10-29',
|
||||
date: '2026-10-29',
|
||||
title: 'Daily Puzzle — Oct 29, 2026',
|
||||
theme: 'Celebrations',
|
||||
difficulty: 'Easy',
|
||||
words: ['celebration', 'fellowship', 'generosity', 'opportunity', 'marshmallow']
|
||||
},
|
||||
{
|
||||
id: '2026-10-30',
|
||||
date: '2026-10-30',
|
||||
title: 'Daily Puzzle — Oct 30, 2026',
|
||||
theme: 'Wild Kingdom',
|
||||
difficulty: 'Normal',
|
||||
words: ['biodiversity', 'environment', 'caterpillar', 'wilderness', 'chameleon']
|
||||
},
|
||||
{
|
||||
id: '2026-10-31',
|
||||
date: '2026-10-31',
|
||||
title: 'Daily Puzzle — Oct 31, 2026',
|
||||
theme: 'Mysteries & Legends',
|
||||
difficulty: 'Challenging',
|
||||
words: ['mysterious', 'supernatural', 'legendary', 'enchanted', 'miraculous']
|
||||
},
|
||||
{
|
||||
id: '2026-10-22',
|
||||
date: '2026-10-22',
|
||||
title: 'Daily Puzzle — Oct 22, 2026',
|
||||
theme: 'Scientific Insight',
|
||||
difficulty: 'Normal',
|
||||
words: ['equivalent', 'hypothesis', 'laboratory', 'microscope', 'phenomenon']
|
||||
},
|
||||
{
|
||||
id: '2026-10-21',
|
||||
date: '2026-10-21',
|
||||
title: 'Daily Puzzle — Oct 21, 2026',
|
||||
theme: 'Wordsmith Almanac',
|
||||
difficulty: 'Normal',
|
||||
words: ['dictionary', 'manuscript', 'encyclopedia', 'literature', 'vocabulary']
|
||||
},
|
||||
{
|
||||
id: '2026-10-20',
|
||||
date: '2026-10-20',
|
||||
title: 'Daily Puzzle — Oct 20, 2026',
|
||||
theme: 'Globe Trekker',
|
||||
difficulty: 'Normal',
|
||||
words: ['backpacker', 'locomotive', 'continental', 'navigation', 'voyagers']
|
||||
},
|
||||
{
|
||||
id: '2026-10-19',
|
||||
date: '2026-10-19',
|
||||
title: 'Daily Puzzle — Oct 19, 2026',
|
||||
theme: 'Atmospheric Skies',
|
||||
difficulty: 'Challenging',
|
||||
words: ['meteorology', 'temperatures', 'precipitation', 'stratosphere', 'cloudburst']
|
||||
},
|
||||
{
|
||||
id: '2026-10-18',
|
||||
date: '2026-10-18',
|
||||
title: 'Daily Puzzle — Oct 18, 2026',
|
||||
theme: 'Green Oasis',
|
||||
difficulty: 'Normal',
|
||||
words: ['photosynthesis', 'marigold', 'evergreen', 'germination', 'sunflowers']
|
||||
},
|
||||
{
|
||||
id: '2026-10-17',
|
||||
date: '2026-10-17',
|
||||
title: 'Daily Puzzle — Oct 17, 2026',
|
||||
theme: 'Grit & Glory',
|
||||
difficulty: 'Normal',
|
||||
words: ['determination', 'courageous', 'championship', 'achievement', 'perseverance']
|
||||
},
|
||||
{
|
||||
id: '2026-10-16',
|
||||
date: '2026-10-16',
|
||||
title: 'Daily Puzzle — Oct 16, 2026',
|
||||
theme: 'Oceanic Depths',
|
||||
difficulty: 'Challenging',
|
||||
words: ['oceanography', 'submarine', 'underwater', 'jellyfish', 'exploration']
|
||||
},
|
||||
{
|
||||
id: '2026-10-15',
|
||||
date: '2026-10-15',
|
||||
title: 'Daily Puzzle — Oct 15, 2026',
|
||||
theme: 'Energy & Waves',
|
||||
difficulty: 'Expert',
|
||||
words: ['electricity', 'spectrum', 'frequency', 'radioactive', 'magnetism']
|
||||
}
|
||||
];
|
||||
|
||||
// Map target words for quick lookup
|
||||
const wordMap = new Map();
|
||||
for (const item of targetWordsPool) {
|
||||
wordMap.set(item.word, item);
|
||||
}
|
||||
|
||||
const solvedPuzzles = [];
|
||||
|
||||
for (const def of dailyPuzzlesDefs) {
|
||||
const quartets = def.words.map(w => {
|
||||
const found = wordMap.get(w);
|
||||
if (!found) throw new Error('Missing word in pool: ' + w);
|
||||
return found.tiles;
|
||||
});
|
||||
|
||||
const allTiles = quartets.flat();
|
||||
// STRICT ASSERTIONS
|
||||
if (new Set(allTiles).size !== 20) {
|
||||
const counts = {};
|
||||
allTiles.forEach(t => counts[t] = (counts[t] || 0) + 1);
|
||||
const dupes = Object.entries(counts).filter(([_, c]) => c > 1);
|
||||
throw new Error(`Puzzle ${def.id} has duplicate tiles: ${JSON.stringify(dupes)}`);
|
||||
}
|
||||
if (allTiles.some(t => t.length < 2)) {
|
||||
throw new Error(`Puzzle ${def.id} has a tile < 2 chars: ${JSON.stringify(allTiles)}`);
|
||||
}
|
||||
|
||||
const solved = solveBoard(quartets, 30);
|
||||
|
||||
if (solved.wordCount > 30) {
|
||||
throw new Error(`Puzzle ${def.id} exceeds 30 words: ${solved.wordCount}`);
|
||||
}
|
||||
|
||||
// Add clues to target quartiles
|
||||
const quartileDetails = def.words.map(w => {
|
||||
const item = wordMap.get(w);
|
||||
return {
|
||||
word: item.word,
|
||||
tiles: item.tiles,
|
||||
clue: item.clue
|
||||
};
|
||||
});
|
||||
|
||||
solvedPuzzles.push({
|
||||
id: def.id,
|
||||
date: def.date,
|
||||
title: def.title,
|
||||
theme: def.theme,
|
||||
difficulty: def.difficulty,
|
||||
quartets,
|
||||
tiles: solved.tiles,
|
||||
targetQuartiles: solved.targetQuartiles,
|
||||
quartileDetails,
|
||||
words: solved.words,
|
||||
wordCount: solved.wordCount,
|
||||
maxPossibleScore: solved.maxPossibleScore
|
||||
});
|
||||
|
||||
console.log(`Generated: ${def.id} [${def.theme}] -> ${solved.wordCount} words, max score: ${solved.maxPossibleScore}`);
|
||||
}
|
||||
|
||||
// Write solved puzzles to src/data/puzzles.js
|
||||
const puzzlesFileContent = `// Pre-computed solved daily and themed puzzles for Quartiles
|
||||
export const CURATED_PUZZLES = ${JSON.stringify(solvedPuzzles, null, 2)};
|
||||
|
||||
// Puzzles are generated from this pool at runtime (src/engine/generator.js):
|
||||
// daily puzzles are seeded by date, unlimited puzzles are random.
|
||||
const puzzlesFileContent = `// Target word pool for generated Quartiles puzzles (built by scripts/build-all-data.mjs)
|
||||
export const TARGET_WORDS_POOL = ${JSON.stringify(targetWordsPool, null, 2)};
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import { buildWordSet, loadComboWords } from './lexicon-sources.mjs';
|
||||
import { buildWordSet, loadComboWords, getCommonnessScore } from './lexicon-sources.mjs';
|
||||
|
||||
// Build a clean, high-frequency recognized vocabulary for client-side unlimited/custom puzzles
|
||||
// (includes regional spellings and the proper-word allowlist)
|
||||
@@ -13,12 +13,26 @@ console.log('Accept-only combo words:', comboWords.length);
|
||||
|
||||
// Pack into space-separated string or array
|
||||
const sortedWords = [...vocabulary].sort();
|
||||
// One digit (0-4) per word, parallel to RAW_LEXICON. Compresses to almost nothing.
|
||||
const commonnessDigits = sortedWords.map(w => getCommonnessScore(w)).join('');
|
||||
const lexiconContent = `// Client-side recognized English words for solver & custom puzzles
|
||||
// Loaded as a Set for O(1) instantaneous lookup
|
||||
const RAW_LEXICON = ${JSON.stringify(sortedWords)};
|
||||
|
||||
export const DICTIONARY_SET = new Set(RAW_LEXICON);
|
||||
|
||||
// Commonness tier per word (4 = most common, 0 = rare), parallel to RAW_LEXICON
|
||||
const RAW_COMMONNESS = ${JSON.stringify(commonnessDigits)};
|
||||
const COMMONNESS_MAP = new Map();
|
||||
for (let i = 0; i < RAW_LEXICON.length; i++) {
|
||||
const c = RAW_COMMONNESS.charCodeAt(i) - 48;
|
||||
if (c > 0) COMMONNESS_MAP.set(RAW_LEXICON[i], c);
|
||||
}
|
||||
|
||||
export function getCommonnessScore(word) {
|
||||
return COMMONNESS_MAP.get(word) || 0;
|
||||
}
|
||||
|
||||
// Accept-only words buildable from pool tiles (generated by
|
||||
// scripts/find-missing-combo-words.mjs). Space-packed to keep the bundle small.
|
||||
const RAW_COMBO_WORDS = ${JSON.stringify(comboWords.join(' '))};
|
||||
|
||||
33
scripts/bump-version.mjs
Normal file
33
scripts/bump-version.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
// Bumps the app version in src/version.json.
|
||||
//
|
||||
// node scripts/bump-version.mjs -> patch (1.0.0 -> 1.0.1)
|
||||
// node scripts/bump-version.mjs minor -> 1.0.1 -> 1.1.0
|
||||
// node scripts/bump-version.mjs major -> 1.1.0 -> 2.0.0
|
||||
//
|
||||
// Run automatically by the pre-commit hook (.githooks/pre-commit).
|
||||
import fs from 'node:fs';
|
||||
|
||||
const VERSION_FILE = new URL('../src/version.json', import.meta.url);
|
||||
const level = (process.argv[2] || 'patch').toLowerCase();
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(VERSION_FILE, 'utf8'));
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(data.version || '');
|
||||
if (!match) {
|
||||
console.error(`bump-version: invalid version "${data.version}" in src/version.json (expected X.Y.Z)`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let [major, minor, patch] = match.slice(1).map(Number);
|
||||
if (level === 'major') { major++; minor = 0; patch = 0; }
|
||||
else if (level === 'minor') { minor++; patch = 0; }
|
||||
else if (level === 'patch') { patch++; }
|
||||
else {
|
||||
console.error(`bump-version: unknown level "${level}" (use patch, minor or major)`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const previous = data.version;
|
||||
data.version = `${major}.${minor}.${patch}`;
|
||||
fs.writeFileSync(VERSION_FILE, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||
console.log(`bump-version: ${previous} -> ${data.version}`);
|
||||
@@ -8,16 +8,15 @@
|
||||
import fs from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { buildWordSet } from './lexicon-sources.mjs';
|
||||
import { TARGET_WORDS_POOL, CURATED_PUZZLES } from '../src/data/puzzles.js';
|
||||
import { TARGET_WORDS_POOL } from '../src/data/puzzles.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const REFERENCE = require('an-array-of-english-words');
|
||||
const OUT_FILE = 'scripts/combo-words.txt';
|
||||
|
||||
// Every tile that can appear on a board (random + daily puzzles)
|
||||
// Every tile that can appear on a board (all puzzles are generated from the pool)
|
||||
const chunks = new Set();
|
||||
TARGET_WORDS_POOL.forEach(t => t.tiles.forEach(c => chunks.add(c.toLowerCase())));
|
||||
CURATED_PUZZLES.forEach(p => p.quartets.flat().forEach(c => chunks.add(c.toLowerCase())));
|
||||
const maxChunkLen = Math.max(...[...chunks].map(c => c.length));
|
||||
|
||||
// Base dictionary WITHOUT the previously generated combo list, so re-runs are stable
|
||||
|
||||
15
scripts/install-hooks.mjs
Normal file
15
scripts/install-hooks.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Points git at the repo's .githooks directory. Runs on `npm install`
|
||||
// (the "prepare" script). Never fails, so installs work without git
|
||||
// (e.g. CI / Nixpacks builds).
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
|
||||
try {
|
||||
if (!fs.existsSync('.git')) process.exit(0);
|
||||
execSync('git config core.hooksPath .githooks', { stdio: 'ignore' });
|
||||
// Make sure the hook is executable on macOS/Linux
|
||||
try { fs.chmodSync('.githooks/pre-commit', 0o755); } catch {}
|
||||
console.log('Git hooks installed (.githooks)');
|
||||
} catch {
|
||||
// git not available: skip silently
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { buildWordSet, getCommonnessScore } from './lexicon-sources.mjs';
|
||||
import { capWordList } from '../src/engine/word-cap.js';
|
||||
|
||||
// Clean dictionary incl. regional spellings + proper-word allowlist
|
||||
// (same source as src/data/lexicon.js)
|
||||
const cleanWordSet = buildWordSet();
|
||||
|
||||
// Build prefix set for super fast solver pruning (up to 6 letters)
|
||||
const prefixSet = new Set();
|
||||
for (const word of cleanWordSet) {
|
||||
for (let len = 1; len <= Math.min(word.length, 6); len++) {
|
||||
prefixSet.add(word.slice(0, len));
|
||||
}
|
||||
}
|
||||
|
||||
// Function to solve a 20-tile board and return all valid words and paths
|
||||
export function solveBoard(quartets, maxWords = 30) {
|
||||
const tiles = quartets.flat();
|
||||
const foundMap = new Map();
|
||||
|
||||
function search(prefix, usedIndices) {
|
||||
if (prefix.length > 0) {
|
||||
const checkLen = Math.min(prefix.length, 6);
|
||||
if (!prefixSet.has(prefix.slice(0, checkLen))) return;
|
||||
}
|
||||
|
||||
if (prefix.length >= 2 && cleanWordSet.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]);
|
||||
}
|
||||
}
|
||||
if (usedIndices.length >= 4) return;
|
||||
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (usedIndices.includes(i)) continue;
|
||||
usedIndices.push(i);
|
||||
search(prefix + tiles[i], usedIndices);
|
||||
usedIndices.pop();
|
||||
}
|
||||
}
|
||||
|
||||
search('', []);
|
||||
|
||||
const quartilesList = quartets.map(q => q.join('').toLowerCase());
|
||||
const quartileSet = new Set(quartilesList);
|
||||
|
||||
// Target quartiles are always valid, even if missing from the dictionary
|
||||
quartilesList.forEach((word, qIdx) => {
|
||||
if (!foundMap.has(word)) {
|
||||
foundMap.set(word, [qIdx * 4, qIdx * 4 + 1, qIdx * 4 + 2, qIdx * 4 + 3]);
|
||||
}
|
||||
});
|
||||
|
||||
let words = [];
|
||||
|
||||
for (const [word, path] of foundMap.entries()) {
|
||||
const tileSegments = path.map(idx => tiles[idx]);
|
||||
const tileCount = path.length;
|
||||
let points = tileCount === 4 ? 8 : tileCount === 3 ? 4 : tileCount;
|
||||
const isTargetQuartile = quartileSet.has(word);
|
||||
const isQuartile = tileCount === 4;
|
||||
|
||||
words.push({
|
||||
word,
|
||||
points,
|
||||
tileCount,
|
||||
tileIndices: path,
|
||||
tileSegments,
|
||||
isTargetQuartile,
|
||||
isQuartile
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Cap words at maxWords (default: 30) while keeping a fair share of 2-tile words.
|
||||
words = capWordList(words, 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,
|
||||
targetQuartiles: quartilesList,
|
||||
words,
|
||||
wordCount: words.length,
|
||||
quartileCount: 5,
|
||||
totalBaseScore,
|
||||
maxPossibleScore
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Solver module ready. Clean words count:', cleanWordSet.size);
|
||||
Reference in New Issue
Block a user