#!/usr/bin/env python3 """Add synonyms (hints) to each target word in word-bank.json and word-bank-ru.json. Uses Datamuse API for English (free, no auth). Falls back to WordNet. Results are reviewed for quality — known-bad mappings are overridden. For Russian, uses a curated synonym dictionary. """ import json import sys import time from pathlib import Path DATA_DIR = Path(__file__).parent.parent / "data" def get_english_synonyms_datamuse(word: str) -> list[str]: """Get related words from Datamuse API (free, no auth).""" import requests try: resp = requests.get( "https://api.datamuse.com/words", params={"ml": word, "max": 8}, timeout=5, ) resp.raise_for_status() results = resp.json() words = [] for r in results: w = r.get("word", "") if " " not in w and w.lower() != word.lower(): words.append(w.lower()) return words except Exception as e: print(f" Datamuse error for '{word}': {e}", file=sys.stderr) return [] def get_english_synonyms_wordnet(word: str) -> list[str]: """Get synonyms from WordNet.""" from nltk.corpus import wordnet as wn syns = wn.synsets(word) if not syns: return [] lemmas = [] for s in syns[:10]: for l in s.lemmas(): name = l.name().lower() if "_" not in name and name != word and name not in lemmas: lemmas.append(name) return lemmas def pick_best_synonym(word: str, candidates: list[str]) -> str | None: """Pick the best synonym from candidates. Prefers common, short words.""" # Filter out multi-word, very long words, and obscure forms good = [c for c in candidates if len(c) <= 10 and "_" not in c] if not good: return None # Prefer shorter words (more likely to be common) good.sort(key=len) return good[0] # Manually curated overrides for words where automated approaches give poor results. # These are common, intuitive synonyms that help a player guess the target word. EN_HINT_OVERRIDES: dict[str, str] = { # Function words and words where Datamuse/WordNet produce confusing hints "about": "around", "there": "present", "which": "that", "other": "different", "would": "could", "these": "those", "while": "during", "being": "living", "where": "location", "still": "quiet", "never": "not ever", "every": "each", "since": "because", "those": "these", "among": "between", "money": "cash", "local": "nearby", "least": "fewest", "month": "april", "river": "stream", "david": "giant", "steve": "stephen", "didnt": "did not", "three": "trio", "years": "decade", "going": "moving", # Quality overrides for bad Datamuse/WordNet results "state": "condition", "games": "sport", "found": "located", "often": "frequent", "south": "southern", "small": "little", "north": "northern", "third": "tertiary", "party": "group", "began": "started", "young": "youth", "final": "last", "given": "provided", "using": "employ", "field": "area", "teams": "squad", "thing": "object", "white": "pale", "march": "parade", "black": "dark", "built": "constructed", "moved": "shifted", "added": "extra", "needs": "requires", "short": "brief", "front": "forward", "seven": "seventh", "title": "name", "comes": "arrives", "hours": "time", "eight": "eighth", "green": "color", "round": "circular", "union": "group", "lives": "exists", "radio": "broadcast", "words": "phrase", "model": "design", "terms": "words", "score": "points", "ready": "prepared", "grand": "large", } # ---- Russian synonym dictionary (curated for the 100 target words) ---- # Manually selected synonyms or related hints for Russian target words. RU_HINTS: dict[str, str] = { "жизнь": "бытие", "слово": "речь", "часть": "доля", "город": "посёлок", "земля": "почва", "право": "закон", "образ": "облик", "закон": "правило", "война": "битва", "голос": "звук", "книга": "роман", "число": "цифра", "народ": "люди", "форма": "фигура", "улица": "дорога", "вечер": "закат", "мысль": "идея", "месяц": "луна", "школа": "учёба", "театр": "сцена", "смысл": "суть", "рынок": "базар", "центр": "середина", "ответ": "отклик", "автор": "писатель", "совет": "наказ", "глава": "лидер", "наука": "знание", "плечо": "рука", "точка": "пятно", "палец": "рука", "метод": "способ", "фильм": "кино", "гость": "визитёр", "район": "зона", "кровь": "рана", "армия": "войско", "класс": "группа", "герой": "смельчак", "спина": "тело", "берег": "пляж", "фирма": "компания", "завод": "фабрика", "повод": "причина", "выход": "уход", "текст": "письмо", "пункт": "место", "линия": "черта", "среда": "окружение", "волос": "прядь", "ветер": "буря", "огонь": "пламя", "страх": "ужас", "сфера": "область", "немец": "германец", "выбор": "отбор", "масса": "вес", "кухня": "готовка", "товар": "продукт", "вывод": "итог", "норма": "правило", "рамка": "граница", "прием": "встреча", "режим": "порядок", "целое": "полное", "доход": "прибыль", "карта": "схема", "акция": "дело", "фраза": "выражение", "толпа": "скопление", "птица": "пернатый", "запах": "аромат", "поезд": "состав", "адрес": "жильё", "лидер": "вождь", "весна": "сезон", "музей": "галерея", "сутки": "день", "еврей": "иудей", "сотня": "сто", "труба": "дымоход", "масло": "жир", "экран": "дисплей", "вагон": "поезд", "сезон": "пора", "длина": "размер", "доска": "дерево", "лодка": "судно", "серия": "ряд", "кулак": "рука", "нефть": "топливо", "кость": "скелет", "взрыв": "удар", "почва": "земля", "хвост": "конец", "строй": "порядок", "отчет": "доклад", "забор": "стена", "пожар": "огонь", "ножка": "нога", } def generate_english_hints(bank: dict) -> int: """Add hints to English targets. Returns count of missing hints.""" targets = bank["targets"] missing = 0 for i, target in enumerate(targets): word = target["word"] # Check manual override first if word in EN_HINT_OVERRIDES: target["hint"] = EN_HINT_OVERRIDES[word] continue # Try Datamuse candidates = get_english_synonyms_datamuse(word) if not candidates: # Fall back to WordNet candidates = get_english_synonyms_wordnet(word) hint = pick_best_synonym(word, candidates) if hint: target["hint"] = hint else: missing += 1 print(f" MISSING: {word} (id={target['id']})") # Be polite to the API if i % 20 == 19: time.sleep(0.5) return missing def generate_russian_hints(bank: dict) -> int: """Add hints to Russian targets from curated dictionary. Returns count missing.""" targets = bank["targets"] missing = 0 for target in targets: word = target["word"] hint = RU_HINTS.get(word) if hint: target["hint"] = hint else: missing += 1 print(f" MISSING: {word} (id={target['id']})") return missing def main(): # English print("=== English synonyms ===") en_path = DATA_DIR / "word-bank.json" with open(en_path, encoding="utf-8") as f: en_bank = json.load(f) en_missing = generate_english_hints(en_bank) with open(en_path, "w", encoding="utf-8") as f: json.dump(en_bank, f, ensure_ascii=False, indent=2) f.write("\n") print(f" {len(en_bank['targets'])} targets, {len(en_bank['targets']) - en_missing} with hints, {en_missing} missing") # Russian print("\n=== Russian synonyms ===") ru_path = DATA_DIR / "word-bank-ru.json" with open(ru_path, encoding="utf-8") as f: ru_bank = json.load(f) ru_missing = generate_russian_hints(ru_bank) with open(ru_path, "w", encoding="utf-8") as f: json.dump(ru_bank, f, ensure_ascii=False, indent=2) f.write("\n") print(f" {len(ru_bank['targets'])} targets, {len(ru_bank['targets']) - ru_missing} with hints, {ru_missing} missing") print("\nDone!") if __name__ == "__main__": main()