Spaces:
Running
v0.2.37 - Improve UI/UX and add dynamic game state handling
Browse filesUpdated the version in `__init__.py` to `0.2.37`. Added a new
`new_game` session state flag in `_new_game` to track game
restarts. Enhanced `_game_over_content` with dynamic
`subscriber_html` for displaying game mode, wordlist, and
subscription details. Updated dialog titles to "Congratulations!"
for a more engaging experience.
Modified `run_app` to adjust fade-in duration and improve session
state handling. Refactored `_render_game_tab` to reload the page
on "New Game" button click using JavaScript. Fixed fade-in
transitions in `start_root_fade_in`.
Enhanced `inject_styles` with hover effects, consistent colors,
and faster transitions. Improved overall UI responsiveness,
visual consistency, and user experience. Minor bug fixes and
optimizations included.
- README.md +11 -2
- battlewords/__init__.py +1 -1
- battlewords/ui.py +29 -18
- battlewords/ui_helpers.py +14 -5
- claude.md +2 -18
- pyproject.toml +1 -1
- specs/requirements.mdx +3 -8
- specs/specs.mdx +3 -8
|
@@ -19,12 +19,17 @@ tags:
|
|
| 19 |
|
| 20 |
> **This project is used by [huggingface.co](https://huggingface.co/spaces/Surn/BattleWords) as a demonstration of interactive word games in Python.**
|
| 21 |
|
| 22 |
-
**Current Version:** 0.2.
|
| 23 |
-
**Last Updated:** 2026-02-
|
| 24 |
|
| 25 |
BattleWords is a vocabulary learning game inspired by classic Battleship mechanics. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
| 26 |
|
| 27 |
## Recent Changes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
- version 0.2.36
|
| 29 |
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 30 |
- Expanded `game_mode` options (includes `easy`)
|
|
@@ -208,6 +213,10 @@ CRYPTO_PK= # Reserved for future signing
|
|
| 208 |
- Local persistent storage for personal game history (offline-capable)
|
| 209 |
- Personal high scores sidebar with filtering
|
| 210 |
- Player statistics tracking (games played, averages, bests)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
- version 0.2.36
|
| 213 |
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
|
|
|
| 19 |
|
| 20 |
> **This project is used by [huggingface.co](https://huggingface.co/spaces/Surn/BattleWords) as a demonstration of interactive word games in Python.**
|
| 21 |
|
| 22 |
+
**Current Version:** 0.2.37
|
| 23 |
+
**Last Updated:** 2026-02-10
|
| 24 |
|
| 25 |
BattleWords is a vocabulary learning game inspired by classic Battleship mechanics. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
| 26 |
|
| 27 |
## Recent Changes
|
| 28 |
+
- version 0.2.37
|
| 29 |
+
- Footer navigation uses query-parameter links (`?page=play|leaderboard|settings`) via custom footer links
|
| 30 |
+
- In-game "New Game" button behavior is now navigation-style (rerun) rather than a full session reset
|
| 31 |
+
- Game over flow continues to support integrated challenge submission and optional share-link visibility
|
| 32 |
+
|
| 33 |
- version 0.2.36
|
| 34 |
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 35 |
- Expanded `game_mode` options (includes `easy`)
|
|
|
|
| 213 |
- Local persistent storage for personal game history (offline-capable)
|
| 214 |
- Personal high scores sidebar with filtering
|
| 215 |
- Player statistics tracking (games played, averages, bests)
|
| 216 |
+
|
| 217 |
+
- version 0.2.37
|
| 218 |
+
- Footer navigation uses query-parameter links (Play/Leaderboard/Settings)
|
| 219 |
+
- Play navigation behaves like a route change (query param) rather than invoking the New Game reset callback
|
| 220 |
|
| 221 |
- version 0.2.36
|
| 222 |
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage", "modules"]
|
|
|
|
| 1 |
+
__version__ = "0.2.37"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage", "modules"]
|
|
@@ -205,6 +205,9 @@ def _new_game() -> None:
|
|
| 205 |
st.session_state.end_time = None
|
| 206 |
st.session_state.incorrect_guesses = [] # Clear incorrect guesses for new game
|
| 207 |
_init_session()
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
def _to_state() -> GameState:
|
|
@@ -1189,17 +1192,19 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1189 |
""",
|
| 1190 |
unsafe_allow_html=True,
|
| 1191 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
|
| 1193 |
st.markdown(
|
| 1194 |
f"""
|
| 1195 |
<div class="bw-dialog-container shiny-border">
|
| 1196 |
-
<div class="p-3 pt-2">
|
| 1197 |
-
<div class="mb-2">
|
| 1198 |
-
<div class="mb-2">Final score: <strong class="text-success">{state.score}</strong></div>
|
| 1199 |
-
<div class="mb-2">Time: <strong>{timer_str}</strong></div>
|
| 1200 |
<div class="mb-2">Tier: <strong>{compute_tier(state.score)}</strong></div>
|
| 1201 |
-
|
| 1202 |
-
<div class="mb-2">Wordlist: <strong>{st.session_state.get('selected_wordlist', '')}</strong></div>
|
| 1203 |
<div class="mb-0">{table_html}</div>
|
| 1204 |
</div>
|
| 1205 |
</div>
|
|
@@ -1395,18 +1400,18 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1395 |
# Prefer st.dialog/experimental_dialog; fallback to st.modal if unavailable
|
| 1396 |
_Dialog = getattr(st, "dialog", getattr(st, "experimental_dialog", None))
|
| 1397 |
if _Dialog:
|
| 1398 |
-
@_Dialog("
|
| 1399 |
def _game_over_dialog(state: GameState):
|
| 1400 |
_game_over_content(state)
|
| 1401 |
else:
|
| 1402 |
def _game_over_dialog(state: GameState):
|
| 1403 |
modal_ctx = getattr(st, "modal", None)
|
| 1404 |
if callable(modal_ctx):
|
| 1405 |
-
with modal_ctx("
|
| 1406 |
_game_over_content(state)
|
| 1407 |
else:
|
| 1408 |
# Last-resort inline render
|
| 1409 |
-
st.subheader("
|
| 1410 |
_game_over_content(state)
|
| 1411 |
|
| 1412 |
def _render_game_over(state: GameState):
|
|
@@ -1479,7 +1484,7 @@ def _on_game_option_change() -> None:
|
|
| 1479 |
_new_game()
|
| 1480 |
|
| 1481 |
def run_app():
|
| 1482 |
-
start_root_fade_in(0.
|
| 1483 |
# Render PWA service worker registration (meta tags in <head> via Docker)
|
| 1484 |
# Streamlit reruns the script frequently; inject this only once per session.
|
| 1485 |
if not st.session_state.get("pwa_injected", False):
|
|
@@ -1518,7 +1523,7 @@ def run_app():
|
|
| 1518 |
spinner_placeholder = st.empty()
|
| 1519 |
with CustomSpinner(spinner_placeholder, "Initial Load..."):
|
| 1520 |
st.session_state["initial_page_loaded"] = True
|
| 1521 |
-
st.session_state["last_nav_page"] = page
|
| 1522 |
# st.rerun()
|
| 1523 |
|
| 1524 |
# Show spinner when navigating via menu (query param page change)
|
|
@@ -1617,7 +1622,7 @@ def _render_game_tab():
|
|
| 1617 |
"""
|
| 1618 |
Render the main game tab layout.
|
| 1619 |
"""
|
| 1620 |
-
# start_root_fade_in(0.
|
| 1621 |
_init_session()
|
| 1622 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 1623 |
#inject_ocean_layers() # <-- add the animated layers
|
|
@@ -1644,11 +1649,17 @@ def _render_game_tab():
|
|
| 1644 |
_render_guess_form(state)
|
| 1645 |
_render_score_panel(state)
|
| 1646 |
with left:
|
| 1647 |
-
st.button("New Game", width=125, on_click=_new_game, key="new_game_btn")
|
| 1648 |
-
|
| 1649 |
-
|
| 1650 |
-
|
| 1651 |
-
|
| 1652 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1653 |
|
| 1654 |
# finish_root_fade_in(0.3)
|
|
|
|
| 205 |
st.session_state.end_time = None
|
| 206 |
st.session_state.incorrect_guesses = [] # Clear incorrect guesses for new game
|
| 207 |
_init_session()
|
| 208 |
+
st.session_state.new_game = True # Flag to indicate a new game was started
|
| 209 |
+
|
| 210 |
+
|
| 211 |
|
| 212 |
|
| 213 |
def _to_state() -> GameState:
|
|
|
|
| 1192 |
""",
|
| 1193 |
unsafe_allow_html=True,
|
| 1194 |
)
|
| 1195 |
+
subscriber_html = (
|
| 1196 |
+
f'<div class=\"mb-2\">Time: <strong>{timer_str}</strong></div>'
|
| 1197 |
+
f'<div class=\"mb-2\">Game Mode: <strong>{state.game_mode}</strong></div>'
|
| 1198 |
+
f'<div class=\"mb-2\">Wordlist: <strong>{st.session_state.get('selected_wordlist', '')}</strong></div>'
|
| 1199 |
+
if st.session_state.get("subscription_level", 0) > 0 else "<div class=\"mb-2\"> </div>")
|
| 1200 |
|
| 1201 |
st.markdown(
|
| 1202 |
f"""
|
| 1203 |
<div class="bw-dialog-container shiny-border">
|
| 1204 |
+
<div class="p-3 pt-2">
|
| 1205 |
+
<div class="mb-2">Final score: <strong class="text-success">{state.score}</strong></div>
|
|
|
|
|
|
|
| 1206 |
<div class="mb-2">Tier: <strong>{compute_tier(state.score)}</strong></div>
|
| 1207 |
+
{subscriber_html}
|
|
|
|
| 1208 |
<div class="mb-0">{table_html}</div>
|
| 1209 |
</div>
|
| 1210 |
</div>
|
|
|
|
| 1400 |
# Prefer st.dialog/experimental_dialog; fallback to st.modal if unavailable
|
| 1401 |
_Dialog = getattr(st, "dialog", getattr(st, "experimental_dialog", None))
|
| 1402 |
if _Dialog:
|
| 1403 |
+
@_Dialog("Congratulations!")
|
| 1404 |
def _game_over_dialog(state: GameState):
|
| 1405 |
_game_over_content(state)
|
| 1406 |
else:
|
| 1407 |
def _game_over_dialog(state: GameState):
|
| 1408 |
modal_ctx = getattr(st, "modal", None)
|
| 1409 |
if callable(modal_ctx):
|
| 1410 |
+
with modal_ctx("Congratulations!"):
|
| 1411 |
_game_over_content(state)
|
| 1412 |
else:
|
| 1413 |
# Last-resort inline render
|
| 1414 |
+
st.subheader("Congratulations!")
|
| 1415 |
_game_over_content(state)
|
| 1416 |
|
| 1417 |
def _render_game_over(state: GameState):
|
|
|
|
| 1484 |
_new_game()
|
| 1485 |
|
| 1486 |
def run_app():
|
| 1487 |
+
start_root_fade_in(0.0)
|
| 1488 |
# Render PWA service worker registration (meta tags in <head> via Docker)
|
| 1489 |
# Streamlit reruns the script frequently; inject this only once per session.
|
| 1490 |
if not st.session_state.get("pwa_injected", False):
|
|
|
|
| 1523 |
spinner_placeholder = st.empty()
|
| 1524 |
with CustomSpinner(spinner_placeholder, "Initial Load..."):
|
| 1525 |
st.session_state["initial_page_loaded"] = True
|
| 1526 |
+
st.session_state["last_nav_page"] = page
|
| 1527 |
# st.rerun()
|
| 1528 |
|
| 1529 |
# Show spinner when navigating via menu (query param page change)
|
|
|
|
| 1622 |
"""
|
| 1623 |
Render the main game tab layout.
|
| 1624 |
"""
|
| 1625 |
+
# start_root_fade_in(0.5)
|
| 1626 |
_init_session()
|
| 1627 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 1628 |
#inject_ocean_layers() # <-- add the animated layers
|
|
|
|
| 1649 |
_render_guess_form(state)
|
| 1650 |
_render_score_panel(state)
|
| 1651 |
with left:
|
| 1652 |
+
#st.button("New Game", width=125, on_click=_new_game, key="new_game_btn")
|
| 1653 |
+
if st.button("New Game", width=125, key="new_game_btn"):
|
| 1654 |
+
components.html(
|
| 1655 |
+
"<script>window.parent.location.reload();</script>",
|
| 1656 |
+
height=0,
|
| 1657 |
+
)
|
| 1658 |
+
else:
|
| 1659 |
+
_render_grid(
|
| 1660 |
+
state,
|
| 1661 |
+
st.session_state.letter_map,
|
| 1662 |
+
show_grid_ticks=st.session_state.get("show_grid_ticks", True),
|
| 1663 |
+
)
|
| 1664 |
|
| 1665 |
# finish_root_fade_in(0.3)
|
|
@@ -329,7 +329,7 @@ def start_root_fade_in(duration_s: float = 0.35) -> None:
|
|
| 329 |
root.style.setProperty('visibility', 'hidden', 'important');
|
| 330 |
// Force style/layout flush so opacity=0 commits before re-enabling transition.
|
| 331 |
void root.offsetHeight;
|
| 332 |
-
|
| 333 |
}} catch (e) {{ /* no-op */ }}
|
| 334 |
}})();
|
| 335 |
</script>
|
|
@@ -461,6 +461,8 @@ def inject_styles() -> None:
|
|
| 461 |
}
|
| 462 |
div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; min-height: 1.75rem; display: flex;}
|
| 463 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button { min-height: calc(100% + 20px) !important; padding: 0.25rem 0.75rem;}
|
|
|
|
|
|
|
| 464 |
/* div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; border-radius: 0; background: #1d64c8; color: #ffffff; font-weight: 700; padding: 0.5rem 0.75rem; min-height: 2.5rem; min-width: 2.5rem;} */
|
| 465 |
.st-key-new_game_btn, .st-key-sort_wordlist_btn, .st-key-filter_wordlist_btn, .st-key-settings_save_apply_btn, .st-key-settings_discard_btn { margin: 0 auto; aspect-ratio: unset; z-index:9999;}
|
| 466 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button, .st-key-settings_save_apply_btn > div[data-testid="stButton"] button, .st-key-settings_discard_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto; width: 160px; border-radius:8px;}
|
|
@@ -475,7 +477,7 @@ def inject_styles() -> None:
|
|
| 475 |
opacity: 1 !important;
|
| 476 |
}
|
| 477 |
|
| 478 |
-
div[data-testid="column"], .st-emotion-cache-zh2fnc, .st-emotion-cache-1tj828o, .st-emotion-cache-1anq8dj { width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important; border-radius:0; background-color:
|
| 479 |
.st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.15rem !important; min-height: 2.5rem; min-width: 2.5rem;}
|
| 480 |
.stColumn.st-emotion-cache-1ruy632, .stColumn.st-emotion-cache-t2z0eb {margin-top:0;}
|
| 481 |
|
|
@@ -494,8 +496,8 @@ def inject_styles() -> None:
|
|
| 494 |
.st-emotion-cache-ckafi0 { gap:0.1rem !important; min-height: calc(100% + 20px) !important; aspect-ratio: 1 / 1; width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important;}
|
| 495 |
/*.st-emotion-cache-1n6tfoc { aspect-ratio: 1 / 1; min-height: calc(100% + 20px) !important;}*/
|
| 496 |
.st-emotion-cache-1n6tfoc::before { min-height: calc(80% + 20px) !important; }
|
| 497 |
-
|
| 498 |
}
|
|
|
|
| 499 |
div[data-testid="stElementToolbarButtonContainer"], button[data-testid="stBaseButton-elementToolbar"], button[data-testid="stBaseButton-elementToolbar"]:hover {
|
| 500 |
display: none;
|
| 501 |
}
|
|
@@ -508,7 +510,14 @@ def inject_styles() -> None:
|
|
| 508 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] { flex-direction: column-reverse !important; width: 100% !important; max-width: 100vw !important; }
|
| 509 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { width: 100% !important; min-width: 100% !important; max-width: 100% !important; flex: 1 1 100% !important; }
|
| 510 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 511 |
-
.st-emotion-cache-17i4tbh, .st-emotion-cache-1yoilpv { min-width: calc(8.33333% - 1rem); aspect-ratio: 1 / 1;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
}
|
| 513 |
|
| 514 |
.bold-text { font-weight: 700; }
|
|
@@ -519,7 +528,7 @@ def inject_styles() -> None:
|
|
| 519 |
.shiny-border:hover::before { left: 100%; }
|
| 520 |
|
| 521 |
.st-emotion-cache-1n1mx2j p {font-size: 1rem;}
|
| 522 |
-
.bw-radio-group { display:flex; align-items:flex-start; gap: 5px; flex-flow: row; min-height: 92px; transition: opacity
|
| 523 |
.bw-radio-item { display:flex; flex-direction:column; align-items:center; gap:6px; text-align:center;}
|
| 524 |
.bw-radio-circle { width: 45px; height: 45px; border-radius: 50%; border: 4px solid; background: rgba(255,255,255,0.06); display: grid; place-items: center; color:#fff; font-weight:700; }
|
| 525 |
.bw-radio-circle .dot { width: 14px; height: 14px; border-radius: 50%; background:#777; box-shadow: inset 0 0 0 2px rgba(255,255,255,0.25); }
|
|
|
|
| 329 |
root.style.setProperty('visibility', 'hidden', 'important');
|
| 330 |
// Force style/layout flush so opacity=0 commits before re-enabling transition.
|
| 331 |
void root.offsetHeight;
|
| 332 |
+
root.style.setProperty('transition', 'opacity {duration_s:.3f}s ease, visibility {duration_s:.3f}s ease', 'important');
|
| 333 |
}} catch (e) {{ /* no-op */ }}
|
| 334 |
}})();
|
| 335 |
</script>
|
|
|
|
| 461 |
}
|
| 462 |
div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; min-height: 1.75rem; display: flex;}
|
| 463 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button { min-height: calc(100% + 20px) !important; padding: 0.25rem 0.75rem;}
|
| 464 |
+
/* Hover should match the default Streamlit blue (rgb(30, 105, 210)). */
|
| 465 |
+
.st-emotion-cache-1tj828o:hover, .st-emotion-cache-1tj828o:active,.st-emotion-cache-1tj828o:focus-visible, div[data-testid="stButton"] button:hover {background-color: rgba(215, 250, 255, 0.75) !important; border-color: rgb(30, 105, 210) !important;}
|
| 466 |
/* div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; border-radius: 0; background: #1d64c8; color: #ffffff; font-weight: 700; padding: 0.5rem 0.75rem; min-height: 2.5rem; min-width: 2.5rem;} */
|
| 467 |
.st-key-new_game_btn, .st-key-sort_wordlist_btn, .st-key-filter_wordlist_btn, .st-key-settings_save_apply_btn, .st-key-settings_discard_btn { margin: 0 auto; aspect-ratio: unset; z-index:9999;}
|
| 468 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button, .st-key-settings_save_apply_btn > div[data-testid="stButton"] button, .st-key-settings_discard_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto; width: 160px; border-radius:8px;}
|
|
|
|
| 477 |
opacity: 1 !important;
|
| 478 |
}
|
| 479 |
|
| 480 |
+
div[data-testid="column"], .st-emotion-cache-zh2fnc, .st-emotion-cache-1tj828o, .st-emotion-cache-1anq8dj { width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important; border-radius:0; background-color: rgb(30, 105, 210); border: 1px solid rgb(30, 105, 210);}
|
| 481 |
.st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.15rem !important; min-height: 2.5rem; min-width: 2.5rem;}
|
| 482 |
.stColumn.st-emotion-cache-1ruy632, .stColumn.st-emotion-cache-t2z0eb {margin-top:0;}
|
| 483 |
|
|
|
|
| 496 |
.st-emotion-cache-ckafi0 { gap:0.1rem !important; min-height: calc(100% + 20px) !important; aspect-ratio: 1 / 1; width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important;}
|
| 497 |
/*.st-emotion-cache-1n6tfoc { aspect-ratio: 1 / 1; min-height: calc(100% + 20px) !important;}*/
|
| 498 |
.st-emotion-cache-1n6tfoc::before { min-height: calc(80% + 20px) !important; }
|
|
|
|
| 499 |
}
|
| 500 |
+
|
| 501 |
div[data-testid="stElementToolbarButtonContainer"], button[data-testid="stBaseButton-elementToolbar"], button[data-testid="stBaseButton-elementToolbar"]:hover {
|
| 502 |
display: none;
|
| 503 |
}
|
|
|
|
| 510 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] { flex-direction: column-reverse !important; width: 100% !important; max-width: 100vw !important; }
|
| 511 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { width: 100% !important; min-width: 100% !important; max-width: 100% !important; flex: 1 1 100% !important; }
|
| 512 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 513 |
+
.st-emotion-cache-17i4tbh, .st-emotion-cache-1yoilpv { min-width: calc(8.33333% - 1rem); aspect-ratio: 1 / 1;}
|
| 514 |
+
.stColumn.st-emotion-cache-1ruy632 {min-width: calc(50% - 1rem);}
|
| 515 |
+
.stColumn.st-emotion-cache-t2z0eb {min-width: calc(46% - 1rem);}
|
| 516 |
+
.stColumn.st-emotion-cache-10dewhz {min-width: calc(3.5% - 1rem);}
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
@media (max-width: 560px) {
|
| 520 |
+
div[data-testid="stButton"] button {min-height: 2.25rem;}
|
| 521 |
}
|
| 522 |
|
| 523 |
.bold-text { font-weight: 700; }
|
|
|
|
| 528 |
.shiny-border:hover::before { left: 100%; }
|
| 529 |
|
| 530 |
.st-emotion-cache-1n1mx2j p {font-size: 1rem;}
|
| 531 |
+
.bw-radio-group { display:flex; align-items:flex-start; gap: 5px; flex-flow: row; min-height: 92px; transition: opacity 0.3s ease-in, visibility 0.3s ease-in, display 0.3s allow-discrete; transition-behavior: allow-discrete; visibility: inherit;}
|
| 532 |
.bw-radio-item { display:flex; flex-direction:column; align-items:center; gap:6px; text-align:center;}
|
| 533 |
.bw-radio-circle { width: 45px; height: 45px; border-radius: 50%; border: 4px solid; background: rgba(255,255,255,0.06); display: grid; place-items: center; color:#fff; font-weight:700; }
|
| 534 |
.bw-radio-circle .dot { width: 14px; height: 14px; border-radius: 50%; background:#777; box-shadow: inset 0 0 0 2px rgba(255,255,255,0.25); }
|
|
@@ -3,8 +3,8 @@
|
|
| 3 |
## Project Overview
|
| 4 |
BattleWords is a vocabulary learning game inspired by Battleship mechanics, built with Streamlit and Python 3.12. Players reveal cells on a 12x12 grid to discover hidden words and earn points for strategic guessing.
|
| 5 |
|
| 6 |
-
**Current Version:** 0.2.
|
| 7 |
-
**Last Updated:** 2026-02-
|
| 8 |
**Next Version:** 0.3.0 (In Development - Wrdler improvements)
|
| 9 |
**Repository:** https://github.com/Oncorporation/BattleWords.git
|
| 10 |
**Live Demo:** https://huggingface.co/spaces/Surn/BattleWords
|
|
@@ -16,22 +16,6 @@ The detailed porting checklist lives in `specs/upgrade.mdx`.
|
|
| 16 |
## Recent Changes
|
| 17 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 18 |
|
| 19 |
-
- v0.2.36
|
| 20 |
-
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 21 |
-
- Expanded `game_mode` options (includes `easy`)
|
| 22 |
-
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 23 |
-
- Conditional rendering for subscription-based features (difficulty and timer display)
|
| 24 |
-
|
| 25 |
-
- v0.2.35
|
| 26 |
-
- New spinner implementation for loading states
|
| 27 |
-
- Updated graphics and improved visual polish
|
| 28 |
-
- Favicon added for browser tab branding
|
| 29 |
-
- Favicon added for browser tab branding
|
| 30 |
-
- Leaderboard navigation moved to footer menu (not sidebar)
|
| 31 |
-
- Game over dialog integrates leaderboard submission and displays qualification results
|
| 32 |
-
- Leaderboard page routing uses query parameters and custom navigation links
|
| 33 |
-
- Footer navigation links to Leaderboard, Play, and Settings pages
|
| 34 |
-
|
| 35 |
## Core Gameplay
|
| 36 |
- 12x12 grid with 6 hidden words (2×4-letter, 2×5-letter, 2×6-letter)
|
| 37 |
- Words placed horizontally or vertically, no overlaps
|
|
|
|
| 3 |
## Project Overview
|
| 4 |
BattleWords is a vocabulary learning game inspired by Battleship mechanics, built with Streamlit and Python 3.12. Players reveal cells on a 12x12 grid to discover hidden words and earn points for strategic guessing.
|
| 5 |
|
| 6 |
+
**Current Version:** 0.2.37 (Stable - User Features, UI Enhancements)
|
| 7 |
+
**Last Updated:** 2026-02-10
|
| 8 |
**Next Version:** 0.3.0 (In Development - Wrdler improvements)
|
| 9 |
**Repository:** https://github.com/Oncorporation/BattleWords.git
|
| 10 |
**Live Demo:** https://huggingface.co/spaces/Surn/BattleWords
|
|
|
|
| 16 |
## Recent Changes
|
| 17 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
## Core Gameplay
|
| 20 |
- 12x12 grid with 6 hidden words (2×4-letter, 2×5-letter, 2×6-letter)
|
| 21 |
- Words placed horizontally or vertically, no overlaps
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "battlewords"
|
| 3 |
-
version = "0.2.
|
| 4 |
description = "BattleWords vocabulary game with game sharing via shortened game_id URL referencing server-side JSON settings"
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "battlewords"
|
| 3 |
+
version = "0.2.37"
|
| 4 |
description = "BattleWords vocabulary game with game sharing via shortened game_id URL referencing server-side JSON settings"
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
|
@@ -1,16 +1,11 @@
|
|
| 1 |
# Battlewords: Implementation Requirements
|
| 2 |
|
| 3 |
-
**Current Version:** 0.2.
|
| 4 |
-
**Last Updated:** 2026-02-
|
| 5 |
|
| 6 |
-
## Recent Changes (v0.2.
|
| 7 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 8 |
|
| 9 |
-
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 10 |
-
- Expanded `game_mode` options (includes `easy`)
|
| 11 |
-
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 12 |
-
- Conditional rendering for subscription-based features (difficulty and timer display)
|
| 13 |
-
|
| 14 |
This document breaks down the tasks to build Battlewords using the game rules described in `specs.md`. It is organized in phases: a minimal Proof of Concept (POC), a Beta Version (0.5.0), and a Full Version (1.0.0).
|
| 15 |
|
| 16 |
Assumptions
|
|
|
|
| 1 |
# Battlewords: Implementation Requirements
|
| 2 |
|
| 3 |
+
**Current Version:** 0.2.37
|
| 4 |
+
**Last Updated:** 2026-02-10
|
| 5 |
|
| 6 |
+
## Recent Changes (v0.2.37)
|
| 7 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
This document breaks down the tasks to build Battlewords using the game rules described in `specs.md`. It is organized in phases: a minimal Proof of Concept (POC), a Beta Version (0.5.0), and a Full Version (1.0.0).
|
| 10 |
|
| 11 |
Assumptions
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Battlewords Game Requirements (specs.md)
|
| 2 |
|
| 3 |
-
**Current Version:** 0.2.
|
| 4 |
-
**Last Updated:** 2026-02-
|
| 5 |
|
| 6 |
## Overview
|
| 7 |
Battlewords is inspired by the classic Battleship game, but uses words instead of ships. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
|
@@ -9,14 +9,9 @@ Battlewords is inspired by the classic Battleship game, but uses words instead o
|
|
| 9 |
## Upgrade Checklist
|
| 10 |
The detailed Wrdler BattleWords porting checklist is tracked in `specs/upgrade.mdx`.
|
| 11 |
|
| 12 |
-
## Recent Changes (v0.2.
|
| 13 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 14 |
|
| 15 |
-
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 16 |
-
- Expanded `game_mode` options (includes `easy`)
|
| 17 |
-
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 18 |
-
- Conditional rendering for subscription-based features (difficulty and timer display)
|
| 19 |
-
|
| 20 |
---
|
| 21 |
|
| 22 |
## Game Board
|
|
|
|
| 1 |
# Battlewords Game Requirements (specs.md)
|
| 2 |
|
| 3 |
+
**Current Version:** 0.2.37
|
| 4 |
+
**Last Updated:** 2026-02-10
|
| 5 |
|
| 6 |
## Overview
|
| 7 |
Battlewords is inspired by the classic Battleship game, but uses words instead of ships. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
|
|
|
| 9 |
## Upgrade Checklist
|
| 10 |
The detailed Wrdler BattleWords porting checklist is tracked in `specs/upgrade.mdx`.
|
| 11 |
|
| 12 |
+
## Recent Changes (v0.2.37)
|
| 13 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
---
|
| 16 |
|
| 17 |
## Game Board
|