Browse Source

refactor: compute difficulty and replays at runtime instead of storing in word-bank

Moved solver algorithms from solver/ to shared/src/solvers/ so the
server can import them. On server startup, difficulty scores are
pre-computed for all targets (~2s). On the last guess, replays are
computed at runtime (~20ms).

Removed 'difficulty' and 'replays' fields from TargetWord type and
both word-bank JSON files. Files shrunk from ~60KB/71KB to ~32KB/39KB.

Entropy solver kept for offline validation only (14s per target);
runtime uses 3 fast solvers (Frequency, Vowel First, Greedy).
Oleg Panashchenko 1 week ago
parent
commit
7ba27407f5

+ 40 - 1551
data/word-bank-ru.json

@@ -2068,1674 +2068,163 @@
   "targets": [
     {
       "id": 1,
-      "word": "дрема",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "грена",
-            "дрема"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "греза",
-            "дрена",
-            "дрема"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "герма",
-            "дрема"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вдова",
-            "груда",
-            "дрема"
-          ]
-        }
-      ]
+      "word": "дрема"
     },
     {
       "id": 2,
-      "word": "басня",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "саман",
-            "нанос",
-            "басня"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "ангел",
-            "мания",
-            "башня",
-            "басня"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "бадья",
-            "басня"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "багет",
-            "бадья",
-            "басня"
-          ]
-        }
-      ]
+      "word": "басня"
     },
     {
       "id": 3,
-      "word": "опала",
-      "difficulty": 4.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "глина",
-            "булла",
-            "опала"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "масон",
-            "олива",
-            "опала"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "доска",
-            "миома",
-            "ольха",
-            "опала"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "влага",
-            "лиана",
-            "опала"
-          ]
-        }
-      ]
+      "word": "опала"
     },
     {
       "id": 4,
-      "word": "накра",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "барка",
-            "накра"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "анкер",
-            "накра"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гарус",
-            "радий",
-            "накра"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вакса",
-            "накра"
-          ]
-        }
-      ]
+      "word": "накра"
     },
     {
       "id": 5,
-      "word": "мерка",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "лерка",
-            "мерка"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "анкер",
-            "лерка",
-            "мерка"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "герма",
-            "мерка"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ветка",
-            "лепка",
-            "мекка",
-            "мерка"
-          ]
-        }
-      ]
+      "word": "мерка"
     },
     {
       "id": 6,
-      "word": "зраза",
-      "difficulty": 4.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "пурга",
-            "флора",
-            "жрица",
-            "зраза"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "греза",
-            "друза",
-            "фраза",
-            "зраза"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гарус",
-            "дойра",
-            "жрица",
-            "зраза"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "влага",
-            "диана",
-            "зраза"
-          ]
-        }
-      ]
+      "word": "зраза"
     },
     {
       "id": 7,
-      "word": "детва",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "ветла",
-            "детва"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "стела",
-            "детва"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "вежда",
-            "детва"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вдова",
-            "детва"
-          ]
-        }
-      ]
+      "word": "детва"
     },
     {
       "id": 8,
-      "word": "гогот",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "нилот",
-            "хобот",
-            "гогот"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "полет",
-            "софит",
-            "хобот",
-            "гогот"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "глист",
-            "гогот"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "гогот"
-          ]
-        }
-      ]
+      "word": "гогот"
     },
     {
       "id": 9,
-      "word": "топаз",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "отпал",
-            "топаз"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "отпал",
-            "топаз"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "показ",
-            "топаз"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вагон",
-            "отмах",
-            "поташ",
-            "топаз"
-          ]
-        }
-      ]
+      "word": "топаз"
     },
     {
       "id": 10,
-      "word": "билль",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "силон",
-            "фильм",
-            "билль"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "селин",
-            "фильм",
-            "билль"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "бидон",
-            "битюг",
-            "билль"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "бегун",
-            "билль"
-          ]
-        }
-      ]
+      "word": "билль"
     },
     {
       "id": 11,
-      "word": "бердо",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "рельс",
-            "бедро",
-            "бердо"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "бедро",
-            "бердо"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "бедро",
-            "бердо"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "бегун",
-            "бедро",
-            "бердо"
-          ]
-        }
-      ]
+      "word": "бердо"
     },
     {
       "id": 12,
-      "word": "гряда",
-      "difficulty": 4.67,
-      "replays": [
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "греза",
-            "груда",
-            "гряда"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гарус",
-            "грань",
-            "гроза",
-            "грыжа",
-            "гряда"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вдова",
-            "груда",
-            "гряда"
-          ]
-        }
-      ]
+      "word": "гряда"
     },
     {
       "id": 13,
-      "word": "манеж",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "налеп",
-            "ганец",
-            "манеж"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "ангел",
-            "навес",
-            "манеж"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "ганец",
-            "манеж"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вагон",
-            "манеж"
-          ]
-        }
-      ]
+      "word": "манеж"
     },
     {
       "id": 14,
-      "word": "мопед",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "селен",
-            "иудей",
-            "мопед"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "силон",
-            "обдув",
-            "мопед"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гинея",
-            "дюшес",
-            "мопед"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "домен",
-            "мопед"
-          ]
-        }
-      ]
+      "word": "мопед"
     },
     {
       "id": 15,
-      "word": "батыр",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "батыр"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "тапир",
-            "батыр"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "багор",
-            "батыр"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "багет",
-            "батон",
-            "батыр"
-          ]
-        }
-      ]
+      "word": "батыр"
     },
     {
       "id": 16,
-      "word": "латыш",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "батон",
-            "жатва",
-            "латыш"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "стела",
-            "латыш"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "калий",
-            "лампа",
-            "латыш"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вагон",
-            "задел",
-            "латыш"
-          ]
-        }
-      ]
+      "word": "латыш"
     },
     {
       "id": 17,
-      "word": "тодес",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "повет",
-            "тодес"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "нотис",
-            "тодес"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гинея",
-            "дюшес",
-            "тодес"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "отдел",
-            "тодес"
-          ]
-        }
-      ]
+      "word": "тодес"
     },
     {
       "id": 18,
-      "word": "маска",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "накал",
-            "маска"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "леска",
-            "миска",
-            "маска"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "калий",
-            "дамка",
-            "маска"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "варка",
-            "гайка",
-            "дамка",
-            "маска"
-          ]
-        }
-      ]
+      "word": "маска"
     },
     {
       "id": 19,
-      "word": "финиш",
-      "difficulty": 4.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "силон",
-            "вишня",
-            "финиш"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "селин",
-            "энзим",
-            "финиш"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "глист",
-            "дожим",
-            "киник",
-            "финиш"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "глист",
-            "зипун",
-            "ничья",
-            "финиш"
-          ]
-        }
-      ]
+      "word": "финиш"
     },
     {
       "id": 20,
-      "word": "оркан",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "рысак",
-            "аркан",
-            "оркан"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "оркан"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гарус",
-            "дойра",
-            "кроат",
-            "оркан"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "валек",
-            "дукат",
-            "оркан"
-          ]
-        }
-      ]
+      "word": "оркан"
     },
     {
       "id": 21,
-      "word": "айран",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "пурга",
-            "мираб",
-            "аврал",
-            "айран"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "буран",
-            "айран"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "адряс",
-            "акрил",
-            "айран"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "аврал",
-            "айран"
-          ]
-        }
-      ]
+      "word": "айран"
     },
     {
       "id": 22,
-      "word": "капля",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "калан",
-            "кабил",
-            "кайло",
-            "капля"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "камин",
-            "кабул",
-            "капля"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "калий",
-            "капля"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "валек",
-            "кайло",
-            "капля"
-          ]
-        }
-      ]
+      "word": "капля"
     },
     {
       "id": 23,
-      "word": "топор",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "отбор",
-            "топор"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "строп",
-            "топор"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гипюр",
-            "отпор",
-            "топор"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "гонор",
-            "мотор",
-            "топор"
-          ]
-        }
-      ]
+      "word": "топор"
     },
     {
       "id": 24,
-      "word": "разум",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "маран",
-            "разум"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "дерма",
-            "маляр",
-            "разум"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гарус",
-            "разум"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вагон",
-            "задел",
-            "мазут",
-            "разум"
-          ]
-        }
-      ]
+      "word": "разум"
     },
     {
       "id": 25,
-      "word": "ореол",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "рельс",
-            "ореол"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "ирмос",
-            "ореол"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "герой",
-            "креол",
-            "ореол"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "мохер",
-            "ореол"
-          ]
-        }
-      ]
+      "word": "ореол"
     },
     {
       "id": 26,
-      "word": "кайра",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "кайра"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "капер",
-            "кариб",
-            "кайра"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гарус",
-            "радий",
-            "кайра"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вакса",
-            "казна",
-            "кайла",
-            "кайма",
-            "кайра"
-          ]
-        }
-      ]
+      "word": "кайра"
     },
     {
       "id": 27,
-      "word": "магма",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "саман",
-            "магма"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "ангел",
-            "сигма",
-            "магма"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "магия",
-            "магма"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ванна",
-            "гарда",
-            "магма"
-          ]
-        }
-      ]
+      "word": "магма"
     },
     {
       "id": 28,
-      "word": "пучка",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "солка",
-            "пучка"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "леска",
-            "чинка",
-            "пучка"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "дужка",
-            "пучка"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ветка",
-            "гичка",
-            "кочка",
-            "пучка"
-          ]
-        }
-      ]
+      "word": "пучка"
     },
     {
       "id": 29,
-      "word": "десна",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "слега",
-            "весна",
-            "десна"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "ангел",
-            "десна"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гейша",
-            "десна"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вдова",
-            "груда",
-            "десна"
-          ]
-        }
-      ]
+      "word": "десна"
     },
     {
       "id": 30,
-      "word": "грудь",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "поруб",
-            "друид",
-            "грудь"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "прием",
-            "грудь"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гирло",
-            "грудь"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "грудь"
-          ]
-        }
-      ]
+      "word": "грудь"
     },
     {
       "id": 31,
-      "word": "ситец",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "эстет",
-            "ситец"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "литье",
-            "ситец"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гинея",
-            "жилец",
-            "ситец"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "гжель",
-            "истец",
-            "ситец"
-          ]
-        }
-      ]
+      "word": "ситец"
     },
     {
       "id": 32,
-      "word": "гуцул",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "силон",
-            "дубль",
-            "гуцул"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "селин",
-            "глушь",
-            "гуцул"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "глист",
-            "гуляш",
-            "гуцул"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "глист",
-            "гуляш",
-            "гуцул"
-          ]
-        }
-      ]
+      "word": "гуцул"
     },
     {
       "id": 33,
-      "word": "рытье",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "рытье"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "рытье"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "герой",
-            "дрель",
-            "ружье",
-            "рытье"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "литер",
-            "рытье"
-          ]
-        }
-      ]
+      "word": "рытье"
     },
     {
       "id": 34,
-      "word": "рвань",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "пурга",
-            "аларм",
-            "брасс",
-            "рвань"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "дерма",
-            "авгур",
-            "рвань"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "варяг",
-            "рвань"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вуаль",
-            "рвань"
-          ]
-        }
-      ]
+      "word": "рвань"
     },
     {
       "id": 35,
-      "word": "обмол",
-      "difficulty": 4.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "силон",
-            "могол",
-            "обмол"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "силон",
-            "любое",
-            "обмол"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "обжиг",
-            "обком",
-            "обмол"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "обвес",
-            "обгон",
-            "обзор",
-            "облом",
-            "обмол"
-          ]
-        }
-      ]
+      "word": "обмол"
     },
     {
       "id": 36,
-      "word": "букля",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "силок",
-            "букля"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "фикус",
-            "букля"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "бидон",
-            "букля"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "бекон",
-            "букля"
-          ]
-        }
-      ]
+      "word": "букля"
     },
     {
       "id": 37,
-      "word": "хасид",
-      "difficulty": 4.33,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "саман",
-            "фазис",
-            "хасид"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "ангел",
-            "спазм",
-            "хадис",
-            "хасид"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "вагон",
-            "задел",
-            "хасид"
-          ]
-        }
-      ]
+      "word": "хасид"
     },
     {
       "id": 38,
-      "word": "житие",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "метил",
-            "житие"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "литье",
-            "житие"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "гелий",
-            "житие"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "гжель",
-            "житие"
-          ]
-        }
-      ]
+      "word": "житие"
     },
     {
       "id": 39,
-      "word": "синод",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "силон",
-            "синод"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "силон",
-            "синод"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "глист",
-            "износ",
-            "синод"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "ведро",
-            "голод",
-            "истод",
-            "синод"
-          ]
-        }
-      ]
+      "word": "синод"
     },
     {
       "id": 40,
-      "word": "гайка",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "катер",
-            "накал",
-            "маска",
-            "цапка",
-            "гайка"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "кроат",
-            "леска",
-            "чинка",
-            "будка",
-            "вышка",
-            "гайка"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "абвер",
-            "газон",
-            "гайка"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "абака",
-            "варка",
-            "гайка"
-          ]
-        }
-      ]
+      "word": "гайка"
     }
   ]
 }

+ 40 - 1559
data/word-bank.json

@@ -2313,1682 +2313,163 @@
   "targets": [
     {
       "id": 1,
-      "word": "crane",
-      "difficulty": 2.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "grace",
-            "crane"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "grace",
-            "crane"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "chafe",
-            "crane"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane"
-          ]
-        }
-      ]
+      "word": "crane"
     },
     {
       "id": 2,
-      "word": "apple",
-      "difficulty": 4.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "plate",
-            "ample",
-            "apple"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "amble",
-            "angle",
-            "apple"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "agree",
-            "amble",
-            "ankle",
-            "apple"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "abide",
-            "algae",
-            "ample",
-            "apple"
-          ]
-        }
-      ]
+      "word": "apple"
     },
     {
       "id": 3,
-      "word": "grape",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "grace",
-            "grade",
-            "grape"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "grace",
-            "grade",
-            "grape"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "blame",
-            "grape"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "brake",
-            "drape",
-            "grape"
-          ]
-        }
-      ]
+      "word": "grape"
     },
     {
       "id": 4,
-      "word": "style",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "stone",
-            "style"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "those",
-            "style"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "emcee",
-            "style"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "belie",
-            "fluke",
-            "lodge",
-            "style"
-          ]
-        }
-      ]
+      "word": "style"
     },
     {
       "id": 5,
-      "word": "hello",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "betel",
-            "cello",
-            "hello"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "olden",
-            "below",
-            "cello",
-            "hello"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "gecko",
-            "metro",
-            "hello"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "beefy",
-            "depot",
-            "hello"
-          ]
-        }
-      ]
+      "word": "hello"
     },
     {
       "id": 6,
-      "word": "world",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "truly",
-            "world"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "scour",
-            "dowry",
-            "world"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "erode",
-            "dowry",
-            "world"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "birth",
-            "forgo",
-            "lorry",
-            "world"
-          ]
-        }
-      ]
+      "word": "world"
     },
     {
       "id": 7,
-      "word": "large",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "barge",
-            "large"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "barge",
-            "large"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "barge",
-            "large"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "adore",
-            "barge",
-            "large"
-          ]
-        }
-      ]
+      "word": "large"
     },
     {
       "id": 8,
-      "word": "house",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "loose",
-            "copse",
-            "house"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "louse",
-            "house"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "boule",
-            "coupe",
-            "gouge",
-            "house"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "belie",
-            "dodge",
-            "house"
-          ]
-        }
-      ]
+      "word": "house"
     },
     {
       "id": 9,
-      "word": "brain",
-      "difficulty": 2.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "trail",
-            "braid",
-            "brain"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "brain"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "email",
-            "brain"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "brain"
-          ]
-        }
-      ]
+      "word": "brain"
     },
     {
       "id": 10,
-      "word": "cloud",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "could",
-            "cloud"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "lousy",
-            "cloud"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "bound",
-            "cloud"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "chick",
-            "cloud"
-          ]
-        }
-      ]
+      "word": "cloud"
     },
     {
       "id": 11,
-      "word": "dance",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "cable",
-            "dance"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "amble",
-            "cause",
-            "dance"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "evade",
-            "dance"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "dance"
-          ]
-        }
-      ]
+      "word": "dance"
     },
     {
       "id": 12,
-      "word": "flame",
-      "difficulty": 4.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "plate",
-            "blade",
-            "flake",
-            "flame"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "shale",
-            "blade",
-            "flake",
-            "flame"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "blame",
-            "flame"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "abase",
-            "elate",
-            "flake",
-            "flame"
-          ]
-        }
-      ]
+      "word": "flame"
     },
     {
       "id": 13,
-      "word": "giant",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tidal",
-            "giant"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "stain",
-            "giant"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "image",
-            "giant"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "bland",
-            "giant"
-          ]
-        }
-      ]
+      "word": "giant"
     },
     {
       "id": 14,
-      "word": "happy",
-      "difficulty": 5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tangy",
-            "bawdy",
-            "happy"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "manly",
-            "saucy",
-            "bawdy",
-            "happy"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "balmy",
-            "fanny",
-            "gawky",
-            "happy"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "abbot",
-            "daddy",
-            "gaily",
-            "happy"
-          ]
-        }
-      ]
+      "word": "happy"
     },
     {
       "id": 15,
-      "word": "lemon",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "betel",
-            "cello",
-            "lemon"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "olden",
-            "lemon"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elope",
-            "below",
-            "lemon"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "begin",
-            "demon",
-            "lemon"
-          ]
-        }
-      ]
+      "word": "lemon"
     },
     {
       "id": 16,
-      "word": "mango",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tangy",
-            "manga",
-            "mango"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "manly",
-            "mango"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cameo",
-            "mango"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "admin",
-            "manga",
-            "mango"
-          ]
-        }
-      ]
+      "word": "mango"
     },
     {
       "id": 17,
-      "word": "noble",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "lunge",
-            "clone",
-            "noble"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "louse",
-            "noble"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elope",
-            "noble"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "binge",
-            "noble"
-          ]
-        }
-      ]
+      "word": "noble"
     },
     {
       "id": 18,
-      "word": "ocean",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "cleat",
-            "ocean"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "laden",
-            "began",
-            "ocean"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "canoe",
-            "ocean"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "ocean"
-          ]
-        }
-      ]
+      "word": "ocean"
     },
     {
       "id": 19,
-      "word": "piano",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tidal",
-            "ninja",
-            "piano"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "slain",
-            "piano"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "piano"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "bland",
-            "giant",
-            "piano"
-          ]
-        }
-      ]
+      "word": "piano"
     },
     {
       "id": 20,
-      "word": "queen",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "betel",
-            "cheek",
-            "queen"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "olden",
-            "hymen",
-            "queen"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "puree",
-            "queen"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "begin",
-            "dozen",
-            "hymen",
-            "queen"
-          ]
-        }
-      ]
+      "word": "queen"
     },
     {
       "id": 21,
-      "word": "sugar",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "strap",
-            "solar",
-            "sugar"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "royal",
-            "scram",
-            "sugar"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "quake",
-            "human",
-            "rural",
-            "sugar"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "abhor",
-            "radar",
-            "sugar"
-          ]
-        }
-      ]
+      "word": "sugar"
     },
     {
       "id": 22,
-      "word": "tiger",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "diner",
-            "giver",
-            "tiger"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "merit",
-            "tiger"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elite",
-            "inept",
-            "tiger"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "beret",
-            "other",
-            "tiger"
-          ]
-        }
-      ]
+      "word": "tiger"
     },
     {
       "id": 23,
-      "word": "vivid",
-      "difficulty": 4.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "pilot",
-            "windy",
-            "vivid"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "lying",
-            "disco",
-            "vivid"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "debit",
-            "lipid",
-            "rigid",
-            "vivid"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "biddy",
-            "digit",
-            "lipid",
-            "vivid"
-          ]
-        }
-      ]
+      "word": "vivid"
     },
     {
       "id": 25,
-      "word": "yield",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "widen",
-            "field",
-            "yield"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "deign",
-            "field",
-            "wield",
-            "yield"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elide",
-            "field",
-            "wield",
-            "yield"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "beefy",
-            "yield"
-          ]
-        }
-      ]
+      "word": "yield"
     },
     {
       "id": 26,
-      "word": "zebra",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tread",
-            "learn",
-            "zebra"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "paler",
-            "rehab",
-            "zebra"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "began",
-            "zebra"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "after",
-            "early",
-            "opera",
-            "zebra"
-          ]
-        }
-      ]
+      "word": "zebra"
     },
     {
       "id": 27,
-      "word": "ghost",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "floss",
-            "boost",
-            "ghost"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "shout",
-            "ghost"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elope",
-            "broth",
-            "ghost"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "biddy",
-            "floss",
-            "ghost"
-          ]
-        }
-      ]
+      "word": "ghost"
     },
     {
       "id": 28,
-      "word": "judge",
-      "difficulty": 5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "lunge",
-            "budge",
-            "fudge",
-            "judge"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "louse",
-            "nudge",
-            "budge",
-            "fudge",
-            "judge"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "budge",
-            "fudge",
-            "judge"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "belie",
-            "dodge",
-            "fudge",
-            "judge"
-          ]
-        }
-      ]
+      "word": "judge"
     },
     {
       "id": 29,
-      "word": "knife",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "utile",
-            "opine",
-            "knife"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "since",
-            "opine",
-            "knife"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elite",
-            "brine",
-            "knife"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "binge",
-            "knife"
-          ]
-        }
-      ]
+      "word": "knife"
     },
     {
       "id": 30,
-      "word": "lucky",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "could",
-            "gulch",
-            "lucky"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "lousy",
-            "lucky"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "puree",
-            "bulky",
-            "lucky"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "block",
-            "lucky"
-          ]
-        }
-      ]
+      "word": "lucky"
     },
     {
       "id": 31,
-      "word": "movie",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "binge",
-            "cutie",
-            "movie"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "since",
-            "glide",
-            "movie"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "movie"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "belie",
-            "movie"
-          ]
-        }
-      ]
+      "word": "movie"
     },
     {
       "id": 32,
-      "word": "prize",
-      "difficulty": 5.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tripe",
-            "price",
-            "pride",
-            "prime",
-            "prize"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "pride",
-            "price",
-            "prime",
-            "prize"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elite",
-            "brine",
-            "crime",
-            "gripe",
-            "prize"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "bribe",
-            "drive",
-            "grime",
-            "prize"
-          ]
-        }
-      ]
+      "word": "prize"
     },
     {
       "id": 33,
-      "word": "quiet",
-      "difficulty": 3.5,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "edict",
-            "quiet"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "fetid",
-            "eight",
-            "quiet"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "guile",
-            "quiet"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "beefy",
-            "dowel",
-            "quiet"
-          ]
-        }
-      ]
+      "word": "quiet"
     },
     {
       "id": 34,
-      "word": "solar",
-      "difficulty": 4.33,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "strap",
-            "solar"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "royal",
-            "molar",
-            "polar",
-            "solar"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "abhor",
-            "molar",
-            "polar",
-            "solar"
-          ]
-        }
-      ]
+      "word": "solar"
     },
     {
       "id": 35,
-      "word": "truly",
-      "difficulty": 3.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "truly"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "crust",
-            "truly"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "ensue",
-            "blurt",
-            "truly"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "brisk",
-            "droll",
-            "truly"
-          ]
-        }
-      ]
+      "word": "truly"
     },
     {
       "id": 36,
-      "word": "vital",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tidal",
-            "vital"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "antic",
-            "vital"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "image",
-            "cairn",
-            "pizza",
-            "vital"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "abbot",
-            "datum",
-            "vital"
-          ]
-        }
-      ]
+      "word": "vital"
     },
     {
       "id": 37,
-      "word": "wheat",
-      "difficulty": 4.25,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "cleat",
-            "wheat"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "steal",
-            "wheat"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "bagel",
-            "extra",
-            "tweak",
-            "wheat"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "abbey",
-            "delta",
-            "steak",
-            "wheat"
-          ]
-        }
-      ]
+      "word": "wheat"
     },
     {
       "id": 39,
-      "word": "yacht",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "tangy",
-            "yacht"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "talon",
-            "match",
-            "yacht"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "cease",
-            "batch",
-            "yacht"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "ascot",
-            "tacit",
-            "yacht"
-          ]
-        }
-      ]
+      "word": "yacht"
     },
     {
       "id": 41,
-      "word": "quack",
-      "difficulty": 3.75,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "cloak",
-            "aback",
-            "knack",
-            "quack"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "slack",
-            "quack"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "quake",
-            "quack"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "aback",
-            "flack",
-            "quack"
-          ]
-        }
-      ]
+      "word": "quack"
     },
     {
       "id": 42,
-      "word": "blend",
-      "difficulty": 3,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "betel",
-            "blend"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "olden",
-            "blend"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "bleed",
-            "blend"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "being",
-            "blend"
-          ]
-        }
-      ]
+      "word": "blend"
     },
     {
       "id": 43,
-      "word": "spire",
-      "difficulty": 4,
-      "replays": [
-        {
-          "solver": "Entropy",
-          "steps": [
-            "raise",
-            "shire",
-            "spire"
-          ]
-        },
-        {
-          "solver": "Frequency",
-          "steps": [
-            "irate",
-            "dirge",
-            "shire",
-            "spire"
-          ]
-        },
-        {
-          "solver": "Vowel First",
-          "steps": [
-            "audio",
-            "elite",
-            "brine",
-            "shire",
-            "spire"
-          ]
-        },
-        {
-          "solver": "Greedy",
-          "steps": [
-            "crane",
-            "dirge",
-            "shire",
-            "spire"
-          ]
-        }
-      ]
+      "word": "spire"
     }
   ]
 }

+ 19 - 3
server/src/index.ts

@@ -3,6 +3,7 @@ import { readFileSync, existsSync } from 'fs';
 import { resolve, dirname } from 'path';
 import { fileURLToPath } from 'url';
 import type { WordBank } from '@wordle/shared';
+import { computeDifficulty } from '@wordle/shared';
 import { createDailyRouter } from './routes/daily.js';
 import { createGuessRouter } from './routes/guess.js';
 import { createPlayAgainRouter } from './routes/play-again.js';
@@ -15,23 +16,38 @@ function loadBank(filename: string): WordBank {
   return JSON.parse(readFileSync(path, 'utf-8'));
 }
 
-function mountApi(prefix: string, bank: WordBank) {
+/** Pre-compute difficulty scores for all targets at startup (used by Play Again). */
+function buildDifficultyMap(bank: WordBank): Map<number, number> {
+  const map = new Map<number, number>();
+  for (const t of bank.targets) {
+    map.set(t.id, computeDifficulty(t.word, bank.guessable));
+  }
+  return map;
+}
+
+function mountApi(prefix: string, bank: WordBank, difficultyMap: Map<number, number>) {
   return [
     [prefix + '/daily', createDailyRouter(bank)],
     [prefix + '/validate', createValidateRouter(bank)],
     [prefix + '/guess', createGuessRouter(bank)],
-    [prefix + '/play-again', createPlayAgainRouter(bank)],
+    [prefix + '/play-again', createPlayAgainRouter(bank, difficultyMap)],
   ] as const;
 }
 
 const enBank = loadBank('word-bank.json');
 const ruBank = loadBank('word-bank-ru.json');
 
+const enDifficulty = buildDifficultyMap(enBank);
+const ruDifficulty = buildDifficultyMap(ruBank);
+
 const app = express();
 app.use(express.json());
 
 // Mount API routes for both languages
-for (const [path, router] of [...mountApi('/api', enBank), ...mountApi('/ru/api', ruBank)]) {
+for (const [path, router] of [
+  ...mountApi('/api', enBank, enDifficulty),
+  ...mountApi('/ru/api', ruBank, ruDifficulty),
+]) {
   app.use(path, router);
 }
 

+ 4 - 2
server/src/play-again.ts

@@ -12,7 +12,8 @@ import type { TargetWord } from '@wordle/shared';
  */
 export function selectPlayAgainWord(
   targets: TargetWord[],
-  skillMetric: number
+  skillMetric: number,
+  difficultyMap: Map<number, number>,
 ): TargetWord {
   if (targets.length === 0) {
     throw new Error('No target words available');
@@ -20,7 +21,8 @@ export function selectPlayAgainWord(
 
   // Compute weights: closer difficulty = higher weight
   const weights = targets.map((t) => {
-    const delta = t.difficulty - skillMetric;
+    const diff = difficultyMap.get(t.id) ?? 3.0;
+    const delta = diff - skillMetric;
     return 1 / (1 + delta * delta);
   });
 

+ 5 - 5
server/src/routes/guess.ts

@@ -2,6 +2,7 @@ import { Router, Request, Response } from 'express';
 import type { WordBank, GuessResponse } from '@wordle/shared';
 import { isValidGuess, getTargetWord } from '../validate.js';
 import { computeFeedback } from '../feedback.js';
+import { computeReplays } from '@wordle/shared';
 
 export function createGuessRouter(wordBank: WordBank): Router {
   const router = Router();
@@ -50,12 +51,11 @@ export function createGuessRouter(wordBank: WordBank): Router {
       response.word = target.word;
     }
 
-    // Include difficulty and replays on final guess (win or loss)
+    // Compute difficulty and replays at runtime on final guess (win or loss)
     if (correct || isLastAttempt) {
-      response.difficulty = target.difficulty;
-      if (target.replays) {
-        response.replays = target.replays;
-      }
+      const { difficulty, replays } = computeReplays(target.word, wordBank.guessable);
+      response.difficulty = difficulty;
+      response.replays = replays;
     }
 
     res.json(response);

+ 2 - 2
server/src/routes/play-again.ts

@@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express';
 import type { WordBank } from '@wordle/shared';
 import { selectPlayAgainWord } from '../play-again.js';
 
-export function createPlayAgainRouter(wordBank: WordBank): Router {
+export function createPlayAgainRouter(wordBank: WordBank, difficultyMap: Map<number, number>): Router {
   const router = Router();
 
   router.post('/', (req: Request, res: Response) => {
@@ -13,7 +13,7 @@ export function createPlayAgainRouter(wordBank: WordBank): Router {
       return;
     }
 
-    const target = selectPlayAgainWord(wordBank.targets, skillMetric);
+    const target = selectPlayAgainWord(wordBank.targets, skillMetric, difficultyMap);
     res.json({ wordId: target.id });
   });
 

+ 1 - 0
shared/package.json

@@ -2,6 +2,7 @@
   "name": "@wordle/shared",
   "version": "1.0.0",
   "private": true,
+  "type": "module",
   "main": "./src/index.ts",
   "types": "./src/index.ts",
   "scripts": {

+ 1 - 0
shared/src/index.ts

@@ -1,3 +1,4 @@
 export * from './api.js';
 export * from './game.js';
 export * from './word-bank.js';
+export * from './solvers/index.js';

+ 2 - 24
solver/src/entropy.ts → shared/src/solvers/entropy.ts

@@ -1,28 +1,6 @@
-const ALL_FEEDBACK: string[] = [];
-
-function buildFeedbackPatterns(): string[] {
-  if (ALL_FEEDBACK.length > 0) return ALL_FEEDBACK;
-  const colors = ['g', 'y', 'x'];
-  for (let i = 0; i < 243; i++) {
-    let n = i; const p: string[] = [];
-    for (let j = 0; j < 5; j++) { p.push(colors[n % 3]); n = Math.floor(n / 3); }
-    ALL_FEEDBACK.push(p.join(''));
-  }
-  return ALL_FEEDBACK;
-}
-
-function getFeedback(target: string, guess: string): string {
-  const tu = target.toLowerCase(), gu = guess.toLowerCase();
-  const counts: Record<string, number> = {};
-  for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
-  const r: string[] = Array(5).fill('x');
-  for (let i = 0; i < 5; i++) { if (gu[i] === tu[i]) { r[i] = 'g'; counts[gu[i]]--; } }
-  for (let i = 0; i < 5; i++) { if (r[i] === 'g') continue; if ((counts[gu[i]] ?? 0) > 0) { r[i] = 'y'; counts[gu[i]]--; } }
-  return r.join('');
-}
+import { getFeedback, matchesFeedback } from './feedback.js';
 
 export function solveEntropy(target: string, allWords: string[], maxAttempts = 6): { attempts: number; steps: string[] } {
-  buildFeedbackPatterns();
   let candidates = [...allWords];
   const steps: string[] = [];
 
@@ -47,7 +25,7 @@ export function solveEntropy(target: string, allWords: string[], maxAttempts = 6
 
     if (fb === 'ggggg') return { attempts: attempt, steps };
 
-    candidates = candidates.filter((c) => getFeedback(c, bestGuess) === fb);
+    candidates = candidates.filter((c) => matchesFeedback(c, bestGuess, fb));
     if (candidates.length === 0) return { attempts: maxAttempts + 1, steps };
   }
 

+ 14 - 0
shared/src/solvers/feedback.ts

@@ -0,0 +1,14 @@
+/** Compute Wordle feedback pattern for a guess against the target word. */
+export function getFeedback(target: string, guess: string): string {
+  const tu = target.toLowerCase(), gu = guess.toLowerCase();
+  const counts: Record<string, number> = {};
+  for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
+  const r: string[] = Array(5).fill('x');
+  for (let i = 0; i < 5; i++) { if (gu[i] === tu[i]) { r[i] = 'g'; counts[gu[i]]--; } }
+  for (let i = 0; i < 5; i++) { if (r[i] === 'g') continue; if ((counts[gu[i]] ?? 0) > 0) { r[i] = 'y'; counts[gu[i]]--; } }
+  return r.join('');
+}
+
+export function matchesFeedback(candidate: string, guess: string, feedback: string): boolean {
+  return getFeedback(candidate, guess) === feedback;
+}

+ 2 - 10
solver/src/frequency.ts → shared/src/solvers/frequency.ts

@@ -1,12 +1,4 @@
-function getFeedback(target: string, guess: string): string {
-  const tu = target.toLowerCase(), gu = guess.toLowerCase();
-  const counts: Record<string, number> = {};
-  for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
-  const r: string[] = Array(5).fill('x');
-  for (let i = 0; i < 5; i++) { if (gu[i] === tu[i]) { r[i] = 'g'; counts[gu[i]]--; } }
-  for (let i = 0; i < 5; i++) { if (r[i] === 'g') continue; if ((counts[gu[i]] ?? 0) > 0) { r[i] = 'y'; counts[gu[i]]--; } }
-  return r.join('');
-}
+import { getFeedback, matchesFeedback } from './feedback.js';
 
 function letterFrequency(words: string[]): Record<string, number> {
   const freq: Record<string, number> = {};
@@ -37,7 +29,7 @@ export function solveFrequency(target: string, allWords: string[], maxAttempts =
     steps.push(bestGuess);
     if (fb === 'ggggg') return { attempts: attempt, steps };
 
-    candidates = candidates.filter((c) => getFeedback(c, bestGuess) === fb);
+    candidates = candidates.filter((c) => matchesFeedback(c, bestGuess, fb));
     if (candidates.length === 0) return { attempts: maxAttempts + 1, steps };
   }
 

+ 1 - 11
solver/src/greedy.ts → shared/src/solvers/greedy.ts

@@ -1,14 +1,4 @@
-function getFeedback(target: string, guess: string): string {
-  const tu = target.toLowerCase(), gu = guess.toLowerCase();
-  const counts: Record<string, number> = {};
-  for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
-  const r: string[] = Array(5).fill('x');
-  for (let i = 0; i < 5; i++) { if (gu[i] === tu[i]) { r[i] = 'g'; counts[gu[i]]--; } }
-  for (let i = 0; i < 5; i++) { if (r[i] === 'g') continue; if ((counts[gu[i]] ?? 0) > 0) { r[i] = 'y'; counts[gu[i]]--; } }
-  return r.join('');
-}
-
-function matchesFeedback(c: string, g: string, fb: string): boolean { return getFeedback(c, g) === fb; }
+import { getFeedback, matchesFeedback } from './feedback.js';
 
 export function solveGreedy(target: string, allWords: string[], maxAttempts = 6): { attempts: number; steps: string[] } {
   let candidates = [...allWords];

+ 49 - 0
shared/src/solvers/index.ts

@@ -0,0 +1,49 @@
+import type { SolverReplay } from '../word-bank.js';
+import { solveEntropy } from './entropy.js';
+import { solveFrequency } from './frequency.js';
+import { solveVowelFirst } from './vowel-first.js';
+import { solveGreedy } from './greedy.js';
+
+export { solveEntropy, solveFrequency, solveVowelFirst, solveGreedy };
+
+const MAX_ATTEMPTS = 6;
+
+interface SolverResult {
+  attempts: number;
+  steps: string[];
+}
+
+// Entropy is excluded from runtime computation — it takes ~14s per target (O(N²))
+// and the 3 fast solvers provide sufficient coverage in under 1ms per target.
+const SOLVERS: Array<{ name: string; fn: (t: string, w: string[], m: number) => SolverResult }> = [
+  { name: 'Frequency', fn: solveFrequency },
+  { name: 'Vowel First', fn: solveVowelFirst },
+  { name: 'Greedy', fn: solveGreedy },
+];
+
+/** Run all solvers against a target word, returning replays and the aggregate difficulty score. */
+export function computeReplays(target: string, allWords: string[]): {
+  difficulty: number;
+  replays: SolverReplay[];
+} {
+  const results = SOLVERS.map((s) => ({
+    name: s.name,
+    ...s.fn(target, allWords, MAX_ATTEMPTS),
+  }));
+
+  const passResults = results.filter((r) => r.attempts <= MAX_ATTEMPTS);
+  const avg = passResults.reduce((sum, r) => sum + r.attempts, 0) / passResults.length;
+  const difficulty = Math.round(avg * 100) / 100;
+
+  const replays: SolverReplay[] = passResults.map((r) => ({
+    solver: r.name,
+    steps: r.steps,
+  }));
+
+  return { difficulty, replays };
+}
+
+/** Run all solvers and return just the difficulty score (faster for pre-caching). */
+export function computeDifficulty(target: string, allWords: string[]): number {
+  return computeReplays(target, allWords).difficulty;
+}

+ 2 - 12
solver/src/vowel-first.ts → shared/src/solvers/vowel-first.ts

@@ -1,16 +1,6 @@
-const VOWELS = new Set(['a','e','i','o','u']);
-
-function getFeedback(target: string, guess: string): string {
-  const tu = target.toLowerCase(), gu = guess.toLowerCase();
-  const counts: Record<string, number> = {};
-  for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
-  const r: string[] = Array(5).fill('x');
-  for (let i = 0; i < 5; i++) { if (gu[i] === tu[i]) { r[i] = 'g'; counts[gu[i]]--; } }
-  for (let i = 0; i < 5; i++) { if (r[i] === 'g') continue; if ((counts[gu[i]] ?? 0) > 0) { r[i] = 'y'; counts[gu[i]]--; } }
-  return r.join('');
-}
+import { getFeedback, matchesFeedback } from './feedback.js';
 
-function matchesFeedback(c: string, g: string, fb: string): boolean { return getFeedback(c, g) === fb; }
+const VOWELS = new Set(['a','e','i','o','u']);
 
 function pickBestVowelWord(pool: string[], knownVowels: Set<string>): string {
   let bestGuess = pool[0];

+ 1 - 3
shared/src/word-bank.ts

@@ -5,12 +5,10 @@ export interface SolverReplay {
   steps: string[];
 }
 
-/** A target word with its pre-computed difficulty score */
+/** A target word (difficulty and replays are computed at runtime by the server) */
 export interface TargetWord {
   id: number;
   word: string;
-  difficulty: number;
-  replays?: SolverReplay[];
 }
 
 /** The complete word bank structure */

+ 26 - 36
solver/src/index.ts

@@ -1,57 +1,47 @@
-import { readFileSync, writeFileSync } from 'fs';
+import { readFileSync } from 'fs';
 import { resolve, dirname } from 'path';
 import { fileURLToPath } from 'url';
-import type { WordBank, SolverReplay } from '@wordle/shared';
-import { solveEntropy } from './entropy.js';
-import { solveFrequency } from './frequency.js';
-import { solveVowelFirst } from './vowel-first.js';
-import { solveGreedy } from './greedy.js';
+import type { WordBank } from '@wordle/shared';
+import { solveEntropy, solveFrequency, solveVowelFirst, solveGreedy } from '@wordle/shared';
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
 const MAX_ATTEMPTS = 6;
 
-function solveBank(bankPath: string) {
+const SOLVERS = [
+  { name: 'Entropy', fn: solveEntropy },
+  { name: 'Frequency', fn: solveFrequency },
+  { name: 'Vowel First', fn: solveVowelFirst },
+  { name: 'Greedy', fn: solveGreedy },
+];
+
+function validateBank(bankPath: string) {
   console.log(`\n=== ${bankPath} ===`);
   const bank: WordBank = JSON.parse(readFileSync(bankPath, 'utf-8'));
   const allWords = bank.guessable;
 
-  console.log(`Solving ${bank.targets.length} targets with ${allWords.length} words...`);
-
-  let solved = 0, failed = 0;
-  const newTargets: WordBank['targets'] = [];
+  console.log(`Validating ${bank.targets.length} targets with ${allWords.length} words...`);
 
+  let ok = 0, unsolvable = 0;
   for (const target of bank.targets) {
-    const results = [
-      { name: 'Entropy', ...solveEntropy(target.word, allWords, MAX_ATTEMPTS) },
-      { name: 'Frequency', ...solveFrequency(target.word, allWords, MAX_ATTEMPTS) },
-      { name: 'Vowel First', ...solveVowelFirst(target.word, allWords, MAX_ATTEMPTS) },
-      { name: 'Greedy', ...solveGreedy(target.word, allWords, MAX_ATTEMPTS) },
-    ];
+    const results = SOLVERS.map((s) => ({
+      name: s.name,
+      ...s.fn(target.word, allWords, MAX_ATTEMPTS),
+    }));
 
     const passResults = results.filter((r) => r.attempts <= MAX_ATTEMPTS);
     if (passResults.length === 0) {
-      console.log(`  REMOVED: ${target.word} (unsolvable)`);
-      failed++;
-      continue;
+      console.log(`  UNSOLVABLE: ${target.word}`);
+      unsolvable++;
+    } else {
+      ok++;
     }
-
-    const avg = passResults.reduce((s, r) => s + r.attempts, 0) / passResults.length;
-    const difficulty = Math.round(avg * 100) / 100;
-    const replays: SolverReplay[] = results
-      .filter((r) => r.attempts <= MAX_ATTEMPTS)
-      .map((r) => ({ solver: r.name, steps: r.steps }));
-
-    newTargets.push({ ...target, difficulty, replays });
-    solved++;
-    if (solved % 10 === 0) console.log(`  ${solved}/${bank.targets.length}...`);
+    if ((ok + unsolvable) % 10 === 0) console.log(`  ${ok + unsolvable}/${bank.targets.length}...`);
   }
 
-  const newBank: WordBank = { guessable: bank.guessable, targets: newTargets };
-  writeFileSync(bankPath, JSON.stringify(newBank, null, 2) + '\n', 'utf-8');
-  console.log(`Done! ${solved} solved, ${failed} removed.`);
+  console.log(`Done! ${ok} solvable, ${unsolvable} unsolvable.`);
 }
 
 const DATA_DIR = resolve(__dirname, '../../data');
-solveBank(resolve(DATA_DIR, 'word-bank.json'));
-solveBank(resolve(DATA_DIR, 'word-bank-ru.json'));
-console.log('\nAll banks updated.');
+validateBank(resolve(DATA_DIR, 'word-bank.json'));
+validateBank(resolve(DATA_DIR, 'word-bank-ru.json'));
+console.log('\nAll banks validated.');