Surn commited on
Commit
4dab390
·
1 Parent(s): 4302853

- upgrade background ocean view
- apply volume control to sound effects

Files changed (3) hide show
  1. README.md +4 -0
  2. battlewords/__init__.py +1 -1
  3. battlewords/ui.py +156 -39
README.md CHANGED
@@ -106,6 +106,10 @@ docker run -p 8501:8501 battlewords
106
 
107
  ## Changelog
108
 
 
 
 
 
109
  -0.2.12
110
  - fix music looping on congratulations screen
111
 
 
106
 
107
  ## Changelog
108
 
109
+ -0.2.13
110
+ - upgrade background ocean view
111
+ - apply volume control to sound effects
112
+
113
  -0.2.12
114
  - fix music looping on congratulations screen
115
 
battlewords/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.2.12"
2
  __all__ = ["models", "generator", "logic", "ui"]
 
1
+ __version__ = "0.2.13"
2
  __all__ = ["models", "generator", "logic", "ui"]
battlewords/ui.py CHANGED
@@ -65,43 +65,144 @@ def _build_letter_map(puzzle) -> dict[CoordLike, str]:
65
 
66
  ocean_background_css = """
67
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
68
  .stApp {
69
- margin: 0;
70
- height: 100vh;
71
- background: linear-gradient(
72
- 45deg,
73
- rgba(29, 100, 200, 0.6) 0%,
74
- rgba(50, 120, 220, 0.8) 25%,
75
- rgba(20, 80, 180, 0.95) 50%,
76
- rgba(50, 120, 220, 0.8) 75%,
77
- rgba(29, 100, 200, 0.6) 100%
78
- );
79
- background-size: 200% 200%;
80
- animation: oceanFlow 12s ease-in-out infinite;
81
- overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
- /* Animation for rolling water effect */
85
- @keyframes oceanFlow {
86
- 0% {
87
- background-position: 0% 50%;
88
- }
89
- 50% {
90
- background-position: 100% 50%;
91
- }
92
- 100% {
93
- background-position: 0% 50%;
94
- }
95
  }
96
 
97
- /* Ensure Streamlit content is visible above the background */
98
- .stApp > div {
99
- position: relative;
100
- z-index: 1;
 
 
 
 
 
 
 
 
 
101
  }
102
  </style>
103
  """
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  def inject_styles() -> None:
106
  st.markdown(
107
  """
@@ -224,6 +325,7 @@ def inject_styles() -> None:
224
  overflow-x: hidden;
225
  scrollbar-width: thin;
226
  scrollbar-color: transparent transparent;
 
227
  }
228
 
229
  .st-emotion-cache-wp60of {
@@ -231,13 +333,13 @@ def inject_styles() -> None:
231
  position: absolute;
232
  max-width:100%;
233
  }
234
- .stImage { max-width:300px;}
235
  #text_input_3,#text_input_1 {
236
  background-color:#fff;
237
  color:#000;
238
  caret-color:#333;}
239
 
240
- @media (min-width:720px) {
241
  .st-emotion-cache-wp60of {
242
  left: calc(calc(100% - 720px) / 2);
243
  }
@@ -433,17 +535,18 @@ def _render_sidebar():
433
  # Audio settings
434
  st.header("Audio")
435
  tracks = get_audio_tracks()
436
- # Show how many audio files were found
437
  st.caption(f"{len(tracks)} audio file{'s' if len(tracks) != 1 else ''} found in battlewords/assets/audio/music")
438
 
439
  if "music_enabled" not in st.session_state:
440
  st.session_state.music_enabled = False
441
  if "music_volume" not in st.session_state:
442
  st.session_state.music_volume = 15
 
 
 
443
 
444
  enabled = st.checkbox("Enable music", value=st.session_state.music_enabled, key="music_enabled")
445
 
446
- # Always show volume slider; disable when music disabled or no tracks
447
  st.slider(
448
  "Volume",
449
  0,
@@ -454,6 +557,16 @@ def _render_sidebar():
454
  disabled=not (enabled and bool(tracks)),
455
  )
456
 
 
 
 
 
 
 
 
 
 
 
457
  selected_path = None
458
  if tracks:
459
  options = [p for _, p in tracks]
@@ -749,6 +862,9 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
749
  flex: 1 1 calc(20% - 1rem);
750
  }
751
  @media (max-width: 640px) {
 
 
 
752
  .st-emotion-cache-1s1xxaz {
753
  min-width: calc(33% - 1.5rem);
754
  }
@@ -834,9 +950,9 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
834
  # Play sound effect based on hit or miss
835
  action = (state.last_action or "").strip()
836
  if action.startswith("Revealed '"):
837
- play_sound_effect("hit", volume=0.5)
838
  elif action.startswith("Revealed empty"):
839
- play_sound_effect("miss", volume=0.5)
840
 
841
  st.rerun()
842
 
@@ -954,12 +1070,12 @@ def _render_guess_form(state: GameState):
954
  if correct:
955
  st.session_state.radar_gif_path = None
956
  st.session_state.radar_gif_signature = None
957
- play_sound_effect("correct_guess", volume=0.6)
958
  else:
959
  # Update incorrect guesses list - keep only last 10
960
  st.session_state.incorrect_guesses.append(guess_text)
961
  st.session_state.incorrect_guesses = st.session_state.incorrect_guesses[-10:]
962
- play_sound_effect("incorrect_guess", volume=0.5)
963
  st.rerun()
964
 
965
 
@@ -1095,7 +1211,7 @@ def _game_over_content(state: GameState) -> None:
1095
  if os.path.exists(congrats_music_path):
1096
  src_url = _load_audio_data_url(congrats_music_path)
1097
  # Play once (no loop) at configured volume
1098
- _mount_background_audio(True, src_url, (st.session_state.get("music_volume", 100)) / 100, loop=False)
1099
  else:
1100
  _mount_background_audio(False, None, 0.0)
1101
 
@@ -1122,7 +1238,7 @@ def _game_over_content(state: GameState) -> None:
1122
  )
1123
 
1124
  table_html = (
1125
- "<table class=\"shiny-border\" style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing:0;\">"
1126
  "<thead><tr>"
1127
  "<th scope=\"col\">Word</th>"
1128
  "<th scope=\"col\">Letters</th>"
@@ -1214,7 +1330,7 @@ def _render_game_over(state: GameState):
1214
  congrats_music_path = os.path.join(music_dir, "congratulations.mp3")
1215
  if os.path.exists(congrats_music_path):
1216
  src_url = _load_audio_data_url(congrats_music_path)
1217
- _mount_background_audio(True, src_url, (st.session_state.get("music_volume", 100)) / 100, loop=False)
1218
  else:
1219
  _mount_background_audio(False, None, 0.0)
1220
  _game_over_dialog(state)
@@ -1262,6 +1378,7 @@ def run_app():
1262
 
1263
  _init_session()
1264
  st.markdown(ocean_background_css, unsafe_allow_html=True)
 
1265
  _render_header()
1266
  _render_sidebar()
1267
 
 
65
 
66
  ocean_background_css = """
67
  <style>
68
+ :root {
69
+ --water-deep: #0b2a4a;
70
+ --water-mid: #0f3968;
71
+ --water-lite: #165ba8;
72
+ --water-sky: #1d64c8;
73
+ --foam: rgba(255,255,255,0.18);
74
+ }
75
+
76
+ .stAppHeader{
77
+ opacity:0.6;
78
+ }
79
+
80
  .stApp {
81
+ margin: 0;
82
+ min-height: 100vh;
83
+ overflow: hidden; /* prevent scrollbars from animated layers */
84
+ background-attachment: scroll;
85
+ position: relative;
86
+
87
+ /* Static base gradient */
88
+ background-image:
89
+ linear-gradient(180deg, var(--water-sky) 0%, var(--water-lite) 35%, var(--water-mid) 70%, var(--water-deep) 100%);
90
+ background-size: 100% 100%;
91
+ background-position: 50% 50%;
92
+
93
+ animation: none !important;
94
+ will-change: background-position;
95
+ }
96
+
97
+ /* Animated overlay waves (under bg layers) */
98
+ .stApp::before {
99
+ content: "";
100
+ position: absolute;
101
+ inset: -10% -10%;
102
+ z-index: 0;
103
+ pointer-events: none;
104
+ background:
105
+ repeating-linear-gradient(0deg, rgba(255,255,255,0.10) 0 2px, transparent 2px 22px),
106
+ repeating-linear-gradient(90deg, rgba(255,255,255,0.06) 0 1px, transparent 1px 18px);
107
+ mix-blend-mode: screen;
108
+ opacity: 0.10;
109
+ animation: waveOverlayScroll 16s linear infinite;
110
+ }
111
+
112
+ @keyframes waveOverlayScroll {
113
+ 0% { background-position: 0px 0px, 0px 0px; }
114
+ 100% { background-position: -800px 0px, 0px -600px; }
115
+ }
116
+
117
+ /* Keep Streamlit content above background/overlay */
118
+ .stApp > div { position: relative; z-index: 5; }
119
+
120
+ /* Slower, more subtle animations */
121
+ @keyframes oceanHighlight {
122
+ 0% { background-position: 50% 0%; }
123
+ 50% { background-position: 60% 8%; }
124
+ 100% { background-position: 50% 0%; }
125
  }
126
 
127
+ @keyframes oceanLong {
128
+ 0% { background-position: 0% 50%; }
129
+ 100% { background-position: -100% 50%; }
 
 
 
 
 
 
 
 
130
  }
131
 
132
+ @keyframes oceanMid {
133
+ 0% { background-position: 100% 50%; }
134
+ 100% { background-position: 200% 50%; }
135
+ }
136
+
137
+ @keyframes oceanFine {
138
+ 0% { background-position: 0% 50%; }
139
+ 100% { background-position: 100% 50%; }
140
+ }
141
+
142
+ /* Reduced motion */
143
+ @media (prefers-reduced-motion: reduce) {
144
+ .stApp, .stApp::before { animation: none; }
145
  }
146
  </style>
147
  """
148
 
149
+ def inject_ocean_layers() -> None:
150
+ st.markdown(
151
+ """
152
+ <style>
153
+ .bw-bg-container {
154
+ position: fixed; /* fixed to viewport, not stApp */
155
+ inset: 0;
156
+ z-index: 1; /* below content (z=5) but above ::before (z=0) */
157
+ pointer-events: none;
158
+ overflow: hidden; /* clip children */
159
+ }
160
+ .bw-bg-layer {
161
+ position: absolute;
162
+ inset: 0;
163
+ width: 100vw;
164
+ height: 100vh;
165
+ pointer-events: none;
166
+ }
167
+ /* Explicit stacking order with slower animations */
168
+ .bw-bg-highlight {
169
+ z-index: 11;
170
+ background: radial-gradient(150% 100% at 50% -20%, rgba(255,255,255,0.10) 0%, transparent 60%);
171
+ background-size: 150% 150%; /* reduced from 300% */
172
+ animation: oceanHighlight 12s ease-in-out infinite; /* doubled from 6s */
173
+ }
174
+ .bw-bg-long {
175
+ z-index: 12;
176
+ background: repeating-linear-gradient(-6deg, rgba(255,255,255,0.08) 0px, rgba(255,255,255,0.08) 18px, rgba(0,0,0,0.04) 18px, rgba(0,0,0,0.04) 48px);
177
+ background-size: 150% 150%; /* reduced from 320% */
178
+ animation: oceanLong 36s linear infinite; /* doubled from 18s */
179
+ opacity: 0.2;
180
+ }
181
+ .bw-bg-mid {
182
+ z-index: 13;
183
+ background: repeating-linear-gradient(-12deg, rgba(255,255,255,0.10) 0px, rgba(255,255,255,0.10) 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 26px);
184
+ background-size: 150% 150%; /* reduced from 260% */
185
+ animation: oceanMid 24s linear infinite; /* doubled from 12s */
186
+ opacity: 0.2;
187
+ }
188
+ .bw-bg-fine {
189
+ z-index: 14;
190
+ background: repeating-linear-gradient(-18deg, var(--foam) 0px, var(--foam) 4px, transparent 4px, transparent 12px);
191
+ background-size: 120% 120%; /* reduced from 200% */
192
+ animation: oceanFine 14s linear infinite; /* doubled from 7s */
193
+ opacity: 0.15;
194
+ }
195
+ </style>
196
+ <div class="bw-bg-container">
197
+ <div class="bw-bg-layer bw-bg-highlight"></div>
198
+ <div class="bw-bg-layer bw-bg-long"></div>
199
+ <div class="bw-bg-layer bw-bg-mid"></div>
200
+ <div class="bw-bg-layer bw-bg-fine"></div>
201
+ </div>
202
+ """,
203
+ unsafe_allow_html=True,
204
+ )
205
+
206
  def inject_styles() -> None:
207
  st.markdown(
208
  """
 
325
  overflow-x: hidden;
326
  scrollbar-width: thin;
327
  scrollbar-color: transparent transparent;
328
+ opacity:0.75;
329
  }
330
 
331
  .st-emotion-cache-wp60of {
 
333
  position: absolute;
334
  max-width:100%;
335
  }
336
+ .stImage {max-width:300px;}
337
  #text_input_3,#text_input_1 {
338
  background-color:#fff;
339
  color:#000;
340
  caret-color:#333;}
341
 
342
+ @media (min-width:720px) {
343
  .st-emotion-cache-wp60of {
344
  left: calc(calc(100% - 720px) / 2);
345
  }
 
535
  # Audio settings
536
  st.header("Audio")
537
  tracks = get_audio_tracks()
 
538
  st.caption(f"{len(tracks)} audio file{'s' if len(tracks) != 1 else ''} found in battlewords/assets/audio/music")
539
 
540
  if "music_enabled" not in st.session_state:
541
  st.session_state.music_enabled = False
542
  if "music_volume" not in st.session_state:
543
  st.session_state.music_volume = 15
544
+ # --- Add sound effects volume ---
545
+ if "effects_volume" not in st.session_state:
546
+ st.session_state.effects_volume = 50
547
 
548
  enabled = st.checkbox("Enable music", value=st.session_state.music_enabled, key="music_enabled")
549
 
 
550
  st.slider(
551
  "Volume",
552
  0,
 
557
  disabled=not (enabled and bool(tracks)),
558
  )
559
 
560
+ # --- Add sound effects volume slider ---
561
+ st.slider(
562
+ "Sound Effects Volume",
563
+ 0,
564
+ 100,
565
+ value=int(st.session_state.effects_volume),
566
+ step=1,
567
+ key="effects_volume",
568
+ )
569
+
570
  selected_path = None
571
  if tracks:
572
  options = [p for _, p in tracks]
 
862
  flex: 1 1 calc(20% - 1rem);
863
  }
864
  @media (max-width: 640px) {
865
+ .stImage {max-width:100%; width:100%;}
866
+ .stImage img {max-width:360px !important; margin:0 auto;}
867
+ .st-emotion-cache-p75nl5 {width:100%;}
868
  .st-emotion-cache-1s1xxaz {
869
  min-width: calc(33% - 1.5rem);
870
  }
 
950
  # Play sound effect based on hit or miss
951
  action = (state.last_action or "").strip()
952
  if action.startswith("Revealed '"):
953
+ play_sound_effect("hit", volume=(st.session_state.get("effects_volume", 50) / 100))
954
  elif action.startswith("Revealed empty"):
955
+ play_sound_effect("miss", volume=(st.session_state.get("effects_volume", 50) / 100))
956
 
957
  st.rerun()
958
 
 
1070
  if correct:
1071
  st.session_state.radar_gif_path = None
1072
  st.session_state.radar_gif_signature = None
1073
+ play_sound_effect("correct_guess", volume=(st.session_state.get("effects_volume", 50) / 100))
1074
  else:
1075
  # Update incorrect guesses list - keep only last 10
1076
  st.session_state.incorrect_guesses.append(guess_text)
1077
  st.session_state.incorrect_guesses = st.session_state.incorrect_guesses[-10:]
1078
+ play_sound_effect("incorrect_guess", volume=(st.session_state.get("effects_volume", 50) / 100))
1079
  st.rerun()
1080
 
1081
 
 
1211
  if os.path.exists(congrats_music_path):
1212
  src_url = _load_audio_data_url(congrats_music_path)
1213
  # Play once (no loop) at configured volume
1214
+ _mount_background_audio(enabled=True, src_url=src_url, volume=(st.session_state.get("music_volume", 100)) / 100, loop=False)
1215
  else:
1216
  _mount_background_audio(False, None, 0.0)
1217
 
 
1238
  )
1239
 
1240
  table_html = (
1241
+ "<table class=\"shiny-border\" style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing: 0;\">"
1242
  "<thead><tr>"
1243
  "<th scope=\"col\">Word</th>"
1244
  "<th scope=\"col\">Letters</th>"
 
1330
  congrats_music_path = os.path.join(music_dir, "congratulations.mp3")
1331
  if os.path.exists(congrats_music_path):
1332
  src_url = _load_audio_data_url(congrats_music_path)
1333
+ _mount_background_audio(enabled=True, src_url=src_url, volume=(st.session_state.get("music_volume", 100)) / 100, loop=False)
1334
  else:
1335
  _mount_background_audio(False, None, 0.0)
1336
  _game_over_dialog(state)
 
1378
 
1379
  _init_session()
1380
  st.markdown(ocean_background_css, unsafe_allow_html=True)
1381
+ inject_ocean_layers() # <-- add the animated layers
1382
  _render_header()
1383
  _render_sidebar()
1384