Surn commited on
Commit
50f9808
·
1 Parent(s): 4e65519

UI update, wordlists logic

Browse files
battlewords.egg-info/SOURCES.txt CHANGED
@@ -13,6 +13,7 @@ battlewords.egg-info/dependency_links.txt
13
  battlewords.egg-info/requires.txt
14
  battlewords.egg-info/top_level.txt
15
  battlewords/words/wordlist.txt
 
16
  tests/test_apptest.py
17
  tests/test_generator.py
18
  tests/test_logic.py
 
13
  battlewords.egg-info/requires.txt
14
  battlewords.egg-info/top_level.txt
15
  battlewords/words/wordlist.txt
16
+ battlewords/words/classic.txt
17
  tests/test_apptest.py
18
  tests/test_generator.py
19
  tests/test_logic.py
battlewords/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.1.1"
2
  __all__ = ["models", "generator", "logic", "ui"]
 
1
+ __version__ = "0.1.2"
2
  __all__ = ["models", "generator", "logic", "ui"]
battlewords/generator.py CHANGED
@@ -126,4 +126,18 @@ def validate_puzzle(puzzle: Puzzle, grid_size: int = 12) -> None:
126
  raise AssertionError("Radar pulse missing for last cell")
127
 
128
  if counts[4] != 2 or counts[5] != 2 or counts[6] != 2:
129
- raise AssertionError("Incorrect counts of word lengths")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  raise AssertionError("Radar pulse missing for last cell")
127
 
128
  if counts[4] != 2 or counts[5] != 2 or counts[6] != 2:
129
+ raise AssertionError("Incorrect counts of word lengths")
130
+
131
+
132
+ def sort_word_file(filepath: str) -> List[str]:
133
+ """
134
+ Reads a word list file, skips header/comment lines, and returns words sorted
135
+ by length (ascending), then alphabetically within each length group.
136
+ """
137
+ with open(filepath, "r", encoding="utf-8") as f:
138
+ lines = f.readlines()
139
+ # Skip header/comment lines
140
+ words = [line.strip() for line in lines if line.strip() and not line.strip().startswith("#")]
141
+ # Sort by length, then alphabetically
142
+ sorted_words = sorted(words, key=lambda w: (len(w), w))
143
+ return sorted_words
battlewords/ui.py CHANGED
@@ -5,9 +5,10 @@ from typing import Iterable, Tuple, Optional
5
  import matplotlib.pyplot as plt
6
  import streamlit as st
7
 
8
- from .generator import generate_puzzle, load_word_list
9
  from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier
10
  from .models import Coord, GameState, Puzzle
 
11
 
12
 
13
  CoordLike = Tuple[int, int]
@@ -43,7 +44,7 @@ def inject_styles() -> None:
43
  """
44
  <style>
45
  /* Base grid cell visuals */
46
- .bw-row { display: flex; gap: 2px; flex-wrap: nowrap; }
47
  .bw-cell {
48
  width: 100%;
49
  aspect-ratio: 1 / 1;
@@ -51,16 +52,21 @@ def inject_styles() -> None:
51
  align-items: center;
52
  justify-content: center;
53
  border: 1px solid #3a3a3a;
54
- border-radius: 4px;
55
  font-weight: 700;
56
  user-select: none;
57
  padding: 0.25rem 0.75rem;
58
  min-height: 2.5rem;
59
- transition: background 0.2s ease;
 
 
60
  }
61
- .bw-cell.letter { background: #1e1e1e; color: #eaeaea; }
62
- .bw-cell.empty { background: #0f0f0f; }
63
- .bw-cell.bw-cell-complete { background: #b7f7b7 !important; color: #1a1a1a !important; }
 
 
 
64
 
65
  /* Final score style */
66
  .bw-final-score { color: #1ca41c !important; font-weight: 800; }
@@ -69,28 +75,52 @@ def inject_styles() -> None:
69
  div[data-testid="stButton"] button {
70
  width: 100%;
71
  aspect-ratio: 1 / 1;
72
- border-radius: 4px;
73
- border: 1px solid #3a3a3a;
 
 
 
 
 
74
  }
75
 
76
  /* Ensure grid cell columns expand equally for both buttons and revealed cells */
77
  div[data-testid="column"], .st-emotion-cache-zh2fnc {
78
  width: auto !important;
79
  flex: 1 1 auto !important;
80
- min-width: 0 !important;
81
  max-width: 100% !important;
82
  }
83
- .st-emotion-cache-1permvm {
84
- gap:0.25rem !important;
85
  }
 
86
  /* Ensure grid rows generated via st.columns do not wrap and can scroll horizontally. */
87
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] {
88
  flex-wrap: nowrap !important;
89
- overflow-x: auto !important;
 
90
  }
91
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] {
92
  flex: 0 0 auto !important;
93
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  /* Mobile styles */
96
  @media (max-width: 640px) {
@@ -111,12 +141,46 @@ def inject_styles() -> None:
111
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] {
112
  flex-wrap: nowrap !important;
113
  overflow-x: auto !important;
 
114
  }
115
  .st-emotion-cache-17i4tbh {
116
  min-width: calc(8.33333% - 1rem);
117
  }
118
-
119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </style>
121
  """,
122
  unsafe_allow_html=True,
@@ -127,7 +191,12 @@ def _init_session() -> None:
127
  if "initialized" in st.session_state and st.session_state.initialized:
128
  return
129
 
130
- words = load_word_list()
 
 
 
 
 
131
  puzzle = generate_puzzle(grid_size=12, words_by_len=words)
132
 
133
  st.session_state.puzzle = puzzle
@@ -143,7 +212,11 @@ def _init_session() -> None:
143
 
144
 
145
  def _new_game() -> None:
 
 
146
  st.session_state.clear()
 
 
147
  _init_session()
148
 
149
 
@@ -189,6 +262,30 @@ def _render_sidebar():
189
  "- Radar pulses show the last letter position of each hidden word.\n"
190
  "- After each reveal, you may submit one word guess below.\n"
191
  "- Scoring: length + unrevealed letters of that word at guess time.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
 
194
  def _render_radar(puzzle: Puzzle, size: int):
@@ -236,29 +333,32 @@ def _render_grid(state: GameState, letter_map):
236
  min-height: 32px !important;
237
  padding: 0 !important;
238
  margin: 0 !important;
239
- border: 1px solid #888 !important;
240
- background: #fff !important;
 
 
241
  font-weight: bold;
242
  font-size: 1rem;
243
  }
 
 
 
 
244
  </style>
245
  """,
246
  unsafe_allow_html=True,
247
  )
248
 
249
  grid_container = st.container()
250
- with grid_container:
251
  for r in range(size):
252
- # Anchor to style the following st.columns row container
253
  st.markdown('<div class="bw-grid-row-anchor"></div>', unsafe_allow_html=True)
254
  cols = st.columns(size, gap="small")
255
  for c in range(size):
256
  coord = Coord(r, c)
257
  revealed = coord in state.revealed
258
- # Get label if revealed
259
  label = letter_map.get(coord, " ") if revealed else " "
260
 
261
- # If this coord belongs to a completed (guessed) word
262
  is_completed_cell = False
263
  if revealed:
264
  for w in state.puzzle.words:
@@ -270,24 +370,27 @@ def _render_grid(state: GameState, letter_map):
270
  tooltip = f"({r+1},{c+1})"
271
 
272
  if is_completed_cell:
273
- # Render a styled non-button cell with green background and native browser tooltip
274
  safe_label = (label or " ")
275
  cols[c].markdown(
276
  f'<div class="bw-cell bw-cell-complete" title="{tooltip}">{safe_label}</div>',
277
  unsafe_allow_html=True,
278
  )
279
  elif revealed:
280
- # Render a styled non-button cell showing the letter with native browser tooltip
281
  safe_label = (label or " ")
 
 
 
282
  cols[c].markdown(
283
- f'<div class="bw-cell letter" title="{tooltip}">{safe_label}</div>',
284
  unsafe_allow_html=True,
285
  )
286
  else:
287
  # Unrevealed: render a button to allow click/reveal with tooltip
288
  if cols[c].button(" ", key=key, help=tooltip):
289
  clicked = coord
290
-
291
  if clicked is not None:
292
  reveal_cell(state, letter_map, clicked)
293
  st.session_state.letter_map = build_letter_map(st.session_state.puzzle)
@@ -309,6 +412,11 @@ def _render_score_panel(state: GameState):
309
  st.metric("Score", state.score)
310
  with col2:
311
  st.markdown(f"Last action: {state.last_action}")
 
 
 
 
 
312
 
313
 
314
  def _render_game_over(state: GameState):
@@ -329,6 +437,26 @@ def _render_game_over(state: GameState):
329
  st.stop()
330
 
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  def run_app():
333
  _init_session()
334
  _render_header()
@@ -338,7 +466,7 @@ def run_app():
338
 
339
  # Anchor to target the main two-column layout for mobile reversal
340
  st.markdown('<div id="bw-main-anchor"></div>', unsafe_allow_html=True)
341
- left, right = st.columns([3, 1], gap="medium")
342
  with left:
343
  _render_grid(state, st.session_state.letter_map)
344
  with right:
 
5
  import matplotlib.pyplot as plt
6
  import streamlit as st
7
 
8
+ from .generator import generate_puzzle, sort_word_file
9
  from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier
10
  from .models import Coord, GameState, Puzzle
11
+ from .word_loader import get_wordlist_files, load_word_list # use loader directly
12
 
13
 
14
  CoordLike = Tuple[int, int]
 
44
  """
45
  <style>
46
  /* Base grid cell visuals */
47
+ .bw-row { display: flex; gap: 0.1rem; flex-wrap: nowrap; }
48
  .bw-cell {
49
  width: 100%;
50
  aspect-ratio: 1 / 1;
 
52
  align-items: center;
53
  justify-content: center;
54
  border: 1px solid #3a3a3a;
55
+ border-radius: 0;
56
  font-weight: 700;
57
  user-select: none;
58
  padding: 0.25rem 0.75rem;
59
  min-height: 2.5rem;
60
+ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
61
+ background: #1d64c8; /* Base cell color */
62
+ color: #ffffff; /* Base text color for contrast */
63
  }
64
+ /* Found letter cells */
65
+ .bw-cell.letter { background: #d7faff; color: #050057; }
66
+ /* Optional empty state if ever used */
67
+ .bw-cell.empty { background: #3a3a3a; color: #ffffff; }
68
+ /* Completed word cells */
69
+ .bw-cell.bw-cell-complete { background: #050057 !important; color: #d7faff !important; }
70
 
71
  /* Final score style */
72
  .bw-final-score { color: #1ca41c !important; font-weight: 800; }
 
75
  div[data-testid="stButton"] button {
76
  width: 100%;
77
  aspect-ratio: 1 / 1;
78
+ border-radius: 0;
79
+ border: 1px solid #1d64c8;
80
+ background: #1d64c8;
81
+ color: #ffffff;
82
+ font-weight: 700;
83
+ padding: 0.25rem 0.75rem;
84
+ min-height: 2.5rem;
85
  }
86
 
87
  /* Ensure grid cell columns expand equally for both buttons and revealed cells */
88
  div[data-testid="column"], .st-emotion-cache-zh2fnc {
89
  width: auto !important;
90
  flex: 1 1 auto !important;
91
+ min-width: 100% !important;
92
  max-width: 100% !important;
93
  }
94
+ .st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc {
95
+ gap:0.1rem !important;
96
  }
97
+
98
  /* Ensure grid rows generated via st.columns do not wrap and can scroll horizontally. */
99
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] {
100
  flex-wrap: nowrap !important;
101
+ overflow-x: auto !important;
102
+ margin: 2px 0 !important; /* Reduce gap between rows */
103
  }
104
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] {
105
  flex: 0 0 auto !important;
106
  }
107
+ .bw-grid-row-anchor { height: 0; margin: 0; padding: 0; }
108
+ .st-emotion-cache-1n6tfoc {
109
+ background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666);;
110
+ gap: 0.1rem !important;
111
+ color: white;
112
+ # border: 10px solid;
113
+ # border-image: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666) 1;
114
+ border-radius:15px;
115
+ padding: 10px;
116
+ }
117
+ .st-emotion-cache-1n6tfoc::before {
118
+ content: '';
119
+ position: absolute;
120
+ top: 0; left: 0; right: 0; bottom: 0;
121
+ border-radius: 10px;
122
+ margin: 5px; /* Border thickness */
123
+ }
124
 
125
  /* Mobile styles */
126
  @media (max-width: 640px) {
 
141
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] {
142
  flex-wrap: nowrap !important;
143
  overflow-x: auto !important;
144
+ margin: 2px 0 !important; /* Keep tighter row gap on mobile */
145
  }
146
  .st-emotion-cache-17i4tbh {
147
  min-width: calc(8.33333% - 1rem);
148
  }
 
149
  }
150
+
151
+ .metal-border {
152
+ position: relative;
153
+ padding: 20px;
154
+ background: #333;
155
+ color: white;
156
+ border: 4px solid;
157
+ border-image: linear-gradient(45deg, #a1a1a1, #ffffff, #a1a1a1, #666666) 1;
158
+ border-radius: 8px;
159
+ }
160
+
161
+ .shiny-border {
162
+ position: relative;
163
+ padding: 20px;
164
+ background: #333;
165
+ color: white;
166
+ border-radius: 8px;
167
+ overflow: hidden;
168
+ }
169
+
170
+ .shiny-border::before {
171
+ content: '';
172
+ position: absolute;
173
+ top: 0;
174
+ left: -100%;
175
+ width: 100%;
176
+ height: 100%;
177
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
178
+ transition: left 0.5s;
179
+ }
180
+
181
+ .shiny-border:hover::before {
182
+ left: 100%;
183
+ }
184
  </style>
185
  """,
186
  unsafe_allow_html=True,
 
191
  if "initialized" in st.session_state and st.session_state.initialized:
192
  return
193
 
194
+ # Ensure a default selection exists before creating the puzzle
195
+ files = get_wordlist_files()
196
+ if "selected_wordlist" not in st.session_state and files:
197
+ st.session_state.selected_wordlist = "wordlist.txt"
198
+
199
+ words = load_word_list(st.session_state.get("selected_wordlist"))
200
  puzzle = generate_puzzle(grid_size=12, words_by_len=words)
201
 
202
  st.session_state.puzzle = puzzle
 
212
 
213
 
214
  def _new_game() -> None:
215
+ # Preserve selected wordlist across resets
216
+ selected = st.session_state.get("selected_wordlist")
217
  st.session_state.clear()
218
+ if selected:
219
+ st.session_state.selected_wordlist = selected
220
  _init_session()
221
 
222
 
 
262
  "- Radar pulses show the last letter position of each hidden word.\n"
263
  "- After each reveal, you may submit one word guess below.\n"
264
  "- Scoring: length + unrevealed letters of that word at guess time.")
265
+
266
+ st.header("Wordlist Controls")
267
+ wordlist_files = get_wordlist_files()
268
+
269
+ if wordlist_files:
270
+ # Ensure current selection is valid
271
+ if st.session_state.get("selected_wordlist") not in wordlist_files:
272
+ st.session_state.selected_wordlist = wordlist_files[0]
273
+
274
+ # Use filenames as options, show without extension
275
+ current_index = wordlist_files.index(st.session_state.selected_wordlist)
276
+ st.selectbox(
277
+ "Select list",
278
+ options=wordlist_files,
279
+ index=current_index,
280
+ format_func=lambda f: f.rsplit(".", 1)[0],
281
+ key="selected_wordlist",
282
+ on_change=_new_game, # immediately start a new game with the selected list
283
+ )
284
+
285
+ if st.button("Sort Wordlist"):
286
+ _sort_wordlist(st.session_state.selected_wordlist)
287
+ else:
288
+ st.info("No word lists found in words/ directory. Using built-in fallback.")
289
 
290
 
291
  def _render_radar(puzzle: Puzzle, size: int):
 
333
  min-height: 32px !important;
334
  padding: 0 !important;
335
  margin: 0 !important;
336
+ border: 1px solid #1d64c8 !important;
337
+ border-radius: 0 !important;
338
+ background: #1d64c8 !important;
339
+ color: #ffffff !important;
340
  font-weight: bold;
341
  font-size: 1rem;
342
  }
343
+ /* Further tighten vertical spacing between rows inside the grid container */
344
+ .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] {
345
+ margin: 2px 0 !important;
346
+ }
347
  </style>
348
  """,
349
  unsafe_allow_html=True,
350
  )
351
 
352
  grid_container = st.container()
353
+ with grid_container:
354
  for r in range(size):
 
355
  st.markdown('<div class="bw-grid-row-anchor"></div>', unsafe_allow_html=True)
356
  cols = st.columns(size, gap="small")
357
  for c in range(size):
358
  coord = Coord(r, c)
359
  revealed = coord in state.revealed
 
360
  label = letter_map.get(coord, " ") if revealed else " "
361
 
 
362
  is_completed_cell = False
363
  if revealed:
364
  for w in state.puzzle.words:
 
370
  tooltip = f"({r+1},{c+1})"
371
 
372
  if is_completed_cell:
373
+ # Render a styled non-button cell for a completed word with native browser tooltip
374
  safe_label = (label or " ")
375
  cols[c].markdown(
376
  f'<div class="bw-cell bw-cell-complete" title="{tooltip}">{safe_label}</div>',
377
  unsafe_allow_html=True,
378
  )
379
  elif revealed:
380
+ # Use 'letter' when a letter exists, otherwise 'empty'
381
  safe_label = (label or " ")
382
+ has_letter = safe_label.strip() != ""
383
+ cell_class = "letter" if has_letter else "empty"
384
+ display = safe_label if has_letter else "&nbsp;"
385
  cols[c].markdown(
386
+ f'<div class="bw-cell {cell_class}" title="{tooltip}">{display}</div>',
387
  unsafe_allow_html=True,
388
  )
389
  else:
390
  # Unrevealed: render a button to allow click/reveal with tooltip
391
  if cols[c].button(" ", key=key, help=tooltip):
392
  clicked = coord
393
+
394
  if clicked is not None:
395
  reveal_cell(state, letter_map, clicked)
396
  st.session_state.letter_map = build_letter_map(st.session_state.puzzle)
 
412
  st.metric("Score", state.score)
413
  with col2:
414
  st.markdown(f"Last action: {state.last_action}")
415
+ with st.expander("Game summary", expanded=True):
416
+ for w in state.puzzle.words:
417
+ pts = state.points_by_word.get(w.text, 0)
418
+ st.markdown(f"- {w.text} ({len(w.text)}): +{pts} points")
419
+ st.markdown(f"**Total**: {state.score}")
420
 
421
 
422
  def _render_game_over(state: GameState):
 
437
  st.stop()
438
 
439
 
440
+ def _sort_wordlist(filename):
441
+ import os
442
+ import time # Add this import
443
+
444
+ WORDS_DIR = os.path.join(os.path.dirname(__file__), "words")
445
+ filepath = os.path.join(WORDS_DIR, filename)
446
+ sorted_words = sort_word_file(filepath)
447
+ # Optionally, write sorted words back to file
448
+ with open(filepath, "w", encoding="utf-8") as f:
449
+ # Re-add header if needed
450
+ f.write("# Optional: place a large A–Z word list here (one word per line).\n")
451
+ f.write("# The app falls back to built-in pools if fewer than 500 words per length are found.\n")
452
+ for word in sorted_words:
453
+ f.write(f"{word}\n")
454
+ # Show a message in Streamlit
455
+ st.success(f"{filename} sorted by length and alphabetically. Starting new game in 5 seconds...")
456
+ time.sleep(5) # 5 second delay before starting new game
457
+ _new_game()
458
+
459
+
460
  def run_app():
461
  _init_session()
462
  _render_header()
 
466
 
467
  # Anchor to target the main two-column layout for mobile reversal
468
  st.markdown('<div id="bw-main-anchor"></div>', unsafe_allow_html=True)
469
+ left, right = st.columns([2, 2], gap="medium")
470
  with left:
471
  _render_grid(state, st.session_state.letter_map)
472
  with right:
battlewords/word_loader.py CHANGED
@@ -1,7 +1,8 @@
1
  from __future__ import annotations
2
 
3
  import re
4
- from typing import Dict, List
 
5
 
6
  import streamlit as st
7
  from importlib import resources
@@ -21,15 +22,23 @@ FALLBACK_WORDS: Dict[int, List[str]] = {
21
  ],
22
  }
23
 
 
 
 
 
 
 
24
 
25
  @st.cache_data(show_spinner=False)
26
- def load_word_list() -> Dict[int, List[str]]:
27
  """
28
- Load the word list from battlewords/words/wordlist.txt, filter to uppercase A–Z,
29
- lengths in {4,5,6}, and dedupe while preserving order.
 
 
30
 
31
  If fewer than 500 entries exist for any required length, fall back to built-ins
32
- for that length (per specs). Sets quick status in session_state for visibility.
33
  """
34
  words_by_len: Dict[int, List[str]] = {4: [], 5: [], 6: []}
35
  used_source = "fallback"
@@ -37,14 +46,27 @@ def load_word_list() -> Dict[int, List[str]]:
37
  def _finalize(wbl: Dict[int, List[str]], source: str) -> Dict[int, List[str]]:
38
  try:
39
  st.session_state.wordlist_source = source
 
40
  st.session_state.word_counts = {k: len(v) for k, v in wbl.items()}
41
  except Exception:
42
  pass
43
  return wbl
44
 
 
 
 
 
 
 
45
  try:
46
- # Read packaged resource
47
- text = resources.files("battlewords.words").joinpath("wordlist.txt").read_text(encoding="utf-8")
 
 
 
 
 
 
48
 
49
  seen = {4: set(), 5: set(), 6: set()}
50
  for raw in text.splitlines():
@@ -62,17 +84,17 @@ def load_word_list() -> Dict[int, List[str]]:
62
  seen[L].add(word)
63
 
64
  counts = {k: len(v) for k, v in words_by_len.items()}
65
- if all(counts[k] >= 500 for k in (4, 5, 6)):
66
  used_source = "file"
67
  return _finalize(words_by_len, used_source)
68
 
69
  # Per spec: fallback for any length below threshold
70
  mixed: Dict[int, List[str]] = {
71
- 4: words_by_len[4] if counts[4] >= 500 else FALLBACK_WORDS[4],
72
- 5: words_by_len[5] if counts[5] >= 500 else FALLBACK_WORDS[5],
73
- 6: words_by_len[6] if counts[6] >= 500 else FALLBACK_WORDS[6],
74
  }
75
- used_source = "file+fallback" if any(counts[k] >= 500 for k in (4, 5, 6)) else "fallback"
76
  return _finalize(mixed, used_source)
77
 
78
  except Exception:
 
1
  from __future__ import annotations
2
 
3
  import re
4
+ import os
5
+ from typing import Dict, List, Optional
6
 
7
  import streamlit as st
8
  from importlib import resources
 
22
  ],
23
  }
24
 
25
+ def get_wordlist_files() -> list[str]:
26
+ words_dir = os.path.join(os.path.dirname(__file__), "words")
27
+ if not os.path.isdir(words_dir):
28
+ return []
29
+ files = [f for f in os.listdir(words_dir) if f.lower().endswith(".txt")]
30
+ return sorted(files)
31
 
32
  @st.cache_data(show_spinner=False)
33
+ def load_word_list(selected_file: Optional[str] = None) -> Dict[int, List[str]]:
34
  """
35
+ Load a word list, filter to uppercase A–Z, lengths in {4,5,6}, and dedupe while preserving order.
36
+
37
+ If `selected_file` is provided, load battlewords/words/<selected_file>.
38
+ Otherwise, try packaged resource battlewords/words/wordlist.txt.
39
 
40
  If fewer than 500 entries exist for any required length, fall back to built-ins
41
+ for that length (per specs).
42
  """
43
  words_by_len: Dict[int, List[str]] = {4: [], 5: [], 6: []}
44
  used_source = "fallback"
 
46
  def _finalize(wbl: Dict[int, List[str]], source: str) -> Dict[int, List[str]]:
47
  try:
48
  st.session_state.wordlist_source = source
49
+ st.session_state.wordlist_selected = selected_file or "wordlist.txt"
50
  st.session_state.word_counts = {k: len(v) for k, v in wbl.items()}
51
  except Exception:
52
  pass
53
  return wbl
54
 
55
+ def _read_text_from_disk(fname: str) -> str:
56
+ words_dir = os.path.join(os.path.dirname(__file__), "words")
57
+ path = os.path.join(words_dir, fname)
58
+ with open(path, "r", encoding="utf-8") as f:
59
+ return f.read()
60
+
61
  try:
62
+ text: Optional[str] = None
63
+
64
+ if selected_file:
65
+ # Prefer explicit selection from words/ directory.
66
+ text = _read_text_from_disk(selected_file)
67
+ else:
68
+ # Fallback to packaged default wordlist.txt
69
+ text = resources.files("battlewords.words").joinpath("wordlist.txt").read_text(encoding="utf-8")
70
 
71
  seen = {4: set(), 5: set(), 6: set()}
72
  for raw in text.splitlines():
 
84
  seen[L].add(word)
85
 
86
  counts = {k: len(v) for k, v in words_by_len.items()}
87
+ if all(counts[k] >= 250 for k in (4, 5, 6)):
88
  used_source = "file"
89
  return _finalize(words_by_len, used_source)
90
 
91
  # Per spec: fallback for any length below threshold
92
  mixed: Dict[int, List[str]] = {
93
+ 4: words_by_len[4] if counts[4] >= 250 else FALLBACK_WORDS[4],
94
+ 5: words_by_len[5] if counts[5] >= 250 else FALLBACK_WORDS[5],
95
+ 6: words_by_len[6] if counts[6] >= 250 else FALLBACK_WORDS[6],
96
  }
97
+ used_source = "file+fallback" if any(counts[k] >= 250 for k in (4, 5, 6)) else "fallback"
98
  return _finalize(mixed, used_source)
99
 
100
  except Exception:
battlewords/words/__init__.py ADDED
File without changes
battlewords/words/classic.txt ADDED
@@ -0,0 +1,800 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Optional: place a large A–Z word list here (one word per line).
2
+ # The app falls back to built-in pools if fewer than 500 words per length are found.
3
+ ABLE
4
+ ACID
5
+ ACRE
6
+ ALSO
7
+ AREA
8
+ ARMY
9
+ AUNT
10
+ BAIT
11
+ BANG
12
+ BANK
13
+ BARE
14
+ BASE
15
+ BEAM
16
+ BEAR
17
+ BEAT
18
+ BEET
19
+ BELT
20
+ BENT
21
+ BILL
22
+ BIND
23
+ BOND
24
+ BORE
25
+ BORN
26
+ BOWL
27
+ BULL
28
+ BUMP
29
+ BURN
30
+ BUSY
31
+ CAGE
32
+ CALF
33
+ CALM
34
+ CANE
35
+ CARE
36
+ CASE
37
+ CASH
38
+ CAVE
39
+ CHEW
40
+ CHOP
41
+ CLAY
42
+ CLUB
43
+ COAL
44
+ COIN
45
+ COPY
46
+ CORN
47
+ COST
48
+ COST
49
+ CUTE
50
+ DASH
51
+ DATE
52
+ DAWN
53
+ DEAD
54
+ DEAF
55
+ DEAL
56
+ DEBT
57
+ DECK
58
+ DEED
59
+ DEER
60
+ DESK
61
+ DIME
62
+ DIRT
63
+ DIVE
64
+ DRAG
65
+ DRAW
66
+ DRUG
67
+ DRUM
68
+ DUCK
69
+ DUTY
70
+ EARN
71
+ EAST
72
+ EASY
73
+ ECHO
74
+ EDGE
75
+ ELSE
76
+ FACT
77
+ FAIR
78
+ FAME
79
+ FARE
80
+ FATE
81
+ FEAR
82
+ FEET
83
+ FILE
84
+ FILM
85
+ FOND
86
+ FOOL
87
+ FORD
88
+ FORK
89
+ FORM
90
+ FORT
91
+ FOUL
92
+ FROG
93
+ GAIN
94
+ GANG
95
+ GOAT
96
+ GOLF
97
+ GOWN
98
+ GRAY
99
+ GROW
100
+ GULF
101
+ HAIR
102
+ HARM
103
+ HATE
104
+ HEAP
105
+ HEAT
106
+ HEEL
107
+ HIGH
108
+ HIKE
109
+ HOLE
110
+ HOLY
111
+ HOOK
112
+ HORN
113
+ HOSE
114
+ HOUR
115
+ HOWL
116
+ HUGE
117
+ HUNT
118
+ HURT
119
+ INCH
120
+ IRON
121
+ JAIL
122
+ JOIN
123
+ JOKE
124
+ KICK
125
+ KITE
126
+ KNEE
127
+ KNOT
128
+ KNOW
129
+ LACE
130
+ LACK
131
+ LAKE
132
+ LAMB
133
+ LAMP
134
+ LAWN
135
+ LAZY
136
+ LEAD
137
+ LEAF
138
+ LEAK
139
+ LEAN
140
+ LESS
141
+ LIFE
142
+ LIFT
143
+ LIMB
144
+ LIMP
145
+ LION
146
+ LOOP
147
+ LOUD
148
+ LUMP
149
+ LUNG
150
+ MAID
151
+ MAIL
152
+ MARK
153
+ MASK
154
+ MATE
155
+ MEAL
156
+ MEAN
157
+ MEET
158
+ MELT
159
+ MEND
160
+ MILD
161
+ MILE
162
+ MILL
163
+ MINT
164
+ MISS
165
+ MIST
166
+ MOOD
167
+ MOON
168
+ MOSS
169
+ NAIL
170
+ NAVY
171
+ NEAT
172
+ NONE
173
+ NOTE
174
+ OMIT
175
+ ONCE
176
+ ONLY
177
+ OVEN
178
+ OVER
179
+ PAIL
180
+ PAIN
181
+ PAIR
182
+ PALM
183
+ PART
184
+ PATH
185
+ PEEK
186
+ PILE
187
+ PIPE
188
+ POEM
189
+ POLE
190
+ POND
191
+ PONY
192
+ POOL
193
+ POST
194
+ QUIT
195
+ RACE
196
+ RAKE
197
+ RANK
198
+ RARE
199
+ REAL
200
+ RICE
201
+ RICH
202
+ RIPE
203
+ RISE
204
+ RISK
205
+ ROAR
206
+ ROOT
207
+ RUIN
208
+ SACK
209
+ SAFE
210
+ SAIL
211
+ SAKE
212
+ SALT
213
+ SAVE
214
+ SCAR
215
+ SEAL
216
+ SEAT
217
+ SEEM
218
+ SHED
219
+ SILK
220
+ SINK
221
+ SIZE
222
+ SKIM
223
+ SKIN
224
+ SLIP
225
+ SNAP
226
+ SOAK
227
+ SOAP
228
+ SOIL
229
+ SORE
230
+ SORT
231
+ SOUR
232
+ SPIN
233
+ SUCH
234
+ SUIT
235
+ SWIM
236
+ TAIL
237
+ TEAM
238
+ TEAR
239
+ TEND
240
+ TENT
241
+ TEST
242
+ TINY
243
+ TIRE
244
+ TONE
245
+ TOSS
246
+ TRAP
247
+ TRIM
248
+ TRUE
249
+ TWIN
250
+ UGLY
251
+ UNIT
252
+ VIEW
253
+ WADE
254
+ WAIT
255
+ WALK
256
+ WEAK
257
+ WEST
258
+ WIFE
259
+ WILD
260
+ WINE
261
+ WIRE
262
+ WISE
263
+ WOOD
264
+ WOOL
265
+ WORD
266
+ YARD
267
+ YELL
268
+ ZONE
269
+ ABOVE
270
+ AGAIN
271
+ AGREE
272
+ AHEAD
273
+ ALARM
274
+ ALIKE
275
+ ALLOW
276
+ ALONE
277
+ AMONG
278
+ ANGER
279
+ ANGRY
280
+ APART
281
+ ARROW
282
+ ATTIC
283
+ AWFUL
284
+ BADGE
285
+ BEAST
286
+ BEGIN
287
+ BELOW
288
+ BLADE
289
+ BLAME
290
+ BLAZE
291
+ BLIND
292
+ BLOCK
293
+ BLOOD
294
+ BOARD
295
+ BRAID
296
+ BRAIN
297
+ BRAKE
298
+ BRASS
299
+ BRAVE
300
+ BREAK
301
+ BRICK
302
+ BRIDE
303
+ BROOK
304
+ BRUSH
305
+ BUGGY
306
+ BUILD
307
+ BUNCH
308
+ BURST
309
+ CABIN
310
+ CABLE
311
+ CANAL
312
+ CAUSE
313
+ CHAIN
314
+ CHASE
315
+ CHEAT
316
+ CHECK
317
+ CHEER
318
+ CHIEF
319
+ CLERK
320
+ CLIFF
321
+ CLIMB
322
+ CLOAK
323
+ CLOTH
324
+ CLOWN
325
+ COACH
326
+ COCOA
327
+ COLOR
328
+ COUGH
329
+ COUNT
330
+ COURT
331
+ CRACK
332
+ CRASH
333
+ CRAWL
334
+ CRAZY
335
+ CREEK
336
+ CRIME
337
+ CROSS
338
+ CROWD
339
+ CRUSH
340
+ DAILY
341
+ DEATH
342
+ DELAY
343
+ DIRTY
344
+ DITCH
345
+ DOZEN
346
+ DREAM
347
+ EARLY
348
+ EARTH
349
+ EMPTY
350
+ ENJOY
351
+ ERECT
352
+ EVENT
353
+ FALSE
354
+ FANCY
355
+ FAVOR
356
+ FEAST
357
+ FENCE
358
+ FEVER
359
+ FIELD
360
+ FIFTH
361
+ FLASH
362
+ FLOCK
363
+ FLOOD
364
+ FLOOR
365
+ FLOUR
366
+ FORTY
367
+ FRAME
368
+ FRANK
369
+ FRESH
370
+ FRONT
371
+ GLASS
372
+ GLOBE
373
+ GRACE
374
+ GRAVE
375
+ GRAZE
376
+ GREAT
377
+ GREET
378
+ GRIND
379
+ GROUP
380
+ GROWL
381
+ GUARD
382
+ GUIDE
383
+ HABIT
384
+ HANDY
385
+ HATCH
386
+ HEART
387
+ HEAVY
388
+ HONEY
389
+ HOTEL
390
+ IDEAL
391
+ JELLY
392
+ JUDGE
393
+ JUICY
394
+ KNIFE
395
+ KNOCK
396
+ LAUGH
397
+ LEMON
398
+ LEVEL
399
+ LODGE
400
+ LOOSE
401
+ LUCKY
402
+ MAGIC
403
+ MARCH
404
+ MARRY
405
+ MAYOR
406
+ MODEL
407
+ MONEY
408
+ MONTH
409
+ MOTOR
410
+ MOUSE
411
+ MUDDY
412
+ MUSIC
413
+ NINTH
414
+ NOISE
415
+ NURSE
416
+ OCEAN
417
+ OFFEN
418
+ OFFER
419
+ ORDER
420
+ ORGAN
421
+ OUNCE
422
+ OWNER
423
+ PAPER
424
+ PARTY
425
+ PASTE
426
+ PATCH
427
+ PEACE
428
+ PHONE
429
+ PIANO
430
+ PIECE
431
+ PLAIN
432
+ PLANE
433
+ POINT
434
+ PORCH
435
+ POUND
436
+ POWER
437
+ PRESS
438
+ PRICE
439
+ PRINT
440
+ PRIZE
441
+ PROUD
442
+ PUPIL
443
+ PURSE
444
+ QUART
445
+ QUEEN
446
+ QUEST
447
+ QUIET
448
+ QUITE
449
+ RAINY
450
+ RAISE
451
+ RANCH
452
+ RANGE
453
+ REACH
454
+ READY
455
+ RIFLE
456
+ RIVER
457
+ ROUGH
458
+ ROUTE
459
+ SCALE
460
+ SCARE
461
+ SCARF
462
+ SCOLD
463
+ SCRAP
464
+ SCREW
465
+ SCRUB
466
+ SENSE
467
+ SHACK
468
+ SHAKE
469
+ SHAME
470
+ SHAPE
471
+ SHARE
472
+ SHEEP
473
+ SHEET
474
+ SHELL
475
+ SHINE
476
+ SHIRT
477
+ SHOCK
478
+ SHORE
479
+ SILLY
480
+ SINCE
481
+ SKATE
482
+ SKILL
483
+ SKIRT
484
+ SLIDE
485
+ SMILE
486
+ SMOKE
487
+ SORRY
488
+ SPACE
489
+ SPEAK
490
+ SPEND
491
+ SPOON
492
+ SPORT
493
+ STACK
494
+ STAIR
495
+ STAKE
496
+ STALK
497
+ STEAM
498
+ STEEL
499
+ STEEP
500
+ STONE
501
+ STOOP
502
+ STRAW
503
+ STRIP
504
+ STUDY
505
+ STUMP
506
+ SUNNY
507
+ SWEEP
508
+ SWING
509
+ SWORD
510
+ TEETH
511
+ THICK
512
+ THROW
513
+ TIRED
514
+ TOOTH
515
+ TOUCH
516
+ TOWER
517
+ TRACE
518
+ TRAMP
519
+ TRIBE
520
+ TRICK
521
+ TROOP
522
+ TROUT
523
+ UNCLE
524
+ UNTIL
525
+ UPPER
526
+ WAIST
527
+ WASTE
528
+ WHALE
529
+ WHEEL
530
+ WHOLE
531
+ WHOSE
532
+ WINDY
533
+ WOMAN
534
+ WOMEN
535
+ WORRY
536
+ WORSE
537
+ WORTH
538
+ WOUND
539
+ WRIST
540
+ WRONG
541
+ ABSENT
542
+ ACCENT
543
+ ACROSS
544
+ ACTING
545
+ ADMIRE
546
+ ADVICE
547
+ AFFECT
548
+ AFRAID
549
+ ALMOST
550
+ ALWAYS
551
+ AMOUNT
552
+ ANSWER
553
+ ANYHOW
554
+ ANYONE
555
+ ANYWAY
556
+ APPEAR
557
+ AROUND
558
+ ARREST
559
+ ARRIVE
560
+ ARTIST
561
+ ATTACK
562
+ ATTEND
563
+ AVENUE
564
+ BARREL
565
+ BASKET
566
+ BATTLE
567
+ BEFORE
568
+ BELONG
569
+ BEYOND
570
+ BIGGER
571
+ BLOUSE
572
+ BORDER
573
+ BOTTOM
574
+ BRANCH
575
+ BREAST
576
+ BREATH
577
+ BRIDGE
578
+ BRUISE
579
+ BUCKET
580
+ BUNDLE
581
+ BUTTON
582
+ CANDLE
583
+ CARPET
584
+ CASTLE
585
+ CATTLE
586
+ CELLAR
587
+ CEMENT
588
+ CENTER
589
+ CHANCE
590
+ CHANGE
591
+ CHARGE
592
+ CHERRY
593
+ CHOICE
594
+ CHOOSE
595
+ CHURCH
596
+ CIRCLE
597
+ CLEVER
598
+ CLOSET
599
+ COLLAR
600
+ COLONY
601
+ COPPER
602
+ CORNER
603
+ COTTON
604
+ COUNTY
605
+ COURSE
606
+ COUSIN
607
+ CREDIT
608
+ CROUCH
609
+ DANGER
610
+ DECIDE
611
+ DEFEAT
612
+ DEMAND
613
+ DEPART
614
+ DEPEND
615
+ DESERT
616
+ DESIRE
617
+ DIFFER
618
+ DIRECT
619
+ DIVIDE
620
+ DOCTOR
621
+ DOLLAR
622
+ DOUBLE
623
+ DRIVER
624
+ DURING
625
+ EFFORT
626
+ EIGHTY
627
+ ELEVEN
628
+ ENGINE
629
+ ENOUGH
630
+ ESCAPE
631
+ FAIRLY
632
+ FAMOUS
633
+ FASTEN
634
+ FIGURE
635
+ FINGER
636
+ FINISH
637
+ FLAVOR
638
+ FLIGHT
639
+ FOLLOW
640
+ FORBID
641
+ FOURTH
642
+ FREEZE
643
+ FRIEND
644
+ FRIGHT
645
+ GALLON
646
+ GARAGE
647
+ GARDEN
648
+ GOLDEN
649
+ GROWTH
650
+ HAMMER
651
+ HANDLE
652
+ HARBOR
653
+ HARDER
654
+ HEALTH
655
+ HEIGHT
656
+ HELMET
657
+ HIGHER
658
+ HONEST
659
+ HOPING
660
+ HUNGRY
661
+ IMPORT
662
+ INDEED
663
+ INJURE
664
+ INTEND
665
+ INTENT
666
+ INVENT
667
+ INVITE
668
+ ISLAND
669
+ ITSELF
670
+ JACKET
671
+ JUNGLE
672
+ JUNIOR
673
+ KETTLE
674
+ LADDER
675
+ LATELY
676
+ LENGTH
677
+ LESSON
678
+ LIKELY
679
+ LINING
680
+ LIQUID
681
+ LISTEN
682
+ LIVELY
683
+ LIVING
684
+ LOCATE
685
+ LONELY
686
+ LOVELY
687
+ LUMBER
688
+ MAKING
689
+ MANUAL
690
+ MARBLE
691
+ MASTER
692
+ MATTER
693
+ MEADOW
694
+ MEDIUM
695
+ MEMBER
696
+ MIDDLE
697
+ MINUTE
698
+ MOMENT
699
+ MOSTLY
700
+ MUSCLE
701
+ NATION
702
+ NATURE
703
+ NEARBY
704
+ NEEDLE
705
+ NEPHEW
706
+ NICKEL
707
+ NOBODY
708
+ NOTICE
709
+ NUMBER
710
+ ORANGE
711
+ OUTFIT
712
+ PADDLE
713
+ PALACE
714
+ PEOPLE
715
+ PERIOD
716
+ PERMIT
717
+ PERSON
718
+ PICKLE
719
+ PICNIC
720
+ PILLOW
721
+ PLENTY
722
+ POCKET
723
+ POSTER
724
+ POTATO
725
+ POWDER
726
+ PRAYER
727
+ PREACH
728
+ PRINCE
729
+ PROPER
730
+ PUBLIC
731
+ PURPLE
732
+ PUZZLE
733
+ RACKET
734
+ RAISIN
735
+ READER
736
+ REALLY
737
+ REASON
738
+ RECESS
739
+ RECORD
740
+ REMAIN
741
+ REMARK
742
+ REMIND
743
+ REMOVE
744
+ REPAIR
745
+ REPORT
746
+ RESULT
747
+ RIBBON
748
+ SADDLE
749
+ SAFETY
750
+ SAILOR
751
+ SAVAGE
752
+ SCARCE
753
+ SCRAPE
754
+ SCREAM
755
+ SEASON
756
+ SECOND
757
+ SECRET
758
+ SECURE
759
+ SELECT
760
+ SHADOW
761
+ SHOVEL
762
+ SHOWER
763
+ SIGNAL
764
+ SILENT
765
+ SILVER
766
+ SINGLE
767
+ SLEEPY
768
+ SLEEVE
769
+ SLOWLY
770
+ SPIRIT
771
+ SPREAD
772
+ SQUARE
773
+ STABLE
774
+ STARVE
775
+ STATUE
776
+ STRAIN
777
+ STREAM
778
+ STRONG
779
+ STUPID
780
+ SUPPLY
781
+ SWITCH
782
+ TABLET
783
+ TACKLE
784
+ TEMPER
785
+ TEMPLE
786
+ TENNIS
787
+ THRILL
788
+ TOMATO
789
+ TOWARD
790
+ TURTLE
791
+ TWENTY
792
+ UNEASY
793
+ UNLOAD
794
+ UNLOCK
795
+ VACANT
796
+ VALLEY
797
+ VOYAGE
798
+ WITHIN
799
+ WONDER
800
+ WRENCH
battlewords/words/wordlist.txt CHANGED
@@ -1,35 +1,5 @@
1
  # Optional: place a large A–Z word list here (one word per line).
2
  # The app falls back to built-in pools if fewer than 500 words per length are found.
3
- TREE
4
- BOAT
5
- WIND
6
- FROG
7
- LION
8
- MOON
9
- FORK
10
- GLOW
11
- GAME
12
- CODE
13
- APPLE
14
- RIVER
15
- STONE
16
- PLANT
17
- MOUSE
18
- BOARD
19
- CHAIR
20
- SCALE
21
- SMILE
22
- CLOUD
23
- ORANGE
24
- PYTHON
25
- STREAM
26
- MARKET
27
- FOREST
28
- THRIVE
29
- LOGGER
30
- BREATH
31
- DOMAIN
32
- GALAXY
33
  ABLE
34
  ACID
35
  AGED
@@ -113,7 +83,6 @@ DECK
113
  DEEP
114
  DEER
115
  DIAL
116
- DICK
117
  DIET
118
  DISC
119
  DISK
@@ -164,14 +133,17 @@ FISH
164
  FIVE
165
  FLAT
166
  FLOW
 
167
  FOAM
168
  FOOD
169
  FOOT
170
  FORD
 
171
  FORM
172
  FORT
173
  FOUR
174
  FREE
 
175
  FROM
176
  FUEL
177
  FULL
@@ -185,6 +157,7 @@ GIFT
185
  GIRL
186
  GIVE
187
  GLAD
 
188
  GOAL
189
  GOAT
190
  GOLD
@@ -192,7 +165,6 @@ GOLF
192
  GONE
193
  GOOD
194
  GRAY
195
- GREAT
196
  GRID
197
  GRIP
198
  GROW
@@ -232,10 +204,6 @@ INCH
232
  INTO
233
  IRON
234
  ITEM
235
- JACK
236
- JANE
237
- JEAN
238
- JOHN
239
  JOIN
240
  JUMP
241
  JURY
@@ -264,6 +232,7 @@ LIFT
264
  LIKE
265
  LINE
266
  LINK
 
267
  LIST
268
  LIVE
269
  LOAD
@@ -299,6 +268,7 @@ MISS
299
  MODE
300
  MOOD
301
  MOON
 
302
  MORE
303
  MOST
304
  MOVE
@@ -309,6 +279,7 @@ NAVY
309
  NEAR
310
  NECK
311
  NEED
 
312
  NEWS
313
  NEXT
314
  NICE
@@ -466,7 +437,6 @@ TINY
466
  TOLD
467
  TOLL
468
  TONE
469
- TONY
470
  TOOL
471
  TOUR
472
  TOWN
@@ -511,6 +481,7 @@ WIFE
511
  WILD
512
  WILL
513
  WIND
 
514
  WINE
515
  WING
516
  WIRE
@@ -525,47 +496,71 @@ YARN
525
  YEAR
526
  YELL
527
  YOGA
528
- YOUNG
529
- YOUR
530
  ZERO
531
  ZONE
532
  APPLE
533
- RIVER
534
- STONE
535
- PLANT
536
- MOUSE
537
  BOARD
 
 
538
  CHAIR
539
- SCALE
540
- SMILE
541
  CLOUD
542
- ORANGE
543
- PYTHON
544
- STREAM
545
- MARKET
546
- FOREST
547
- THRIVE
548
- LOGGER
549
- BREATH
550
- DOMAIN
551
- GALAXY
552
- BREAD
553
  CRANE
 
 
 
554
  FLAME
 
 
555
  GRAPE
 
 
 
 
556
  LEMON
 
557
  MARCH
 
558
  NURSE
 
 
 
559
  PRIZE
 
 
 
560
  SHINE
 
 
561
  TIGER
562
- BANNER
 
563
  CANDLE
 
 
 
 
564
  FAMILY
 
 
 
565
  GARDEN
 
566
  JACKET
567
  LADDER
 
 
 
 
 
 
568
  POCKET
569
  SILVER
 
 
 
570
  TUNNEL
571
- WINNER
 
 
1
  # Optional: place a large A–Z word list here (one word per line).
2
  # The app falls back to built-in pools if fewer than 500 words per length are found.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ABLE
4
  ACID
5
  AGED
 
83
  DEEP
84
  DEER
85
  DIAL
 
86
  DIET
87
  DISC
88
  DISK
 
133
  FIVE
134
  FLAT
135
  FLOW
136
+ FLTE
137
  FOAM
138
  FOOD
139
  FOOT
140
  FORD
141
+ FORK
142
  FORM
143
  FORT
144
  FOUR
145
  FREE
146
+ FROG
147
  FROM
148
  FUEL
149
  FULL
 
157
  GIRL
158
  GIVE
159
  GLAD
160
+ GLOW
161
  GOAL
162
  GOAT
163
  GOLD
 
165
  GONE
166
  GOOD
167
  GRAY
 
168
  GRID
169
  GRIP
170
  GROW
 
204
  INTO
205
  IRON
206
  ITEM
 
 
 
 
207
  JOIN
208
  JUMP
209
  JURY
 
232
  LIKE
233
  LINE
234
  LINK
235
+ LION
236
  LIST
237
  LIVE
238
  LOAD
 
268
  MODE
269
  MOOD
270
  MOON
271
+ MOON
272
  MORE
273
  MOST
274
  MOVE
 
279
  NEAR
280
  NECK
281
  NEED
282
+ NERD
283
  NEWS
284
  NEXT
285
  NICE
 
437
  TOLD
438
  TOLL
439
  TONE
 
440
  TOOL
441
  TOUR
442
  TOWN
 
481
  WILD
482
  WILL
483
  WIND
484
+ WIND
485
  WINE
486
  WING
487
  WIRE
 
496
  YEAR
497
  YELL
498
  YOGA
 
 
499
  ZERO
500
  ZONE
501
  APPLE
502
+ BLAST
 
 
 
503
  BOARD
504
+ BRAVE
505
+ BREAD
506
  CHAIR
507
+ CHALK
508
+ CHESS
509
  CLOUD
 
 
 
 
 
 
 
 
 
 
 
510
  CRANE
511
+ DANCE
512
+ EARTH
513
+ FAITH
514
  FLAME
515
+ FLUTE
516
+ GHOST
517
  GRAPE
518
+ GRASS
519
+ GREAT
520
+ HEART
521
+ HEART
522
  LEMON
523
+ LIGHT
524
  MARCH
525
+ MOUSE
526
  NURSE
527
+ PANEL
528
+ PANEL
529
+ PLANT
530
  PRIZE
531
+ QUEST
532
+ RIVER
533
+ SCALE
534
  SHINE
535
+ SMILE
536
+ STONE
537
  TIGER
538
+ YOUNG
539
+ BUNDLE
540
  CANDLE
541
+ CHERRY
542
+ CIRCLE
543
+ DOCTOR
544
+ DOMAIN
545
  FAMILY
546
+ FOREST
547
+ FRIEND
548
+ GALAXY
549
  GARDEN
550
+ HUNTER
551
  JACKET
552
  LADDER
553
+ LAUNCH
554
+ LOGGER
555
+ MARKET
556
+ MOTHER
557
+ ORANGE
558
+ PALACE
559
  POCKET
560
  SILVER
561
+ SPIRIT
562
+ STREAM
563
+ THRIVE
564
  TUNNEL
565
+ WINNER
566
+ WINTER