generate-banks.py 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. #!/usr/bin/env python3
  2. """Generate word-bank.json and word-bank-ru.json with Gaussian-distributed targets.
  3. Reads frequency-sorted word lists (most common first) from raw/ and produces
  4. word banks with 100 Gaussian-picked targets per language.
  5. Gaussian parameters:
  6. mean = 0 (index of the most frequent word)
  7. sigma = 100 (index 100 is at 1σ → ~68% of picks land in the first 100 words)
  8. Target IDs are assigned in ascending index order, so ID 1 is the most common
  9. selected word and ID 100 is the least common.
  10. """
  11. import json
  12. from pathlib import Path
  13. import numpy as np
  14. RAW_DIR = Path(__file__).parent
  15. DATA_DIR = RAW_DIR.parent / "data"
  16. SEED = 42
  17. TARGET_COUNT = 100
  18. SIGMA = 100
  19. def load_dictionary(path: Path) -> list[str]:
  20. """Read frequency-sorted word list (one word per line). Preserves order."""
  21. words = [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
  22. return words
  23. def select_target_indices(rng: np.random.Generator, dict_len: int, count: int) -> list[int]:
  24. """Gaussian sample indices, de-duplicating via rejection.
  25. Sorted ascending so ID 1 = most common word."""
  26. selected: set[int] = set()
  27. while len(selected) < count:
  28. samples = np.abs(np.floor(rng.normal(loc=0, scale=SIGMA, size=count * 2)))
  29. for s in samples:
  30. idx = int(s)
  31. if 0 <= idx < dict_len:
  32. selected.add(idx)
  33. if len(selected) >= count:
  34. break
  35. return sorted(selected)
  36. def generate_bank(dict_path: Path, output_path: Path) -> None:
  37. rng = np.random.default_rng(SEED)
  38. guessable = load_dictionary(dict_path)
  39. indices = select_target_indices(rng, len(guessable), TARGET_COUNT)
  40. targets = [{"id": i + 1, "word": guessable[idx]} for i, idx in enumerate(indices)]
  41. bank = {"guessable": guessable, "targets": targets}
  42. output_path.write_text(
  43. json.dumps(bank, ensure_ascii=False, indent=2) + "\n",
  44. encoding="utf-8",
  45. )
  46. print(f"Wrote {output_path}: {len(guessable)} guessable, {len(targets)} targets")
  47. # Sanity checks
  48. target_words = [t["word"] for t in targets]
  49. assert all(w in guessable for w in target_words), "Target word not in guessable!"
  50. assert len(set(target_words)) == TARGET_COUNT, f"Duplicate target words ({len(set(target_words))} unique)!"
  51. assert [t["id"] for t in targets] == list(range(1, TARGET_COUNT + 1)), "IDs not contiguous 1..100!"
  52. print(f" Top 5: {[targets[i]['word'] for i in range(5)]}")
  53. def main():
  54. generate_bank(
  55. RAW_DIR / "english-5letter-freq-sortfreq-wordsonly.txt",
  56. DATA_DIR / "word-bank.json",
  57. )
  58. generate_bank(
  59. RAW_DIR / "russian-5letter-freq-sortfreq-wordsonly.txt",
  60. DATA_DIR / "word-bank-ru.json",
  61. )
  62. print("Done. Run 'npm run solve' to validate solvability.")
  63. if __name__ == "__main__":
  64. main()