Spaces:
Running
Running
Smoothing transitions Part 2
Browse files- README.md +1 -1
- battlewords/logic.py +3 -1
- battlewords/ui.py +9 -8
- battlewords/ui_helpers.py +27 -16
- claude.md +1 -1
- specs/specs.mdx +2 -2
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-
|
| 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 >=
|
|
|
|
|
|
|
| 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 =
|
| 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(
|
| 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 |
-
|
| 1477 |
-
|
| 1478 |
-
|
| 1479 |
-
|
| 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 |
-
//
|
| 328 |
-
root.style.setProperty('
|
| 329 |
-
|
|
|
|
| 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.
|
| 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:
|
| 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;
|
| 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
|
| 680 |
-
background:
|
|
|
|
| 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:**
|
| 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-
|
| 55 |
-
- Legendary:
|
| 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
|