Spaces:
Running
Running
Version 0.2.11: UI Enhancements and Bug Fixes
Browse filesUpdated 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 +1 -0
- battlewords/__init__.py +1 -1
- battlewords/assets/audio/effects/incorrect_guess.mp3 +1 -1
- battlewords/ui.py +105 -70
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.
|
| 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:
|
| 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 |
-
|
| 220 |
width: 720px;
|
| 221 |
position: absolute;
|
| 222 |
max-width:100%;
|
| 223 |
}
|
| 224 |
-
|
| 225 |
-
|
| 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
|
| 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
|
| 701 |
padding: 0 !important;
|
| 702 |
}
|
| 703 |
-
button[data-testid
|
| 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 |
-
#
|
| 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 |
-
|
| 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;'> ⏱ {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 |
-
|
| 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 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
}}
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
function
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
}}
|
| 1047 |
-
}}
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
if (endMs === null) {{
|
| 1051 |
-
window.bwTimers[key] = setInterval(tick, 1000);
|
| 1052 |
-
}}
|
| 1053 |
-
}} catch (e) {{
|
| 1054 |
-
// no-op
|
| 1055 |
-
}}
|
| 1056 |
-
}})();
|
| 1057 |
-
</script>
|
| 1058 |
"""
|
| 1059 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
|
| 1061 |
# -------------------- Game Over Dialog --------------------
|
| 1062 |
|
|
@@ -1157,13 +1185,10 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1157 |
)
|
| 1158 |
|
| 1159 |
# Dialog actions
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 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;'> ⏱ {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)
|