Surn commited on
Commit
4415803
·
1 Parent(s): 8c564f0

v0.2.37 - Improve UI/UX and add dynamic game state handling

Browse files

Updated 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 CHANGED
@@ -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.36
23
- **Last Updated:** 2026-02-01
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
battlewords/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.2.36"
2
  __all__ = ["models", "generator", "logic", "ui", "game_storage", "modules"]
 
1
+ __version__ = "0.2.37"
2
  __all__ = ["models", "generator", "logic", "ui", "game_storage", "modules"]
battlewords/ui.py CHANGED
@@ -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">Congratulations!</div>
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
- <div class="mb-2">Game Mode: <strong>{state.game_mode}</strong></div>
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("Game Over")
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("Game Over"):
1406
  _game_over_content(state)
1407
  else:
1408
  # Last-resort inline render
1409
- st.subheader("Game Over")
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.2)
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.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
- _render_grid(
1649
- state,
1650
- st.session_state.letter_map,
1651
- show_grid_ticks=st.session_state.get("show_grid_ticks", True),
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\">&nbsp;</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)
battlewords/ui_helpers.py CHANGED
@@ -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
- // 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,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: #1e69d2; border: 1px solid transparent;}
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 1.5s ease-in, visibility 1.5s ease-in, display 1.5s allow-discrete; transition-behavior: allow-discrete; visibility: inherit;}
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); }
claude.md CHANGED
@@ -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.36 (Stable - User Features, UI Enhancements)
7
- **Last Updated:** 2026-02-01
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
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "battlewords"
3
- version = "0.2.36"
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"
specs/requirements.mdx CHANGED
@@ -1,16 +1,11 @@
1
  # Battlewords: Implementation Requirements
2
 
3
- **Current Version:** 0.2.36
4
- **Last Updated:** 2026-02-01
5
 
6
- ## Recent Changes (v0.2.36)
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
specs/specs.mdx CHANGED
@@ -1,7 +1,7 @@
1
  # Battlewords Game Requirements (specs.md)
2
 
3
- **Current Version:** 0.2.36
4
- **Last Updated:** 2026-02-01
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.36)
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