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'; 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 — 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)`); // 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); }); export default router;