Surn commited on
Commit
d779f16
·
1 Parent(s): 7896205

Version 0.2.11: UI Enhancements and Bug Fixes

Browse files

Updated version to 0.2.11 in `__init__.py`. Removed fullscreen image tooltip from `README.md`. Updated `incorrect_guess.mp3` using Git LFS. Added `streamlit.components.v1` import in `ui.py`. Enhanced `inject_styles` with new CSS for `.stHeading` and toolbar button hiding. Improved layout and styling in `ui.py`. Clarified game instructions in `_render_header`. Refactored timer logic in `_render_score_panel` for better accuracy and reduced layout jumps. Simplified game over dialog actions and added audio remount logic. Made minor formatting adjustments in `run_app`.

README.md CHANGED
@@ -109,6 +109,7 @@ docker run -p 8501:8501 battlewords
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
 
109
  -0.2.11
110
  - update timer to be live during gameplay, but reset with each action
111
  - compact design
112
+ - remove fullscreen image tooltip
113
 
114
  -0.2.10
115
  - reduce sonar graphic size
battlewords/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.2.10"
2
  __all__ = ["models", "generator", "logic", "ui"]
 
1
+ __version__ = "0.2.11"
2
  __all__ = ["models", "generator", "logic", "ui"]
battlewords/assets/audio/effects/incorrect_guess.mp3 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1cc06c8c57a1f5acd81661723bcfbf945a253110279b9db7eaca02f89c61667d
3
  size 49663
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dda2212a4c7fab5d3a402d4639c11fc80d93f01e14523a20bbbfd747e873d13d
3
  size 49663
battlewords/ui.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
  from . import __version__ as version
3
  from typing import Iterable, Tuple, Optional
4
  import streamlit as st
 
5
 
6
  import matplotlib.pyplot as plt
7
  from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
@@ -112,6 +113,12 @@ def inject_styles() -> None:
112
  .stMainBlockContainer {
113
  max-width: 1100px;
114
  }
 
 
 
 
 
 
115
  /* Base grid cell visuals */
116
  .bw-row { display: flex; gap: 0.1rem; flex-wrap: nowrap; min-height: 48px;}
117
  .bw-cell {
@@ -172,6 +179,9 @@ def inject_styles() -> None:
172
  .st-emotion-cache-1n6tfoc::before { min-height: calc(100% + 20px) !important; }
173
 
174
  }
 
 
 
175
 
176
  /* Mobile styles */
177
  @media (max-width: 640px) {
@@ -216,17 +226,17 @@ def inject_styles() -> None:
216
  scrollbar-color: transparent transparent;
217
  }
218
 
219
- .st-emotion-cache-wp60of {
220
  width: 720px;
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 {
232
  left: calc(calc(100% - 720px) / 2);
@@ -339,7 +349,7 @@ def _sync_back(state: GameState) -> None:
339
 
340
  def _render_header():
341
  st.title(f"Battlewords v{version}")
342
- st.subheader("Reveal cells, then guess the hidden words.")
343
  # st.markdown(
344
  # "- Grid is 12×12 with 6 words (two 4-letter, two 5-letter, two 6-letter).\n"
345
  # "- After each reveal, you may submit one word guess below.\n"
@@ -348,7 +358,15 @@ def _render_header():
348
  # "- Words do not overlap, but may be touching.")
349
  inject_styles()
350
 
 
 
 
 
351
 
 
 
 
 
352
  def _render_sidebar():
353
  with st.sidebar:
354
  st.header("SETTINGS")
@@ -697,10 +715,10 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
697
  st.markdown(
698
  """
699
  <style>
700
- div[data-testid=\"column\"] {
701
  padding: 0 !important;
702
  }
703
- button[data-testid=\"stButton\"] {
704
  width: 32px !important;
705
  height: 32px !important;
706
  min-width: 32px !important;
@@ -983,20 +1001,16 @@ def _render_score_panel(state: GameState):
983
  )
984
  rows_html.append(row_html)
985
 
986
- # Timer calculation (initial render value)
987
  now = datetime.now()
988
  start = state.start_time or now
989
  end = state.end_time or (now if is_game_over(state) else None)
990
  elapsed = (end or now) - start
991
- elapsed_seconds = int(elapsed.total_seconds())
992
- mins, secs = divmod(elapsed_seconds, 60)
993
  timer_str = f"{mins:02d}:{secs:02d}"
994
 
995
- # Unique id for the timer span (use puzzle uid when available)
996
  span_id = f"bw-timer-{getattr(state.puzzle, 'uid', 'default')}"
997
- timer_span_html = (
998
- f"<span id=\"{span_id}\" style='font-size:1rem; color:#ffffff;'>&nbsp;⏱ {timer_str}</span>"
999
- )
1000
 
1001
  total_row_html = (
1002
  f"<tr class=\"blue-background\"><td colspan='3'>"
@@ -1005,58 +1019,72 @@ def _render_score_panel(state: GameState):
1005
  )
1006
  rows_html.append(total_row_html)
1007
 
1008
- table_html = (
1009
- "<table class='shiny-border' style=\"background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666); width:100%; margin: 0 auto;border-collapse:separate; border-spacing:0;\">"
1010
- f"{''.join(rows_html)}"
1011
- "</table>"
1012
- )
1013
- st.markdown(f"<div class='bw-score-panel-container'>{table_html}</div>", unsafe_allow_html=True)
1014
-
1015
- # Inject client-side timer updater script so only the timer text updates
1016
  start_ms = int(start.timestamp() * 1000)
1017
  end_ms = int(end.timestamp() * 1000) if end else None
1018
- js = f"""
1019
- <script>
1020
- (function() {{
1021
- try {{
1022
- window.bwTimers = window.bwTimers || {{}};
1023
- var key = "{span_id}";
1024
- if (window.bwTimers[key]) {{
1025
- clearInterval(window.bwTimers[key]);
1026
- }}
1027
- var span = document.getElementById("{span_id}");
1028
- if (!span) return;
1029
-
1030
- var startMs = {start_ms};
1031
- var endMs = {"null" if end_ms is None else end_ms};
1032
-
1033
- function fmt(ms) {{
1034
- var total = Math.max(0, Math.floor(ms / 1000));
1035
- var m = Math.floor(total / 60);
1036
- var s = total % 60;
1037
- return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
1038
- }}
1039
-
1040
- function tick() {{
1041
- var now = Date.now();
1042
- var t = (endMs !== null ? endMs : now) - startMs;
1043
- span.textContent = "⏱ " + fmt(t);
1044
- if (endMs !== null) {{
1045
- clearInterval(window.bwTimers[key]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1046
  }}
1047
- }}
1048
-
1049
- tick();
1050
- if (endMs === null) {{
1051
- window.bwTimers[key] = setInterval(tick, 1000);
1052
- }}
1053
- }} catch (e) {{
1054
- // no-op
1055
- }}
1056
- }})();
1057
- </script>
1058
  """
1059
- st.markdown(js, unsafe_allow_html=True)
 
 
 
 
1060
 
1061
  # -------------------- Game Over Dialog --------------------
1062
 
@@ -1157,13 +1185,10 @@ def _game_over_content(state: GameState) -> None:
1157
  )
1158
 
1159
  # Dialog actions
1160
- cols = st.columns([1, 1])
1161
- with cols[0]:
1162
- if st.button("Close", key="close_game_over"):
1163
- st.session_state["show_gameover_overlay"] = False
1164
- st.rerun()
1165
- with cols[1]:
1166
- st.button("New Game", key="new_game_btn_dialog", on_click=_new_game)
1167
 
1168
  # Prefer st.dialog/experimental_dialog; fallback to st.modal if unavailable
1169
  _Dialog = getattr(st, "dialog", getattr(st, "experimental_dialog", None))
@@ -1185,6 +1210,16 @@ else:
1185
  def _render_game_over(state: GameState):
1186
  # Determine visibility
1187
  visible = bool(st.session_state.get("show_gameover_overlay", True)) and is_game_over(state)
 
 
 
 
 
 
 
 
 
 
1188
  if visible:
1189
  _game_over_dialog(state)
1190
 
@@ -1232,7 +1267,7 @@ def run_app():
1232
  st.markdown('<div id="bw-main-anchor"></div>', unsafe_allow_html=True)
1233
  left, right = st.columns([3, 2], gap="medium")
1234
  with right:
1235
- _render_radar(state.puzzle, size=state.grid_size, r_max=0.8, max_frames=25, sinusoid_expand=True, stagger_radar=False, show_ticks=st.session_state.get("show_grid_ticks", False))
1236
  one, two = st.columns([1, 2], gap="medium")
1237
  with one:
1238
  _render_correct_try_again(state)
 
2
  from . import __version__ as version
3
  from typing import Iterable, Tuple, Optional
4
  import streamlit as st
5
+ import streamlit.components.v1 as components
6
 
7
  import matplotlib.pyplot as plt
8
  from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
 
113
  .stMainBlockContainer {
114
  max-width: 1100px;
115
  }
116
+ .stHeading {
117
+ margin-bottom: 0rem !important;
118
+ margin-top: 0rem !important;
119
+ font-size: 2rem !important; /* Title */
120
+ line-height: 1.1 !important;
121
+ }
122
  /* Base grid cell visuals */
123
  .bw-row { display: flex; gap: 0.1rem; flex-wrap: nowrap; min-height: 48px;}
124
  .bw-cell {
 
179
  .st-emotion-cache-1n6tfoc::before { min-height: calc(100% + 20px) !important; }
180
 
181
  }
182
+ div[data-testid="stElementToolbarButtonContainer"], button[data-testid="stBaseButton-elementToolbar"], button[data-testid="stBaseButton-elementToolbar"]:hover {
183
+ display: none;
184
+ }
185
 
186
  /* Mobile styles */
187
  @media (max-width: 640px) {
 
226
  scrollbar-color: transparent transparent;
227
  }
228
 
229
+ .st-emotion-cache-wp60of {
230
  width: 720px;
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);
 
349
 
350
  def _render_header():
351
  st.title(f"Battlewords v{version}")
352
+ st.subheader("Reveal letters in cells, then guess the words!")
353
  # st.markdown(
354
  # "- Grid is 12×12 with 6 words (two 4-letter, two 5-letter, two 6-letter).\n"
355
  # "- After each reveal, you may submit one word guess below.\n"
 
358
  # "- Words do not overlap, but may be touching.")
359
  inject_styles()
360
 
361
+ st.markdown(
362
+ """
363
+ <style>
364
+ /* Compact title and subheader */
365
 
366
+ </style>
367
+ """,
368
+ unsafe_allow_html=True,
369
+ )
370
  def _render_sidebar():
371
  with st.sidebar:
372
  st.header("SETTINGS")
 
715
  st.markdown(
716
  """
717
  <style>
718
+ div[data-testid="column"] {
719
  padding: 0 !important;
720
  }
721
+ button[data-testid="stButton"] {
722
  width: 32px !important;
723
  height: 32px !important;
724
  min-width: 32px !important;
 
1001
  )
1002
  rows_html.append(row_html)
1003
 
1004
+ # Initial time shown from server; JS will tick client-side
1005
  now = datetime.now()
1006
  start = state.start_time or now
1007
  end = state.end_time or (now if is_game_over(state) else None)
1008
  elapsed = (end or now) - start
1009
+ mins, secs = divmod(int(elapsed.total_seconds()), 60)
 
1010
  timer_str = f"{mins:02d}:{secs:02d}"
1011
 
 
1012
  span_id = f"bw-timer-{getattr(state.puzzle, 'uid', 'default')}"
1013
+ timer_span_html = f"<span id=\"{span_id}\" style='font-size:1rem; color:#ffffff;'>&nbsp;⏱ {timer_str}</span>"
 
 
1014
 
1015
  total_row_html = (
1016
  f"<tr class=\"blue-background\"><td colspan='3'>"
 
1019
  )
1020
  rows_html.append(total_row_html)
1021
 
1022
+ # Build a self-contained HTML document so JS runs inside the component iframe
 
 
 
 
 
 
 
1023
  start_ms = int(start.timestamp() * 1000)
1024
  end_ms = int(end.timestamp() * 1000) if end else None
1025
+
1026
+ table_inner = "".join(rows_html)
1027
+ html_doc = f"""
1028
+ <div class='bw-score-panel-container'>
1029
+ <style>
1030
+ .bold-text {{ font-weight: 700; }}
1031
+ .blue-background {{ background:#1d64c8; opacity:0.9; color:#fff; }}
1032
+ .shiny-border {{ position: relative; padding: 12px; background: #333; color: white; border-radius: 1.25rem; overflow: hidden; }}
1033
+ table {{ width: 100%; margin: 0 auto; border-collapse: separate; border-spacing: 0; }}
1034
+ th, td {{ padding: 6px 8px; }}
1035
+ </style>
1036
+ <table class='shiny-border' style="background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666);">
1037
+ {table_inner}
1038
+ </table>
1039
+ <script>
1040
+ (function() {{
1041
+ try {{
1042
+ var span = document.getElementById("{span_id}");
1043
+ if (!span) return;
1044
+ var startMs = {start_ms};
1045
+ var endMs = {"null" if end_ms is None else end_ms};
1046
+
1047
+ function fmt(ms) {{
1048
+ var total = Math.max(0, Math.floor(ms / 1000));
1049
+ var m = Math.floor(total / 60);
1050
+ var s = total % 60;
1051
+ return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
1052
+ }}
1053
+
1054
+ function render(ms) {{
1055
+ if (endMs !== null) {{
1056
+ span.textContent = "⏱ " + fmt(endMs - startMs);
1057
+ return;
1058
+ }}
1059
+ var now = Date.now();
1060
+ span.textContent = "⏱ " + fmt(Math.max(0, now - startMs));
1061
+ }}
1062
+
1063
+ function tick() {{
1064
+ if (endMs !== null) {{
1065
+ render(endMs - startMs);
1066
+ return;
1067
+ }}
1068
+ var now = Date.now();
1069
+ render(Math.max(0, now - startMs));
1070
+ }}
1071
+
1072
+ tick();
1073
+ if (endMs === null) {{
1074
+ setInterval(tick, 1000);
1075
+ }}
1076
+ }} catch (e) {{
1077
+ // no-op
1078
  }}
1079
+ }})();
1080
+ </script>
1081
+ </div>
 
 
 
 
 
 
 
 
1082
  """
1083
+
1084
+ # Height heuristic to avoid layout jumps
1085
+ num_rows = len(rows_html)
1086
+ height = 40 + (num_rows * 36)
1087
+ components.html(html_doc, height=height, scrolling=False)
1088
 
1089
  # -------------------- Game Over Dialog --------------------
1090
 
 
1185
  )
1186
 
1187
  # Dialog actions
1188
+ if st.button("Close", key="close_game_over"):
1189
+ st.session_state["show_gameover_overlay"] = False
1190
+ st.session_state["remount_background_audio"] = True # <-- set flag
1191
+ st.rerun()
 
 
 
1192
 
1193
  # Prefer st.dialog/experimental_dialog; fallback to st.modal if unavailable
1194
  _Dialog = getattr(st, "dialog", getattr(st, "experimental_dialog", None))
 
1210
  def _render_game_over(state: GameState):
1211
  # Determine visibility
1212
  visible = bool(st.session_state.get("show_gameover_overlay", True)) and is_game_over(state)
1213
+ if not visible and st.session_state.get("remount_background_audio", False):
1214
+ # Remount background audio when modal is dismissed (X or button)
1215
+ music_dir = _get_music_dir()
1216
+ background_path = os.path.join(music_dir, "background.mp3")
1217
+ if os.path.exists(background_path):
1218
+ src_url = _load_audio_data_url(background_path)
1219
+ _mount_background_audio(True, src_url, (st.session_state.get("music_volume", 100)) / 100)
1220
+ else:
1221
+ _mount_background_audio(False, None, 0.0)
1222
+ st.session_state["remount_background_audio"] = False # reset flag
1223
  if visible:
1224
  _game_over_dialog(state)
1225
 
 
1267
  st.markdown('<div id="bw-main-anchor"></div>', unsafe_allow_html=True)
1268
  left, right = st.columns([3, 2], gap="medium")
1269
  with right:
1270
+ _render_radar(state.puzzle, size=state.grid_size, r_max=0.8, max_frames=25, sinusoid_expand=True, stagger_radar=False, show_ticks=st.session_state.get("show_grid_ticks", False))
1271
  one, two = st.columns([1, 2], gap="medium")
1272
  with one:
1273
  _render_correct_try_again(state)