Surn commited on
Commit
d3fd607
·
1 Parent(s): 1d39d8d

Smoothing transitions Part 2

Browse files
README.md CHANGED
@@ -44,7 +44,7 @@ BattleWords is a vocabulary learning game inspired by classic Battleship mechani
44
  - Words placed horizontally or vertically
45
  - Animated radar visualization showing word boundaries
46
  - Reveal grid cells and guess words for points
47
- - Scoring tiers: Good (34–37), Great (38–41), Fantastic (42-44), Legendary (45+)
48
  - Game ends when all words are guessed or all word letters are revealed
49
  - Incorrect guess history with tooltip and optional display (enabled by default)
50
  - 10 incorrect guess limit per game
 
44
  - Words placed horizontally or vertically
45
  - Animated radar visualization showing word boundaries
46
  - Reveal grid cells and guess words for points
47
+ - Scoring tiers: Good (34–37), Great (38–41), Fantastic (42-45), Legendary (46+)
48
  - Game ends when all words are guessed or all word letters are revealed
49
  - Incorrect guess history with tooltip and optional display (enabled by default)
50
  - 10 incorrect guess limit per game
battlewords/logic.py CHANGED
@@ -139,7 +139,9 @@ def is_game_over(state: GameState) -> bool:
139
 
140
 
141
  def compute_tier(score: int) -> str:
142
- if score >= 42:
 
 
143
  return "Fantastic"
144
  if 38 <= score <= 41:
145
  return "Great"
 
139
 
140
 
141
  def compute_tier(score: int) -> str:
142
+ if score >= 46:
143
+ return "Legendary"
144
+ if 42 <= score <= 45:
145
  return "Fantastic"
146
  if 38 <= score <= 41:
147
  return "Great"
battlewords/ui.py CHANGED
@@ -33,7 +33,7 @@ from .ui_helpers import render_footer, inject_styles, fig_to_pil_rgba, ocean_bac
33
  # --- Spinner context manager for custom spinner ---
34
  class CustomSpinner:
35
  """Context manager for showing custom spinner with wrdler.gif"""
36
- def __init__(self, placeholder, message: str = "Loading...", duration: float = 1.0):
37
  self.placeholder = placeholder
38
  self.message = message
39
  self.duration = duration
@@ -42,7 +42,7 @@ class CustomSpinner:
42
  with self.placeholder.container():
43
  show_spinner(self.message)
44
  #wait 1 sec to ensure spinner is visible
45
- time.sleep(0.7)
46
  return self
47
 
48
  def __exit__(self, *args):
@@ -1426,6 +1426,7 @@ def _on_game_option_change() -> None:
1426
  _new_game()
1427
 
1428
  def run_app():
 
1429
  # Render PWA service worker registration (meta tags in <head> via Docker)
1430
  # Streamlit reruns the script frequently; inject this only once per session.
1431
  if not st.session_state.get("pwa_injected", False):
@@ -1461,7 +1462,7 @@ def run_app():
1461
  with CustomSpinner(spinner_placeholder, "Initial Load..."):
1462
  st.session_state["initial_page_loaded"] = True
1463
  st.session_state["last_nav_page"] = page
1464
- st.rerun()
1465
 
1466
  # Show spinner when navigating via menu (query param page change)
1467
  last_nav_page = st.session_state.get("last_nav_page")
@@ -1472,11 +1473,11 @@ def run_app():
1472
  st.rerun()
1473
 
1474
  # One-time spinner on initial display of the main UI.
1475
- if page == "play" and not st.session_state.get("initial_page_loaded", False):
1476
- spinner_placeholder = st.empty()
1477
- with CustomSpinner(spinner_placeholder, "Loading Game..."):
1478
- st.session_state["initial_page_loaded"] = True
1479
- st.rerun()
1480
 
1481
  if page == "settings":
1482
  spinner_placeholder = st.empty()
 
33
  # --- Spinner context manager for custom spinner ---
34
  class CustomSpinner:
35
  """Context manager for showing custom spinner with wrdler.gif"""
36
+ def __init__(self, placeholder, message: str = "Loading...", duration: float = 0.75):
37
  self.placeholder = placeholder
38
  self.message = message
39
  self.duration = duration
 
42
  with self.placeholder.container():
43
  show_spinner(self.message)
44
  #wait 1 sec to ensure spinner is visible
45
+ time.sleep(self.duration)
46
  return self
47
 
48
  def __exit__(self, *args):
 
1426
  _new_game()
1427
 
1428
  def run_app():
1429
+ start_root_fade_in(0.3)
1430
  # Render PWA service worker registration (meta tags in <head> via Docker)
1431
  # Streamlit reruns the script frequently; inject this only once per session.
1432
  if not st.session_state.get("pwa_injected", False):
 
1462
  with CustomSpinner(spinner_placeholder, "Initial Load..."):
1463
  st.session_state["initial_page_loaded"] = True
1464
  st.session_state["last_nav_page"] = page
1465
+ # st.rerun()
1466
 
1467
  # Show spinner when navigating via menu (query param page change)
1468
  last_nav_page = st.session_state.get("last_nav_page")
 
1473
  st.rerun()
1474
 
1475
  # One-time spinner on initial display of the main UI.
1476
+ # if page == "play" and not st.session_state.get("initial_page_loaded", False):
1477
+ # spinner_placeholder = st.empty()
1478
+ # with CustomSpinner(spinner_placeholder, "Loading Game..."):
1479
+ # st.session_state["initial_page_loaded"] = True
1480
+ # st.rerun()
1481
 
1482
  if page == "settings":
1483
  spinner_placeholder = st.empty()
battlewords/ui_helpers.py CHANGED
@@ -324,9 +324,10 @@ def start_root_fade_in(duration_s: float = 0.35) -> None:
324
  var root = parentDoc.getElementById('root');
325
  if (!root) return;
326
  root.classList.add('bw-fadein-prep');
327
- // Immediately hide before any Streamlit render updates become visible.
328
- root.style.setProperty('opacity', '0', 'important');
329
- // Force style/layout flush so the opacity=0 takes effect before transition is applied.
 
330
  void root.offsetHeight;
331
  root.style.setProperty('transition', 'opacity {duration_s:.3f}s ease', 'important');
332
  }} catch (e) {{ /* no-op */ }}
@@ -347,6 +348,8 @@ def finish_root_fade_in(duration_s: float = 0.35) -> None:
347
  var parentDoc = (window.parent && window.parent.document) ? window.parent.document : document;
348
  var root = parentDoc.getElementById('root');
349
  if (!root) return;
 
 
350
  root.style.transition = 'opacity {duration_s:.3f}s ease';
351
  root.style.opacity = '1';
352
  root.classList.remove('bw-fadein-prep');
@@ -370,9 +373,9 @@ def fade_out_spinner(duration_s: float = 0.5) -> None:
370
  try {{
371
  var parentDoc = (window.parent && window.parent.document) ? window.parent.document : document;
372
  // Ensure the app stays hidden while we perform the fade-out.
373
- try {{
374
  var root = parentDoc.getElementById('root');
375
- if (root) root.classList.add('bw-spinner-active');
376
  }} catch (e) {{ /* no-op */ }}
377
  var el = parentDoc.getElementById('bw-spinner-overlay');
378
  if (!el) return;
@@ -429,7 +432,7 @@ def inject_styles() -> None:
429
  font-weight: 700;
430
  user-select: none;
431
  padding: 0.5rem 0.75rem;
432
- font-size: 1.4rem;
433
  min-height: 2.5rem;
434
  min-width: 1.25em;
435
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
@@ -455,7 +458,7 @@ def inject_styles() -> None:
455
  margin: 0 auto;
456
  text-align: center;
457
  }
458
- div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 16 / 11; min-height: 1.75rem; display: flex;}
459
  .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;}
460
  /* 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;} */
461
  .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;}
@@ -463,7 +466,13 @@ def inject_styles() -> None:
463
  .stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:first-child { display: none; }
464
  .stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:nth-child(2) { display: none; }
465
  .stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:nth-child(3) { display: none; }
466
- .st-emotion-cache-18kf3ut.e1wguzas41 { display: none; }
 
 
 
 
 
 
467
 
468
  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;}
469
  .st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.15rem !important; min-height: 2.5rem; min-width: 2.5rem;}
@@ -478,7 +487,7 @@ def inject_styles() -> None:
478
 
479
  /* grid adjustments */
480
  @media (min-width: 560px){
481
- div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; min-height: 40px !important;}
482
  .st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button { min-height: calc(100% + 20px) !important;}
483
  .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;}
484
  /*.st-emotion-cache-1n6tfoc { aspect-ratio: 1 / 1; min-height: calc(100% + 20px) !important;}*/
@@ -495,7 +504,7 @@ def inject_styles() -> None:
495
  #bw-main-anchor + div[data-testid="stHorizontalBlock"] { flex-direction: column-reverse !important; width: 100% !important; max-width: 100vw !important; }
496
  #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; }
497
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
498
- .st-emotion-cache-17i4tbh, .st-emotion-cache-1yoilpv { min-width: calc(8.33333% - 1rem); }
499
  }
500
 
501
  .bold-text { font-weight: 700; }
@@ -556,11 +565,12 @@ def inject_styles() -> None:
556
  .invisible { visibility: hidden !important; }
557
 
558
  /* While spinner is active, hide the underlying app to prevent flashes. */
559
- #root.bw-spinner-active .stApp {
560
  visibility: hidden;
561
  }
562
- #root.bw-spinner-active #bw-spinner-overlay {
563
- visibility: visible;
 
564
  }
565
  </style>
566
  """,
@@ -676,8 +686,9 @@ def show_spinner(message: str = "Loading..."):
676
  .bw-spinner-overlay {{
677
  position: fixed; z-index: 99999; top: 0; left: 0; width: 100vw; height: 100vh;
678
  opacity: 1;
679
- transition: opacity 0.5s ease;
680
- background: rgba(11,42,74,1.0); display: flex; align-items: center; justify-content: center;
 
681
  }}
682
  .bw-spinner-overlay.bw-spinner-fadeout {{
683
  opacity: 0;
@@ -700,7 +711,7 @@ def show_spinner(message: str = "Loading..."):
700
  (function() {{
701
  try {{
702
  var root = document.getElementById('root');
703
- if (root) root.classList.add('bw-spinner-active');
704
  }} catch (e) {{ /* no-op */ }}
705
  }})();
706
  </script>
 
324
  var root = parentDoc.getElementById('root');
325
  if (!root) return;
326
  root.classList.add('bw-fadein-prep');
327
+ // Hide immediately with no transition so a later step can fade in.
328
+ root.style.setProperty('transition', 'none', 'important');
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', 'important');
333
  }} catch (e) {{ /* no-op */ }}
 
348
  var parentDoc = (window.parent && window.parent.document) ? window.parent.document : document;
349
  var root = parentDoc.getElementById('root');
350
  if (!root) return;
351
+ root.style.opacity = '0';
352
+ root.style.visibility = 'visible';
353
  root.style.transition = 'opacity {duration_s:.3f}s ease';
354
  root.style.opacity = '1';
355
  root.classList.remove('bw-fadein-prep');
 
373
  try {{
374
  var parentDoc = (window.parent && window.parent.document) ? window.parent.document : document;
375
  // Ensure the app stays hidden while we perform the fade-out.
376
+ try {{
377
  var root = parentDoc.getElementById('root');
378
+ if (root && !root.classList.contains('bw-spinner-active')) root.classList.add('bw-spinner-active');
379
  }} catch (e) {{ /* no-op */ }}
380
  var el = parentDoc.getElementById('bw-spinner-overlay');
381
  if (!el) return;
 
432
  font-weight: 700;
433
  user-select: none;
434
  padding: 0.5rem 0.75rem;
435
+ font-size: 1.8rem;
436
  min-height: 2.5rem;
437
  min-width: 1.25em;
438
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
 
458
  margin: 0 auto;
459
  text-align: center;
460
  }
461
+ div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; min-height: 1.75rem; display: flex;}
462
  .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;}
463
  /* 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;} */
464
  .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
  .stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:first-child { display: none; }
467
  .stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:nth-child(2) { display: none; }
468
  .stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:nth-child(3) { display: none; }
469
+ .st-emotion-cache-18kf3ut.e1wguzas41 { display: none; }
470
+
471
+ .bw-fadein-prep {visibility: hidden !important;}
472
+ .bw-fadein-prep #bw-spinner-overlay {
473
+ visibility: visible !important;
474
+ opacity: 1 !important;
475
+ }
476
 
477
  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;}
478
  .st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.15rem !important; min-height: 2.5rem; min-width: 2.5rem;}
 
487
 
488
  /* grid adjustments */
489
  @media (min-width: 560px){
490
+ div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1;}
491
  .st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button { min-height: calc(100% + 20px) !important;}
492
  .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;}
493
  /*.st-emotion-cache-1n6tfoc { aspect-ratio: 1 / 1; min-height: calc(100% + 20px) !important;}*/
 
504
  #bw-main-anchor + div[data-testid="stHorizontalBlock"] { flex-direction: column-reverse !important; width: 100% !important; max-width: 100vw !important; }
505
  #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; }
506
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
507
+ .st-emotion-cache-17i4tbh, .st-emotion-cache-1yoilpv { min-width: calc(8.33333% - 1rem); aspect-ratio: 1 / 1;}
508
  }
509
 
510
  .bold-text { font-weight: 700; }
 
565
  .invisible { visibility: hidden !important; }
566
 
567
  /* While spinner is active, hide the underlying app to prevent flashes. */
568
+ #root.bw-spinner-active .stApp, #root.bw-fadein-prep .stApp {
569
  visibility: hidden;
570
  }
571
+ #root.bw-spinner-active #bw-spinner-overlay, #root.bw-fadein-prep #bw-spinner-overlay {
572
+ visibility: visible !important;
573
+ opacity: 1 !important;
574
  }
575
  </style>
576
  """,
 
686
  .bw-spinner-overlay {{
687
  position: fixed; z-index: 99999; top: 0; left: 0; width: 100vw; height: 100vh;
688
  opacity: 1;
689
+ transition: opacity 1.5s ease;
690
+ background: rgb(29, 100, 200); display: flex; align-items: center; justify-content: center;
691
+ visibility: visible;
692
  }}
693
  .bw-spinner-overlay.bw-spinner-fadeout {{
694
  opacity: 0;
 
711
  (function() {{
712
  try {{
713
  var root = document.getElementById('root');
714
+ if (root && !root.classList.contains('bw-spinner-active')) root.classList.add('bw-spinner-active');
715
  }} catch (e) {{ /* no-op */ }}
716
  }})();
717
  </script>
claude.md CHANGED
@@ -40,7 +40,7 @@ See `README.md` for the canonical release notes: [Recent Changes](README.md#rece
40
  - **PLANNED (v0.3.0):** Local persistent storage for individual player results and high scores
41
 
42
  ### Scoring Tiers
43
- - **Legendary:** 45+ points (perfect game)
44
  - **Fantastic:** 42-45 points
45
  - **Great:** 38-41 points
46
  - **Good:** 34-37 points
 
40
  - **PLANNED (v0.3.0):** Local persistent storage for individual player results and high scores
41
 
42
  ### Scoring Tiers
43
+ - **Legendary:** 46+ points (perfect game)
44
  - **Fantastic:** 42-45 points
45
  - **Great:** 38-41 points
46
  - **Good:** 34-37 points
specs/specs.mdx CHANGED
@@ -51,8 +51,8 @@ See `README.md` for the canonical release notes: [Recent Changes](README.md#rece
51
  - Score tiers:
52
  - Good: 34-37
53
  - Great: 38-41
54
- - Fantastic: 42-44
55
- - Legendary: 45+ (perfect game)
56
  - **Game over is triggered by either all words being guessed or all word letters being revealed.**
57
 
58
  ## POC (0.1.0) Rules
 
51
  - Score tiers:
52
  - Good: 34-37
53
  - Great: 38-41
54
+ - Fantastic: 42-45
55
+ - Legendary: 46+ (perfect game)
56
  - **Game over is triggered by either all words being guessed or all word letters being revealed.**
57
 
58
  ## POC (0.1.0) Rules