Surn commited on
Commit
99bf0ab
·
1 Parent(s): 3666fb8

Add EASY MODE and fix wordlist selections

Browse files
battlewords/logic.py CHANGED
@@ -64,7 +64,10 @@ def guess_word(state: GameState, guess_text: str) -> Tuple[bool, int]:
64
  state.guessed.add(target.text)
65
 
66
  state.last_action = f"Correct! +{points} points for {target.text}."
67
- state.can_guess = True # <-- Allow another guess after a correct guess
 
 
 
68
  return True, points
69
 
70
 
 
64
  state.guessed.add(target.text)
65
 
66
  state.last_action = f"Correct! +{points} points for {target.text}."
67
+ if state.game_mode == "standard":
68
+ state.can_guess = True # <-- Allow another guess after a correct guess
69
+ else:
70
+ state.can_guess = False
71
  return True, points
72
 
73
 
battlewords/models.py CHANGED
@@ -67,4 +67,5 @@ class GameState:
67
  score: int
68
  last_action: str
69
  can_guess: bool
 
70
  points_by_word: Dict[str, int] = field(default_factory=dict)
 
67
  score: int
68
  last_action: str
69
  can_guess: bool
70
+ game_mode: Literal["standard", "easy"] = "standard"
71
  points_by_word: Dict[str, int] = field(default_factory=dict)
battlewords/ui.py CHANGED
@@ -221,6 +221,8 @@ def _init_session() -> None:
221
  files = get_wordlist_files()
222
  if "selected_wordlist" not in st.session_state and files:
223
  st.session_state.selected_wordlist = "wordlist.txt"
 
 
224
 
225
  words = load_word_list(st.session_state.get("selected_wordlist"))
226
  puzzle = generate_puzzle(grid_size=12, words_by_len=words)
@@ -236,12 +238,19 @@ def _init_session() -> None:
236
  st.session_state.letter_map = build_letter_map(puzzle)
237
  st.session_state.initialized = True
238
  st.session_state.radar_gif_path = None # Add this line
 
 
 
 
239
 
240
  def _new_game() -> None:
241
  selected = st.session_state.get("selected_wordlist")
 
242
  st.session_state.clear()
243
  if selected:
244
  st.session_state.selected_wordlist = selected
 
 
245
  st.session_state.radar_gif_path = None # Reset radar GIF path
246
  _init_session()
247
 
@@ -255,6 +264,7 @@ def _to_state() -> GameState:
255
  score=st.session_state.score,
256
  last_action=st.session_state.last_action,
257
  can_guess=st.session_state.can_guess,
 
258
  points_by_word=st.session_state.points_by_word,
259
  )
260
 
@@ -288,7 +298,21 @@ def _render_sidebar():
288
  "- Radar pulses show the last letter position of each hidden word.\n"
289
  "- After each reveal, you may submit one word guess below.\n"
290
  "- Scoring: length + unrevealed letters of that word at guess time.")
291
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  st.header("Wordlist Controls")
293
  wordlist_files = get_wordlist_files()
294
 
@@ -620,7 +644,7 @@ def _render_score_panel(state: GameState):
620
  with st.expander("Game summary", expanded=True):
621
  for w in state.puzzle.words:
622
  pts = state.points_by_word.get(w.text, 0)
623
- if pts > 0:
624
  st.markdown(f"- {w.text} ({len(w.text)}): +{pts} points")
625
  st.markdown(f"**Total**: {state.score}")
626
 
 
221
  files = get_wordlist_files()
222
  if "selected_wordlist" not in st.session_state and files:
223
  st.session_state.selected_wordlist = "wordlist.txt"
224
+ if "game_mode" not in st.session_state:
225
+ st.session_state.game_mode = "standard"
226
 
227
  words = load_word_list(st.session_state.get("selected_wordlist"))
228
  puzzle = generate_puzzle(grid_size=12, words_by_len=words)
 
238
  st.session_state.letter_map = build_letter_map(puzzle)
239
  st.session_state.initialized = True
240
  st.session_state.radar_gif_path = None # Add this line
241
+ # Ensure game_mode is set
242
+ if "game_mode" not in st.session_state:
243
+ st.session_state.game_mode = "standard"
244
+
245
 
246
  def _new_game() -> None:
247
  selected = st.session_state.get("selected_wordlist")
248
+ mode = st.session_state.get("game_mode")
249
  st.session_state.clear()
250
  if selected:
251
  st.session_state.selected_wordlist = selected
252
+ if mode:
253
+ st.session_state.game_mode = mode
254
  st.session_state.radar_gif_path = None # Reset radar GIF path
255
  _init_session()
256
 
 
264
  score=st.session_state.score,
265
  last_action=st.session_state.last_action,
266
  can_guess=st.session_state.can_guess,
267
+ game_mode=st.session_state.get("game_mode", "standard"),
268
  points_by_word=st.session_state.points_by_word,
269
  )
270
 
 
298
  "- Radar pulses show the last letter position of each hidden word.\n"
299
  "- After each reveal, you may submit one word guess below.\n"
300
  "- Scoring: length + unrevealed letters of that word at guess time.")
301
+
302
+ st.header("Game Mode")
303
+ game_modes = ["standard", "easy"]
304
+ default_mode = "standard"
305
+ if "game_mode" not in st.session_state:
306
+ st.session_state.game_mode = default_mode
307
+ current_mode = st.session_state.game_mode
308
+ st.selectbox(
309
+ "Select game mode",
310
+ options=game_modes,
311
+ index=game_modes.index(current_mode) if current_mode in game_modes else 0,
312
+ key="game_mode",
313
+ on_change=_new_game,
314
+ )
315
+
316
  st.header("Wordlist Controls")
317
  wordlist_files = get_wordlist_files()
318
 
 
644
  with st.expander("Game summary", expanded=True):
645
  for w in state.puzzle.words:
646
  pts = state.points_by_word.get(w.text, 0)
647
+ if pts > 0 or state.game_mode=="easy":
648
  st.markdown(f"- {w.text} ({len(w.text)}): +{pts} points")
649
  st.markdown(f"**Total**: {state.score}")
650
 
battlewords/word_loader.py CHANGED
@@ -22,6 +22,9 @@ FALLBACK_WORDS: Dict[int, List[str]] = {
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):
@@ -29,16 +32,20 @@ def get_wordlist_files() -> list[str]:
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"
@@ -58,15 +65,36 @@ def load_word_list(selected_file: Optional[str] = None) -> Dict[int, List[str]]:
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,17 +112,17 @@ def load_word_list(selected_file: Optional[str] = None) -> Dict[int, List[str]]:
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:
 
22
  ],
23
  }
24
 
25
+ MIN_REQUIRED = 25 # Per specs: require >= 500 per length before using file contents
26
+
27
+
28
  def get_wordlist_files() -> list[str]:
29
  words_dir = os.path.join(os.path.dirname(__file__), "words")
30
  if not os.path.isdir(words_dir):
 
32
  files = [f for f in os.listdir(words_dir) if f.lower().endswith(".txt")]
33
  return sorted(files)
34
 
35
+
36
  @st.cache_data(show_spinner=False)
37
  def load_word_list(selected_file: Optional[str] = None) -> Dict[int, List[str]]:
38
  """
39
  Load a word list, filter to uppercase A–Z, lengths in {4,5,6}, and dedupe while preserving order.
40
 
41
  If `selected_file` is provided, load battlewords/words/<selected_file>.
42
+ Otherwise, try on-disk default battlewords/words/wordlist.txt; if unavailable, try packaged resource.
43
 
44
  If fewer than 500 entries exist for any required length, fall back to built-ins
45
  for that length (per specs).
46
+
47
+ NOTE: To ensure cache updates when the user picks a different file, always pass
48
+ the `selected_file` argument from the UI/generator.
49
  """
50
  words_by_len: Dict[int, List[str]] = {4: [], 5: [], 6: []}
51
  used_source = "fallback"
 
65
  with open(path, "r", encoding="utf-8") as f:
66
  return f.read()
67
 
68
+ def _read_default_text() -> Optional[str]:
69
+ # Prefer the on-disk default in the editable repo
70
+ try:
71
+ return _read_text_from_disk("wordlist.txt")
72
+ except Exception:
73
+ pass
74
+ # Fallback to packaged data if available
75
+ try:
76
+ return resources.files("battlewords.words").joinpath("wordlist.txt").read_text(encoding="utf-8")
77
+ except Exception:
78
+ return None
79
+
80
  try:
81
  text: Optional[str] = None
82
+ source_label = "fallback"
83
 
84
  if selected_file:
85
+ # Validate selection against available files to avoid bad paths
86
+ available = set(get_wordlist_files())
87
+ if selected_file not in available:
88
+ raise FileNotFoundError(f"Selected word list '{selected_file}' not found in words/ directory.")
89
  text = _read_text_from_disk(selected_file)
90
+ source_label = f"file:{selected_file}"
91
  else:
92
+ text = _read_default_text()
93
+ if text is not None:
94
+ source_label = "default"
95
+
96
+ if text is None:
97
+ raise FileNotFoundError("No word list file found on disk or in packaged resources.")
98
 
99
  seen = {4: set(), 5: set(), 6: set()}
100
  for raw in text.splitlines():
 
112
  seen[L].add(word)
113
 
114
  counts = {k: len(v) for k, v in words_by_len.items()}
115
+ if all(counts[k] >= MIN_REQUIRED for k in (4, 5, 6)):
116
+ used_source = source_label
117
  return _finalize(words_by_len, used_source)
118
 
119
  # Per spec: fallback for any length below threshold
120
  mixed: Dict[int, List[str]] = {
121
+ 4: words_by_len[4] if counts[4] >= MIN_REQUIRED else FALLBACK_WORDS[4],
122
+ 5: words_by_len[5] if counts[5] >= MIN_REQUIRED else FALLBACK_WORDS[5],
123
+ 6: words_by_len[6] if counts[6] >= MIN_REQUIRED else FALLBACK_WORDS[6],
124
  }
125
+ used_source = f"{source_label}+fallback" if any(counts[k] >= MIN_REQUIRED for k in (4, 5, 6)) else "fallback"
126
  return _finalize(mixed, used_source)
127
 
128
  except Exception: