Spaces:
Running
Running
Word Generator Minor Update
Browse files- battlewords/generator.py +22 -41
- requirements.txt +5 -1
battlewords/generator.py
CHANGED
|
@@ -1,47 +1,12 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import random
|
| 4 |
-
from
|
| 5 |
-
from typing import Dict, List, Optional, Tuple
|
| 6 |
-
|
| 7 |
-
import streamlit as st
|
| 8 |
|
|
|
|
| 9 |
from .models import Coord, Word, Puzzle
|
| 10 |
|
| 11 |
|
| 12 |
-
# Fallback minimal word pools if file missing or too small
|
| 13 |
-
_FALLBACK: Dict[int, List[str]] = {
|
| 14 |
-
4: ["TREE", "BOAT", "WIND", "FROG", "LION", "MOON", "FORK", "GLOW", "GAME", "CODE"],
|
| 15 |
-
5: ["APPLE", "RIVER", "STONE", "PLANT", "MOUSE", "BOARD", "CHAIR", "SCALE", "SMILE", "CLOUD"],
|
| 16 |
-
6: ["ORANGE", "PYTHON", "STREAM", "MARKET", "FOREST", "THRIVE", "LOGGER", "BREATH", "DOMAIN", "GALAXY"],
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@st.cache_data(show_spinner=False)
|
| 21 |
-
def load_word_list() -> Dict[int, List[str]]:
|
| 22 |
-
"""
|
| 23 |
-
Loads and filters a word list for lengths 4, 5, 6.
|
| 24 |
-
Returns a dict length->words (uppercase, A–Z only).
|
| 25 |
-
"""
|
| 26 |
-
base = Path(__file__).parent / "words" / "wordlist.txt"
|
| 27 |
-
words_by_len: Dict[int, List[str]] = {4: [], 5: [], 6: []}
|
| 28 |
-
if base.exists():
|
| 29 |
-
try:
|
| 30 |
-
for line in base.read_text(encoding="utf-8").splitlines():
|
| 31 |
-
w = line.strip().upper()
|
| 32 |
-
if w.isalpha() and len(w) in (4, 5, 6):
|
| 33 |
-
words_by_len[len(w)].append(w)
|
| 34 |
-
except Exception:
|
| 35 |
-
pass
|
| 36 |
-
|
| 37 |
-
# Ensure minimum pools; otherwise fallback
|
| 38 |
-
for L in (4, 5, 6):
|
| 39 |
-
if len(words_by_len[L]) < 500:
|
| 40 |
-
words_by_len[L] = _FALLBACK[L].copy()
|
| 41 |
-
|
| 42 |
-
return words_by_len
|
| 43 |
-
|
| 44 |
-
|
| 45 |
def _fits_and_free(cells: List[Coord], used: set[Coord], size: int) -> bool:
|
| 46 |
for c in cells:
|
| 47 |
if not c.in_bounds(size) or c in used:
|
|
@@ -65,18 +30,24 @@ def generate_puzzle(
|
|
| 65 |
"""
|
| 66 |
Place exactly six words: 2x4, 2x5, 2x6, horizontal or vertical,
|
| 67 |
no cell overlaps. Radar pulses are last-letter cells.
|
|
|
|
| 68 |
"""
|
| 69 |
rng = random.Random(seed)
|
| 70 |
words_by_len = words_by_len or load_word_list()
|
| 71 |
target_lengths = [4, 4, 5, 5, 6, 6]
|
| 72 |
|
| 73 |
used: set[Coord] = set()
|
|
|
|
| 74 |
placed: List[Word] = []
|
| 75 |
|
| 76 |
-
# Pre-shuffle the word pools for variety but deterministic with seed
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
attempts = 0
|
| 82 |
for L in target_lengths:
|
|
@@ -94,6 +65,10 @@ def generate_puzzle(
|
|
| 94 |
break
|
| 95 |
attempts += 1
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
# Try a variety of starts/orientations for this word
|
| 98 |
for _ in range(50):
|
| 99 |
direction = rng.choice(["H", "V"])
|
|
@@ -109,6 +84,12 @@ def generate_puzzle(
|
|
| 109 |
w = Word(cand_text, Coord(row, col), direction)
|
| 110 |
placed.append(w)
|
| 111 |
used.update(cells)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
placed_ok = True
|
| 113 |
break
|
| 114 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import random
|
| 4 |
+
from typing import Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
from .word_loader import load_word_list
|
| 7 |
from .models import Coord, Word, Puzzle
|
| 8 |
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
def _fits_and_free(cells: List[Coord], used: set[Coord], size: int) -> bool:
|
| 11 |
for c in cells:
|
| 12 |
if not c.in_bounds(size) or c in used:
|
|
|
|
| 30 |
"""
|
| 31 |
Place exactly six words: 2x4, 2x5, 2x6, horizontal or vertical,
|
| 32 |
no cell overlaps. Radar pulses are last-letter cells.
|
| 33 |
+
Ensures the same word text is not selected more than once.
|
| 34 |
"""
|
| 35 |
rng = random.Random(seed)
|
| 36 |
words_by_len = words_by_len or load_word_list()
|
| 37 |
target_lengths = [4, 4, 5, 5, 6, 6]
|
| 38 |
|
| 39 |
used: set[Coord] = set()
|
| 40 |
+
used_texts: set[str] = set()
|
| 41 |
placed: List[Word] = []
|
| 42 |
|
| 43 |
+
# Pre-shuffle the word pools for variety but deterministic with seed.
|
| 44 |
+
# Also de-duplicate within each length pool while preserving order.
|
| 45 |
+
pools: Dict[int, List[str]] = {}
|
| 46 |
+
for L in (4, 5, 6):
|
| 47 |
+
# Preserve order and dedupe
|
| 48 |
+
unique_words = list(dict.fromkeys(words_by_len.get(L, [])))
|
| 49 |
+
rng.shuffle(unique_words)
|
| 50 |
+
pools[L] = unique_words
|
| 51 |
|
| 52 |
attempts = 0
|
| 53 |
for L in target_lengths:
|
|
|
|
| 65 |
break
|
| 66 |
attempts += 1
|
| 67 |
|
| 68 |
+
# Skip words already used to avoid duplicates across placements
|
| 69 |
+
if cand_text in used_texts:
|
| 70 |
+
continue
|
| 71 |
+
|
| 72 |
# Try a variety of starts/orientations for this word
|
| 73 |
for _ in range(50):
|
| 74 |
direction = rng.choice(["H", "V"])
|
|
|
|
| 84 |
w = Word(cand_text, Coord(row, col), direction)
|
| 85 |
placed.append(w)
|
| 86 |
used.update(cells)
|
| 87 |
+
used_texts.add(cand_text)
|
| 88 |
+
# Remove from pool so it can't be picked again later
|
| 89 |
+
try:
|
| 90 |
+
pool.remove(cand_text)
|
| 91 |
+
except ValueError:
|
| 92 |
+
pass
|
| 93 |
placed_ok = True
|
| 94 |
break
|
| 95 |
|
requirements.txt
CHANGED
|
@@ -1,4 +1,8 @@
|
|
| 1 |
altair
|
| 2 |
pandas
|
| 3 |
streamlit
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
altair
|
| 2 |
pandas
|
| 3 |
streamlit
|
| 4 |
+
matplotlib
|
| 5 |
+
numpy
|
| 6 |
+
pytest
|
| 7 |
+
flake8
|
| 8 |
+
mypy
|