373 lines
22 KiB
JavaScript
373 lines
22 KiB
JavaScript
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';
|
||
|
||
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 (2–15 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 (2–15 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)`);
|
||
|
||
// 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) });
|
||
});
|
||
|
||
// ── 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() });
|
||
});
|
||
|
||
export default router;
|