Bläddra i källkod

feat: Epic 2 complete — solver engine with word ranking

- Entropy-based solver algorithm (information theory)
- Frequency/heuristic-based solver (letter frequency scoring)
- Aggregated difficulty scores replace default 3.0 values
- All 40 target words solvable within 6 attempts
- Real difficulty range: 2.0 (large) to 5.0 (prize)
- Solver runs as standalone TypeScript script (npm run solve)

Co-Authored-By: Claude <noreply@anthropic.com>
Oleg Panashchenko 2 veckor sedan
förälder
incheckning
40c7811108

+ 4 - 1
.claude/settings.local.json

@@ -24,7 +24,10 @@
       "Bash(PORT=3001 npx tsx server/src/index.ts)",
       "Bash(curl -s -X POST http://localhost:3001/api/play-again -H 'Content-Type: application/json' -d '{\"skillMetric\":3.0}')",
       "Bash(curl -s -X POST http://localhost:3001/api/guess -H 'Content-Type: application/json' -d '{\"wordId\":4,\"guess\":\"hello\",\"tryNo\":1}')",
-      "Bash(git rm *)"
+      "Bash(git rm *)",
+      "Bash(git add *)",
+      "Bash(git commit *)",
+      "Bash(echo \"solver: $?\")"
     ]
   }
 }

+ 0 - 31
.claude/settings.local.json.tmp.58031.cc064010c3f0

@@ -1,31 +0,0 @@
-{
-  "permissions": {
-    "allow": [
-      "Bash(uv run *)",
-      "Bash(npx tsx *)",
-      "Bash(python3 *)",
-      "Bash(npx tsc *)",
-      "Bash(echo \"server: $?\")",
-      "Bash(echo \"client: $?\")",
-      "Bash(echo \"shared: $?\")",
-      "Bash(npx vite *)",
-      "Skill(code-review)",
-      "Bash(curl -s -X POST http://localhost:3000/api/play-again -H 'Content-Type: application/json' -d '{\"skillMetric\":3.0}')",
-      "Bash(kill %1)",
-      "Bash(pkill -f \"tsx.*server\")",
-      "Bash(curl -s http://localhost:3000/api/daily)",
-      "Bash(wait)",
-      "Bash(pkill -f \"tsx\")",
-      "Bash(pkill -f \"node.*server\")",
-      "Read(//tmp/**)",
-      "Bash(pkill -9 -f \"node\")",
-      "Bash(curl -s -X POST http://localhost:3000/api/guess -H 'Content-Type: application/json' -d '{\"wordId\":4,\"guess\":\"style\",\"tryNo\":1}')",
-      "Bash(fuser -k 3000/tcp)",
-      "Bash(PORT=3001 npx tsx server/src/index.ts)",
-      "Bash(curl -s -X POST http://localhost:3001/api/play-again -H 'Content-Type: application/json' -d '{\"skillMetric\":3.0}')",
-      "Bash(curl -s -X POST http://localhost:3001/api/guess -H 'Content-Type: application/json' -d '{\"wordId\":4,\"guess\":\"hello\",\"tryNo\":1}')",
-      "Bash(git rm *)",
-      "Bash(git add *)"
-    ]
-  }
-}

+ 69 - 0
_bmad-output/implementation-artifacts/2-1-solver-script-word-ranking.md

@@ -0,0 +1,69 @@
+# Story 2.1: Solver Script & Word Ranking
+
+Status: ready-for-dev
+
+## Story
+
+As a developer,
+I want a solver script that pre-computes attempt counts for every target word using multiple algorithms,
+So that word difficulty scores are data-driven, not manual estimates.
+
+## Acceptance Criteria
+
+1. **Multi-algorithm solver:** Given the solver script runs against the word bank, when it processes each target word with at least two solving algorithms, then it records attempt counts and computes an aggregated difficulty score per word.
+
+2. **Writes updated word bank:** Given the solver completes, when it writes `data/word-bank.json`, then every target has a real `difficulty` value replacing the default 3.0.
+
+3. **Removes unsolvable words:** Given a word is not solvable within 6 attempts by any algorithm, when the solver processes it, then that word is flagged or removed from the target set.
+
+4. **Shared types:** Given the solver is TypeScript, when it imports types from `shared/`, then it uses the same `WordBank` and `TargetWord` interfaces as the server.
+
+## Tasks / Subtasks
+
+- [ ] Task 1: Initialize solver package (AC: 4)
+  - [ ] Create `solver/package.json` with TypeScript + tsx + `@wordle/shared` dependency
+  - [ ] Create `solver/tsconfig.json`
+  - [ ] Add `solver` to root workspace
+
+- [ ] Task 2: Implement solver algorithms (AC: 1, 3)
+  - [ ] Create `solver/src/index.ts` — orchestrates solvers, aggregates results, writes output
+  - [ ] Create `solver/src/entropy.ts` — entropy/information-theory based solver
+  - [ ] Create `solver/src/minimax.ts` — minimax or alternative solver
+
+- [ ] Task 3: Test and run solver (AC: 2)
+  - [ ] Run solver against `data/word-bank.json`
+  - [ ] Verify difficulty scores are populated (not all 3.0)
+  - [ ] Verify no unsolvable words remain in targets
+
+## Dev Notes
+
+### Solver Algorithm: Entropy-Based
+
+1. Start with all target words as candidates
+2. For each possible guess, compute expected information gain (how many candidates each feedback pattern eliminates)
+3. Pick the guess with highest entropy
+4. Filter candidates based on feedback
+5. Repeat until solved or out of attempts
+
+### Solver Algorithm: Minimax
+
+1. Start with all target words as candidates
+2. For each guess, compute the worst-case candidate-elimination count
+3. Pick the guess that minimizes the worst case (minimax)
+4. Filter, repeat
+
+### Difficulty Score
+
+`difficulty = (entropyAttempts + minimaxAttempts) / 2`
+
+### Files to Create
+
+- `solver/package.json`, `solver/tsconfig.json`
+- `solver/src/index.ts`, `solver/src/entropy.ts`, `solver/src/minimax.ts`
+- Update root `package.json` to add `solver` workspace
+
+### References
+
+- [Source: _bmad-output/planning-artifacts/prds/prd-wordle-2026-07-07/prd.md#FR-16, FR-17]
+- [Source: _bmad-output/planning-artifacts/architecture/architecture-wordle-2026-07-07/ARCHITECTURE-SPINE.md]
+- [Source: _bmad-output/planning-artifacts/epics.md#Epic 2]

+ 2 - 2
_bmad-output/implementation-artifacts/sprint-status.yaml

@@ -53,6 +53,6 @@ development_status:
   1-2-guess-validation-feedback: review
   1-3-play-again-skill-tracking: review
   epic-1-retrospective: optional
-  epic-2: backlog
-  2-1-solver-script-word-ranking: backlog
+  epic-2: done
+  2-1-solver-script-word-ranking: review
   epic-2-retrospective: optional

+ 967 - 136
data/word-bank.json

@@ -1,142 +1,973 @@
 {
   "guessable": [
-    "crane", "apple", "grape", "style", "hello", "world", "large", "house",
-    "brain", "cloud", "dance", "eager", "flame", "giant", "happy", "igloo",
-    "jolly", "kitey", "lemon", "mango", "noble", "ocean", "piano", "queen",
-    "radio", "sugar", "tiger", "ultra", "vivid", "waste", "xenon", "yield", "zebra",
-    "about", "above", "abuse", "actor", "acute", "admit", "adopt", "adult",
-    "after", "again", "agent", "agree", "ahead", "alarm", "album", "alert",
-    "alien", "align", "alive", "alloy", "alone", "along", "alter", "among",
-    "ample", "angel", "anger", "angle", "angry", "anime", "ankle", "annex",
-    "apart", "arena", "argue", "arise", "armor", "array", "arrow", "aside",
-    "asset", "atlas", "atoll", "attic", "audio", "audit", "avoid", "awake",
-    "award", "aware", "awful", "bacon", "badge", "badly", "basic", "basin",
-    "basis", "batch", "beach", "beard", "beast", "begin", "being", "below",
-    "bench", "berry", "birth", "black", "blade", "blame", "bland", "blank",
-    "blast", "blaze", "bleak", "blend", "bless", "blind", "block", "bloom",
-    "board", "bonus", "boost", "bound", "brand", "brave", "bread", "break",
-    "breed", "brief", "bring", "broad", "brook", "brown", "brush", "build",
-    "bunch", "burnt", "burst", "buyer", "cabin", "cable", "candy", "cargo",
-    "carry", "catch", "cause", "chain", "chair", "chaos", "charm", "chart",
-    "chase", "cheap", "check", "cheek", "cheer", "chess", "chest", "chief",
-    "child", "chill", "china", "choir", "chunk", "churn", "cinch", "circa",
-    "civil", "claim", "class", "clean", "clear", "clerk", "click", "cliff",
-    "climb", "cling", "clock", "clone", "close", "cloth", "coach", "coast",
-    "color", "comic", "coral", "could", "count", "court", "cover", "crack",
-    "craft", "crash", "cream", "crime", "cross", "crowd", "crown", "crude",
-    "crush", "curve", "cycle", "daily", "death", "debug", "decay", "delay",
-    "delta", "dense", "depth", "derby", "desert", "devil", "digit", "dirty",
-    "doubt", "dough", "dozen", "draft", "drain", "drama", "drank", "dress",
-    "drift", "drill", "drink", "drive", "drops", "drove", "dying", "eagle",
-    "early", "earth", "eight", "elder", "elect", "elite", "email", "empty",
-    "enemy", "enjoy", "enter", "entry", "equal", "error", "essay", "event",
-    "every", "exact", "exile", "exist", "extra", "faint", "faith", "false",
-    "fancy", "fatal", "fault", "feast", "fiber", "field", "fight", "final",
-    "first", "fixed", "flame", "fleet", "flesh", "flick", "float", "flock",
-    "flood", "floor", "flour", "fluid", "flush", "focus", "force", "forge",
-    "forth", "forum", "found", "frame", "frank", "fraud", "fresh", "front",
-    "frost", "fruit", "fully", "funny", "ghost", "giant", "given", "glass",
-    "globe", "gloom", "glory", "gloss", "glyph", "going", "grace", "grade",
-    "grain", "grand", "grant", "graph", "grasp", "grass", "grave", "great",
-    "green", "greet", "grief", "grill", "grind", "gross", "group", "grove",
-    "grown", "guard", "guess", "guest", "guide", "guild", "guilt", "habit",
-    "happy", "harsh", "heart", "heavy", "hence", "hobby", "honor", "horse",
-    "hotel", "house", "human", "humor", "ideal", "image", "imply", "index",
-    "indie", "inner", "input", "irony", "ivory", "jewel", "joint", "joker",
-    "judge", "juice", "kebab", "knife", "knock", "known", "label", "labor",
-    "large", "laser", "later", "laugh", "layer", "learn", "lease", "least",
-    "leave", "legal", "lemon", "level", "light", "limit", "linen", "liver",
-    "local", "logic", "loose", "lover", "lower", "loyal", "lucky", "lunch",
-    "lyric", "magic", "major", "maker", "manor", "maple", "march", "match",
-    "mayor", "media", "mercy", "merge", "merit", "metal", "midst", "might",
-    "minor", "minus", "mixed", "model", "money", "month", "moral", "motor",
-    "mount", "mouse", "mouth", "movie", "music", "naive", "nerve", "never",
-    "newly", "night", "noble", "noise", "north", "noted", "novel", "nurse",
-    "nylon", "occur", "ocean", "offer", "often", "olive", "onset", "opera",
-    "orbit", "order", "organ", "other", "outer", "owner", "oxide", "ozone",
-    "paint", "panel", "panic", "paper", "party", "patch", "pause", "peace",
-    "pearl", "penny", "phase", "phone", "photo", "piano", "piece", "pilot",
-    "pinch", "pixel", "pizza", "place", "plain", "plane", "plant", "plate",
-    "plaza", "plead", "pluck", "plumb", "plume", "plush", "point", "polar",
-    "pouch", "pound", "power", "press", "price", "pride", "prime", "print",
-    "prior", "prize", "probe", "proof", "proud", "prove", "proxy", "pulse",
-    "pupil", "purse", "quest", "queue", "quick", "quiet", "quilt", "quirk",
-    "quota", "quote", "radar", "radio", "raise", "rally", "ranch", "range",
-    "rapid", "ratio", "reach", "react", "realm", "rebel", "refer", "reign",
-    "relax", "reply", "rider", "ridge", "rifle", "right", "rigid", "rival",
-    "river", "robot", "rocky", "roman", "rouge", "rough", "round", "route",
-    "royal", "rugby", "ruler", "rural", "sadly", "saint", "salad", "salon",
-    "sauce", "scale", "scare", "scene", "scent", "scope", "score", "scout",
-    "screw", "sense", "serve", "setup", "seven", "shade", "shaft", "shake",
-    "shall", "shame", "shape", "share", "shark", "sharp", "sheep", "sheer",
-    "sheet", "shelf", "shell", "shift", "shine", "shirt", "shock", "shoot",
-    "shore", "short", "shout", "sight", "sigma", "since", "sixth", "sixty",
-    "sized", "skill", "skull", "slash", "sleep", "slice", "slide", "small",
-    "smart", "smell", "smile", "smith", "smoke", "snack", "snake", "solar",
-    "solid", "solve", "sorry", "sound", "south", "space", "spare", "spark",
-    "speak", "speed", "spell", "spend", "spice", "spill", "spine", "spite",
-    "split", "spoke", "sport", "spray", "squad", "stack", "staff", "stage",
-    "stain", "stair", "stake", "stale", "stall", "stamp", "stand", "stare",
-    "start", "state", "steam", "steel", "steep", "steer", "stern", "stick",
-    "still", "stock", "stone", "stood", "store", "storm", "story", "stove",
-    "stuff", "style", "sugar", "suite", "sunny", "super", "surge", "swamp",
-    "swear", "sweep", "sweet", "swept", "swift", "swing", "sword", "swore",
-    "sworn", "syrup", "table", "taken", "taste", "teach", "teeth", "temple",
-    "their", "theme", "there", "these", "thick", "thing", "think", "third",
-    "those", "three", "threw", "throw", "thumb", "tidal", "tight", "timer",
-    "tired", "title", "today", "token", "topic", "total", "touch", "tough",
-    "tower", "toxic", "trace", "track", "trade", "trail", "train", "trait",
-    "trash", "treat", "trend", "trial", "tribe", "trick", "troop", "truck",
-    "truly", "trunk", "trust", "truth", "tumor", "twice", "twist", "ultra",
-    "uncle", "under", "undue", "unfit", "union", "unite", "unity", "until",
-    "upper", "upset", "urban", "usage", "usual", "valid", "value", "valve",
-    "vault", "venue", "verse", "video", "vigor", "vinyl", "viral", "virtue",
-    "visit", "vista", "vital", "vivid", "vocal", "voice", "voter", "waist",
-    "watch", "water", "weigh", "weird", "wheat", "wheel", "where", "which",
-    "while", "white", "whole", "whose", "wider", "witch", "woman", "world",
-    "worry", "worse", "worst", "worth", "would", "wound", "wrist", "write",
-    "wrong", "wrote", "yacht", "yield", "young", "youth"
+    "crane",
+    "apple",
+    "grape",
+    "style",
+    "hello",
+    "world",
+    "large",
+    "house",
+    "brain",
+    "cloud",
+    "dance",
+    "eager",
+    "flame",
+    "giant",
+    "happy",
+    "igloo",
+    "jolly",
+    "kitey",
+    "lemon",
+    "mango",
+    "noble",
+    "ocean",
+    "piano",
+    "queen",
+    "radio",
+    "sugar",
+    "tiger",
+    "ultra",
+    "vivid",
+    "waste",
+    "xenon",
+    "yield",
+    "zebra",
+    "about",
+    "above",
+    "abuse",
+    "actor",
+    "acute",
+    "admit",
+    "adopt",
+    "adult",
+    "after",
+    "again",
+    "agent",
+    "agree",
+    "ahead",
+    "alarm",
+    "album",
+    "alert",
+    "alien",
+    "align",
+    "alive",
+    "alloy",
+    "alone",
+    "along",
+    "alter",
+    "among",
+    "ample",
+    "angel",
+    "anger",
+    "angle",
+    "angry",
+    "anime",
+    "ankle",
+    "annex",
+    "apart",
+    "arena",
+    "argue",
+    "arise",
+    "armor",
+    "array",
+    "arrow",
+    "aside",
+    "asset",
+    "atlas",
+    "atoll",
+    "attic",
+    "audio",
+    "audit",
+    "avoid",
+    "awake",
+    "award",
+    "aware",
+    "awful",
+    "bacon",
+    "badge",
+    "badly",
+    "basic",
+    "basin",
+    "basis",
+    "batch",
+    "beach",
+    "beard",
+    "beast",
+    "begin",
+    "being",
+    "below",
+    "bench",
+    "berry",
+    "birth",
+    "black",
+    "blade",
+    "blame",
+    "bland",
+    "blank",
+    "blast",
+    "blaze",
+    "bleak",
+    "blend",
+    "bless",
+    "blind",
+    "block",
+    "bloom",
+    "board",
+    "bonus",
+    "boost",
+    "bound",
+    "brand",
+    "brave",
+    "bread",
+    "break",
+    "breed",
+    "brief",
+    "bring",
+    "broad",
+    "brook",
+    "brown",
+    "brush",
+    "build",
+    "bunch",
+    "burnt",
+    "burst",
+    "buyer",
+    "cabin",
+    "cable",
+    "candy",
+    "cargo",
+    "carry",
+    "catch",
+    "cause",
+    "chain",
+    "chair",
+    "chaos",
+    "charm",
+    "chart",
+    "chase",
+    "cheap",
+    "check",
+    "cheek",
+    "cheer",
+    "chess",
+    "chest",
+    "chief",
+    "child",
+    "chill",
+    "china",
+    "choir",
+    "chunk",
+    "churn",
+    "cinch",
+    "circa",
+    "civil",
+    "claim",
+    "class",
+    "clean",
+    "clear",
+    "clerk",
+    "click",
+    "cliff",
+    "climb",
+    "cling",
+    "clock",
+    "clone",
+    "close",
+    "cloth",
+    "coach",
+    "coast",
+    "color",
+    "comic",
+    "coral",
+    "could",
+    "count",
+    "court",
+    "cover",
+    "crack",
+    "craft",
+    "crash",
+    "cream",
+    "crime",
+    "cross",
+    "crowd",
+    "crown",
+    "crude",
+    "crush",
+    "curve",
+    "cycle",
+    "daily",
+    "death",
+    "debug",
+    "decay",
+    "delay",
+    "delta",
+    "dense",
+    "depth",
+    "derby",
+    "desert",
+    "devil",
+    "digit",
+    "dirty",
+    "doubt",
+    "dough",
+    "dozen",
+    "draft",
+    "drain",
+    "drama",
+    "drank",
+    "dress",
+    "drift",
+    "drill",
+    "drink",
+    "drive",
+    "drops",
+    "drove",
+    "dying",
+    "eagle",
+    "early",
+    "earth",
+    "eight",
+    "elder",
+    "elect",
+    "elite",
+    "email",
+    "empty",
+    "enemy",
+    "enjoy",
+    "enter",
+    "entry",
+    "equal",
+    "error",
+    "essay",
+    "event",
+    "every",
+    "exact",
+    "exile",
+    "exist",
+    "extra",
+    "faint",
+    "faith",
+    "false",
+    "fancy",
+    "fatal",
+    "fault",
+    "feast",
+    "fiber",
+    "field",
+    "fight",
+    "final",
+    "first",
+    "fixed",
+    "flame",
+    "fleet",
+    "flesh",
+    "flick",
+    "float",
+    "flock",
+    "flood",
+    "floor",
+    "flour",
+    "fluid",
+    "flush",
+    "focus",
+    "force",
+    "forge",
+    "forth",
+    "forum",
+    "found",
+    "frame",
+    "frank",
+    "fraud",
+    "fresh",
+    "front",
+    "frost",
+    "fruit",
+    "fully",
+    "funny",
+    "ghost",
+    "giant",
+    "given",
+    "glass",
+    "globe",
+    "gloom",
+    "glory",
+    "gloss",
+    "glyph",
+    "going",
+    "grace",
+    "grade",
+    "grain",
+    "grand",
+    "grant",
+    "graph",
+    "grasp",
+    "grass",
+    "grave",
+    "great",
+    "green",
+    "greet",
+    "grief",
+    "grill",
+    "grind",
+    "gross",
+    "group",
+    "grove",
+    "grown",
+    "guard",
+    "guess",
+    "guest",
+    "guide",
+    "guild",
+    "guilt",
+    "habit",
+    "happy",
+    "harsh",
+    "heart",
+    "heavy",
+    "hence",
+    "hobby",
+    "honor",
+    "horse",
+    "hotel",
+    "house",
+    "human",
+    "humor",
+    "ideal",
+    "image",
+    "imply",
+    "index",
+    "indie",
+    "inner",
+    "input",
+    "irony",
+    "ivory",
+    "jewel",
+    "joint",
+    "joker",
+    "judge",
+    "juice",
+    "kebab",
+    "knife",
+    "knock",
+    "known",
+    "label",
+    "labor",
+    "large",
+    "laser",
+    "later",
+    "laugh",
+    "layer",
+    "learn",
+    "lease",
+    "least",
+    "leave",
+    "legal",
+    "lemon",
+    "level",
+    "light",
+    "limit",
+    "linen",
+    "liver",
+    "local",
+    "logic",
+    "loose",
+    "lover",
+    "lower",
+    "loyal",
+    "lucky",
+    "lunch",
+    "lyric",
+    "magic",
+    "major",
+    "maker",
+    "manor",
+    "maple",
+    "march",
+    "match",
+    "mayor",
+    "media",
+    "mercy",
+    "merge",
+    "merit",
+    "metal",
+    "midst",
+    "might",
+    "minor",
+    "minus",
+    "mixed",
+    "model",
+    "money",
+    "month",
+    "moral",
+    "motor",
+    "mount",
+    "mouse",
+    "mouth",
+    "movie",
+    "music",
+    "naive",
+    "nerve",
+    "never",
+    "newly",
+    "night",
+    "noble",
+    "noise",
+    "north",
+    "noted",
+    "novel",
+    "nurse",
+    "nylon",
+    "occur",
+    "ocean",
+    "offer",
+    "often",
+    "olive",
+    "onset",
+    "opera",
+    "orbit",
+    "order",
+    "organ",
+    "other",
+    "outer",
+    "owner",
+    "oxide",
+    "ozone",
+    "paint",
+    "panel",
+    "panic",
+    "paper",
+    "party",
+    "patch",
+    "pause",
+    "peace",
+    "pearl",
+    "penny",
+    "phase",
+    "phone",
+    "photo",
+    "piano",
+    "piece",
+    "pilot",
+    "pinch",
+    "pixel",
+    "pizza",
+    "place",
+    "plain",
+    "plane",
+    "plant",
+    "plate",
+    "plaza",
+    "plead",
+    "pluck",
+    "plumb",
+    "plume",
+    "plush",
+    "point",
+    "polar",
+    "pouch",
+    "pound",
+    "power",
+    "press",
+    "price",
+    "pride",
+    "prime",
+    "print",
+    "prior",
+    "prize",
+    "probe",
+    "proof",
+    "proud",
+    "prove",
+    "proxy",
+    "pulse",
+    "pupil",
+    "purse",
+    "quest",
+    "queue",
+    "quick",
+    "quiet",
+    "quilt",
+    "quirk",
+    "quota",
+    "quote",
+    "radar",
+    "radio",
+    "raise",
+    "rally",
+    "ranch",
+    "range",
+    "rapid",
+    "ratio",
+    "reach",
+    "react",
+    "realm",
+    "rebel",
+    "refer",
+    "reign",
+    "relax",
+    "reply",
+    "rider",
+    "ridge",
+    "rifle",
+    "right",
+    "rigid",
+    "rival",
+    "river",
+    "robot",
+    "rocky",
+    "roman",
+    "rouge",
+    "rough",
+    "round",
+    "route",
+    "royal",
+    "rugby",
+    "ruler",
+    "rural",
+    "sadly",
+    "saint",
+    "salad",
+    "salon",
+    "sauce",
+    "scale",
+    "scare",
+    "scene",
+    "scent",
+    "scope",
+    "score",
+    "scout",
+    "screw",
+    "sense",
+    "serve",
+    "setup",
+    "seven",
+    "shade",
+    "shaft",
+    "shake",
+    "shall",
+    "shame",
+    "shape",
+    "share",
+    "shark",
+    "sharp",
+    "sheep",
+    "sheer",
+    "sheet",
+    "shelf",
+    "shell",
+    "shift",
+    "shine",
+    "shirt",
+    "shock",
+    "shoot",
+    "shore",
+    "short",
+    "shout",
+    "sight",
+    "sigma",
+    "since",
+    "sixth",
+    "sixty",
+    "sized",
+    "skill",
+    "skull",
+    "slash",
+    "sleep",
+    "slice",
+    "slide",
+    "small",
+    "smart",
+    "smell",
+    "smile",
+    "smith",
+    "smoke",
+    "snack",
+    "snake",
+    "solar",
+    "solid",
+    "solve",
+    "sorry",
+    "sound",
+    "south",
+    "space",
+    "spare",
+    "spark",
+    "speak",
+    "speed",
+    "spell",
+    "spend",
+    "spice",
+    "spill",
+    "spine",
+    "spite",
+    "split",
+    "spoke",
+    "sport",
+    "spray",
+    "squad",
+    "stack",
+    "staff",
+    "stage",
+    "stain",
+    "stair",
+    "stake",
+    "stale",
+    "stall",
+    "stamp",
+    "stand",
+    "stare",
+    "start",
+    "state",
+    "steam",
+    "steel",
+    "steep",
+    "steer",
+    "stern",
+    "stick",
+    "still",
+    "stock",
+    "stone",
+    "stood",
+    "store",
+    "storm",
+    "story",
+    "stove",
+    "stuff",
+    "style",
+    "sugar",
+    "suite",
+    "sunny",
+    "super",
+    "surge",
+    "swamp",
+    "swear",
+    "sweep",
+    "sweet",
+    "swept",
+    "swift",
+    "swing",
+    "sword",
+    "swore",
+    "sworn",
+    "syrup",
+    "table",
+    "taken",
+    "taste",
+    "teach",
+    "teeth",
+    "temple",
+    "their",
+    "theme",
+    "there",
+    "these",
+    "thick",
+    "thing",
+    "think",
+    "third",
+    "those",
+    "three",
+    "threw",
+    "throw",
+    "thumb",
+    "tidal",
+    "tight",
+    "timer",
+    "tired",
+    "title",
+    "today",
+    "token",
+    "topic",
+    "total",
+    "touch",
+    "tough",
+    "tower",
+    "toxic",
+    "trace",
+    "track",
+    "trade",
+    "trail",
+    "train",
+    "trait",
+    "trash",
+    "treat",
+    "trend",
+    "trial",
+    "tribe",
+    "trick",
+    "troop",
+    "truck",
+    "truly",
+    "trunk",
+    "trust",
+    "truth",
+    "tumor",
+    "twice",
+    "twist",
+    "ultra",
+    "uncle",
+    "under",
+    "undue",
+    "unfit",
+    "union",
+    "unite",
+    "unity",
+    "until",
+    "upper",
+    "upset",
+    "urban",
+    "usage",
+    "usual",
+    "valid",
+    "value",
+    "valve",
+    "vault",
+    "venue",
+    "verse",
+    "video",
+    "vigor",
+    "vinyl",
+    "viral",
+    "virtue",
+    "visit",
+    "vista",
+    "vital",
+    "vivid",
+    "vocal",
+    "voice",
+    "voter",
+    "waist",
+    "watch",
+    "water",
+    "weigh",
+    "weird",
+    "wheat",
+    "wheel",
+    "where",
+    "which",
+    "while",
+    "white",
+    "whole",
+    "whose",
+    "wider",
+    "witch",
+    "woman",
+    "world",
+    "worry",
+    "worse",
+    "worst",
+    "worth",
+    "would",
+    "wound",
+    "wrist",
+    "write",
+    "wrong",
+    "wrote",
+    "yacht",
+    "yield",
+    "young",
+    "youth"
   ],
   "targets": [
-    { "id": 1, "word": "crane", "difficulty": 3.0 },
-    { "id": 2, "word": "apple", "difficulty": 3.0 },
-    { "id": 3, "word": "grape", "difficulty": 3.0 },
-    { "id": 4, "word": "style", "difficulty": 3.0 },
-    { "id": 5, "word": "hello", "difficulty": 3.0 },
-    { "id": 6, "word": "world", "difficulty": 3.0 },
-    { "id": 7, "word": "large", "difficulty": 3.0 },
-    { "id": 8, "word": "house", "difficulty": 3.0 },
-    { "id": 9, "word": "brain", "difficulty": 3.0 },
-    { "id": 10, "word": "cloud", "difficulty": 3.0 },
-    { "id": 11, "word": "dance", "difficulty": 3.0 },
-    { "id": 12, "word": "flame", "difficulty": 3.0 },
-    { "id": 13, "word": "giant", "difficulty": 3.0 },
-    { "id": 14, "word": "happy", "difficulty": 3.0 },
-    { "id": 15, "word": "lemon", "difficulty": 3.0 },
-    { "id": 16, "word": "mango", "difficulty": 3.0 },
-    { "id": 17, "word": "noble", "difficulty": 3.0 },
-    { "id": 18, "word": "ocean", "difficulty": 3.0 },
-    { "id": 19, "word": "piano", "difficulty": 3.0 },
-    { "id": 20, "word": "queen", "difficulty": 3.0 },
-    { "id": 21, "word": "sugar", "difficulty": 3.0 },
-    { "id": 22, "word": "tiger", "difficulty": 3.0 },
-    { "id": 23, "word": "vivid", "difficulty": 3.0 },
-    { "id": 24, "word": "waste", "difficulty": 3.0 },
-    { "id": 25, "word": "yield", "difficulty": 3.0 },
-    { "id": 26, "word": "zebra", "difficulty": 3.0 },
-    { "id": 27, "word": "ghost", "difficulty": 3.0 },
-    { "id": 28, "word": "judge", "difficulty": 3.0 },
-    { "id": 29, "word": "knife", "difficulty": 3.0 },
-    { "id": 30, "word": "lucky", "difficulty": 3.0 },
-    { "id": 31, "word": "movie", "difficulty": 3.0 },
-    { "id": 32, "word": "prize", "difficulty": 3.0 },
-    { "id": 33, "word": "quiet", "difficulty": 3.0 },
-    { "id": 34, "word": "solar", "difficulty": 3.0 },
-    { "id": 35, "word": "truly", "difficulty": 3.0 },
-    { "id": 36, "word": "vital", "difficulty": 3.0 },
-    { "id": 37, "word": "wheat", "difficulty": 3.0 },
-    { "id": 38, "word": "xenon", "difficulty": 3.0 },
-    { "id": 39, "word": "yacht", "difficulty": 3.0 },
-    { "id": 40, "word": "zebra", "difficulty": 3.0 }
+    {
+      "id": 1,
+      "word": "crane",
+      "difficulty": 3
+    },
+    {
+      "id": 2,
+      "word": "apple",
+      "difficulty": 3.5
+    },
+    {
+      "id": 3,
+      "word": "grape",
+      "difficulty": 3
+    },
+    {
+      "id": 4,
+      "word": "style",
+      "difficulty": 2.5
+    },
+    {
+      "id": 5,
+      "word": "hello",
+      "difficulty": 3
+    },
+    {
+      "id": 6,
+      "word": "world",
+      "difficulty": 2.5
+    },
+    {
+      "id": 7,
+      "word": "large",
+      "difficulty": 2
+    },
+    {
+      "id": 8,
+      "word": "house",
+      "difficulty": 2.5
+    },
+    {
+      "id": 9,
+      "word": "brain",
+      "difficulty": 3
+    },
+    {
+      "id": 10,
+      "word": "cloud",
+      "difficulty": 3
+    },
+    {
+      "id": 11,
+      "word": "dance",
+      "difficulty": 3
+    },
+    {
+      "id": 12,
+      "word": "flame",
+      "difficulty": 3
+    },
+    {
+      "id": 13,
+      "word": "giant",
+      "difficulty": 3
+    },
+    {
+      "id": 14,
+      "word": "happy",
+      "difficulty": 3
+    },
+    {
+      "id": 15,
+      "word": "lemon",
+      "difficulty": 2
+    },
+    {
+      "id": 16,
+      "word": "mango",
+      "difficulty": 3
+    },
+    {
+      "id": 17,
+      "word": "noble",
+      "difficulty": 3
+    },
+    {
+      "id": 18,
+      "word": "ocean",
+      "difficulty": 2.5
+    },
+    {
+      "id": 19,
+      "word": "piano",
+      "difficulty": 3
+    },
+    {
+      "id": 20,
+      "word": "queen",
+      "difficulty": 3
+    },
+    {
+      "id": 21,
+      "word": "sugar",
+      "difficulty": 3
+    },
+    {
+      "id": 22,
+      "word": "tiger",
+      "difficulty": 3
+    },
+    {
+      "id": 23,
+      "word": "vivid",
+      "difficulty": 3
+    },
+    {
+      "id": 24,
+      "word": "waste",
+      "difficulty": 3
+    },
+    {
+      "id": 25,
+      "word": "yield",
+      "difficulty": 3
+    },
+    {
+      "id": 26,
+      "word": "zebra",
+      "difficulty": 3
+    },
+    {
+      "id": 27,
+      "word": "ghost",
+      "difficulty": 3
+    },
+    {
+      "id": 28,
+      "word": "judge",
+      "difficulty": 3.5
+    },
+    {
+      "id": 29,
+      "word": "knife",
+      "difficulty": 4
+    },
+    {
+      "id": 30,
+      "word": "lucky",
+      "difficulty": 3
+    },
+    {
+      "id": 31,
+      "word": "movie",
+      "difficulty": 3
+    },
+    {
+      "id": 32,
+      "word": "prize",
+      "difficulty": 5
+    },
+    {
+      "id": 33,
+      "word": "quiet",
+      "difficulty": 3
+    },
+    {
+      "id": 34,
+      "word": "solar",
+      "difficulty": 4
+    },
+    {
+      "id": 35,
+      "word": "truly",
+      "difficulty": 2.5
+    },
+    {
+      "id": 36,
+      "word": "vital",
+      "difficulty": 3.5
+    },
+    {
+      "id": 37,
+      "word": "wheat",
+      "difficulty": 2.5
+    },
+    {
+      "id": 38,
+      "word": "xenon",
+      "difficulty": 3.5
+    },
+    {
+      "id": 39,
+      "word": "yacht",
+      "difficulty": 3
+    },
+    {
+      "id": 40,
+      "word": "zebra",
+      "difficulty": 3
+    }
   ]
 }

+ 1 - 1
package.json

@@ -1,5 +1,5 @@
 {
   "name": "wordle",
   "private": true,
-  "workspaces": ["client", "server", "shared"]
+  "workspaces": ["client", "server", "shared", "solver"]
 }

+ 18 - 0
solver/package.json

@@ -0,0 +1,18 @@
+{
+  "name": "@wordle/solver",
+  "version": "1.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "solve": "tsx src/index.ts",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@wordle/shared": "*"
+  },
+  "devDependencies": {
+    "@types/node": "^22.0.0",
+    "tsx": "^4.0.0",
+    "typescript": "^6.0.0"
+  }
+}

+ 90 - 0
solver/src/entropy.ts

@@ -0,0 +1,90 @@
+/**
+ * Entropy-based Wordle solver.
+ * At each step, picks the guess that maximizes expected information gain
+ * (minimizes expected remaining candidates).
+ */
+
+const ALL_FEEDBACK: string[] = [];
+
+function buildFeedbackPatterns(): string[] {
+  if (ALL_FEEDBACK.length > 0) return ALL_FEEDBACK;
+  const colors = ['g', 'y', 'x'];
+  // Generate all 3^5 = 243 possible feedback patterns
+  for (let i = 0; i < 243; i++) {
+    let n = i;
+    const pattern: string[] = [];
+    for (let j = 0; j < 5; j++) {
+      pattern.push(colors[n % 3]);
+      n = Math.floor(n / 3);
+    }
+    ALL_FEEDBACK.push(pattern.join(''));
+  }
+  return ALL_FEEDBACK;
+}
+
+function getFeedback(target: string, guess: string): string {
+  const targetUpper = target.toLowerCase();
+  const guessUpper = guess.toLowerCase();
+  const counts: Record<string, number> = {};
+  for (const ch of targetUpper) counts[ch] = (counts[ch] ?? 0) + 1;
+
+  const result: string[] = Array(5).fill('x');
+  for (let i = 0; i < 5; i++) {
+    if (guessUpper[i] === targetUpper[i]) {
+      result[i] = 'g';
+      counts[guessUpper[i]]--;
+    }
+  }
+  for (let i = 0; i < 5; i++) {
+    if (result[i] === 'g') continue;
+    if ((counts[guessUpper[i]] ?? 0) > 0) {
+      result[i] = 'y';
+      counts[guessUpper[i]]--;
+    }
+  }
+  return result.join('');
+}
+
+/**
+ * Solve a single word using entropy-based strategy.
+ * Returns the number of attempts (1-6) or 7+ if failed.
+ */
+export function solveEntropy(target: string, allWords: string[], maxAttempts = 6): number {
+  buildFeedbackPatterns();
+  let candidates = [...allWords];
+
+  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+    // Pick best guess
+    let bestGuess = candidates[0];
+    let bestEntropy = Infinity;
+
+    const guessPool = attempt === 1 ? allWords : candidates;
+
+    for (const guess of guessPool) {
+      // Group candidates by feedback pattern for this guess
+      const buckets: Map<string, number> = new Map();
+      for (const candidate of candidates) {
+        const fb = getFeedback(candidate, guess);
+        buckets.set(fb, (buckets.get(fb) ?? 0) + 1);
+      }
+      // Expected remaining candidates = weighted average of bucket sizes
+      let expectedSize = 0;
+      for (const count of buckets.values()) {
+        expectedSize += (count / candidates.length) * count;
+      }
+      if (expectedSize < bestEntropy) {
+        bestEntropy = expectedSize;
+        bestGuess = guess;
+      }
+    }
+
+    const fb = getFeedback(target, bestGuess);
+    if (fb === 'ggggg') return attempt;
+
+    // Filter candidates
+    candidates = candidates.filter((c) => getFeedback(c, bestGuess) === fb);
+    if (candidates.length === 0) return maxAttempts + 1; // should not happen
+  }
+
+  return maxAttempts + 1; // failed
+}

+ 76 - 0
solver/src/frequency.ts

@@ -0,0 +1,76 @@
+/**
+ * Frequency/heuristic-based Wordle solver.
+ * Picks guesses based on letter frequency in remaining candidates.
+ * Simpler and faster than entropy, providing an alternative perspective on difficulty.
+ */
+
+function getFeedback(target: string, guess: string): string {
+  const tu = target.toLowerCase();
+  const gu = guess.toLowerCase();
+  const counts: Record<string, number> = {};
+  for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
+
+  const result: string[] = Array(5).fill('x');
+  for (let i = 0; i < 5; i++) {
+    if (gu[i] === tu[i]) { result[i] = 'g'; counts[gu[i]]--; }
+  }
+  for (let i = 0; i < 5; i++) {
+    if (result[i] === 'g') continue;
+    if ((counts[gu[i]] ?? 0) > 0) { result[i] = 'y'; counts[gu[i]]--; }
+  }
+  return result.join('');
+}
+
+function letterFrequency(words: string[]): Record<string, number> {
+  const freq: Record<string, number> = {};
+  for (const w of words) {
+    const seen = new Set<string>();
+    for (const ch of w) {
+      if (!seen.has(ch)) {
+        freq[ch] = (freq[ch] ?? 0) + 1;
+        seen.add(ch);
+      }
+    }
+  }
+  return freq;
+}
+
+function scoreWord(word: string, freq: Record<string, number>): number {
+  const seen = new Set<string>();
+  let score = 0;
+  for (const ch of word) {
+    if (!seen.has(ch)) {
+      score += freq[ch] ?? 0;
+      seen.add(ch);
+    }
+  }
+  return score;
+}
+
+/**
+ * Solve a single word using frequency-based strategy.
+ * Returns number of attempts.
+ */
+export function solveFrequency(target: string, allWords: string[], maxAttempts = 6): number {
+  let candidates = [...allWords];
+
+  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+    const freq = letterFrequency(candidates);
+    const pool = attempt === 1 ? allWords : candidates;
+
+    let bestGuess = pool[0];
+    let bestScore = -1;
+    for (const g of pool) {
+      const s = scoreWord(g, freq);
+      if (s > bestScore) { bestScore = s; bestGuess = g; }
+    }
+
+    const fb = getFeedback(target, bestGuess);
+    if (fb === 'ggggg') return attempt;
+
+    candidates = candidates.filter((c) => getFeedback(c, bestGuess) === fb);
+    if (candidates.length === 0) return maxAttempts + 1;
+  }
+
+  return maxAttempts + 1;
+}

+ 54 - 0
solver/src/index.ts

@@ -0,0 +1,54 @@
+import { readFileSync, writeFileSync } from 'fs';
+import { resolve, dirname } from 'path';
+import { fileURLToPath } from 'url';
+import type { WordBank } from '@wordle/shared';
+import { solveEntropy } from './entropy.js';
+import { solveFrequency } from './frequency.js';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const BANK_PATH = resolve(__dirname, '../../data/word-bank.json');
+const MAX_ATTEMPTS = 6;
+
+console.log('Loading word bank...');
+const bank: WordBank = JSON.parse(readFileSync(BANK_PATH, 'utf-8'));
+const allWords = bank.guessable;
+
+console.log(`Solving ${bank.targets.length} target words with ${allWords.length} guessable words...`);
+
+let solved = 0;
+let failed = 0;
+const newTargets = [];
+
+for (const target of bank.targets) {
+  const entropyAttempts = solveEntropy(target.word, allWords, MAX_ATTEMPTS);
+  const freqAttempts = solveFrequency(target.word, allWords, MAX_ATTEMPTS);
+
+  if (entropyAttempts > MAX_ATTEMPTS && freqAttempts > MAX_ATTEMPTS) {
+    console.log(`  REMOVED: ${target.word} (unsolvable in ${MAX_ATTEMPTS})`);
+    failed++;
+    continue;
+  }
+
+  const difficulty = Math.round(((entropyAttempts + freqAttempts) / 2) * 100) / 100;
+
+  newTargets.push({
+    ...target,
+    difficulty,
+  });
+
+  solved++;
+  if (solved % 10 === 0) {
+    console.log(`  ${solved}/${bank.targets.length}...`);
+  }
+}
+
+// Write updated bank
+const newBank: WordBank = {
+  guessable: bank.guessable,
+  targets: newTargets,
+};
+
+writeFileSync(BANK_PATH, JSON.stringify(newBank, null, 2) + '\n', 'utf-8');
+
+console.log(`\nDone! ${solved} solved, ${failed} removed.`);
+console.log(`Updated ${BANK_PATH}`);

+ 16 - 0
solver/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "declaration": true
+  },
+  "include": ["src/**/*.ts"]
+}