Surn commited on
Commit
7896205
Β·
1 Parent(s): 8c718b2

Update audio handling and UI improvements

Browse files

Version 0.2.10 introduces several enhancements and fixes:
- Updated software version in `__init__.py` to 0.2.10.
- Enhanced README.md with sections on "Assets Setup" and "Sound Asset Generation."
- Refactored `audio.py` to organize music and sound effects into separate directories.
- Improved UI in `ui.py` with better text input color and scoreboard adjustments.
- Modified sound management to play "congratulations" music at game end.
- Added metadata to audio files like `correct_guess.mp3` and `hit.mp3`.
- Refactored code with a new `hidden_word_display` function in `logic.py`.
- Made minor bug fixes and UI adjustments for better game experience.
- Updated `incorrect_guess.mp3` and `miss.mp3` with new metadata and audio content.
- Added significant binary data to `background.mp3`, re-encoded with LAME3.100.

README.md CHANGED
@@ -106,6 +106,18 @@ docker run -p 8501:8501 battlewords
106
 
107
  ## Changelog
108
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  -0.2.9
110
  - fix sonar grid alignment issue on some browsers
111
  - When all letters of a word are revealed, it is automatically marked as found.
@@ -240,3 +252,86 @@ Spaces can be embedded in other sites using an `<iframe>`:
240
 
241
  For full configuration options, see [Spaces Config Reference](https://huggingface.co/docs/hub/spaces-config-reference) and [Streamlit SDK Guide](https://huggingface.co/docs/hub/spaces-sdks-streamlit).
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  ## Changelog
108
 
109
+ -0.2.11
110
+ - update timer to be live during gameplay, but reset with each action
111
+ - compact design
112
+
113
+ -0.2.10
114
+ - reduce sonar graphic size
115
+ - update music and special effects file locations
116
+ - remove some music and sound effects
117
+ - change Guess Text input color
118
+ - incorrect guess UI update
119
+ - scoreboard update
120
+
121
  -0.2.9
122
  - fix sonar grid alignment issue on some browsers
123
  - When all letters of a word are revealed, it is automatically marked as found.
 
252
 
253
  For full configuration options, see [Spaces Config Reference](https://huggingface.co/docs/hub/spaces-config-reference) and [Streamlit SDK Guide](https://huggingface.co/docs/hub/spaces-sdks-streamlit).
254
 
255
+ # Assets Setup
256
+
257
+ To fully experience BattleWords, especially the audio elements, ensure you set up the following assets:
258
+
259
+ - Place your background music `.mp3` files in `battlewords/assets/audio/music/` to enable music.
260
+ - Place your sound effect files (`.mp3` or `.wav`) in `battlewords/assets/audio/effects/` for sound effects.
261
+
262
+ Refer to the documentation for guidance on compatible audio formats and common troubleshooting tips.
263
+
264
+ # Sound Asset Generation
265
+
266
+ To generate and save custom sound effects for BattleWords, you can use the `generate_sound_effect` function.
267
+
268
+ ## Function: `generate_sound_effect`
269
+
270
+ ```python
271
+ def generate_sound_effect(effect: str, save_to_assets: bool = False, use_api: str = "huggingface") -> str:
272
+ """
273
+ Generate a sound effect and save it as a file.
274
+
275
+ Parameters:
276
+ - `effect`: Name of the effect to generate.
277
+ - `save_to_assets`: If `True`, saves the effect to the assets directory;
278
+ if `False`, saves to a temporary location. Default is `False`.
279
+ - `use_api`: API to use for generation. Options are "huggingface" or "replicate". Default is "huggingface".
280
+
281
+ Returns:
282
+ - File path to the saved sound effect.
283
+ """
284
+
285
+ # ... [sound generation code] ...
286
+
287
+ if save_to_assets:
288
+ # Save to effects directory
289
+ assets_dir = os.path.join(os.path.dirname(__file__), "assets", "audio", "effects")
290
+ os.makedirs(assets_dir, exist_ok=True)
291
+ filename = f"{effect}.wav"
292
+ path = os.path.join(assets_dir, filename)
293
+ with open(path, "wb") as f:
294
+ f.write(audio_bytes)
295
+ print(f" Saved to: {path}")
296
+ else:
297
+ # Save to temporary file
298
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpfile:
299
+ tmpfile.write(audio_bytes)
300
+ path = tmpfile.name
301
+ print(f" Saved to: {path}")
302
+
303
+ return path
304
+ ```
305
+
306
+ ## Parameters
307
+
308
+ - `effect`: The name of the sound effect you want to generate (e.g., "explosion", "powerup").
309
+ - `save_to_assets` (optional): Set to `True` to save the generated sound effect to the game's assets directory. If `False`, the effect is saved to a temporary location. Default is `False`.
310
+ - `use_api` (optional): The API to use for generating the sound. Options are `"huggingface"` or `"replicate"`. Default is `"huggingface"`.
311
+
312
+ ## Returns
313
+
314
+ - The function returns the file path to the saved sound effect, whether it's in the assets directory or a temporary location.
315
+
316
+ ## Usage Example
317
+
318
+ To generate a sound effect and save it to the assets directory:
319
+
320
+ ```python
321
+ generate_sound_effect("your_effect_name", save_to_assets=True)
322
+ ```
323
+
324
+ To generate a sound effect and keep it in a temporary location:
325
+
326
+ ```python
327
+ temp_path = generate_sound_effect("your_effect_name", save_to_assets=False)
328
+ ```
329
+
330
+ ## Note
331
+
332
+ Ensure you have the necessary permissions and API access (if required) to use the sound generation service. Generated sounds are subject to the terms of use of the respective API.
333
+
334
+ For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
335
+
336
+ Happy gaming and sound designing!
337
+
battlewords/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.2.9"
2
  __all__ = ["models", "generator", "logic", "ui"]
 
1
+ __version__ = "0.2.10"
2
  __all__ = ["models", "generator", "logic", "ui"]
battlewords/assets/audio/congratulations.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:da52c73b00948b8c7fe0197f62a57af5ee439b5b1acef31c7add3868d23af437
3
- size 74446
 
 
 
 
battlewords/assets/audio/{correct_guess.mp3 β†’ effects/correct_guess.mp3} RENAMED
File without changes
battlewords/assets/audio/{hit.mp3 β†’ effects/hit.mp3} RENAMED
File without changes
battlewords/assets/audio/{incorrect_guess.mp3 β†’ effects/incorrect_guess.mp3} RENAMED
File without changes
battlewords/assets/audio/{miss.mp3 β†’ effects/miss.mp3} RENAMED
File without changes
battlewords/assets/audio/game-10.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b588435cd1bd29ab5876c9737ad217899db0f46c5370f80e29c8f11cdb1c92bc
3
- size 1201966
 
 
 
 
battlewords/assets/audio/game-11.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:8efa6142714a74c57c8b8bbe77c19db3a2635e07ada9d59ec30b9215553b1442
3
- size 2209458
 
 
 
 
battlewords/assets/audio/game-2.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:348786a20423e706a4966155f966d78e6e8bf5608ffdc6bb673269f03da81810
3
- size 1782099
 
 
 
 
battlewords/assets/audio/game-3.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:233b892a347322b8071a2daada0c092000d11ce85abb7a0c2df24ddcdc4f6271
3
- size 1201972
 
 
 
 
battlewords/assets/audio/game-8.mp3 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:edb317a64cd37bf8b5fefe9efb0553d302ed9b0570a5eba3ad01a67077a9f49a
3
- size 2161810
 
 
 
 
battlewords/assets/audio/{game-0.mp3 β†’ music/background.mp3} RENAMED
File without changes
battlewords/assets/audio/{game-1.mp3 β†’ music/congratulations.mp3} RENAMED
File without changes
battlewords/audio.py CHANGED
@@ -2,15 +2,17 @@ import os
2
  from typing import Optional
3
  import streamlit as st
4
 
5
- def _get_audio_dir() -> str:
6
- return os.path.join(os.path.dirname(__file__), "assets", "audio")
 
 
 
7
 
8
  def get_audio_tracks() -> list[tuple[str, str]]:
9
- """Return list of (label, absolute_path) for .mp3 files in assets/audio."""
10
- audio_dir = _get_audio_dir()
11
  if not os.path.isdir(audio_dir):
12
  return []
13
- # Only include .mp3 files, ignore .wav files
14
  tracks = []
15
  for fname in os.listdir(audio_dir):
16
  if fname.lower().endswith('.mp3'):
@@ -145,7 +147,7 @@ def get_sound_effect_files() -> dict[str, str]:
145
  Return dictionary of sound effect name -> absolute path.
146
  Prefers .mp3 files; falls back to .wav if no .mp3 is found.
147
  """
148
- audio_dir = _get_audio_dir()
149
  if not os.path.isdir(audio_dir):
150
  return {}
151
 
 
2
  from typing import Optional
3
  import streamlit as st
4
 
5
+ def _get_music_dir() -> str:
6
+ return os.path.join(os.path.dirname(__file__), "assets", "audio", "music")
7
+
8
+ def _get_effects_dir() -> str:
9
+ return os.path.join(os.path.dirname(__file__), "assets", "audio", "effects")
10
 
11
  def get_audio_tracks() -> list[tuple[str, str]]:
12
+ """Return list of (label, absolute_path) for .mp3 files in assets/audio/music."""
13
+ audio_dir = _get_music_dir()
14
  if not os.path.isdir(audio_dir):
15
  return []
 
16
  tracks = []
17
  for fname in os.listdir(audio_dir):
18
  if fname.lower().endswith('.mp3'):
 
147
  Return dictionary of sound effect name -> absolute path.
148
  Prefers .mp3 files; falls back to .wav if no .mp3 is found.
149
  """
150
+ audio_dir = _get_effects_dir()
151
  if not os.path.isdir(audio_dir):
152
  return {}
153
 
battlewords/logic.py CHANGED
@@ -143,6 +143,9 @@ def compute_tier(score: int) -> str:
143
  return "Good"
144
  return "Keep practicing"
145
 
 
 
 
146
 
147
  def auto_mark_completed_words(state: GameState) -> bool:
148
  """Automatically mark words as found when all their letters are revealed.
 
143
  return "Good"
144
  return "Keep practicing"
145
 
146
+ def hidden_word_display(letters_display: int) -> str:
147
+ """Return a string of '?' of length letters_display."""
148
+ return "?" * letters_display
149
 
150
  def auto_mark_completed_words(state: GameState) -> bool:
151
  """Automatically mark words as found when all their letters are revealed.
battlewords/ui.py CHANGED
@@ -14,12 +14,12 @@ import time
14
  from datetime import datetime
15
 
16
  from .generator import generate_puzzle, sort_word_file
17
- from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier, auto_mark_completed_words
18
  from .models import Coord, GameState, Puzzle
19
  from .word_loader import get_wordlist_files, load_word_list # use loader directly
20
  from .version_info import versions_html # version info footer
21
  from .audio import (
22
- _get_audio_dir,
23
  get_audio_tracks,
24
  _load_audio_data_url,
25
  _mount_background_audio,
@@ -221,6 +221,11 @@ def inject_styles() -> None:
221
  position: absolute;
222
  max-width:100%;
223
  }
 
 
 
 
 
224
 
225
  @media (min-width:720px) {
226
  .st-emotion-cache-wp60of {
@@ -411,11 +416,10 @@ def _render_sidebar():
411
  st.header("Audio")
412
  tracks = get_audio_tracks()
413
  # Show how many audio files were found
414
- st.caption(f"{len(tracks)} audio file{'s' if len(tracks) != 1 else ''} found in battlewords/assets/audio")
415
 
416
  if "music_enabled" not in st.session_state:
417
- # disabled by default
418
- st.session_state.music_enabled = False #if tracks else True
419
  if "music_volume" not in st.session_state:
420
  st.session_state.music_volume = 15
421
 
@@ -457,7 +461,7 @@ def _render_sidebar():
457
  src_url = _load_audio_data_url(selected_path) if enabled else None
458
  _mount_background_audio(enabled, src_url, (st.session_state.music_volume or 0) / 100)
459
  else:
460
- st.caption("Place .mp3 files in battlewords/assets/audio to enable music.")
461
  _mount_background_audio(False, None, 0.0)
462
 
463
  _inject_audio_control_sync()
@@ -667,7 +671,7 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
667
  if cached_path and os.path.exists(cached_path) and cached_sig == gif_signature:
668
  with open(cached_path, "rb") as f:
669
  gif_bytes = f.read()
670
- st.image(gif_bytes, use_container_width=True)
671
  plt.close(fig)
672
  return
673
 
@@ -680,7 +684,7 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
680
  gif_bytes = tmpfile.read()
681
  st.session_state.radar_gif_path = tmpfile.name # Save path for reuse
682
  st.session_state.radar_gif_signature = gif_signature # Save signature to detect changes
683
- st.image(gif_bytes, use_container_width=True)
684
 
685
  def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
686
  size = state.grid_size
@@ -892,13 +896,13 @@ def _render_guess_form(state: GameState):
892
  """
893
  <style>
894
  .bw-incorrect-guesses {
895
- font-size: 0.85rem;
896
- color: #ff9999;
897
- margin-top: 4px;
898
- padding: 4px 8px;
899
- background: rgba(255, 255, 255, 0.05);
900
- border-radius: 4px;
901
- font-style: italic;
902
  }
903
  </style>
904
  """,
@@ -913,8 +917,7 @@ def _render_guess_form(state: GameState):
913
  value="",
914
  max_chars=10,
915
  width=200,
916
- key="guess_input",
917
- help=tooltip_text # Use Streamlit's built-in help parameter for tooltip
918
  )
919
  with col2:
920
  submitted = st.form_submit_button("OK", disabled=not state.can_guess, width=100, key="guess_submit")
@@ -922,7 +925,7 @@ def _render_guess_form(state: GameState):
922
  # Show compact list below input if setting is enabled
923
  if st.session_state.get("show_incorrect_guesses", False) and recent_incorrect:
924
  st.markdown(
925
- f'<div class="bw-incorrect-guesses">Recent: {", ".join(recent_incorrect)}</div>',
926
  unsafe_allow_html=True,
927
  )
928
 
@@ -957,8 +960,8 @@ def _render_score_panel(state: GameState):
957
 
958
  for w in state.puzzle.words:
959
  pts = state.points_by_word.get(w.text, 0)
960
- if pts > 0 or state.game_mode == "too easy":
961
- letters_display = len(w.text)
962
  # Extra = total points for the word minus its length (bonus earned)
963
  extra_pts = max(0, pts - letters_display)
964
  row_html = (
@@ -969,6 +972,16 @@ def _render_score_panel(state: GameState):
969
  "</tr>"
970
  )
971
  rows_html.append(row_html)
 
 
 
 
 
 
 
 
 
 
972
 
973
  # Timer calculation (initial render value)
974
  now = datetime.now()
@@ -1048,8 +1061,16 @@ def _render_score_panel(state: GameState):
1048
  # -------------------- Game Over Dialog --------------------
1049
 
1050
  def _game_over_content(state: GameState) -> None:
1051
- # Play congratulations sound when game ends
1052
- play_sound_effect("congratulations", volume=0.7)
 
 
 
 
 
 
 
 
1053
 
1054
  # Set end_time if not already set
1055
  if state.end_time is None:
 
14
  from datetime import datetime
15
 
16
  from .generator import generate_puzzle, sort_word_file
17
+ from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier, auto_mark_completed_words, hidden_word_display
18
  from .models import Coord, GameState, Puzzle
19
  from .word_loader import get_wordlist_files, load_word_list # use loader directly
20
  from .version_info import versions_html # version info footer
21
  from .audio import (
22
+ _get_music_dir,
23
  get_audio_tracks,
24
  _load_audio_data_url,
25
  _mount_background_audio,
 
221
  position: absolute;
222
  max-width:100%;
223
  }
224
+ .stImage { max-width:300px;}
225
+ #text_input_2 {
226
+ background-color:#fff;
227
+ color:#000;
228
+ caret-color:#333;}
229
 
230
  @media (min-width:720px) {
231
  .st-emotion-cache-wp60of {
 
416
  st.header("Audio")
417
  tracks = get_audio_tracks()
418
  # Show how many audio files were found
419
+ st.caption(f"{len(tracks)} audio file{'s' if len(tracks) != 1 else ''} found in battlewords/assets/audio/music")
420
 
421
  if "music_enabled" not in st.session_state:
422
+ st.session_state.music_enabled = False
 
423
  if "music_volume" not in st.session_state:
424
  st.session_state.music_volume = 15
425
 
 
461
  src_url = _load_audio_data_url(selected_path) if enabled else None
462
  _mount_background_audio(enabled, src_url, (st.session_state.music_volume or 0) / 100)
463
  else:
464
+ st.caption("Place .mp3 files in battlewords/assets/audio/music to enable music.")
465
  _mount_background_audio(False, None, 0.0)
466
 
467
  _inject_audio_control_sync()
 
671
  if cached_path and os.path.exists(cached_path) and cached_sig == gif_signature:
672
  with open(cached_path, "rb") as f:
673
  gif_bytes = f.read()
674
+ st.image(gif_bytes, width='stretch',)
675
  plt.close(fig)
676
  return
677
 
 
684
  gif_bytes = tmpfile.read()
685
  st.session_state.radar_gif_path = tmpfile.name # Save path for reuse
686
  st.session_state.radar_gif_signature = gif_signature # Save signature to detect changes
687
+ st.image(gif_bytes, width='stretch')
688
 
689
  def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
690
  size = state.grid_size
 
896
  """
897
  <style>
898
  .bw-incorrect-guesses {
899
+ font-size: 0.7rem;
900
+ color: #fff;
901
+ margin-top: -10px;
902
+ # padding: 2px 4px;
903
+ # background: rgba(255, 255, 255, 0.05);
904
+ # border-radius: 10px;
905
+ # font-style: italic;
906
  }
907
  </style>
908
  """,
 
917
  value="",
918
  max_chars=10,
919
  width=200,
920
+ key="guess_input"
 
921
  )
922
  with col2:
923
  submitted = st.form_submit_button("OK", disabled=not state.can_guess, width=100, key="guess_submit")
 
925
  # Show compact list below input if setting is enabled
926
  if st.session_state.get("show_incorrect_guesses", False) and recent_incorrect:
927
  st.markdown(
928
+ f'<div class="bw-incorrect-guesses" title="{tooltip_text}">Recent incorrect guesses:</div>',
929
  unsafe_allow_html=True,
930
  )
931
 
 
960
 
961
  for w in state.puzzle.words:
962
  pts = state.points_by_word.get(w.text, 0)
963
+ letters_display = len(w.text)
964
+ if pts > 0 or state.game_mode == "too easy":
965
  # Extra = total points for the word minus its length (bonus earned)
966
  extra_pts = max(0, pts - letters_display)
967
  row_html = (
 
972
  "</tr>"
973
  )
974
  rows_html.append(row_html)
975
+ else:
976
+ # Hide unguessed words in classic mode
977
+ row_html = (
978
+ "<tr>"
979
+ f"<td class=\"blue-background \">{hidden_word_display(letters_display)}</td>"
980
+ f"<td class=\"blue-background \">{letters_display}</td>"
981
+ f"<td class=\"blue-background \">?</td>"
982
+ "</tr>"
983
+ )
984
+ rows_html.append(row_html)
985
 
986
  # Timer calculation (initial render value)
987
  now = datetime.now()
 
1061
  # -------------------- Game Over Dialog --------------------
1062
 
1063
  def _game_over_content(state: GameState) -> None:
1064
+ # Play congratulations music (not sound effect) as background
1065
+ music_dir = _get_music_dir()
1066
+ congrats_music_path = os.path.join(music_dir, "congratulations.mp3")
1067
+ if os.path.exists(congrats_music_path):
1068
+ src_url = _load_audio_data_url(congrats_music_path)
1069
+ # Play at full volume (or use previous music_volume setting)
1070
+ _mount_background_audio(True, src_url, (st.session_state.get("music_volume", 100)) / 100)
1071
+ else:
1072
+ # Fallback: keep previous music or silence
1073
+ _mount_background_audio(False, None, 0.0)
1074
 
1075
  # Set end_time if not already set
1076
  if state.end_time is None: