Surn commited on
Commit
a33cd25
·
1 Parent(s): e008df8

Add background music support and audio diagnostics

Browse files

Updated the version to 0.1.4 in `__init__.py`. Added `render_background_music_html` to `gradio_ui.py` for managing background music playback and volume. Modified `render_audio_player_html` to handle sound effects with temporary audio elements. Enhanced `handle_audio_settings_change` to return additional HTML for music and audio previews. Introduced `handle_sfx_volume_change` and `handle_music_volume_change` for real-time audio feedback. Updated `create_app` to integrate new audio elements. Added `check_audio_setup.py` for verifying audio file configurations.

Files changed (3) hide show
  1. check_audio_setup.py +109 -0
  2. wrdler/__init__.py +1 -1
  3. wrdler/gradio_ui.py +243 -108
check_audio_setup.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Diagnostic script to check audio setup for Wrdler Gradio UI.
4
+ Run this to verify audio files and directories are properly configured.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+
10
+ def check_audio_setup():
11
+ """Check if audio directories and files exist."""
12
+ print("=" * 70)
13
+ print("Wrdler Audio Setup Diagnostic")
14
+ print("=" * 70)
15
+
16
+ wrdler_dir = Path(__file__).parent / "wrdler"
17
+
18
+ # Check music directory
19
+ music_dir = wrdler_dir / "assets" / "audio" / "music"
20
+ print(f"\n?? Music Directory: {music_dir}")
21
+ if music_dir.exists():
22
+ print(" ? Directory exists")
23
+ music_files = list(music_dir.glob("*.mp3"))
24
+ if music_files:
25
+ print(f" ?? Found {len(music_files)} MP3 file(s):")
26
+ for f in music_files:
27
+ size_kb = f.stat().st_size / 1024
28
+ print(f" - {f.name} ({size_kb:.1f} KB)")
29
+ else:
30
+ print(" ?? No MP3 files found")
31
+ else:
32
+ print(" ? Directory does NOT exist")
33
+ print(f" ?? Create it with: mkdir -p {music_dir}")
34
+
35
+ # Check effects directory
36
+ effects_dir = wrdler_dir / "assets" / "audio" / "effects"
37
+ print(f"\n?? Sound Effects Directory: {effects_dir}")
38
+ if effects_dir.exists():
39
+ print(" ? Directory exists")
40
+
41
+ required_effects = ["correct_guess", "incorrect_guess", "hit", "miss", "congratulations"]
42
+ found_effects = []
43
+ missing_effects = []
44
+
45
+ for effect in required_effects:
46
+ mp3_file = effects_dir / f"{effect}.mp3"
47
+ wav_file = effects_dir / f"{effect}.wav"
48
+
49
+ if mp3_file.exists():
50
+ size_kb = mp3_file.stat().st_size / 1024
51
+ found_effects.append(f"{effect}.mp3 ({size_kb:.1f} KB)")
52
+ elif wav_file.exists():
53
+ size_kb = wav_file.stat().st_size / 1024
54
+ found_effects.append(f"{effect}.wav ({size_kb:.1f} KB)")
55
+ else:
56
+ missing_effects.append(effect)
57
+
58
+ if found_effects:
59
+ print(f" ?? Found {len(found_effects)} effect file(s):")
60
+ for f in found_effects:
61
+ print(f" - {f}")
62
+
63
+ if missing_effects:
64
+ print(f" ?? Missing {len(missing_effects)} effect file(s):")
65
+ for f in missing_effects:
66
+ print(f" - {f}.mp3 or {f}.wav")
67
+ else:
68
+ print(" ? Directory does NOT exist")
69
+ print(f" ?? Create it with: mkdir -p {effects_dir}")
70
+
71
+ # Summary
72
+ print("\n" + "=" * 70)
73
+ print("Summary:")
74
+ print("=" * 70)
75
+
76
+ all_ok = True
77
+
78
+ if not music_dir.exists() or not list(music_dir.glob("*.mp3")):
79
+ print("?? Background music will not work (no music files found)")
80
+ all_ok = False
81
+ else:
82
+ print("? Background music should work")
83
+
84
+ if not effects_dir.exists():
85
+ print("? Sound effects will not work (effects directory missing)")
86
+ all_ok = False
87
+ else:
88
+ required_effects = ["correct_guess", "incorrect_guess", "hit", "miss", "congratulations"]
89
+ missing_count = sum(1 for e in required_effects
90
+ if not (effects_dir / f"{e}.mp3").exists()
91
+ and not (effects_dir / f"{e}.wav").exists())
92
+ if missing_count > 0:
93
+ print(f"?? Sound effects partially working ({missing_count} missing)")
94
+ all_ok = False
95
+ else:
96
+ print("? All sound effects should work")
97
+
98
+ if all_ok:
99
+ print("\n?? Audio setup is complete!")
100
+ else:
101
+ print("\n?? To generate missing sound effects, run:")
102
+ print(" python wrdler/sounds.py")
103
+ print(" or")
104
+ print(" python wrdler/generate_sounds.py")
105
+
106
+ print("=" * 70)
107
+
108
+ if __name__ == "__main__":
109
+ check_audio_setup()
wrdler/__init__.py CHANGED
@@ -8,5 +8,5 @@ Key differences from BattleWords:
8
  - 2 free letter guesses at game start
9
  """
10
 
11
- __version__ = "0.1.3"
12
  __all__ = ["models", "generator", "logic", "ui", "word_loader"]
 
8
  - 2 free letter guesses at game start
9
  """
10
 
11
+ __version__ = "0.1.4"
12
  __all__ = ["models", "generator", "logic", "ui", "word_loader"]
wrdler/gradio_ui.py CHANGED
@@ -158,12 +158,122 @@ def load_audio_data_url(path: str) -> str:
158
  return ""
159
 
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  def render_audio_player_html(
162
  effect_name: Optional[str] = None,
163
  volume: float = 0.5,
164
  enabled: bool = True
165
  ) -> str:
166
- """Generate HTML to play a sound effect."""
 
 
 
 
 
 
 
 
 
 
 
 
167
  if not enabled or not effect_name:
168
  return ""
169
 
@@ -180,9 +290,34 @@ def render_audio_player_html(
180
  return f'''
181
  <script>
182
  (function(){{
183
- const audio = new Audio("{data_url}");
 
 
184
  audio.volume = {vol:.3f};
185
- audio.play().catch(e => console.log('Audio play blocked'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }})();
187
  </script>
188
  '''
@@ -1053,8 +1188,12 @@ def handle_audio_settings_change(
1053
  music_enabled: bool,
1054
  music_volume: int,
1055
  state: Dict[str, Any]
1056
- ) -> Dict[str, Any]:
1057
- """Handle audio settings changes."""
 
 
 
 
1058
  state = ensure_state(state)
1059
 
1060
  # Create a deep copy to ensure Gradio detects the change
@@ -1063,90 +1202,92 @@ def handle_audio_settings_change(
1063
  new_state["sound_effects_volume"] = sfx_volume
1064
  new_state["music_enabled"] = music_enabled
1065
  new_state["music_volume"] = music_volume
1066
- return new_state
 
 
 
 
 
 
 
1067
 
1068
 
1069
- def handle_share_challenge(
1070
- username: str,
 
 
 
1071
  state: Dict[str, Any]
1072
- ) -> Tuple[str, str, Dict[str, Any]]:
1073
- """Handle share challenge button click."""
 
 
 
 
1074
  state = ensure_state(state)
1075
 
1076
- if not state.get("game_over"):
1077
- return "Game not finished yet.", "", state
1078
-
1079
- username = username.strip() if username else "Anonymous"
1080
- if not username:
1081
- username = "Anonymous"
1082
-
1083
- # Get game data
1084
- word_list = [text for text, x, y, direction in state["puzzle_words"]]
1085
- wordlist_source = state.get("wordlist", "classic.txt")
1086
- game_mode = state.get("game_mode", "classic")
1087
- score = state.get("score", 0)
1088
-
1089
- # Calculate elapsed time
1090
- start_time = state.get("start_time")
1091
- end_time = state.get("end_time")
1092
- if end_time and start_time:
1093
- start = datetime.fromisoformat(start_time)
1094
- end = datetime.fromisoformat(end_time)
1095
- elapsed_seconds = int((end - start).total_seconds())
1096
- else:
1097
- elapsed_seconds = 0
1098
-
1099
- # Check if this is a shared challenge being completed
1100
- is_shared_game = state.get("challenge_sid") is not None
1101
- existing_sid = state.get("challenge_sid")
1102
-
1103
- try:
1104
- if is_shared_game and existing_sid:
1105
- # Add result to existing challenge
1106
- success = add_user_result_to_game(
1107
- sid=existing_sid,
1108
- username=username,
1109
- word_list=word_list,
1110
- score=score,
1111
- time_seconds=elapsed_seconds
1112
- )
1113
-
1114
- if success:
1115
- share_url = get_shareable_url(existing_sid)
1116
- new_state = copy.deepcopy(state)
1117
- new_state["share_url"] = share_url
1118
- new_state["share_sid"] = existing_sid
1119
- return f"Result submitted for {username}!", share_url, new_state
1120
- else:
1121
- return "Failed to submit result.", "", state
1122
- else:
1123
- # Create new challenge
1124
- challenge_id, full_url, sid = save_game_to_hf(
1125
- word_list=word_list,
1126
- username=username,
1127
- score=score,
1128
- time_seconds=elapsed_seconds,
1129
- game_mode=game_mode,
1130
- grid_size=6, # Wrdler: 6 rows
1131
- spacer=0,
1132
- may_overlap=False,
1133
- wordlist_source=wordlist_source,
1134
- game_title=APP_SETTINGS.get("game_title", "Wrdler Gradio AI"),
1135
- show_incorrect_guesses=state.get("show_incorrect_guesses", True),
1136
- enable_free_letters=state.get("enable_free_letters", True)
1137
- )
1138
 
1139
- if sid:
1140
- share_url = get_shareable_url(sid)
1141
- new_state = copy.deepcopy(state)
1142
- new_state["share_url"] = share_url
1143
- new_state["share_sid"] = sid
1144
- return f"Share link generated!", share_url, new_state
1145
- else:
1146
- return "Failed to generate share link.", "", state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1147
 
1148
- except Exception as e:
1149
- return f"Error: {str(e)}", "", state
1150
 
1151
 
1152
  # ---------------------------------------------------------------------------
@@ -1254,8 +1395,6 @@ def create_app() -> gr.Blocks:
1254
  )
1255
  grid_buttons.append(btn)
1256
 
1257
- # Guess form
1258
- with gr.Row():
1259
  # New Game button
1260
  new_game_btn = gr.Button("New Game", variant="primary", size="lg")
1261
 
@@ -1311,7 +1450,7 @@ def create_app() -> gr.Blocks:
1311
  choices=["classic", "easy", "too easy"],
1312
  value="classic",
1313
  label="Game Mode"
1314
- )
1315
  show_incorrect_guesses = gr.Checkbox(
1316
  value=APP_SETTINGS.get("show_incorrect_guesses", True),
1317
  label="Show Incorrect Guesses"
@@ -1352,11 +1491,14 @@ def create_app() -> gr.Blocks:
1352
 
1353
  # Audio player for sound effects (hidden via CSS but still executes)
1354
  audio_player_html = gr.HTML(value="", elem_id="audio-player", elem_classes=["hidden-audio"])
 
 
 
1355
 
1356
  # Timer for updating elapsed time (ticks every second)
1357
  game_timer = gr.Timer(value=1, active=True)
1358
 
1359
- # Game Over Modal Dialog (outside tabs) - contains game over display and share challenge section
1360
  with Modal(visible=False) as game_over_modal:
1361
  # Game Over Content
1362
  game_over_html = gr.HTML(
@@ -1541,17 +1683,6 @@ def create_app() -> gr.Blocks:
1541
  fn=lambda: gr.Timer(active=True),
1542
  inputs=None,
1543
  outputs=game_timer
1544
- ).then(
1545
- fn=lambda topic: topic.strip() if topic.strip() else "AI Generated",
1546
- inputs=[ai_topic_input],
1547
- outputs=topic_display
1548
- )
1549
-
1550
- # AI Topic textbox submit (pressing Enter)
1551
- ai_topic_input.submit(
1552
- fn=handle_new_game,
1553
- inputs=[wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, show_challenge_links, game_state],
1554
- outputs=common_outputs
1555
  ).then(
1556
  fn=lambda: gr.Timer(active=True),
1557
  inputs=None,
@@ -1625,24 +1756,24 @@ def create_app() -> gr.Blocks:
1625
 
1626
  # Audio settings change handlers
1627
  sfx_enabled.change(
1628
- fn=handle_audio_settings_change,
1629
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1630
- outputs=[game_state]
1631
  )
1632
  sfx_volume.change(
1633
- fn=handle_audio_settings_change,
1634
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1635
- outputs=[game_state]
1636
  )
1637
  music_enabled.change(
1638
  fn=handle_audio_settings_change,
1639
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1640
- outputs=[game_state]
1641
  )
1642
  music_volume.change(
1643
- fn=handle_audio_settings_change,
1644
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1645
- outputs=[game_state]
1646
  )
1647
 
1648
  # Show incorrect guesses toggle handler - starts new game
@@ -1688,7 +1819,6 @@ def create_app() -> gr.Blocks:
1688
  inputs=[enable_free_letters, wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, show_challenge_links, game_state],
1689
  outputs=common_outputs
1690
  )
1691
-
1692
  # Show challenge links toggle handler - starts new game
1693
  def handle_show_challenge_links_change(enabled, wordlist, ai_topic, game_mode, sfx_en, sfx_vol, music_en, music_vol, show_inc, enable_fl, state):
1694
  state = ensure_state(state)
@@ -1754,12 +1884,17 @@ def create_app() -> gr.Blocks:
1754
  def initialize_ui(state):
1755
  """Initialize UI components on app load."""
1756
  state = ensure_state(state)
1757
- return build_ui_outputs(state)
 
 
 
 
 
1758
 
1759
  demo.load(
1760
  fn=initialize_ui,
1761
  inputs=[game_state],
1762
- outputs=common_outputs
1763
  )
1764
 
1765
- return demo
 
158
  return ""
159
 
160
 
161
+ def render_background_music_html(enabled: bool, volume: float) -> str:
162
+ """Generate HTML to control background music player.
163
+
164
+ Creates or updates a persistent audio element in the parent document.
165
+ The music will loop continuously when enabled.
166
+
167
+ Args:
168
+ enabled: Whether background music should play
169
+ volume: Volume level (0.0 to 1.0)
170
+
171
+ Returns:
172
+ HTML/JavaScript to inject into the page
173
+ """
174
+ # Get the first available music track (background.mp3 preferred)
175
+ tracks = get_audio_tracks()
176
+ if not tracks:
177
+ return ""
178
+
179
+ # Prefer background.mp3, otherwise use first track
180
+ music_path = None
181
+ for name, path in tracks:
182
+ if name.lower() == "background":
183
+ music_path = path
184
+ break
185
+ if not music_path:
186
+ music_path = tracks[0][1]
187
+
188
+ data_url = load_audio_data_url(music_path)
189
+ if not data_url:
190
+ return ""
191
+
192
+ vol = max(0.0, min(1.0, float(volume)))
193
+
194
+ if enabled:
195
+ return f'''
196
+ <script>
197
+ (function(){{
198
+ const doc = window.parent?.document || document;
199
+ let audio = doc.getElementById('wrdler-bg-music');
200
+
201
+ if (!audio) {{
202
+ audio = doc.createElement('audio');
203
+ audio.id = 'wrdler-bg-music';
204
+ audio.loop = true;
205
+ audio.style.display = 'none';
206
+ doc.body.appendChild(audio);
207
+ }}
208
+
209
+ const newSrc = "{data_url}";
210
+ if (audio.src !== newSrc) {{
211
+ audio.src = newSrc;
212
+ }}
213
+
214
+ audio.volume = {vol:.3f};
215
+ audio.muted = false;
216
+
217
+ const tryPlay = () => {{
218
+ const p = audio.play();
219
+ if (p && p.catch) {{
220
+ p.catch(() => {{
221
+ console.log('Music autoplay blocked - waiting for user interaction');
222
+ console.log('Click anywhere on the page to start music');
223
+ }});
224
+ }}
225
+ }};
226
+
227
+ tryPlay();
228
+
229
+ // Add user interaction listeners to unlock autoplay
230
+ const unlock = () => {{
231
+ tryPlay();
232
+ doc.removeEventListener('pointerdown', unlock);
233
+ doc.removeEventListener('keydown', unlock);
234
+ doc.removeEventListener('touchstart', unlock);
235
+ }};
236
+
237
+ doc.addEventListener('pointerdown', unlock, {{ once: true }});
238
+ doc.addEventListener('keydown', unlock, {{ once: true }});
239
+ doc.addEventListener('touchstart', unlock, {{ once: true }});
240
+ }})();
241
+ </script>
242
+ '''
243
+ else:
244
+ # Pause music when disabled
245
+ return '''
246
+ <script>
247
+ (function(){
248
+ const doc = window.parent?.document || document;
249
+ const audio = doc.getElementById('wrdler-bg-music');
250
+ if (audio) {
251
+ audio.pause();
252
+ audio.muted = true;
253
+ }
254
+ })();
255
+ </script>
256
+ '''
257
+
258
+
259
  def render_audio_player_html(
260
  effect_name: Optional[str] = None,
261
  volume: float = 0.5,
262
  enabled: bool = True
263
  ) -> str:
264
+ """Generate HTML to play a sound effect.
265
+
266
+ Creates a temporary audio element that plays once and removes itself.
267
+ Sound effects are independent of background music.
268
+
269
+ Args:
270
+ effect_name: Name of the sound effect to play
271
+ volume: Volume level (0.0 to 1.0)
272
+ enabled: Whether sound effects are enabled
273
+
274
+ Returns:
275
+ HTML/JavaScript to inject into the page
276
+ """
277
  if not enabled or not effect_name:
278
  return ""
279
 
 
290
  return f'''
291
  <script>
292
  (function(){{
293
+ const doc = window.parent?.document || document;
294
+ const audio = doc.createElement('audio');
295
+ audio.src = "{data_url}";
296
  audio.volume = {vol:.3f};
297
+ audio.style.display = 'none';
298
+ doc.body.appendChild(audio);
299
+
300
+ // Play and remove after playback
301
+ const playPromise = audio.play();
302
+ if (playPromise) {{
303
+ playPromise.catch(e => {{
304
+ console.log('Sound effect play blocked:', e.message);
305
+ doc.body.removeChild(audio);
306
+ }});
307
+ }}
308
+
309
+ audio.addEventListener('ended', () => {{
310
+ if (audio.parentNode) {{
311
+ doc.body.removeChild(audio);
312
+ }}
313
+ }});
314
+
315
+ // Fallback cleanup after 10 seconds
316
+ setTimeout(() => {{
317
+ if (audio.parentNode) {{
318
+ doc.body.removeChild(audio);
319
+ }}
320
+ }}, 10000);
321
  }})();
322
  </script>
323
  '''
 
1188
  music_enabled: bool,
1189
  music_volume: int,
1190
  state: Dict[str, Any]
1191
+ ) -> Tuple[Dict[str, Any], str, str]:
1192
+ """Handle audio settings changes and return updated state + music HTML + preview audio.
1193
+
1194
+ Returns:
1195
+ Tuple of (updated_state, music_html, preview_audio_html)
1196
+ """
1197
  state = ensure_state(state)
1198
 
1199
  # Create a deep copy to ensure Gradio detects the change
 
1202
  new_state["sound_effects_volume"] = sfx_volume
1203
  new_state["music_enabled"] = music_enabled
1204
  new_state["music_volume"] = music_volume
1205
+
1206
+ # Generate music control HTML (updates background music volume)
1207
+ music_html = render_background_music_html(music_enabled, music_volume / 100.0)
1208
+
1209
+ # No preview audio by default
1210
+ preview_audio = ""
1211
+
1212
+ return new_state, music_html, preview_audio
1213
 
1214
 
1215
+ def handle_sfx_volume_change(
1216
+ sfx_enabled: bool,
1217
+ sfx_volume: int,
1218
+ music_enabled: bool,
1219
+ music_volume: int,
1220
  state: Dict[str, Any]
1221
+ ) -> Tuple[Dict[str, Any], str, str]:
1222
+ """Handle sound effects volume change with preview sound.
1223
+
1224
+ Returns:
1225
+ Tuple of (updated_state, music_html, preview_audio_html)
1226
+ """
1227
  state = ensure_state(state)
1228
 
1229
+ # Create a deep copy to ensure Gradio detects the change
1230
+ new_state = copy.deepcopy(state)
1231
+ new_state["sound_effects_enabled"] = sfx_enabled
1232
+ new_state["sound_effects_volume"] = sfx_volume
1233
+ new_state["music_enabled"] = music_enabled
1234
+ new_state["music_volume"] = music_volume
1235
+
1236
+ # Generate music control HTML
1237
+ music_html = render_background_music_html(music_enabled, music_volume / 100.0)
1238
+
1239
+ # Play preview sound effect at new volume level (only if enabled)
1240
+ preview_audio = ""
1241
+ if sfx_enabled:
1242
+ preview_audio = render_audio_player_html("hit", sfx_volume / 100.0, True)
1243
+
1244
+ return new_state, music_html, preview_audio
1245
+
1246
+
1247
+ def handle_music_volume_change(
1248
+ sfx_enabled: bool,
1249
+ sfx_volume: int,
1250
+ music_enabled: bool,
1251
+ music_volume: int,
1252
+ state: Dict[str, Any]
1253
+ ) -> Tuple[Dict[str, Any], str, str]:
1254
+ """Handle music volume change with preview (updates background music).
1255
+
1256
+ Returns:
1257
+ Tuple of (updated_state, music_html, preview_audio_html)
1258
+ """
1259
+ state = ensure_state(state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1260
 
1261
+ # Create a deep copy to ensure Gradio detects the change
1262
+ new_state = copy.deepcopy(state)
1263
+ new_state["sound_effects_enabled"] = sfx_enabled
1264
+ new_state["sound_effects_volume"] = sfx_volume
1265
+ new_state["music_enabled"] = music_enabled
1266
+ new_state["music_volume"] = music_volume
1267
+
1268
+ # Generate music control HTML (this updates the volume in real-time)
1269
+ music_html = render_background_music_html(music_enabled, music_volume / 100.0)
1270
+
1271
+ # No preview audio by default
1272
+ preview_audio = ""
1273
+
1274
+ return new_state, music_html, preview_audio
1275
+
1276
+ def handle_show_incorrect_change(show_incorrect, wordlist, ai_topic, game_mode, sfx_en, sfx_vol, music_en, music_vol, enable_fl, show_chal, state):
1277
+ state = ensure_state(state)
1278
+ new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode, ai_topic=ai_topic)
1279
+ # Apply all settings
1280
+ new_state["sound_effects_enabled"] = sfx_en
1281
+ new_state["sound_effects_volume"] = sfx_vol
1282
+ new_state["music_enabled"] = music_en
1283
+ new_state["music_volume"] = music_vol
1284
+ new_state["show_incorrect_guesses"] = show_incorrect
1285
+ new_state["enable_free_letters"] = enable_fl
1286
+ new_state["show_challenge_links"] = show_chal
1287
+ if not enable_fl:
1288
+ new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
1289
 
1290
+ return build_ui_outputs(new_state)
 
1291
 
1292
 
1293
  # ---------------------------------------------------------------------------
 
1395
  )
1396
  grid_buttons.append(btn)
1397
 
 
 
1398
  # New Game button
1399
  new_game_btn = gr.Button("New Game", variant="primary", size="lg")
1400
 
 
1450
  choices=["classic", "easy", "too easy"],
1451
  value="classic",
1452
  label="Game Mode"
1453
+ )
1454
  show_incorrect_guesses = gr.Checkbox(
1455
  value=APP_SETTINGS.get("show_incorrect_guesses", True),
1456
  label="Show Incorrect Guesses"
 
1491
 
1492
  # Audio player for sound effects (hidden via CSS but still executes)
1493
  audio_player_html = gr.HTML(value="", elem_id="audio-player", elem_classes=["hidden-audio"])
1494
+
1495
+ # Background music player (hidden but persistent)
1496
+ music_player_html = gr.HTML(value="", elem_id="music-player", elem_classes=["hidden-audio"])
1497
 
1498
  # Timer for updating elapsed time (ticks every second)
1499
  game_timer = gr.Timer(value=1, active=True)
1500
 
1501
+ # Game Over Modal Dialog (contains game over display and share challenge section)
1502
  with Modal(visible=False) as game_over_modal:
1503
  # Game Over Content
1504
  game_over_html = gr.HTML(
 
1683
  fn=lambda: gr.Timer(active=True),
1684
  inputs=None,
1685
  outputs=game_timer
 
 
 
 
 
 
 
 
 
 
 
1686
  ).then(
1687
  fn=lambda: gr.Timer(active=True),
1688
  inputs=None,
 
1756
 
1757
  # Audio settings change handlers
1758
  sfx_enabled.change(
1759
+ fn=handle_audio_settings_change,
1760
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1761
+ outputs=[game_state, music_player_html, audio_player_html]
1762
  )
1763
  sfx_volume.change(
1764
+ fn=handle_sfx_volume_change,
1765
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1766
+ outputs=[game_state, music_player_html, audio_player_html]
1767
  )
1768
  music_enabled.change(
1769
  fn=handle_audio_settings_change,
1770
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1771
+ outputs=[game_state, music_player_html, audio_player_html]
1772
  )
1773
  music_volume.change(
1774
+ fn=handle_music_volume_change,
1775
  inputs=[sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
1776
+ outputs=[game_state, music_player_html, audio_player_html]
1777
  )
1778
 
1779
  # Show incorrect guesses toggle handler - starts new game
 
1819
  inputs=[enable_free_letters, wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, show_challenge_links, game_state],
1820
  outputs=common_outputs
1821
  )
 
1822
  # Show challenge links toggle handler - starts new game
1823
  def handle_show_challenge_links_change(enabled, wordlist, ai_topic, game_mode, sfx_en, sfx_vol, music_en, music_vol, show_inc, enable_fl, state):
1824
  state = ensure_state(state)
 
1884
  def initialize_ui(state):
1885
  """Initialize UI components on app load."""
1886
  state = ensure_state(state)
1887
+ # Generate initial music HTML based on settings
1888
+ music_html = render_background_music_html(
1889
+ state.get("music_enabled", False),
1890
+ state.get("music_volume", 30) / 100.0
1891
+ )
1892
+ return (*build_ui_outputs(state), music_html)
1893
 
1894
  demo.load(
1895
  fn=initialize_ui,
1896
  inputs=[game_state],
1897
+ outputs=[*common_outputs, music_player_html]
1898
  )
1899
 
1900
+ return demo