fertig-classic-games/server/words/wordRoutes.js

429 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Router } from 'express';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { initScrabbleDictionary, isValidWord, chooseMove } from './scrabbleEngine.js';
import { initGhostDictionary, judge as ghostJudge, chooseLetter as ghostChooseLetter, suggestWords as ghostSuggestWords } from './ghostEngine.js';
import {
initWordLadderDictionary,
generatePuzzle as ladderGeneratePuzzle,
generateVersusPuzzles as ladderGenerateVersus,
chooseAIMove as ladderChooseAIMove,
hintMove as ladderHintMove,
validWordsOfLength as ladderValidWords,
} from './wordLadderEngine.js';
import {
generatePuzzle as wordSearchGenerate,
listThemes as wordSearchThemes,
} from './wordSearchEngine.js';
import { generatePuzzle as sudokuGenerate } from './sudokuEngine.js';
import { initBoggleDictionary, rollBoard, solveBoard } from './boggleEngine.js';
import { initSpellingBeeDictionary, generatePuzzle as spellingBeeGenerate } from './spellingBeeEngine.js';
import { initMiniCrosswordPuzzles, getPuzzle as miniCrosswordGet } from './miniCrosswordEngine.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const WORDLIST_PATH = path.join(__dirname, '../data/wordlists/enable1.txt');
// Common words used to build a player-friendly Wordle answer pool.
// These are well-known 5-letter English words that make fair Wordle puzzles.
const COMMON_WORDS = new Set([
'ABOUT','ABOVE','ABUSE','ACTOR','ACUTE','ADMIT','ADOPT','ADULT','AFTER',
'AGAIN','AGENT','AGREE','AHEAD','ALARM','ALBUM','ALERT','ALIGN','ALIVE',
'ALLEY','ALLOW','ALONE','ALONG','ALTER','ANGEL','ANGLE','ANGRY','ANKLE',
'ANNEX','ANNOY','APART','APPLE','APPLY','APRIL','ARENA','ARGUE','ARISE',
'ARMOR','ARRAY','ARROW','ASIDE','ASKED','ASSET','AVOID','AWARD','AWARE',
'AWFUL','BADLY','BAKER','BASIC','BASIS','BATCH','BEACH','BEGAN','BEGIN',
'BEING','BELOW','BENCH','BIBLE','BIRTH','BLACK','BLADE','BLAME','BLAND',
'BLANK','BLAST','BLAZE','BLEED','BLEND','BLESS','BLIND','BLOCK','BLOOD',
'BLOWN','BOARD','BONUS','BOOST','BOUND','BRAIN','BRAND','BRAVE','BREAD',
'BREAK','BREED','BRICK','BRIDE','BRIEF','BRING','BROAD','BROKE','BROOK',
'BROWN','BRUSH','BUILD','BUILT','BUNCH','BURNT','BUYER','CACHE','CANDY',
'CARGO','CARRY','CATCH','CAUSE','CHAIN','CHAIR','CHAOS','CHARM','CHASE',
'CHEAP','CHECK','CHEEK','CHEER','CHESS','CHEST','CHIEF','CHILD','CHINA',
'CIVIC','CIVIL','CLAIM','CLASS','CLEAN','CLEAR','CLERK','CLICK','CLIFF',
'CLIMB','CLING','CLOCK','CLOSE','CLOUD','COACH','COAST','COULD','COUNT',
'COURT','COVER','CRACK','CRAFT','CRASH','CRAZY','CREAM','CREEK','CRIME',
'CRISP','CROSS','CROWD','CROWN','CRUEL','CRUSH','CURVE','CYCLE','DAILY',
'DANCE','DEATH','DEBUT','DELAY','DENSE','DEPOT','DEPTH','DERBY','DEVIL',
'DIGIT','DIRTY','DOING','DOUBT','DOUGH','DRAFT','DRAIN','DRAMA','DRANK',
'DRAWN','DREAM','DRESS','DRIED','DRIFT','DRILL','DRINK','DRIVE','DROVE',
'DRUGS','DRUMS','DRUNK','DWELL','DYING','EAGER','EARLY','EARTH','EIGHT',
'ELECT','EMAIL','EMPTY','ENEMY','ENJOY','ENTER','ENTRY','EQUAL','ERROR',
'ESSAY','EVERY','EVENT','EXACT','EXIST','EXTRA','FAINT','FAIRY','FAITH',
'FALSE','FANCY','FAULT','FEAST','FENCE','FETCH','FEVER','FEWER','FIELD',
'FIFTH','FIFTY','FIGHT','FINAL','FLAIR','FLAME','FLASH','FLEET','FLESH',
'FLOAT','FLOOD','FLOOR','FLOUR','FLUID','FOCUS','FORCE','FORGE','FORUM',
'FOUND','FRANK','FRAUD','FRESH','FRONT','FROST','FROZE','FRUIT','FULLY',
'FUNDS','FUNNY','GHOST','GIANT','GIVEN','GLASS','GLOBE','GLOOM','GLOSS',
'GLOVE','GOING','GRACE','GRADE','GRAIN','GRAND','GRANT','GRASP','GRASS',
'GRAVE','GREAT','GREEN','GREET','GRIEF','GRIND','GROAN','GROSS','GROUP',
'GROVE','GROWN','GUARD','GUESS','GUEST','GUIDE','GUILT','GUISE','GUSTO',
'HABIT','HAPPY','HARSH','HEART','HEAVY','HENCE','HINGE','HONEY','HONOR',
'HORSE','HOTEL','HOUSE','HUMAN','HUMOR','HURRY','IDEAL','IMAGE','IMPLY',
'INBOX','INDEX','INNER','INPUT','INTER','INTRO','ISSUE','JAPAN','JOINT',
'JOUST','JUDGE','JUICE','JUICY','JUMBO','KARMA','KNIFE','KNOCK','KNOWN',
'LABEL','LARGE','LASER','LATER','LAUGH','LAYER','LEARN','LEASE','LEAVE',
'LEGAL','LEVEL','LIGHT','LIMIT','LINEN','LIVER','LOCAL','LODGE','LOGIC',
'LOOSE','LOVER','LOWER','LUCKY','LUNAR','LYING','MAGIC','MAJOR','MAKER',
'MARCH','MATCH','MAYOR','MEDIA','MERIT','METAL','MIGHT','MINOR','MINUS',
'MIXED','MODEL','MONEY','MONTH','MORAL','MOTOR','MOUNT','MOUSE','MOUTH',
'MOVED','MOVIE','MUSIC','NAIVE','NEVER','NIGHT','NOISE','NORTH','NOTED',
'NOVEL','NURSE','OCCUR','OFFER','OFTEN','ONSET','OPERA','ORBIT','ORDER',
'OTHER','OUTER','OUNCE','OWNER','PAINT','PANEL','PAPER','PARTY','PASTA',
'PATCH','PAUSE','PEACE','PEARL','PEDAL','PENNY','PERCH','PHASE','PHONE',
'PHOTO','PIANO','PIECE','PILOT','PITCH','PIXEL','PIZZA','PLACE','PLAIN',
'PLANE','PLANT','PLATE','PLAZA','PLEAD','PLUCK','PLUMB','PLUME','PLUMP',
'PLUNGE','POINT','POKER','POLAR','POUND','POWER','PRESS','PRICE','PRIDE',
'PRIME','PRINT','PRIOR','PRIZE','PROBE','PROOF','PROSE','PROUD','PROVE',
'PROXY','PULSE','PUPIL','QUEEN','QUERY','QUEST','QUEUE','QUICK','QUIET',
'QUITE','QUOTA','QUOTE','RADAR','RADIO','RAISE','RALLY','RANCH','RANGE',
'RAPID','RATIO','REACH','READY','REALM','REBEL','REFER','REIGN','RELAX',
'REPLY','RIGHT','RIGID','RISKY','RIVAL','RIVER','ROBOT','ROCKY','ROMAN',
'ROUGH','ROUND','ROUTE','ROYAL','RULER','RUMOR','RURAL','SAINT','SALAD',
'SAUCE','SCALE','SCALD','SCENE','SCOUT','SCREW','SEIZE','SENSE','SERVE',
'SEVEN','SHADE','SHAKE','SHALL','SHAME','SHAPE','SHARE','SHARP','SHELF',
'SHIFT','SHIRT','SHOCK','SHOOT','SHORT','SHOUT','SIGHT','SINCE','SIXTH',
'SIXTY','SIZED','SKILL','SLEEP','SLICE','SLIDE','SLOPE','SLUMP','SMALL',
'SMART','SMELL','SMILE','SMOKE','SOLAR','SOLID','SOLVE','SORRY','SOUND',
'SOUTH','SPACE','SPARK','SPEAK','SPELL','SPEND','SPICE','SPILL','SPINE',
'SPLIT','SPOKE','SPORT','SPRAY','SQUAD','STACK','STAFF','STAGE','STAIN',
'STAKE','STALE','STALL','STAND','START','STATE','STEAL','STEEL','STEEP',
'STICK','STILL','STOCK','STOOD','STORE','STORM','STORY','STRAP','STRAW',
'STRIP','STUDY','STUFF','STYLE','SUGAR','SUITE','SUPER','SURGE','SWAMP',
'SWEAR','SWEAT','SWEEP','SWEET','SWEPT','SWIFT','SWING','SWORE','SWORN',
'TABLE','TAKEN','TASTE','TEACH','TEETH','TENSE','TERMS','THANK','THEIR',
'THEME','THERE','THESE','THICK','THING','THINK','THIRD','THOSE','THREE',
'THREW','THROW','TIGHT','TIMER','TIRED','TITLE','TODAY','TOKEN','TONAL',
'TOPIC','TOTAL','TOUCH','TOUGH','TRACE','TRACK','TRADE','TRAIL','TRAIN',
'TRAIT','TRASH','TREAT','TREND','TRIAL','TRIBE','TRICK','TRIED','TROOP',
'TRUCK','TRULY','TRUMP','TRUNK','TRUTH','TUMOR','TWICE','TWIST','TYPICAL',
'ULTRA','UNDER','UNION','UNITE','UNITY','UNTIL','UPPER','UPSET','URBAN',
'USAGE','UTTER','VALID','VALUE','VALVE','VERSE','VIDEO','VIGOR','VIRAL',
'VIRUS','VISIT','VITAL','VIVID','VOICE','VOTER','WAIST','WASTE','WATCH',
'WATER','WEARY','WEIGH','WEIRD','WHILE','WHITE','WHOLE','WHOSE','WIDER',
'WITCH','WOMAN','WOMEN','WORLD','WORRY','WORSE','WORST','WORTH','WOULD',
'WOUND','WRIST','WRITE','WROTE','YOUNG','YOURS','YOUTH','ZONAL',
]);
// ── Load word sets at startup ─────────────────────────────────────────────────
let allFiveLetterWords = null; // Set<string> — all valid 5-letter ENABLE words
let answerPool = null; // string[] — shuffleable answer list
function loadWordLists() {
if (allFiveLetterWords) return;
let raw = '';
try {
raw = fs.readFileSync(WORDLIST_PATH, 'utf8');
} catch (err) {
console.warn('[words] ENABLE word list not found, using fallback.');
raw = [...COMMON_WORDS].join('\n');
}
const allWords = raw.split('\n').map(w => w.trim().toUpperCase());
const enableFive = new Set(
allWords.filter(w => w.length === 5 && /^[A-Z]{5}$/.test(w)),
);
allFiveLetterWords = enableFive;
// Scrabble dictionary: every ENABLE word of legal play length (215 letters).
const scrabbleWords = new Set(
allWords.filter(w => w.length >= 2 && w.length <= 15 && /^[A-Z]+$/.test(w)),
);
initScrabbleDictionary(scrabbleWords);
console.log(`[words] loaded ${scrabbleWords.size} Scrabble words (215 letters)`);
// Ghost dictionary: every ENABLE word of length >= 4 (shorter words don't count).
const ghostWords = allWords.filter(w => w.length >= 4 && /^[A-Z]+$/.test(w));
initGhostDictionary(ghostWords);
console.log(`[words] loaded ${ghostWords.length} Ghost words (4+ letters)`);
// Word Ladder dictionaries: 3- and 4-letter words for the two game variants.
const ladderThree = allWords.filter(w => /^[A-Z]{3}$/.test(w));
const ladderFour = allWords.filter(w => /^[A-Z]{4}$/.test(w));
initWordLadderDictionary(ladderThree, ladderFour);
console.log(`[words] loaded Word Ladder dictionaries (${ladderThree.length} 3-letter, ${ladderFour.length} 4-letter)`);
// Boggle dictionary: every ENABLE word of length 316 (Boggle scores words 3+).
const boggleWords = allWords.filter(w => w.length >= 3 && w.length <= 16 && /^[A-Z]+$/.test(w));
initBoggleDictionary(boggleWords);
console.log(`[words] loaded ${boggleWords.length} Boggle words (316 letters)`);
// Spelling Bee dictionary: ENABLE words of length 4+, no S, ≤7 distinct letters.
const beeStats = initSpellingBeeDictionary(allWords);
console.log(`[words] loaded ${beeStats.words} Spelling Bee words (${beeStats.pangrams} pangram sets)`);
// Mini Crossword: curated 5x5 puzzle bank (independent of the ENABLE list).
const crosswordStats = initMiniCrosswordPuzzles();
console.log(`[words] loaded ${crosswordStats.puzzles} Mini Crossword puzzles`);
// Answer pool: prefer curated common words that are also in ENABLE;
// supplement with additional ENABLE words up to a healthy pool size.
const curated = [...COMMON_WORDS].filter(w => enableFive.has(w));
answerPool = curated.length >= 200 ? curated : [...enableFive];
console.log(`[words] loaded ${enableFive.size} valid 5-letter words, ${answerPool.length} answer candidates`);
}
loadWordLists();
// ── Router ────────────────────────────────────────────────────────────────────
const router = Router();
// GET /api/words/wordle/start
// Returns a random answer word plus the full valid-word pool for the client.
router.get('/wordle/start', (_req, res) => {
const answer = answerPool[Math.floor(Math.random() * answerPool.length)];
res.json({ answer, validWords: [...allFiveLetterWords] });
});
// POST /api/words/wordle/validate { word: string }
// Quick single-word validation (used as a lightweight alternative to the full pool).
router.post('/wordle/validate', (req, res) => {
const word = (req.body?.word ?? '').trim().toUpperCase();
if (!/^[A-Z]{5}$/.test(word)) {
return res.json({ valid: false });
}
res.json({ valid: allFiveLetterWords.has(word) });
});
// ── Boggle ──────────────────────────────────────────────────────────────────────
// GET /api/words/boggle/start
// Rolls a fresh 4×4 board and returns it with the full solution set (every valid
// word and one representative path). The client validates player words and drives
// the AI from this set — no per-word round trips needed.
router.get('/boggle/start', (_req, res) => {
const board = rollBoard();
res.json({ board, solutions: solveBoard(board) });
});
// ── Spelling Bee ────────────────────────────────────────────────────────────────
// GET /api/words/spellingbee/start?difficulty=easy|normal|hard
// Builds a honeycomb puzzle (7 letters incl. a required center) and returns it
// with the full valid-word list so the client validates locally.
router.get('/spellingbee/start', (req, res) => {
const VALID = ['easy', 'normal', 'hard'];
const difficulty = VALID.includes(req.query.difficulty) ? req.query.difficulty : 'normal';
res.json(spellingBeeGenerate(difficulty));
});
// ── Mini Crossword ────────────────────────────────────────────────────────────
// GET /api/words/minicrossword/start?difficulty=easy|medium|hard
// Returns a curated 5x5 puzzle (grid + numbered across/down clues with answers).
router.get('/minicrossword/start', (req, res) => {
const VALID = ['easy', 'medium', 'hard'];
const difficulty = VALID.includes(req.query.difficulty) ? req.query.difficulty : 'medium';
res.json(miniCrosswordGet(difficulty));
});
// ── Scrabble ──────────────────────────────────────────────────────────────────
// POST /api/words/scrabble/validate { words: string[] }
// Validates every word a human move forms. Returns the list of invalid ones.
router.post('/scrabble/validate', (req, res) => {
const words = Array.isArray(req.body?.words) ? req.body.words : [];
const invalid = words
.map(w => String(w).trim().toUpperCase())
.filter(w => !isValidWord(w));
res.json({ valid: invalid.length === 0, invalid });
});
// POST /api/words/scrabble/ai-move { board, rack, skill, firstMove, bagCount }
// Returns the AI's chosen move: a play, an exchange, or a pass.
router.post('/scrabble/ai-move', (req, res) => {
const { board, rack, skill, firstMove, bagCount } = req.body ?? {};
if (!Array.isArray(board) || !Array.isArray(rack)) {
return res.status(400).json({ error: 'board and rack are required' });
}
const move = chooseMove({
board,
rack,
skill: Number(skill) || 3,
firstMove: !!firstMove,
bagCount: Number(bagCount) || 0,
});
res.json(move);
});
// ── Ghost ───────────────────────────────────────────────────────────────────
// POST /api/words/ghost/judge { fragment: string }
// Grades the fragment a player just formed: a completed word, or off-dictionary,
// both lose the round for whoever made the move.
router.post('/ghost/judge', (req, res) => {
const fragment = String(req.body?.fragment ?? '');
res.json(ghostJudge(fragment));
});
// POST /api/words/ghost/ai-move { fragment: string, skill: number }
// Returns the AI's letter and how the resulting fragment grades.
router.post('/ghost/ai-move', (req, res) => {
const fragment = String(req.body?.fragment ?? '');
const skill = Number(req.body?.skill) || 3;
res.json(ghostChooseLetter(fragment, skill));
});
// POST /api/words/ghost/suggest { prefix: string, max?: number }
// Example safe words the loser could have headed toward from the faced prefix.
router.post('/ghost/suggest', (req, res) => {
const prefix = String(req.body?.prefix ?? '');
const max = Number(req.body?.max) || 3;
res.json({ words: ghostSuggestWords(prefix, max) });
});
// ── Word Ladder ────────────────────────────────────────────────────────────────
// GET /api/words/wordladder/start?length=3|4&versus=true|false
// Solo: { player, opponent: null, validWords }.
// Versus: { player, opponent, validWords } — two puzzles of equal par.
router.get('/wordladder/start', (req, res) => {
const length = Number(req.query.length) === 3 ? 3 : 4;
const versus = String(req.query.versus) === 'true';
const validWords = ladderValidWords(length);
if (versus) {
const { player, opponent } = ladderGenerateVersus(length);
return res.json({ player, opponent, validWords });
}
res.json({ player: ladderGeneratePuzzle(length), opponent: null, validWords });
});
// POST /api/words/wordladder/ai-move { current, target, skill }
// Advances the AI opponent one rung toward its target.
router.post('/wordladder/ai-move', (req, res) => {
const current = String(req.body?.current ?? '');
const target = String(req.body?.target ?? '');
const skill = Number(req.body?.skill) || 3;
res.json(ladderChooseAIMove(current, target, skill));
});
// POST /api/words/wordladder/hint { current, target }
// One valid next step on a shortest path toward the target.
router.post('/wordladder/hint', (req, res) => {
const current = String(req.body?.current ?? '');
const target = String(req.body?.target ?? '');
res.json({ word: ladderHintMove(current, target) });
});
// ── Word Search ──────────────────────────────────────────────────────────────
// ── Hangman ──────────────────────────────────────────────────────────────────
const HANGMAN_WORDS = {
easy: [
{ word: 'CAT', hint: 'A common household pet', category: 'Animals' },
{ word: 'DOG', hint: 'Man\'s best friend', category: 'Animals' },
{ word: 'SUN', hint: 'It lights up the day', category: 'Nature' },
{ word: 'FISH', hint: 'Lives in water', category: 'Animals' },
{ word: 'BIRD', hint: 'Has wings and feathers', category: 'Animals' },
{ word: 'TREE', hint: 'Tall plant with a trunk', category: 'Nature' },
{ word: 'CAKE', hint: 'Sweet birthday treat', category: 'Food' },
{ word: 'STAR', hint: 'Twinkles in the night sky', category: 'Nature' },
{ word: 'FROG', hint: 'Hops and croaks', category: 'Animals' },
{ word: 'RAIN', hint: 'Falls from clouds', category: 'Nature' },
{ word: 'DUCK', hint: 'Says quack', category: 'Animals' },
{ word: 'SNOW', hint: 'White and cold', category: 'Nature' },
{ word: 'BEAR', hint: 'Loves honey', category: 'Animals' },
{ word: 'MILK', hint: 'Comes from a cow', category: 'Food' },
{ word: 'MOON', hint: 'Orbits the Earth', category: 'Nature' },
{ word: 'WOLF', hint: 'Howls at the moon', category: 'Animals' },
{ word: 'CORN', hint: 'Yellow vegetable on a cob', category: 'Food' },
{ word: 'ROSE', hint: 'A thorny flower', category: 'Nature' },
{ word: 'LION', hint: 'King of the jungle', category: 'Animals' },
{ word: 'LEAF', hint: 'Falls from trees in autumn', category: 'Nature' },
],
medium: [
{ word: 'CASTLE', hint: 'A medieval fortress', category: 'Places' },
{ word: 'JUNGLE', hint: 'Dense tropical forest', category: 'Nature' },
{ word: 'PLANET', hint: 'Orbits a star', category: 'Space' },
{ word: 'BRIDGE', hint: 'Spans a river or gap', category: 'Structures'},
{ word: 'CAMERA', hint: 'Used to take photos', category: 'Objects' },
{ word: 'DRAGON', hint: 'Mythical fire-breathing creature', category: 'Fantasy' },
{ word: 'FOREST', hint: 'Dense woodland', category: 'Nature' },
{ word: 'GUITAR', hint: 'Six-stringed instrument', category: 'Music' },
{ word: 'ISLAND', hint: 'Land surrounded by water', category: 'Places' },
{ word: 'KNIGHT', hint: 'Armored medieval warrior', category: 'History' },
{ word: 'LEMON', hint: 'Sour yellow citrus fruit', category: 'Food' },
{ word: 'MARBLE', hint: 'Smooth polished stone', category: 'Objects' },
{ word: 'ORANGE', hint: 'A citrus fruit or color', category: 'Food' },
{ word: 'PENCIL', hint: 'Used for writing and drawing', category: 'Objects' },
{ word: 'PUZZLE', hint: 'A brain-teasing challenge', category: 'Games' },
{ word: 'RABBIT', hint: 'Hops and has long ears', category: 'Animals' },
{ word: 'ROCKET', hint: 'Launches into space', category: 'Space' },
{ word: 'SCHOOL', hint: 'Where students learn', category: 'Places' },
{ word: 'SPIDER', hint: 'Eight-legged arachnid', category: 'Animals' },
{ word: 'TURTLE', hint: 'Has a shell on its back', category: 'Animals' },
{ word: 'VIOLIN', hint: 'Bowed string instrument', category: 'Music' },
{ word: 'WINDOW', hint: 'Lets in light and air', category: 'Structures'},
{ word: 'WINTER', hint: 'The coldest season', category: 'Nature' },
{ word: 'WIZARD', hint: 'Casts magical spells', category: 'Fantasy' },
],
hard: [
{ word: 'ARCHITECT', hint: 'Designs buildings', category: 'Professions' },
{ word: 'BACKPACK', hint: 'Worn on your back for carrying', category: 'Objects' },
{ word: 'BREAKFAST', hint: 'The first meal of the day', category: 'Food' },
{ word: 'BUTTERFLY', hint: 'Colorful winged insect', category: 'Animals' },
{ word: 'CALENDAR', hint: 'Tracks days and months', category: 'Objects' },
{ word: 'CHOCOLATE', hint: 'Sweet treat from cacao beans', category: 'Food' },
{ word: 'CLOCKWORK', hint: 'Gears and springs mechanism', category: 'Objects' },
{ word: 'CROCODILE', hint: 'Large reptile in rivers', category: 'Animals' },
{ word: 'DAUGHTER', hint: 'Female offspring', category: 'People' },
{ word: 'DISCOVERY', hint: 'Finding something new', category: 'Concepts' },
{ word: 'EARTHQUAKE', hint: 'Shaking of the ground', category: 'Nature' },
{ word: 'FINGERTIP', hint: 'End of a finger', category: 'Body' },
{ word: 'FIREWORKS', hint: 'Colorful sky explosions', category: 'Events' },
{ word: 'FLAMINGO', hint: 'Pink bird that stands on one leg', category: 'Animals' },
{ word: 'GEOGRAPHY', hint: 'Study of Earth and its features', category: 'Science' },
{ word: 'HURRICANE', hint: 'Powerful tropical storm', category: 'Nature' },
{ word: 'JELLYFISH', hint: 'Translucent ocean creature', category: 'Animals' },
{ word: 'KANGAROO', hint: 'Marsupial that hops', category: 'Animals' },
{ word: 'LIGHTHOUSE', hint: 'Tower with a guiding beacon', category: 'Structures' },
{ word: 'MUSHROOM', hint: 'A fungus with a cap', category: 'Nature' },
{ word: 'PORCUPINE', hint: 'Covered in sharp quills', category: 'Animals' },
{ word: 'QUICKSAND', hint: 'Wet sand that sucks you in', category: 'Nature' },
{ word: 'SUBMARINE', hint: 'Vessel that travels underwater', category: 'Vehicles' },
{ word: 'TELESCOPE', hint: 'Used to observe distant objects', category: 'Science' },
{ word: 'THUNDERSTORM',hint: 'Rain with lightning and thunder', category: 'Nature' },
{ word: 'WATERFALL', hint: 'Water cascading over a cliff', category: 'Nature' },
],
};
// GET /api/words/hangman/start?difficulty=easy|medium|hard
router.get('/hangman/start', (req, res) => {
const difficulty = ['easy', 'medium', 'hard'].includes(req.query.difficulty)
? req.query.difficulty : 'medium';
const pool = HANGMAN_WORDS[difficulty];
const entry = pool[Math.floor(Math.random() * pool.length)];
res.json({ word: entry.word, hint: entry.hint, category: entry.category, maxWrong: 7 });
});
// ── Word Search ──────────────────────────────────────────────────────────────
// GET /api/words/wordsearch/start?difficulty=easy|medium|hard&theme=random|space|...
// Returns a generated puzzle: { difficulty, theme, themeLabel, size, words, placements, grid }.
router.get('/wordsearch/start', (req, res) => {
const difficulty = String(req.query.difficulty ?? 'medium').toLowerCase();
const theme = req.query.theme ? String(req.query.theme).toLowerCase() : 'random';
res.json(wordSearchGenerate({ difficulty, theme }));
});
// GET /api/words/wordsearch/themes — list of { id, label } for the start panel.
router.get('/wordsearch/themes', (_req, res) => {
res.json({ themes: wordSearchThemes() });
});
// GET /api/words/sudoku/start?difficulty=very-easy|easy|regular|hard|brutal
router.get('/sudoku/start', (req, res) => {
const VALID = ['very-easy', 'easy', 'regular', 'hard', 'brutal'];
const difficulty = VALID.includes(req.query.difficulty) ? req.query.difficulty : 'regular';
res.json(sudokuGenerate(difficulty));
});
export default router;