Spaces:
Running
Running
Implement pop-up as streamlit dialog
Browse files- battlewords/ui.py +107 -189
battlewords/ui.py
CHANGED
|
@@ -151,7 +151,7 @@ def inject_styles() -> None:
|
|
| 151 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto;}
|
| 152 |
|
| 153 |
div[data-testid="column"], .st-emotion-cache-zh2fnc { width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important; }
|
| 154 |
-
.st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.
|
| 155 |
|
| 156 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 157 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { flex: 0 0 auto !important; }
|
|
@@ -905,18 +905,18 @@ def _render_hit_miss(state: GameState):
|
|
| 905 |
# Render as a circular radio group, side-by-side
|
| 906 |
st.markdown(
|
| 907 |
f"""
|
| 908 |
-
<div class
|
| 909 |
-
<div class
|
| 910 |
-
<div class
|
| 911 |
-
<span class
|
| 912 |
</div>
|
| 913 |
-
<div class
|
| 914 |
</div>
|
| 915 |
-
<div class
|
| 916 |
-
<div class
|
| 917 |
-
<span class
|
| 918 |
</div>
|
| 919 |
-
<div class
|
| 920 |
</div>
|
| 921 |
</div>
|
| 922 |
""",
|
|
@@ -941,18 +941,18 @@ def _render_correct_try_again(state: GameState):
|
|
| 941 |
|
| 942 |
st.markdown(
|
| 943 |
f"""
|
| 944 |
-
<div class
|
| 945 |
-
<div class
|
| 946 |
-
<div class
|
| 947 |
-
<span class
|
| 948 |
</div>
|
| 949 |
-
<div class
|
| 950 |
</div>
|
| 951 |
-
<div class
|
| 952 |
-
<div class
|
| 953 |
-
<span class
|
| 954 |
</div>
|
| 955 |
-
<div class
|
| 956 |
</div>
|
| 957 |
</div>
|
| 958 |
""",
|
|
@@ -1013,14 +1013,10 @@ def _render_score_panel(state: GameState):
|
|
| 1013 |
)
|
| 1014 |
st.markdown(f"<div class='bw-score-panel-container'>{table_html}</div>", unsafe_allow_html=True)
|
| 1015 |
|
| 1016 |
-
# -------------------- Game Over
|
| 1017 |
-
def _render_game_over(state: GameState):
|
| 1018 |
-
import streamlit.components.v1 as components
|
| 1019 |
-
|
| 1020 |
-
# Determine visibility
|
| 1021 |
-
visible = bool(st.session_state.get("show_gameover_overlay", True)) and is_game_over(state)
|
| 1022 |
|
| 1023 |
-
|
|
|
|
| 1024 |
word_rows = []
|
| 1025 |
for w in state.puzzle.words:
|
| 1026 |
pts = st.session_state.points_by_word.get(w.text, 0)
|
|
@@ -1028,8 +1024,9 @@ def _render_game_over(state: GameState):
|
|
| 1028 |
word_rows.append(
|
| 1029 |
f"<tr><td class=\"blue-background\">{w.text}</td><td class=\"blue-background\">{len(w.text)}</td><td class=\"blue-background\">{extra_pts}</td></tr>"
|
| 1030 |
)
|
|
|
|
| 1031 |
table_html = (
|
| 1032 |
-
"<table class=\"
|
| 1033 |
"<thead><tr>"
|
| 1034 |
"<th scope=\"col\">Word</th>"
|
| 1035 |
"<th scope=\"col\">Letters</th>"
|
|
@@ -1041,168 +1038,89 @@ def _render_game_over(state: GameState):
|
|
| 1041 |
"</table>"
|
| 1042 |
)
|
| 1043 |
|
| 1044 |
-
#
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
try {{ m.hide(); }} catch(e) {{ console.warn('[BW Modal] hide() failed', e); }}
|
| 1128 |
-
}}
|
| 1129 |
-
// Reset styles after hide
|
| 1130 |
-
modalEl.classList.remove('show');
|
| 1131 |
-
modalEl.style.display = '';
|
| 1132 |
-
}}
|
| 1133 |
-
|
| 1134 |
-
// Helper to notify the parent page (Streamlit) to toggle wrapper around this iframe
|
| 1135 |
-
function notifyParent(visible) {{
|
| 1136 |
-
try {{
|
| 1137 |
-
window.parent.postMessage({{
|
| 1138 |
-
type: 'bw-bootstrap-modal-visibility',
|
| 1139 |
-
visible: visible
|
| 1140 |
-
}}, '*');
|
| 1141 |
-
}} catch (e) {{
|
| 1142 |
-
console.warn('[BW Modal] notifyParent failed', e);
|
| 1143 |
-
}}
|
| 1144 |
-
}}
|
| 1145 |
-
|
| 1146 |
-
// Send initial visibility to parent
|
| 1147 |
-
notifyParent(shouldShow);
|
| 1148 |
-
|
| 1149 |
-
// On show, remove any aria-hidden that might conflict and set focus to close button
|
| 1150 |
-
modalEl.addEventListener('show.bs.modal', function() {{
|
| 1151 |
-
try {{ modalEl.removeAttribute('aria-hidden'); }} catch (e) {{}}
|
| 1152 |
-
}});
|
| 1153 |
-
|
| 1154 |
-
modalEl.addEventListener('shown.bs.modal', function() {{
|
| 1155 |
-
try {{
|
| 1156 |
-
var btn = modalEl.querySelector('.btn-close');
|
| 1157 |
-
if (btn) btn.focus();
|
| 1158 |
-
}} catch (e) {{}}
|
| 1159 |
-
notifyParent(true);
|
| 1160 |
-
}});
|
| 1161 |
-
|
| 1162 |
-
// Utility to clear focus to avoid aria-hidden focus conflicts
|
| 1163 |
-
function blurActive() {{
|
| 1164 |
-
try {{
|
| 1165 |
-
if (document.activeElement) document.activeElement.blur();
|
| 1166 |
-
}} catch (e) {{}}
|
| 1167 |
-
}}
|
| 1168 |
-
|
| 1169 |
-
// On hide start, clear focus
|
| 1170 |
-
modalEl.addEventListener('hide.bs.modal', function() {{
|
| 1171 |
-
blurActive();
|
| 1172 |
-
}});
|
| 1173 |
-
|
| 1174 |
-
// On fully hidden, let Streamlit know and parent wrapper update
|
| 1175 |
-
function closeActions() {{
|
| 1176 |
-
try {{
|
| 1177 |
-
blurActive();
|
| 1178 |
-
window.parent.postMessage({{
|
| 1179 |
-
type: 'streamlit:setComponentValue',
|
| 1180 |
-
key: 'show_gameover_overlay',
|
| 1181 |
-
value: false
|
| 1182 |
-
}}, '*');
|
| 1183 |
-
}} catch (e) {{}}
|
| 1184 |
-
notifyParent(false);
|
| 1185 |
-
}}
|
| 1186 |
-
modalEl.addEventListener('hidden.bs.modal', closeActions);
|
| 1187 |
-
|
| 1188 |
-
// Close if backdrop clicked (click outside modal content)
|
| 1189 |
-
modalEl.addEventListener('click', function(ev) {{
|
| 1190 |
-
if (ev.target === modalEl) {{
|
| 1191 |
-
try {{ m.hide(); }} catch (e) {{}}
|
| 1192 |
-
}}
|
| 1193 |
-
}});
|
| 1194 |
-
|
| 1195 |
-
// Note: Streamlit components use an iframe sandbox that may include allow-scripts and allow-same-origin.
|
| 1196 |
-
// Browsers may warn that this combination allows escape of sandboxing; this is expected for Streamlit components.
|
| 1197 |
-
}})();
|
| 1198 |
-
</script>\n
|
| 1199 |
-
"""
|
| 1200 |
-
|
| 1201 |
-
# Mount the iframe; keep zero height when hidden, parent bridge will hide wrapper too
|
| 1202 |
-
components.html(bootstrap_html, height=(720 if visible else 0), scrolling=False, width=720)
|
| 1203 |
-
|
| 1204 |
-
# Ensure parent bridge exists to control wrapper visibility/position
|
| 1205 |
-
_inject_parent_modal_bridge()
|
| 1206 |
|
| 1207 |
def _sort_wordlist(filename):
|
| 1208 |
import time # Add this import
|
|
|
|
| 151 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto;}
|
| 152 |
|
| 153 |
div[data-testid="column"], .st-emotion-cache-zh2fnc { width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important; }
|
| 154 |
+
.st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.15rem !important; min-height: 2.5rem; min-width: 2.5rem;}
|
| 155 |
|
| 156 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 157 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { flex: 0 0 auto !important; }
|
|
|
|
| 905 |
# Render as a circular radio group, side-by-side
|
| 906 |
st.markdown(
|
| 907 |
f"""
|
| 908 |
+
<div class=\"bw-radio-group\" role=\"radiogroup\" aria-label=\"Hit or Miss\">
|
| 909 |
+
<div class=\"bw-radio-item\">
|
| 910 |
+
<div class=\"bw-radio-circle {'active hit' if is_hit else ''}\" role=\"radio\" aria-checked=\"{'true' if is_hit else 'false'}\" aria-label=\"Hit\">
|
| 911 |
+
<span class=\"dot\"></span>
|
| 912 |
</div>
|
| 913 |
+
<div class=\"bw-radio-caption\">HIT</div>
|
| 914 |
</div>
|
| 915 |
+
<div class=\"bw-radio-item\">
|
| 916 |
+
<div class=\"bw-radio-circle {'active miss' if is_miss else ''}\" role=\"radio\" aria-checked=\"{'true' if is_miss else 'false'}\" aria-label=\"Miss\">
|
| 917 |
+
<span class=\"dot\"></span>
|
| 918 |
</div>
|
| 919 |
+
<div class=\"bw-radio-caption\">MISS</div>
|
| 920 |
</div>
|
| 921 |
</div>
|
| 922 |
""",
|
|
|
|
| 941 |
|
| 942 |
st.markdown(
|
| 943 |
f"""
|
| 944 |
+
<div class=\"bw-radio-group\" role=\"radiogroup\" aria-label=\"Correct or Try Again\">
|
| 945 |
+
<div class=\"bw-radio-item\">
|
| 946 |
+
<div class=\"bw-radio-circle {'active hit' if is_correct else ''}\" role=\"radio\" aria-checked=\"{'true' if is_correct else 'false'}\" aria-label=\"Correct\">
|
| 947 |
+
<span class=\"dot\"></span>
|
| 948 |
</div>
|
| 949 |
+
<div class=\"bw-radio-caption{' inactive' if not is_correct else ''}\">CORRECT!</div>
|
| 950 |
</div>
|
| 951 |
+
<div class=\"bw-radio-item\">
|
| 952 |
+
<div class=\"bw-radio-circle {'active miss' if is_try_again else ''}\" role=\"radio\" aria-checked=\"{'true' if is_try_again else 'false'}\" aria-label=\"Try Again\">
|
| 953 |
+
<span class=\"dot\"></span>
|
| 954 |
</div>
|
| 955 |
+
<div class=\"bw-radio-caption{' inactive' if not is_try_again else ''}\">TRY AGAIN</div>
|
| 956 |
</div>
|
| 957 |
</div>
|
| 958 |
""",
|
|
|
|
| 1013 |
)
|
| 1014 |
st.markdown(f"<div class='bw-score-panel-container'>{table_html}</div>", unsafe_allow_html=True)
|
| 1015 |
|
| 1016 |
+
# -------------------- Game Over Dialog --------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
|
| 1018 |
+
def _game_over_content(state: GameState) -> None:
|
| 1019 |
+
# Build table body HTML for dialog content
|
| 1020 |
word_rows = []
|
| 1021 |
for w in state.puzzle.words:
|
| 1022 |
pts = st.session_state.points_by_word.get(w.text, 0)
|
|
|
|
| 1024 |
word_rows.append(
|
| 1025 |
f"<tr><td class=\"blue-background\">{w.text}</td><td class=\"blue-background\">{len(w.text)}</td><td class=\"blue-background\">{extra_pts}</td></tr>"
|
| 1026 |
)
|
| 1027 |
+
|
| 1028 |
table_html = (
|
| 1029 |
+
"<table class=\"shiny-border\" style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing:0;\">"
|
| 1030 |
"<thead><tr>"
|
| 1031 |
"<th scope=\"col\">Word</th>"
|
| 1032 |
"<th scope=\"col\">Letters</th>"
|
|
|
|
| 1038 |
"</table>"
|
| 1039 |
)
|
| 1040 |
|
| 1041 |
+
# Optional extra styles for this dialog content
|
| 1042 |
+
st.markdown(
|
| 1043 |
+
"""
|
| 1044 |
+
<style>
|
| 1045 |
+
.bw-dialog-container {
|
| 1046 |
+
border-radius: 1rem;
|
| 1047 |
+
box-shadow: 0 0 32px #1d64c8;
|
| 1048 |
+
background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666);
|
| 1049 |
+
color: #fff;
|
| 1050 |
+
padding: 16px;
|
| 1051 |
+
}
|
| 1052 |
+
.bw-dialog-header { display:flex; justify-content: space-between; align-items:center; }
|
| 1053 |
+
.bw-dialog-title, .st-emotion-cache-11elpad p { margin: 0; font-weight: bold;font-size: 1.5rem;text-align: center;flex: auto;filter:drop-shadow(1px 1px 2px #003);}
|
| 1054 |
+
.text-success { color: #20d46c;font-weight: bold;font-size: 1.5rem;text-align: center;flex: auto;filter:drop-shadow(1px 1px 2px #003); }
|
| 1055 |
+
.st-key-new_game_btn_dialog, .st-key-close_game_over { width: 50% !important;
|
| 1056 |
+
height: auto;
|
| 1057 |
+
min-width: unset !important;
|
| 1058 |
+
margin: 25px auto 0;
|
| 1059 |
+
}
|
| 1060 |
+
.st-key-new_game_btn_dialog button, .st-key-close_game_over button {
|
| 1061 |
+
height: 50px !important;
|
| 1062 |
+
}
|
| 1063 |
+
.st-key-new_game_btn_dialog:hover, .st-key-close_game_over:hover{
|
| 1064 |
+
/*background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666);*/
|
| 1065 |
+
background: #1d64c8 !important;
|
| 1066 |
+
/*filter:drop-shadow(1px 1px 2px #003);*/
|
| 1067 |
+
/*filter: invert(1);*/
|
| 1068 |
+
}
|
| 1069 |
+
.st-bb {background-color: rgba(29, 100, 200, 0.5);}
|
| 1070 |
+
</style>
|
| 1071 |
+
""",
|
| 1072 |
+
unsafe_allow_html=True,
|
| 1073 |
+
)
|
| 1074 |
+
|
| 1075 |
+
st.markdown(
|
| 1076 |
+
f"""
|
| 1077 |
+
<div class=\"bw-dialog-container shiny-border\">
|
| 1078 |
+
<div class=\"bw-dialog-header\">
|
| 1079 |
+
<h5 class=\"bw-dialog-title\">Game Over</h5>
|
| 1080 |
+
</div>
|
| 1081 |
+
<div class=\"p-3 pt-2\">\n <div class=\"mb-2\">Congratulations!</div>
|
| 1082 |
+
<div class=\"mb-2\">Final score: <strong class=\"text-success\">{state.score}</strong></div>
|
| 1083 |
+
<div class=\"mb-2\">Tier: <strong>{compute_tier(state.score)}</strong></div>
|
| 1084 |
+
<div class=\"mb-2\">Game Mode: <strong>{state.game_mode}</strong></div>
|
| 1085 |
+
<div class=\"mb-2\">Wordlist: <strong>{st.session_state.get('selected_wordlist', '')}</strong></div>
|
| 1086 |
+
<div class=\"mb-0\">{table_html}</div>
|
| 1087 |
+
</div>
|
| 1088 |
+
</div>
|
| 1089 |
+
""",
|
| 1090 |
+
unsafe_allow_html=True,
|
| 1091 |
+
)
|
| 1092 |
+
|
| 1093 |
+
# Dialog actions
|
| 1094 |
+
cols = st.columns([1, 1])
|
| 1095 |
+
with cols[0]:
|
| 1096 |
+
if st.button("Close", key="close_game_over"):
|
| 1097 |
+
st.session_state["show_gameover_overlay"] = False
|
| 1098 |
+
st.rerun()
|
| 1099 |
+
with cols[1]:
|
| 1100 |
+
st.button("New Game", key="new_game_btn_dialog", on_click=_new_game)
|
| 1101 |
+
|
| 1102 |
+
# Prefer st.dialog/experimental_dialog; fallback to st.modal if unavailable
|
| 1103 |
+
_Dialog = getattr(st, "dialog", getattr(st, "experimental_dialog", None))
|
| 1104 |
+
if _Dialog:
|
| 1105 |
+
@_Dialog("Game Over")
|
| 1106 |
+
def _game_over_dialog(state: GameState):
|
| 1107 |
+
_game_over_content(state)
|
| 1108 |
+
else:
|
| 1109 |
+
def _game_over_dialog(state: GameState):
|
| 1110 |
+
modal_ctx = getattr(st, "modal", None)
|
| 1111 |
+
if callable(modal_ctx):
|
| 1112 |
+
with modal_ctx("Game Over"):
|
| 1113 |
+
_game_over_content(state)
|
| 1114 |
+
else:
|
| 1115 |
+
# Last-resort inline render
|
| 1116 |
+
st.subheader("Game Over")
|
| 1117 |
+
_game_over_content(state)
|
| 1118 |
+
|
| 1119 |
+
def _render_game_over(state: GameState):
|
| 1120 |
+
# Determine visibility
|
| 1121 |
+
visible = bool(st.session_state.get("show_gameover_overlay", True)) and is_game_over(state)
|
| 1122 |
+
if visible:
|
| 1123 |
+
_game_over_dialog(state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1124 |
|
| 1125 |
def _sort_wordlist(filename):
|
| 1126 |
import time # Add this import
|