Spaces:
Running
Update audio handling and UI improvements
Browse filesVersion 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 +95 -0
- battlewords/__init__.py +1 -1
- battlewords/assets/audio/congratulations.mp3 +0 -3
- battlewords/assets/audio/{correct_guess.mp3 β effects/correct_guess.mp3} +0 -0
- battlewords/assets/audio/{hit.mp3 β effects/hit.mp3} +0 -0
- battlewords/assets/audio/{incorrect_guess.mp3 β effects/incorrect_guess.mp3} +0 -0
- battlewords/assets/audio/{miss.mp3 β effects/miss.mp3} +0 -0
- battlewords/assets/audio/game-10.mp3 +0 -3
- battlewords/assets/audio/game-11.mp3 +0 -3
- battlewords/assets/audio/game-2.mp3 +0 -3
- battlewords/assets/audio/game-3.mp3 +0 -3
- battlewords/assets/audio/game-8.mp3 +0 -3
- battlewords/assets/audio/{game-0.mp3 β music/background.mp3} +0 -0
- battlewords/assets/audio/{game-1.mp3 β music/congratulations.mp3} +0 -0
- battlewords/audio.py +8 -6
- battlewords/logic.py +3 -0
- battlewords/ui.py +43 -22
|
@@ -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 |
+
|
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui"]
|
|
|
|
| 1 |
+
__version__ = "0.2.10"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui"]
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:da52c73b00948b8c7fe0197f62a57af5ee439b5b1acef31c7add3868d23af437
|
| 3 |
-
size 74446
|
|
|
|
|
|
|
|
|
|
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:b588435cd1bd29ab5876c9737ad217899db0f46c5370f80e29c8f11cdb1c92bc
|
| 3 |
-
size 1201966
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:8efa6142714a74c57c8b8bbe77c19db3a2635e07ada9d59ec30b9215553b1442
|
| 3 |
-
size 2209458
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:348786a20423e706a4966155f966d78e6e8bf5608ffdc6bb673269f03da81810
|
| 3 |
-
size 1782099
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:233b892a347322b8071a2daada0c092000d11ce85abb7a0c2df24ddcdc4f6271
|
| 3 |
-
size 1201972
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:edb317a64cd37bf8b5fefe9efb0553d302ed9b0570a5eba3ad01a67077a9f49a
|
| 3 |
-
size 2161810
|
|
|
|
|
|
|
|
|
|
|
|
|
File without changes
|
|
File without changes
|
|
@@ -2,15 +2,17 @@ import os
|
|
| 2 |
from typing import Optional
|
| 3 |
import streamlit as st
|
| 4 |
|
| 5 |
-
def
|
| 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 =
|
| 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 =
|
| 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 |
|
|
@@ -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.
|
|
@@ -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 |
-
|
| 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 |
-
|
| 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,
|
| 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,
|
| 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.
|
| 896 |
-
color: #
|
| 897 |
-
margin-top:
|
| 898 |
-
padding: 4px
|
| 899 |
-
background: rgba(255, 255, 255, 0.05);
|
| 900 |
-
border-radius:
|
| 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"
|
| 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 |
-
|
| 961 |
-
|
| 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
|
| 1052 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|