Spaces:
Running
Running
Enhance gameplay and UI; add Playwright tests
Browse filesUpdated README.md to document version 0.2.9 changes, including a fix for sonar grid alignment and a new feature to auto-mark words as found when all letters are revealed. Updated __init__.py to version 0.2.9. Added `auto_mark_completed_words` function in logic.py and integrated it into ui.py. Refactored UI functions for improved layout stability and style consistency. Modified scope_blue.png (details not specified).
- .gitignore +2 -0
- README.md +4 -0
- battlewords/__init__.py +1 -1
- battlewords/assets/scope_blue.png +0 -0
- battlewords/logic.py +25 -1
- battlewords/ui.py +44 -63
.gitignore
CHANGED
|
@@ -488,3 +488,5 @@ secrets.*
|
|
| 488 |
/.vs
|
| 489 |
/battlewords/__pycache__/ui.cpython-311.pyc
|
| 490 |
/battlewords/__pycache__/__init__.cpython-311.pyc
|
|
|
|
|
|
|
|
|
| 488 |
/.vs
|
| 489 |
/battlewords/__pycache__/ui.cpython-311.pyc
|
| 490 |
/battlewords/__pycache__/__init__.cpython-311.pyc
|
| 491 |
+
/package.json
|
| 492 |
+
/package-lock.json
|
README.md
CHANGED
|
@@ -106,6 +106,10 @@ docker run -p 8501:8501 battlewords
|
|
| 106 |
|
| 107 |
## Changelog
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
-0.2.8
|
| 110 |
- Add 10 incorrect guess limit per game
|
| 111 |
|
|
|
|
| 106 |
|
| 107 |
## Changelog
|
| 108 |
|
| 109 |
+
-0.2.9
|
| 110 |
+
- fix sonar grid alignment issue on some browsers
|
| 111 |
+
- When all letters of a word are revealed, it is automatically marked as found.
|
| 112 |
+
|
| 113 |
-0.2.8
|
| 114 |
- Add 10 incorrect guess limit per game
|
| 115 |
|
battlewords/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui"]
|
|
|
|
| 1 |
+
__version__ = "0.2.9"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui"]
|
battlewords/assets/scope_blue.png
CHANGED
|
|
battlewords/logic.py
CHANGED
|
@@ -141,4 +141,28 @@ def compute_tier(score: int) -> str:
|
|
| 141 |
return "Great"
|
| 142 |
if 34 <= score <= 37:
|
| 143 |
return "Good"
|
| 144 |
-
return "Keep practicing"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
return "Great"
|
| 142 |
if 34 <= score <= 37:
|
| 143 |
return "Good"
|
| 144 |
+
return "Keep practicing"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def auto_mark_completed_words(state: GameState) -> bool:
|
| 148 |
+
"""Automatically mark words as found when all their letters are revealed.
|
| 149 |
+
|
| 150 |
+
Returns True if any word state changed (e.g., guessed/score/points).
|
| 151 |
+
Scoring in this case is base length only (no unrevealed bonus).
|
| 152 |
+
"""
|
| 153 |
+
changed = False
|
| 154 |
+
for w in state.puzzle.words:
|
| 155 |
+
if w.text in state.guessed:
|
| 156 |
+
continue
|
| 157 |
+
if all(c in state.revealed for c in w.cells):
|
| 158 |
+
# Award base points if not already assigned
|
| 159 |
+
if w.text not in state.points_by_word:
|
| 160 |
+
base_points = w.length
|
| 161 |
+
state.points_by_word[w.text] = base_points
|
| 162 |
+
state.score += base_points
|
| 163 |
+
state.guessed.add(w.text)
|
| 164 |
+
changed = True
|
| 165 |
+
if changed:
|
| 166 |
+
# Do not alter can_guess; just note the auto-complete
|
| 167 |
+
state.last_action = (state.last_action or "") + "\nAuto-complete: revealed word(s) marked as found."
|
| 168 |
+
return changed
|
battlewords/ui.py
CHANGED
|
@@ -14,7 +14,7 @@ import time
|
|
| 14 |
from datetime import datetime
|
| 15 |
|
| 16 |
from .generator import generate_puzzle, sort_word_file
|
| 17 |
-
from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier
|
| 18 |
from .models import Coord, GameState, Puzzle
|
| 19 |
from .word_loader import get_wordlist_files, load_word_list # use loader directly
|
| 20 |
from .version_info import versions_html # version info footer
|
|
@@ -491,7 +491,7 @@ def get_scope_image(uid: str, size=4, bgcolor="none", scope_color="green", img_n
|
|
| 491 |
return imgscope
|
| 492 |
|
| 493 |
def _create_radar_scope(size=4, bgcolor="none", scope_color="green"):
|
| 494 |
-
fig, ax = plt.subplots(figsize=(size, size), dpi=
|
| 495 |
ax.set_facecolor(bgcolor)
|
| 496 |
fig.patch.set_alpha(0.5)
|
| 497 |
ax.set_zorder(0)
|
|
@@ -523,10 +523,10 @@ def _create_radar_scope(size=4, bgcolor="none", scope_color="green"):
|
|
| 523 |
ax.plot([0, x], [0, y], color=scope_color, alpha=0.5, zorder=1)
|
| 524 |
|
| 525 |
# Set limits and remove axes
|
| 526 |
-
|
| 527 |
-
|
| 528 |
ax.set_aspect('equal', adjustable='box')
|
| 529 |
-
|
| 530 |
|
| 531 |
return fig, ax
|
| 532 |
|
|
@@ -548,7 +548,7 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
|
|
| 548 |
max_x = max(xs - 0.5) + r_max
|
| 549 |
min_y = min(ys - 0.5) - r_max # Note: ys are inverted in plot
|
| 550 |
max_y = max(ys - 0.5) + r_max
|
| 551 |
-
ring_linewidth =
|
| 552 |
|
| 553 |
rgba_labels = mcolors.to_rgba("#FFFFFF", 0.7)
|
| 554 |
rgba_ticks = mcolors.to_rgba("#FFFFFF", 0.66)
|
|
@@ -565,23 +565,13 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
|
|
| 565 |
|
| 566 |
# Use per-puzzle scope image keyed by puzzle.uid
|
| 567 |
imgscope = get_scope_image(uid=puzzle.uid, size=scope_size, bgcolor=bgcolor, scope_color=scope_color)
|
| 568 |
-
fig, ax = plt.subplots(figsize=(scope_size, scope_size))
|
| 569 |
-
fig.subplots_adjust(left=0, right=0.9, top=0.9, bottom=0)
|
| 570 |
-
fig.patch.set_alpha(0.0)
|
| 571 |
-
ax.set_position([0.02, 0.02, 0.98, 0.98])
|
| 572 |
-
ax.set_xlim(min_x, max_x)
|
| 573 |
-
ax.set_ylim(max_y, min_y) # Inverted for grid coordinates
|
| 574 |
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
ax.set_xticks([])
|
| 582 |
-
ax.set_yticks([])
|
| 583 |
-
|
| 584 |
-
ax.set_aspect('equal', adjustable='box')
|
| 585 |
|
| 586 |
def _make_linear_gradient(width: int, height: int, angle_deg: float,
|
| 587 |
colors_hex: list[str], stops: list[float]) -> np.ndarray:
|
|
@@ -605,7 +595,6 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
|
|
| 605 |
img = (1.0 - w) * c0 + w * c1
|
| 606 |
return img
|
| 607 |
|
| 608 |
-
fig_w, fig_h = [int(v) for v in fig.canvas.get_width_height()]
|
| 609 |
grad_img = _make_linear_gradient(
|
| 610 |
width=fig_w,
|
| 611 |
height=fig_h,
|
|
@@ -613,23 +602,37 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
|
|
| 613 |
colors_hex=['#a1a1a1', '#ffffff', '#a1a1a1', '#666666'],
|
| 614 |
stops=[0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0],
|
| 615 |
)
|
| 616 |
-
bg_ax = fig.add_axes([0, 0, 1, 1], zorder=0)
|
| 617 |
bg_ax.imshow(grad_img, aspect='auto', interpolation='bilinear')
|
| 618 |
bg_ax.axis('off')
|
| 619 |
|
| 620 |
-
|
| 621 |
-
|
| 622 |
scope_ax.imshow(imgscope, aspect='auto', interpolation='lanczos')
|
| 623 |
scope_ax.axis('off')
|
| 624 |
|
|
|
|
|
|
|
| 625 |
ax.set_facecolor('none')
|
| 626 |
-
ax.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
for spine in ax.spines.values():
|
| 628 |
spine.set_visible(False)
|
| 629 |
|
|
|
|
| 630 |
rings: list[Circle] = []
|
| 631 |
for x, y in zip(xs, ys):
|
| 632 |
-
ring = Circle((x
|
| 633 |
ax.add_patch(ring)
|
| 634 |
rings.append(ring)
|
| 635 |
|
|
@@ -664,7 +667,7 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
|
|
| 664 |
if cached_path and os.path.exists(cached_path) and cached_sig == gif_signature:
|
| 665 |
with open(cached_path, "rb") as f:
|
| 666 |
gif_bytes = f.read()
|
| 667 |
-
st.image(gif_bytes,
|
| 668 |
plt.close(fig)
|
| 669 |
return
|
| 670 |
|
|
@@ -677,7 +680,7 @@ def _render_radar(puzzle: Puzzle, size: int, r_max: float = 0.8, max_frames: int
|
|
| 677 |
gif_bytes = tmpfile.read()
|
| 678 |
st.session_state.radar_gif_path = tmpfile.name # Save path for reuse
|
| 679 |
st.session_state.radar_gif_signature = gif_signature # Save signature to detect changes
|
| 680 |
-
st.image(gif_bytes,
|
| 681 |
|
| 682 |
def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
|
| 683 |
size = state.grid_size
|
|
@@ -798,6 +801,11 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
|
|
| 798 |
|
| 799 |
if clicked is not None:
|
| 800 |
reveal_cell(state, letter_map, clicked)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
st.session_state.letter_map = build_letter_map(st.session_state.puzzle)
|
| 802 |
_sync_back(state)
|
| 803 |
|
|
@@ -838,20 +846,7 @@ def _render_hit_miss(state: GameState):
|
|
| 838 |
# Render as a circular radio group, side-by-side
|
| 839 |
st.markdown(
|
| 840 |
f"""
|
| 841 |
-
<div class=\"bw-radio-group\" role=\"radiogroup\" aria-label=\"Hit or Miss\">
|
| 842 |
-
<div class=\"bw-radio-item\">
|
| 843 |
-
<div class=\"bw-radio-circle {'active hit' if is_hit else ''}\" role=\"radio\" aria-checked=\"{'true' if is_hit else 'false'}\" aria-label=\"Hit\">
|
| 844 |
-
<span class=\"dot\"></span>
|
| 845 |
-
</div>
|
| 846 |
-
<div class=\"bw-radio-caption\">HIT</div>
|
| 847 |
-
</div>
|
| 848 |
-
<div class=\"bw-radio-item\">
|
| 849 |
-
<div class=\"bw-radio-circle {'active miss' if is_miss else ''}\" role=\"radio\" aria-checked=\"{'true' if is_miss else 'false'}\" aria-label=\"Miss\">
|
| 850 |
-
<span class=\"dot\"></span>
|
| 851 |
-
</div>
|
| 852 |
-
<div class=\"bw-radio-caption\">MISS</div>
|
| 853 |
-
</div>
|
| 854 |
-
</div>
|
| 855 |
""",
|
| 856 |
unsafe_allow_html=True,
|
| 857 |
)
|
|
@@ -874,20 +869,7 @@ def _render_correct_try_again(state: GameState):
|
|
| 874 |
|
| 875 |
st.markdown(
|
| 876 |
f"""
|
| 877 |
-
<div class=\"bw-radio-group\" role=\"radiogroup\" aria-label=\"Correct or Try Again\">
|
| 878 |
-
<div class=\"bw-radio-item\">
|
| 879 |
-
<div class=\"bw-radio-circle {'active hit' if is_correct else ''}\" role=\"radio\" aria-checked=\"{'true' if is_correct else 'false'}\" aria-label=\"Correct\">
|
| 880 |
-
<span class=\"dot\"></span>
|
| 881 |
-
</div>
|
| 882 |
-
<div class=\"bw-radio-caption{' inactive' if not is_correct else ''}\">CORRECT!</div>
|
| 883 |
-
</div>
|
| 884 |
-
<div class=\"bw-radio-item\">
|
| 885 |
-
<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\">
|
| 886 |
-
<span class=\"dot\"></span>
|
| 887 |
-
</div>
|
| 888 |
-
<div class=\"bw-radio-caption{' inactive' if not is_try_again else ''}\">TRY AGAIN</div>
|
| 889 |
-
</div>
|
| 890 |
-
</div>
|
| 891 |
""",
|
| 892 |
unsafe_allow_html=True,
|
| 893 |
)
|
|
@@ -1116,7 +1098,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1116 |
padding: 16px;
|
| 1117 |
}
|
| 1118 |
.bw-dialog-header { display:flex; justify-content: space-between; align-items:center; }
|
| 1119 |
-
.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);}
|
| 1120 |
.text-success { color: #20d46c;font-weight: bold;font-size: 1.5rem;text-align: center;flex: auto;filter:drop-shadow(1px 1px 2px #003); }
|
| 1121 |
.st-key-new_game_btn_dialog, .st-key-close_game_over { width: 50% !important;
|
| 1122 |
height: auto;
|
|
@@ -1132,7 +1114,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1132 |
/*filter:drop-shadow(1px 1px 2px #003);*/
|
| 1133 |
/*filter: invert(1);*/
|
| 1134 |
}
|
| 1135 |
-
.st-bb {background-color: rgba(29, 100, 200, 0.5);}
|
| 1136 |
</style>
|
| 1137 |
""",
|
| 1138 |
unsafe_allow_html=True,
|
|
@@ -1140,8 +1122,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1140 |
|
| 1141 |
st.markdown(
|
| 1142 |
f"""
|
| 1143 |
-
<div class=\"bw-dialog-container shiny-border\">
|
| 1144 |
-
<div class=\"p-3 pt-2\">\n <div class=\"mb-2\">Congratulations!</div>
|
| 1145 |
<div class=\"mb-2\">Final score: <strong class=\"text-success\">{state.score}</strong></div>
|
| 1146 |
<div class=\"mb-2\">Time: <strong>{timer_str}</strong></div>
|
| 1147 |
<div class=\"mb-2\">Tier: <strong>{compute_tier(state.score)}</strong></div>
|
|
@@ -1243,7 +1224,7 @@ def run_app():
|
|
| 1243 |
_render_grid(state, st.session_state.letter_map, show_grid_ticks=st.session_state.get("show_grid_ticks", True))
|
| 1244 |
st.button("New Game", width=125, on_click=_new_game, key="new_game_btn")
|
| 1245 |
|
| 1246 |
-
# End condition (only show overlay if
|
| 1247 |
state = _to_state()
|
| 1248 |
if is_game_over(state) and not st.session_state.get("hide_gameover_overlay", False):
|
| 1249 |
_render_game_over(state)
|
|
|
|
| 14 |
from datetime import datetime
|
| 15 |
|
| 16 |
from .generator import generate_puzzle, sort_word_file
|
| 17 |
+
from .logic import build_letter_map, reveal_cell, guess_word, is_game_over, compute_tier, auto_mark_completed_words
|
| 18 |
from .models import Coord, GameState, Puzzle
|
| 19 |
from .word_loader import get_wordlist_files, load_word_list # use loader directly
|
| 20 |
from .version_info import versions_html # version info footer
|
|
|
|
| 491 |
return imgscope
|
| 492 |
|
| 493 |
def _create_radar_scope(size=4, bgcolor="none", scope_color="green"):
|
| 494 |
+
fig, ax = plt.subplots(figsize=(size, size), dpi=144)
|
| 495 |
ax.set_facecolor(bgcolor)
|
| 496 |
fig.patch.set_alpha(0.5)
|
| 497 |
ax.set_zorder(0)
|
|
|
|
| 523 |
ax.plot([0, x], [0, y], color=scope_color, alpha=0.5, zorder=1)
|
| 524 |
|
| 525 |
# Set limits and remove axes
|
| 526 |
+
ax.set_xlim(-0.5, 0.5)
|
| 527 |
+
ax.set_ylim(-0.5, 0.5)
|
| 528 |
ax.set_aspect('equal', adjustable='box')
|
| 529 |
+
ax.axis('off')
|
| 530 |
|
| 531 |
return fig, ax
|
| 532 |
|
|
|
|
| 548 |
max_x = max(xs - 0.5) + r_max
|
| 549 |
min_y = min(ys - 0.5) - r_max # Note: ys are inverted in plot
|
| 550 |
max_y = max(ys - 0.5) + r_max
|
| 551 |
+
ring_linewidth = 3
|
| 552 |
|
| 553 |
rgba_labels = mcolors.to_rgba("#FFFFFF", 0.7)
|
| 554 |
rgba_ticks = mcolors.to_rgba("#FFFFFF", 0.66)
|
|
|
|
| 565 |
|
| 566 |
# Use per-puzzle scope image keyed by puzzle.uid
|
| 567 |
imgscope = get_scope_image(uid=puzzle.uid, size=scope_size, bgcolor=bgcolor, scope_color=scope_color)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
|
| 569 |
+
# Build figure with explicit axes occupying full canvas to avoid browser-specific padding
|
| 570 |
+
fig = plt.figure(figsize=(scope_size, scope_size), dpi=144)
|
| 571 |
+
# Background gradient covering full figure
|
| 572 |
+
bg_ax = fig.add_axes([0, 0, 1, 1], zorder=0)
|
| 573 |
+
fig.canvas.draw() # ensure size
|
| 574 |
+
fig_w, fig_h = [int(v) for v in fig.canvas.get_width_height()]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
|
| 576 |
def _make_linear_gradient(width: int, height: int, angle_deg: float,
|
| 577 |
colors_hex: list[str], stops: list[float]) -> np.ndarray:
|
|
|
|
| 595 |
img = (1.0 - w) * c0 + w * c1
|
| 596 |
return img
|
| 597 |
|
|
|
|
| 598 |
grad_img = _make_linear_gradient(
|
| 599 |
width=fig_w,
|
| 600 |
height=fig_h,
|
|
|
|
| 602 |
colors_hex=['#a1a1a1', '#ffffff', '#a1a1a1', '#666666'],
|
| 603 |
stops=[0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0],
|
| 604 |
)
|
|
|
|
| 605 |
bg_ax.imshow(grad_img, aspect='auto', interpolation='bilinear')
|
| 606 |
bg_ax.axis('off')
|
| 607 |
|
| 608 |
+
# Decorative scope image as overlay (does not affect coordinates)
|
| 609 |
+
scope_ax = fig.add_axes([0, 0, 1, 1], zorder=1)
|
| 610 |
scope_ax.imshow(imgscope, aspect='auto', interpolation='lanczos')
|
| 611 |
scope_ax.axis('off')
|
| 612 |
|
| 613 |
+
# Main axes for rings and ticks with fixed limits to stabilize layout across browsers
|
| 614 |
+
ax = fig.add_axes([0, 0, 1, 1], zorder=2)
|
| 615 |
ax.set_facecolor('none')
|
| 616 |
+
ax.set_xlim(0.5, size + 0.5)
|
| 617 |
+
ax.set_ylim(size + 0.5, 0.5) # Inverted for grid coordinates
|
| 618 |
+
|
| 619 |
+
if show_ticks:
|
| 620 |
+
ax.set_xticks(range(1, size + 1))
|
| 621 |
+
ax.set_yticks(range(1, size + 1))
|
| 622 |
+
ax.tick_params(axis="both", which="both", labelcolor=rgba_labels)
|
| 623 |
+
ax.tick_params(axis="both", which="both", colors=rgba_ticks)
|
| 624 |
+
else:
|
| 625 |
+
ax.set_xticks([])
|
| 626 |
+
ax.set_yticks([])
|
| 627 |
+
|
| 628 |
+
ax.set_aspect('equal', adjustable='box')
|
| 629 |
for spine in ax.spines.values():
|
| 630 |
spine.set_visible(False)
|
| 631 |
|
| 632 |
+
# Build rings centered on exact cell centers (integer coords)
|
| 633 |
rings: list[Circle] = []
|
| 634 |
for x, y in zip(xs, ys):
|
| 635 |
+
ring = Circle((x, y), radius=r_min, fill=False, edgecolor='#9ceffe', linewidth=ring_linewidth, alpha=1.0, zorder=3)
|
| 636 |
ax.add_patch(ring)
|
| 637 |
rings.append(ring)
|
| 638 |
|
|
|
|
| 667 |
if cached_path and os.path.exists(cached_path) and cached_sig == gif_signature:
|
| 668 |
with open(cached_path, "rb") as f:
|
| 669 |
gif_bytes = f.read()
|
| 670 |
+
st.image(gif_bytes, use_container_width=True)
|
| 671 |
plt.close(fig)
|
| 672 |
return
|
| 673 |
|
|
|
|
| 680 |
gif_bytes = tmpfile.read()
|
| 681 |
st.session_state.radar_gif_path = tmpfile.name # Save path for reuse
|
| 682 |
st.session_state.radar_gif_signature = gif_signature # Save signature to detect changes
|
| 683 |
+
st.image(gif_bytes, use_container_width=True)
|
| 684 |
|
| 685 |
def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
|
| 686 |
size = state.grid_size
|
|
|
|
| 801 |
|
| 802 |
if clicked is not None:
|
| 803 |
reveal_cell(state, letter_map, clicked)
|
| 804 |
+
# Auto-mark and award base points for any fully revealed words
|
| 805 |
+
if auto_mark_completed_words(state):
|
| 806 |
+
# Invalidate radar GIF cache to hide completed rings
|
| 807 |
+
st.session_state.radar_gif_path = None
|
| 808 |
+
st.session_state.radar_gif_signature = None
|
| 809 |
st.session_state.letter_map = build_letter_map(st.session_state.puzzle)
|
| 810 |
_sync_back(state)
|
| 811 |
|
|
|
|
| 846 |
# Render as a circular radio group, side-by-side
|
| 847 |
st.markdown(
|
| 848 |
f"""
|
| 849 |
+
<div class=\"bw-radio-group\" role=\"radiogroup\" aria-label=\"Hit or Miss\">\n <div class=\"bw-radio-item\">\n <div class=\"bw-radio-circle {'active hit' if is_hit else ''}\" role=\"radio\" aria-checked=\"{'true' if is_hit else 'false'}\" aria-label=\"Hit\">\n <span class=\"dot\"></span>\n </div>\n <div class=\"bw-radio-caption\">HIT</div>\n </div>\n <div class=\"bw-radio-item\">\n <div class=\"bw-radio-circle {'active miss' if is_miss else ''}\" role=\"radio\" aria-checked=\"{'true' if is_miss else 'false'}\" aria-label=\"Miss\">\n <span class=\"dot\"></span>\n </div>\n <div class=\"bw-radio-caption\">MISS</div>\n </div>\n </div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
""",
|
| 851 |
unsafe_allow_html=True,
|
| 852 |
)
|
|
|
|
| 869 |
|
| 870 |
st.markdown(
|
| 871 |
f"""
|
| 872 |
+
<div class=\"bw-radio-group\" role=\"radiogroup\" aria-label=\"Correct or Try Again\">\n <div class=\"bw-radio-item\">\n <div class=\"bw-radio-circle {'active hit' if is_correct else ''}\" role=\"radio\" aria-checked=\"{'true' if is_correct else 'false'}\" aria-label=\"Correct\">\n <span class=\"dot\"></span>\n </div>\n <div class=\"bw-radio-caption{' inactive' if not is_correct else ''}\">CORRECT!</div>\n </div>\n <div class=\"bw-radio-item\">\n <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\">\n <span class=\"dot\"></span>\n </div>\n <div class=\"bw-radio-caption{' inactive' if not is_try_again else ''}\">TRY AGAIN</div>\n </div>\n </div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 873 |
""",
|
| 874 |
unsafe_allow_html=True,
|
| 875 |
)
|
|
|
|
| 1098 |
padding: 16px;
|
| 1099 |
}
|
| 1100 |
.bw-dialog-header { display:flex; justify-content: space-between; align-items:center; }
|
| 1101 |
+
.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);}
|
| 1102 |
.text-success { color: #20d46c;font-weight: bold;font-size: 1.5rem;text-align: center;flex: auto;filter:drop-shadow(1px 1px 2px #003); }
|
| 1103 |
.st-key-new_game_btn_dialog, .st-key-close_game_over { width: 50% !important;
|
| 1104 |
height: auto;
|
|
|
|
| 1114 |
/*filter:drop-shadow(1px 1px 2px #003);*/
|
| 1115 |
/*filter: invert(1);*/
|
| 1116 |
}
|
| 1117 |
+
.st-bb {background-color: rgba(29, 100, 200, 0.5);}
|
| 1118 |
</style>
|
| 1119 |
""",
|
| 1120 |
unsafe_allow_html=True,
|
|
|
|
| 1122 |
|
| 1123 |
st.markdown(
|
| 1124 |
f"""
|
| 1125 |
+
<div class=\"bw-dialog-container shiny-border\">\n <div class=\"p-3 pt-2\">\n <div class=\"mb-2\">Congratulations!</div>
|
|
|
|
| 1126 |
<div class=\"mb-2\">Final score: <strong class=\"text-success\">{state.score}</strong></div>
|
| 1127 |
<div class=\"mb-2\">Time: <strong>{timer_str}</strong></div>
|
| 1128 |
<div class=\"mb-2\">Tier: <strong>{compute_tier(state.score)}</strong></div>
|
|
|
|
| 1224 |
_render_grid(state, st.session_state.letter_map, show_grid_ticks=st.session_state.get("show_grid_ticks", True))
|
| 1225 |
st.button("New Game", width=125, on_click=_new_game, key="new_game_btn")
|
| 1226 |
|
| 1227 |
+
# End condition (only show overlay if dismissed)
|
| 1228 |
state = _to_state()
|
| 1229 |
if is_game_over(state) and not st.session_state.get("hide_gameover_overlay", False):
|
| 1230 |
_render_game_over(state)
|