Add Hall of Legends feature and update to v0.3.2
Browse filesIntroduce Hall of Legends leaderboard for legendary scores (46+), accessible from the footer. Remove Hall of Fantastics. Refactor leaderboard logic to use is_legendary_score utility. Update UI, navigation, documentation, and version numbers to v0.3.2. Includes minor bug fixes and display improvements.
- CLAUDE.md +10 -6
- GAMEPLAY_GUIDE.mdx +4 -4
- README.md +23 -11
- pyproject.toml +1 -1
- specs/leaderboard_spec.mdx +4 -4
- specs/requirements.mdx +4 -4
- specs/specs.mdx +4 -4
- wrdler/__init__.py +2 -2
- wrdler/game_storage.py +1 -1
- wrdler/hall_of_legends.py +12 -211
- wrdler/ui.py +13 -4
- wrdler/ui_helpers.py +18 -0
CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# CLAUDE
|
| 2 |
|
| 3 |
-
Wrdler v0.3.
|
| 4 |
|
| 5 |
# Wrdler - Project Context
|
| 6 |
|
|
@@ -12,20 +12,24 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
|
|
| 12 |
- **2 free letter guesses at game start** (all instances revealed)
|
| 13 |
- **Word composition:** 2 four-letter, 2 five-letter, 2 six-letter words per puzzle
|
| 14 |
|
| 15 |
-
**Current Version:** 0.3.
|
| 16 |
-
**Last Updated:** 2026-
|
| 17 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 18 |
**Branch:** main
|
| 19 |
|
| 20 |
-
## Recent Changes (v0.3.
|
|
|
|
|
|
|
|
|
|
| 21 |
- Fixed "Your Guess" targeting bug
|
| 22 |
- Fixed mobile "incorrect guesses" link
|
| 23 |
-
|
|
|
|
| 24 |
- Minor UI and documentation updates
|
| 25 |
- Small fixes to footer navigation and game-over dialog layout
|
| 26 |
- Synchronized documentation headers and version strings to 0.3.0
|
| 27 |
|
| 28 |
-
##
|
| 29 |
- Version bump to 0.2.16
|
| 30 |
- Documentation and version updates
|
| 31 |
- Update documentation versions from .md to .mdx
|
|
|
|
| 1 |
# CLAUDE
|
| 2 |
|
| 3 |
+
Wrdler v0.3.2
|
| 4 |
|
| 5 |
# Wrdler - Project Context
|
| 6 |
|
|
|
|
| 12 |
- **2 free letter guesses at game start** (all instances revealed)
|
| 13 |
- **Word composition:** 2 four-letter, 2 five-letter, 2 six-letter words per puzzle
|
| 14 |
|
| 15 |
+
**Current Version:** 0.3.2
|
| 16 |
+
**Last Updated:** 2026-04-15
|
| 17 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 18 |
**Branch:** main
|
| 19 |
|
| 20 |
+
## Recent Changes (v0.3.2)
|
| 21 |
+
- Added Hall of Legends for legendary scores
|
| 22 |
+
|
| 23 |
+
## v0.3.1
|
| 24 |
- Fixed "Your Guess" targeting bug
|
| 25 |
- Fixed mobile "incorrect guesses" link
|
| 26 |
+
|
| 27 |
+
## v0.3.0
|
| 28 |
- Minor UI and documentation updates
|
| 29 |
- Small fixes to footer navigation and game-over dialog layout
|
| 30 |
- Synchronized documentation headers and version strings to 0.3.0
|
| 31 |
|
| 32 |
+
## v0.2.16
|
| 33 |
- Version bump to 0.2.16
|
| 34 |
- Documentation and version updates
|
| 35 |
- Update documentation versions from .md to .mdx
|
GAMEPLAY_GUIDE.mdx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
# Wrdler Gameplay Guide
|
| 2 |
|
| 3 |
-
Version 0.3.
|
| 4 |
|
| 5 |
-
**Last Updated:** 2026-
|
| 6 |
|
| 7 |
## Welcome to Wrdler!
|
| 8 |
|
|
@@ -11,8 +11,8 @@ Wrdler is a simplified vocabulary puzzle game where you discover 6 hidden words
|
|
| 11 |
---
|
| 12 |
|
| 13 |
|
| 14 |
-
## Recent Changes (v0.3.
|
| 15 |
-
See `README.md` for the canonical release notes: [Recent Changes (v0.3.
|
| 16 |
|
| 17 |
---
|
| 18 |
|
|
|
|
| 1 |
# Wrdler Gameplay Guide
|
| 2 |
|
| 3 |
+
Version 0.3.2
|
| 4 |
|
| 5 |
+
**Last Updated:** 2026-04-15
|
| 6 |
|
| 7 |
## Welcome to Wrdler!
|
| 8 |
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
|
| 14 |
+
## Recent Changes (v0.3.2)
|
| 15 |
+
See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](README.md#recent-changes-v032)
|
| 16 |
|
| 17 |
---
|
| 18 |
|
README.md
CHANGED
|
@@ -21,7 +21,7 @@ thumbnail: >-
|
|
| 21 |
|
| 22 |
# Wrdler
|
| 23 |
|
| 24 |
-
Version 0.3.
|
| 25 |
|
| 26 |
Wrdler is a vocabulary puzzle game with daily and weekly leaderboards, 8x6 grid, and two free letter guesses at the start. See CHANGELOG or specs for details on the latest updates.
|
| 27 |
|
|
@@ -29,16 +29,16 @@ Wrdler is a vocabulary puzzle game with daily and weekly leaderboards, 8x6 grid,
|
|
| 29 |
|
| 30 |
Wrdler is a vocabulary learning game with a simplified grid and strategic letter guessing. The objective is to discover hidden words on a grid by making smart guesses before all letters are revealed.
|
| 31 |
|
| 32 |
-
**Current Version:** v0.3.
|
| 33 |
-
**Last Updated:** 2026-
|
| 34 |
|
| 35 |
-
## Recent Changes (v0.3.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
-
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
|
| 43 |
## Key Differences from BattleWords
|
| 44 |
|
|
@@ -268,7 +268,19 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
|
|
| 268 |
|
| 269 |
## Changelog
|
| 270 |
|
| 271 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
- Enhance UI/UX and update documentation
|
| 273 |
- Updated README.md and documentation to reflect version bump to 0.2.17 and grid simplification
|
| 274 |
- Refactored JavaScript in `ui.py` for improved input focus management with exponential backoff
|
|
|
|
| 21 |
|
| 22 |
# Wrdler
|
| 23 |
|
| 24 |
+
Version 0.3.2
|
| 25 |
|
| 26 |
Wrdler is a vocabulary puzzle game with daily and weekly leaderboards, 8x6 grid, and two free letter guesses at the start. See CHANGELOG or specs for details on the latest updates.
|
| 27 |
|
|
|
|
| 29 |
|
| 30 |
Wrdler is a vocabulary learning game with a simplified grid and strategic letter guessing. The objective is to discover hidden words on a grid by making smart guesses before all letters are revealed.
|
| 31 |
|
| 32 |
+
**Current Version:** v0.3.2
|
| 33 |
+
**Last Updated:** 2026-04-15
|
| 34 |
|
| 35 |
+
## Recent Changes (v0.3.2)
|
| 36 |
+
A new Hall of Legends feature was added in v0.3.2 to celebrate and preserve outstanding player performances.
|
| 37 |
+
|
| 38 |
+
- Permanent "Hall of Legends" leaderboard that highlights all-time top scorers across challenges and daily/weekly boards
|
| 39 |
+
- Hall entries include player name, score, date, and settings snapshot; accessible from the footer navigation
|
| 40 |
+
- UI updates to leaderboard pages (icons/badges and compact entry layout) and integration with existing leaderboard submission flow
|
| 41 |
+
- Minor bug fixes and display improvements related to leaderboard sorting and tie-breaker presentation
|
| 42 |
|
| 43 |
## Key Differences from BattleWords
|
| 44 |
|
|
|
|
| 268 |
|
| 269 |
## Changelog
|
| 270 |
|
| 271 |
+
## v0.3.2 (Current) โ
|
| 272 |
+
- Added Hall of Legends for legendary scores
|
| 273 |
+
-
|
| 274 |
+
## v0.3.1
|
| 275 |
+
- Fixed "Your Guess" targeting bug
|
| 276 |
+
- Fixed mobile "incorrect guesses" link
|
| 277 |
+
|
| 278 |
+
## v0.3.0
|
| 279 |
+
- Comment out graphics modules to improve speed
|
| 280 |
+
- Small fixes to footer navigation and game-over dialog layout
|
| 281 |
+
- Synchronized documentation and spec headers to v0.3.0 (Last Updated 2026-03-04)
|
| 282 |
+
|
| 283 |
+
### v0.2.17 โ
|
| 284 |
- Enhance UI/UX and update documentation
|
| 285 |
- Updated README.md and documentation to reflect version bump to 0.2.17 and grid simplification
|
| 286 |
- Refactored JavaScript in `ui.py` for improved input focus management with exponential backoff
|
pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "wrdler"
|
| 3 |
-
version = "0.3.
|
| 4 |
description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, 2 free letter guesses, and a settings-based daily/weekly leaderboard system. Features leaderboard UI, challenge sharing, and AI word lists."
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "wrdler"
|
| 3 |
+
version = "0.3.2"
|
| 4 |
description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, 2 free letter guesses, and a settings-based daily/weekly leaderboard system. Features leaderboard UI, challenge sharing, and AI word lists."
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
specs/leaderboard_spec.mdx
CHANGED
|
@@ -2,12 +2,12 @@
|
|
| 2 |
|
| 3 |
**Document Version:** 1.4.7
|
| 4 |
**Author:** GitHub Copilot
|
| 5 |
-
**Project Version:** 0.3.
|
| 6 |
-
**Last Updated:** 2026-
|
| 7 |
**Status:** โ
Implemented and Documented
|
| 8 |
|
| 9 |
-
## Recent Changes (v0.3.
|
| 10 |
-
See `README.md` for the canonical release notes: [Recent Changes (v0.3.
|
| 11 |
|
| 12 |
---
|
| 13 |
|
|
|
|
| 2 |
|
| 3 |
**Document Version:** 1.4.7
|
| 4 |
**Author:** GitHub Copilot
|
| 5 |
+
**Project Version:** 0.3.2
|
| 6 |
+
**Last Updated:** 2026-04-15
|
| 7 |
**Status:** โ
Implemented and Documented
|
| 8 |
|
| 9 |
+
## Recent Changes (v0.3.2)
|
| 10 |
+
See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](../README.md#recent-changes-v032)
|
| 11 |
|
| 12 |
---
|
| 13 |
|
specs/requirements.mdx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
๏ปฟ# Wrdler Requirements
|
| 2 |
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
-
**Version:** 0.3.
|
| 5 |
-
**Last Updated:** 2026-
|
| 6 |
|
| 7 |
-
## Recent Changes (v0.
|
| 8 |
-
See `README.md` for the canonical release notes: [Recent Changes (v0.
|
| 9 |
|
| 10 |
This document breaks down the implementation tasks for Wrdler using the game rules described in `specs.md`. Wrdler is a Python/Streamlit project based on BattleWords but with a simplified 8x6 grid, horizontal-only words, and free letter guesses at the start.
|
| 11 |
|
|
|
|
| 1 |
๏ปฟ# Wrdler Requirements
|
| 2 |
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
+
**Version:** 0.3.2
|
| 5 |
+
**Last Updated:** 2026-04-15
|
| 6 |
|
| 7 |
+
## Recent Changes (v0.3.2)
|
| 8 |
+
See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](../README.md#recent-changes-v032)
|
| 9 |
|
| 10 |
This document breaks down the implementation tasks for Wrdler using the game rules described in `specs.md`. Wrdler is a Python/Streamlit project based on BattleWords but with a simplified 8x6 grid, horizontal-only words, and free letter guesses at the start.
|
| 11 |
|
specs/specs.mdx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
# Wrdler Specifications
|
| 2 |
|
| 3 |
-
**Version:** 0.3.
|
| 4 |
-
**Last Updated:** 2026-
|
| 5 |
|
| 6 |
**Status:** Production Ready - Leaderboards & Enhanced Settings Page Implemented
|
| 7 |
|
| 8 |
-
## Recent Changes (v0.
|
| 9 |
-
See `README.md` for the canonical release notes: [Recent Changes (v0.
|
| 10 |
|
| 11 |
## Overview
|
| 12 |
Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, but with key differences. The objective is to discover hidden words on a grid by making strategic guesses and using free letter reveals at the game start.
|
|
|
|
| 1 |
# Wrdler Specifications
|
| 2 |
|
| 3 |
+
**Version:** 0.3.2
|
| 4 |
+
**Last Updated:** 2026-04-15
|
| 5 |
|
| 6 |
**Status:** Production Ready - Leaderboards & Enhanced Settings Page Implemented
|
| 7 |
|
| 8 |
+
## Recent Changes (v0.3.2)
|
| 9 |
+
See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](../README.md#recent-changes-v032)
|
| 10 |
|
| 11 |
## Overview
|
| 12 |
Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, but with key differences. The objective is to discover hidden words on a grid by making strategic guesses and using free letter reveals at the game start.
|
wrdler/__init__.py
CHANGED
|
@@ -8,8 +8,8 @@ Key differences from BattleWords:
|
|
| 8 |
- 2 free letter guesses at game start
|
| 9 |
- Daily and weekly leaderboards
|
| 10 |
|
| 11 |
-
v0.3.
|
| 12 |
"""
|
| 13 |
|
| 14 |
-
__version__ = "0.3.
|
| 15 |
__all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
|
|
|
|
| 8 |
- 2 free letter guesses at game start
|
| 9 |
- Daily and weekly leaderboards
|
| 10 |
|
| 11 |
+
v0.3.2: Added Hall of Legends for legendary scores.
|
| 12 |
"""
|
| 13 |
|
| 14 |
+
__version__ = "0.3.2"
|
| 15 |
__all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
|
wrdler/game_storage.py
CHANGED
|
@@ -581,7 +581,7 @@ if __name__ == "__main__":
|
|
| 581 |
score=42,
|
| 582 |
time_seconds=180.01,
|
| 583 |
game_mode="classic",
|
| 584 |
-
grid_size=
|
| 585 |
wordlist_source="classic.txt"
|
| 586 |
)
|
| 587 |
print(f"Challenge ID: {challenge_id}")
|
|
|
|
| 581 |
score=42,
|
| 582 |
time_seconds=180.01,
|
| 583 |
game_mode="classic",
|
| 584 |
+
grid_size=6, # Wrdler default
|
| 585 |
wordlist_source="classic.txt"
|
| 586 |
)
|
| 587 |
print(f"Challenge ID: {challenge_id}")
|
wrdler/hall_of_legends.py
CHANGED
|
@@ -7,29 +7,12 @@ import streamlit as st
|
|
| 7 |
|
| 8 |
from .modules.constants import HF_REPO_ID, HALL_OF_LEGENDS_FILE, APP_SETTINGS, MAX_DISPLAY_ENTRIES
|
| 9 |
from .modules.storage import _get_json_from_repo, _upload_json_to_repo
|
| 10 |
-
from .ui_helpers import
|
| 11 |
|
| 12 |
|
| 13 |
CURRENT_SETTINGS_VERSION = 0
|
| 14 |
|
| 15 |
|
| 16 |
-
def _get_weekly_fantastic_metadata(game_mode: str | None = None) -> tuple[str, str, str, int, str]:
|
| 17 |
-
current_time = datetime.now(timezone.utc)
|
| 18 |
-
iso_year, iso_week, _ = current_time.isocalendar()
|
| 19 |
-
week_id = f"{iso_year}-W{iso_week:02d}"
|
| 20 |
-
mode = game_mode or st.session_state.get("game_mode", "classic")
|
| 21 |
-
wordlist_source = st.session_state.get("selected_wordlist", "classic.txt")
|
| 22 |
-
wordlist_stem = os.path.splitext(os.path.basename(wordlist_source))[0] or "classic"
|
| 23 |
-
settings_version = CURRENT_SETTINGS_VERSION
|
| 24 |
-
slug = f"{wordlist_stem}-{mode}-{settings_version}-fantastic"
|
| 25 |
-
return week_id, mode, wordlist_source, settings_version, slug
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _weekly_fantastic_settings_path(game_mode: str | None = None) -> str:
|
| 29 |
-
week_id, _, _, _, slug = _get_weekly_fantastic_metadata(game_mode)
|
| 30 |
-
return f"games/leaderboards/weekly/{week_id}/{slug}/settings.json"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
def _default_hall_document() -> dict[str, Any]:
|
| 34 |
game_mode = st.session_state.get("game_mode", "classic")
|
| 35 |
wordlist_source = st.session_state.get("selected_wordlist", "classic.txt")
|
|
@@ -40,7 +23,9 @@ def _default_hall_document() -> dict[str, Any]:
|
|
| 40 |
"challenge_id": f"hall-of-legends/{game_mode}-{wordlist_stem}-{spacer}",
|
| 41 |
"entry_type": "hall",
|
| 42 |
"game_mode": game_mode,
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
"puzzle_options": {
|
| 45 |
"spacer": spacer,
|
| 46 |
"may_overlap": False,
|
|
@@ -51,7 +36,7 @@ def _default_hall_document() -> dict[str, Any]:
|
|
| 51 |
"show_incorrect_guesses": bool(st.session_state.get("show_incorrect_guesses", True)),
|
| 52 |
"enable_free_letters": bool(st.session_state.get("enable_free_letters", False)),
|
| 53 |
"wordlist_source": wordlist_source,
|
| 54 |
-
"game_title":
|
| 55 |
"max_display_entries": int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES)),
|
| 56 |
}
|
| 57 |
|
|
@@ -74,57 +59,12 @@ def load_hall_document() -> dict[str, Any]:
|
|
| 74 |
return default_doc
|
| 75 |
|
| 76 |
|
| 77 |
-
def _default_fantastic_document(game_mode: str | None = None) -> dict[str, Any]:
|
| 78 |
-
week_id, mode, wordlist_source, settings_version, slug = _get_weekly_fantastic_metadata(game_mode)
|
| 79 |
-
spacer = int(st.session_state.get("spacer", 1))
|
| 80 |
-
|
| 81 |
-
return {
|
| 82 |
-
"challenge_id": f"{week_id}/{slug}",
|
| 83 |
-
"entry_type": "fantastic",
|
| 84 |
-
"game_mode": mode,
|
| 85 |
-
"grid_size": 12,
|
| 86 |
-
"puzzle_options": {
|
| 87 |
-
"spacer": spacer,
|
| 88 |
-
"may_overlap": False,
|
| 89 |
-
},
|
| 90 |
-
"users": [],
|
| 91 |
-
"created_at": datetime.now().astimezone().isoformat(),
|
| 92 |
-
"version": st.session_state.get("app_version", ""),
|
| 93 |
-
"show_incorrect_guesses": bool(st.session_state.get("show_incorrect_guesses", True)),
|
| 94 |
-
"enable_free_letters": bool(st.session_state.get("enable_free_letters", False)),
|
| 95 |
-
"wordlist_source": wordlist_source,
|
| 96 |
-
"game_title": APP_SETTINGS.get("game_title", "Battlewords"),
|
| 97 |
-
"max_display_entries": int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES)),
|
| 98 |
-
"settings_version": settings_version,
|
| 99 |
-
"leaderboard_scope": "weekly",
|
| 100 |
-
"week_id": week_id,
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def load_fantastic_document(game_mode: str | None = None) -> dict[str, Any]:
|
| 105 |
-
default_doc = _default_fantastic_document(game_mode)
|
| 106 |
-
if not HF_REPO_ID:
|
| 107 |
-
return default_doc
|
| 108 |
-
|
| 109 |
-
data = _get_json_from_repo(HF_REPO_ID, _weekly_fantastic_settings_path(game_mode), "dataset")
|
| 110 |
-
|
| 111 |
-
if isinstance(data, dict):
|
| 112 |
-
merged = {**default_doc, **data}
|
| 113 |
-
if not isinstance(merged.get("users"), list):
|
| 114 |
-
merged["users"] = []
|
| 115 |
-
return merged
|
| 116 |
-
|
| 117 |
-
if isinstance(data, list):
|
| 118 |
-
default_doc["users"] = data
|
| 119 |
-
return default_doc
|
| 120 |
-
|
| 121 |
-
|
| 122 |
def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
|
| 123 |
if not HF_REPO_ID:
|
| 124 |
return False, "HF_REPO_ID is not configured."
|
| 125 |
|
| 126 |
score = int(user_entry.get("score", 0))
|
| 127 |
-
if
|
| 128 |
return False, "Only Legendary runs (46+) can be saved."
|
| 129 |
|
| 130 |
hall_doc = load_hall_document()
|
|
@@ -144,7 +84,9 @@ def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
|
|
| 144 |
hall_doc["challenge_id"] = f"hall-of-legends/{state.game_mode}-{wordlist_stem}-{spacer}"
|
| 145 |
hall_doc["entry_type"] = "hall"
|
| 146 |
hall_doc["game_mode"] = state.game_mode
|
| 147 |
-
|
|
|
|
|
|
|
| 148 |
hall_doc["puzzle_options"] = {
|
| 149 |
"spacer": spacer,
|
| 150 |
"may_overlap": False,
|
|
@@ -163,52 +105,6 @@ def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
|
|
| 163 |
return False, "Unable to save to Hall of Legends."
|
| 164 |
|
| 165 |
|
| 166 |
-
def save_fantastic_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
|
| 167 |
-
if not HF_REPO_ID:
|
| 168 |
-
return False, "HF_REPO_ID is not configured."
|
| 169 |
-
|
| 170 |
-
score = int(user_entry.get("score", 0))
|
| 171 |
-
if score < 42 or score > 45:
|
| 172 |
-
return False, "Only Fantastic runs (42-45) can be saved."
|
| 173 |
-
|
| 174 |
-
fantastic_doc = load_fantastic_document(state.game_mode)
|
| 175 |
-
users = fantastic_doc.setdefault("users", [])
|
| 176 |
-
uid = str(user_entry.get("uid", "")).strip()
|
| 177 |
-
|
| 178 |
-
if uid and any(str(u.get("uid", "")) == uid for u in users):
|
| 179 |
-
return True, "Already saved to Hall of Fantastics."
|
| 180 |
-
|
| 181 |
-
users.append(user_entry)
|
| 182 |
-
users.sort(key=lambda x: (-int(x.get("score", 0)), float(x.get("time", 999999.0))))
|
| 183 |
-
|
| 184 |
-
week_id, _, wordlist_source, settings_version, slug = _get_weekly_fantastic_metadata(state.game_mode)
|
| 185 |
-
spacer = int(st.session_state.get("spacer", 1))
|
| 186 |
-
|
| 187 |
-
fantastic_doc["challenge_id"] = f"{week_id}/{slug}"
|
| 188 |
-
fantastic_doc["entry_type"] = "fantastic"
|
| 189 |
-
fantastic_doc["game_mode"] = state.game_mode
|
| 190 |
-
fantastic_doc["grid_size"] = state.grid_size
|
| 191 |
-
fantastic_doc["puzzle_options"] = {
|
| 192 |
-
"spacer": spacer,
|
| 193 |
-
"may_overlap": False,
|
| 194 |
-
}
|
| 195 |
-
fantastic_doc["created_at"] = datetime.now().astimezone().isoformat()
|
| 196 |
-
fantastic_doc["version"] = st.session_state.get("app_version", "")
|
| 197 |
-
fantastic_doc["show_incorrect_guesses"] = bool(st.session_state.get("show_incorrect_guesses", True))
|
| 198 |
-
fantastic_doc["enable_free_letters"] = bool(st.session_state.get("enable_free_letters", False))
|
| 199 |
-
fantastic_doc["wordlist_source"] = wordlist_source
|
| 200 |
-
fantastic_doc["game_title"] = APP_SETTINGS.get("game_title", "Battlewords")
|
| 201 |
-
fantastic_doc["max_display_entries"] = int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES))
|
| 202 |
-
fantastic_doc["settings_version"] = settings_version
|
| 203 |
-
fantastic_doc["leaderboard_scope"] = "weekly"
|
| 204 |
-
fantastic_doc["week_id"] = week_id
|
| 205 |
-
|
| 206 |
-
ok = _upload_json_to_repo(fantastic_doc, HF_REPO_ID, _weekly_fantastic_settings_path(state.game_mode), "dataset")
|
| 207 |
-
if ok:
|
| 208 |
-
return True, "Saved to Hall of Fantastics."
|
| 209 |
-
return False, "Unable to save to Hall of Fantastics."
|
| 210 |
-
|
| 211 |
-
|
| 212 |
def render_hall_page() -> None:
|
| 213 |
# Epic hero header
|
| 214 |
st.markdown(
|
|
@@ -243,7 +139,7 @@ def render_hall_page() -> None:
|
|
| 243 |
""",
|
| 244 |
unsafe_allow_html=True,
|
| 245 |
)
|
| 246 |
-
|
| 247 |
|
| 248 |
hall_doc = load_hall_document()
|
| 249 |
users = hall_doc.get("users", []) if isinstance(hall_doc, dict) else []
|
|
@@ -255,7 +151,7 @@ def render_hall_page() -> None:
|
|
| 255 |
total = len(unique_names)
|
| 256 |
if total == 0:
|
| 257 |
st.info("No Hall of Legends entries yet. Be the first to etch your name into legend!")
|
| 258 |
-
st.markdown(
|
| 259 |
return
|
| 260 |
|
| 261 |
# Summary/top-stats
|
|
@@ -321,100 +217,5 @@ def render_hall_page() -> None:
|
|
| 321 |
"""
|
| 322 |
|
| 323 |
st.markdown(table_html, unsafe_allow_html=True)
|
| 324 |
-
st.markdown(
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
def render_fantastic_page() -> None:
|
| 328 |
-
week_id, _, _, _, _ = _get_weekly_fantastic_metadata()
|
| 329 |
-
st.markdown(
|
| 330 |
-
f"""
|
| 331 |
-
<style>
|
| 332 |
-
.bw-hall-hero {{
|
| 333 |
-
background: linear-gradient(90deg,#5b21b6 0%, #7c3aed 50%, #2563eb 100%);
|
| 334 |
-
color: #fff;
|
| 335 |
-
padding: 1.25rem;
|
| 336 |
-
border-radius: 1rem;
|
| 337 |
-
box-shadow: 0 12px 40px rgba(37,99,235,0.4);
|
| 338 |
-
text-align: center;
|
| 339 |
-
margin-bottom: 1rem;
|
| 340 |
-
}}
|
| 341 |
-
.bw-hall-hero h1 {{ margin: 0; font-size: 2.1rem; letter-spacing: 1px; }}
|
| 342 |
-
.bw-hall-hero p {{ margin: 0.25rem 0 0; opacity: 0.92; }}
|
| 343 |
-
.bw-top-cards {{ display:flex; gap:12px; margin-top:1rem; justify-content:center; flex-wrap:wrap; }}
|
| 344 |
-
.bw-card {{ background: rgba(255,255,255,0.05); padding: 0.75rem 1rem; border-radius: 0.75rem; min-width: 180px; text-align:center; box-shadow: 0 6px 18px rgba(37,99,235,0.28); }}
|
| 345 |
-
.bw-card h3 {{ margin:0; font-size:1.1rem; }}
|
| 346 |
-
.bw-card p {{ margin:0.25rem 0 0; font-size:0.95rem; color:#e9ddff; }}
|
| 347 |
-
.bw-table {{ width:100%; border-collapse:collapse; margin-top:1rem; }}
|
| 348 |
-
.bw-table th, .bw-table td {{ padding:8px 10px; border-bottom:1px solid rgba(255,255,255,0.08); }}
|
| 349 |
-
.bw-table th {{ text-align:left; background: rgba(255,255,255,0.03); }}
|
| 350 |
-
.bw-top-row {{ background: linear-gradient(90deg,#facc1540,#60a5fa40); }}
|
| 351 |
-
.bw-return {{ display:block; text-align:center; margin-top:1rem; }}
|
| 352 |
-
</style>
|
| 353 |
-
<div class="bw-hall-hero">
|
| 354 |
-
<div style="font-size:2.4rem;">โจ HALL OF FANTASTICS</div>
|
| 355 |
-
<h1>Fantastic Players โข Weekly Honors</h1>
|
| 356 |
-
<p>Runs scoring 42-45 are celebrated here for the current week: <strong>{week_id}</strong>.</p>
|
| 357 |
-
</div>
|
| 358 |
-
""",
|
| 359 |
-
unsafe_allow_html=True,
|
| 360 |
-
)
|
| 361 |
-
|
| 362 |
-
fantastic_doc = load_fantastic_document()
|
| 363 |
-
users = fantastic_doc.get("users", []) if isinstance(fantastic_doc, dict) else []
|
| 364 |
-
entries = [u for u in users if 42 <= int(u.get("score", 0)) <= 45]
|
| 365 |
-
entries.sort(key=lambda x: (-int(x.get("score", 0)), float(x.get("time", 999999.0))))
|
| 366 |
-
|
| 367 |
-
unique_names = {(str(u.get("username", "Anonymous") or "Anonymous").strip().lower()) for u in entries}
|
| 368 |
-
total = len(unique_names)
|
| 369 |
-
if total == 0:
|
| 370 |
-
st.info("No Hall of Fantastics entries yet for this week. Claim the first Fantastic finish!")
|
| 371 |
-
st.markdown(f"<div style='text-align:center; margin-top:0.75rem;'><a href=\"{hall_page_url()}\" target=\"_self\">View Hall of Legends</a></div>", unsafe_allow_html=True)
|
| 372 |
-
st.markdown(f"<a href=\"{game_page_url()}\" target=\"_self\">Return to game</a>", unsafe_allow_html=True)
|
| 373 |
-
return
|
| 374 |
-
|
| 375 |
-
best = entries[0]
|
| 376 |
-
best_score = int(best.get("score", 0))
|
| 377 |
-
best_player = best.get("username", "Anonymous")
|
| 378 |
-
best_time = float(best.get("time", 0.0))
|
| 379 |
-
wordlist_name = fantastic_doc.get("wordlist_source", "")
|
| 380 |
|
| 381 |
-
st.markdown(
|
| 382 |
-
f"""
|
| 383 |
-
<div class="bw-top-cards">
|
| 384 |
-
<div class="bw-card"><h3>๐ Top Fantastic</h3><p><strong>{best_score}</strong> pts by <strong>{best_player}</strong></p></div>
|
| 385 |
-
<div class="bw-card"><h3>โก Fastest Fantastic</h3><p><strong>{best_time:.3f}s</strong></p></div>
|
| 386 |
-
<div class="bw-card"><h3>๐๏ธ Current Week</h3><p><strong>{week_id}</strong></p></div>
|
| 387 |
-
<div class="bw-card"><h3>๐ฏ Wordlist</h3><p><strong>{wordlist_name}</strong></p></div>
|
| 388 |
-
<div class="bw-card"><h3>๐ Total Fantastics</h3><p><strong>{total}</strong> saved runs</p></div>
|
| 389 |
-
</div>
|
| 390 |
-
""",
|
| 391 |
-
unsafe_allow_html=True,
|
| 392 |
-
)
|
| 393 |
-
st.markdown(f"<div style='text-align:center; margin-top:0.75rem;'><a href=\"{hall_page_url()}\" target=\"_self\">View Hall of Legends</a></div>", unsafe_allow_html=True)
|
| 394 |
-
|
| 395 |
-
display_limit = min(int(fantastic_doc.get("max_display_entries", APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES))), MAX_DISPLAY_ENTRIES)
|
| 396 |
-
rows_html = []
|
| 397 |
-
for idx, entry in enumerate(entries[:display_limit], 1):
|
| 398 |
-
score = int(entry.get("score", 0))
|
| 399 |
-
player = entry.get("username", "Anonymous")
|
| 400 |
-
tier = entry.get("tier", "Fantastic")
|
| 401 |
-
saved = entry.get("timestamp", "")
|
| 402 |
-
tr_class = "bw-top-row" if idx <= 3 else ""
|
| 403 |
-
medal = "๐ฅ" if idx == 1 else ("๐ฅ" if idx == 2 else ("๐ฅ" if idx == 3 else "โจ"))
|
| 404 |
-
rows_html.append(f"<tr class=\"{tr_class}\"><td style='width:64px'>{medal} {idx}</td><td><strong>{player}</strong></td><td>{score}</td><td>{tier}</td><td>{wordlist_name}</td><td style='white-space:nowrap'>{saved}</td></tr>")
|
| 405 |
-
|
| 406 |
-
table_html = f"""
|
| 407 |
-
<div style="overflow:auto; max-height:520px;">
|
| 408 |
-
<table class="bw-table shiny-border">
|
| 409 |
-
<thead>
|
| 410 |
-
<tr><th>Rank</th><th>Player</th><th>Score</th><th>Tier</th><th>Wordlist</th><th>Saved</th></tr>
|
| 411 |
-
</thead>
|
| 412 |
-
<tbody>
|
| 413 |
-
{''.join(rows_html)}
|
| 414 |
-
</tbody>
|
| 415 |
-
</table>
|
| 416 |
-
</div>
|
| 417 |
-
"""
|
| 418 |
-
|
| 419 |
-
st.markdown(table_html, unsafe_allow_html=True)
|
| 420 |
-
st.markdown(f"<a class=\"bw-return\" href=\"{game_page_url()}\" target=\"_self\">โต Return to game</a>", unsafe_allow_html=True)
|
|
|
|
| 7 |
|
| 8 |
from .modules.constants import HF_REPO_ID, HALL_OF_LEGENDS_FILE, APP_SETTINGS, MAX_DISPLAY_ENTRIES
|
| 9 |
from .modules.storage import _get_json_from_repo, _upload_json_to_repo
|
| 10 |
+
from .ui_helpers import hall_page_url, is_legendary_score
|
| 11 |
|
| 12 |
|
| 13 |
CURRENT_SETTINGS_VERSION = 0
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def _default_hall_document() -> dict[str, Any]:
|
| 17 |
game_mode = st.session_state.get("game_mode", "classic")
|
| 18 |
wordlist_source = st.session_state.get("selected_wordlist", "classic.txt")
|
|
|
|
| 23 |
"challenge_id": f"hall-of-legends/{game_mode}-{wordlist_stem}-{spacer}",
|
| 24 |
"entry_type": "hall",
|
| 25 |
"game_mode": game_mode,
|
| 26 |
+
# Store rectangular grid dimensions explicitly for Wrdler (rows ร cols)
|
| 27 |
+
"grid_rows": int(st.session_state.get("grid_rows", 6)),
|
| 28 |
+
"grid_cols": int(st.session_state.get("grid_cols", 8)),
|
| 29 |
"puzzle_options": {
|
| 30 |
"spacer": spacer,
|
| 31 |
"may_overlap": False,
|
|
|
|
| 36 |
"show_incorrect_guesses": bool(st.session_state.get("show_incorrect_guesses", True)),
|
| 37 |
"enable_free_letters": bool(st.session_state.get("enable_free_letters", False)),
|
| 38 |
"wordlist_source": wordlist_source,
|
| 39 |
+
"game_title": "Wrdler",
|
| 40 |
"max_display_entries": int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES)),
|
| 41 |
}
|
| 42 |
|
|
|
|
| 59 |
return default_doc
|
| 60 |
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
|
| 63 |
if not HF_REPO_ID:
|
| 64 |
return False, "HF_REPO_ID is not configured."
|
| 65 |
|
| 66 |
score = int(user_entry.get("score", 0))
|
| 67 |
+
if not is_legendary_score(score):
|
| 68 |
return False, "Only Legendary runs (46+) can be saved."
|
| 69 |
|
| 70 |
hall_doc = load_hall_document()
|
|
|
|
| 84 |
hall_doc["challenge_id"] = f"hall-of-legends/{state.game_mode}-{wordlist_stem}-{spacer}"
|
| 85 |
hall_doc["entry_type"] = "hall"
|
| 86 |
hall_doc["game_mode"] = state.game_mode
|
| 87 |
+
# Ensure we persist rectangular grid metadata (rows and cols)
|
| 88 |
+
hall_doc["grid_rows"] = int(getattr(state, "grid_rows", 6) or 6)
|
| 89 |
+
hall_doc["grid_cols"] = int(getattr(state, "grid_cols", 8) or 8)
|
| 90 |
hall_doc["puzzle_options"] = {
|
| 91 |
"spacer": spacer,
|
| 92 |
"may_overlap": False,
|
|
|
|
| 105 |
return False, "Unable to save to Hall of Legends."
|
| 106 |
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
def render_hall_page() -> None:
|
| 109 |
# Epic hero header
|
| 110 |
st.markdown(
|
|
|
|
| 139 |
""",
|
| 140 |
unsafe_allow_html=True,
|
| 141 |
)
|
| 142 |
+
|
| 143 |
|
| 144 |
hall_doc = load_hall_document()
|
| 145 |
users = hall_doc.get("users", []) if isinstance(hall_doc, dict) else []
|
|
|
|
| 151 |
total = len(unique_names)
|
| 152 |
if total == 0:
|
| 153 |
st.info("No Hall of Legends entries yet. Be the first to etch your name into legend!")
|
| 154 |
+
st.markdown("<a href='/' target='_self'>Return to game</a>", unsafe_allow_html=True)
|
| 155 |
return
|
| 156 |
|
| 157 |
# Summary/top-stats
|
|
|
|
| 217 |
"""
|
| 218 |
|
| 219 |
st.markdown(table_html, unsafe_allow_html=True)
|
| 220 |
+
st.markdown("<a class='bw-return' href='/' target='_self'>โต Return to game</a>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
wrdler/ui.py
CHANGED
|
@@ -44,6 +44,7 @@ from .ui_helpers import (
|
|
| 44 |
show_spinner,
|
| 45 |
get_effective_game_title,
|
| 46 |
custom_rising_text,
|
|
|
|
| 47 |
)
|
| 48 |
|
| 49 |
# --- Spinner context manager for custom spinner ---
|
|
@@ -1245,7 +1246,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1245 |
else:
|
| 1246 |
_mount_background_audio(False, None, 0.0)
|
| 1247 |
|
| 1248 |
-
if state.score
|
| 1249 |
custom_rising_text(["LEGENDARY!", "๐๏ธ HALL OF LEGENDS"], font_size=[34, 28], upward_speed=[4, 5], animation_length=4, color=["#ffd54d", "#ffffff"])
|
| 1250 |
|
| 1251 |
# Set end_time if not already set
|
|
@@ -1500,7 +1501,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1500 |
return results
|
| 1501 |
|
| 1502 |
def _save_legendary_hall_entry(username: str, score: int, time_secs: float, word_list: list, leaderboard_results: dict, challenge_id: str = None) -> None:
|
| 1503 |
-
if score
|
| 1504 |
return
|
| 1505 |
|
| 1506 |
daily_info = (leaderboard_results or {}).get("daily", {})
|
|
@@ -1720,15 +1721,23 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1720 |
|
| 1721 |
if daily_info.get("qualified"):
|
| 1722 |
daily_rankings_text = f"<a href='{daily_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{daily_rankings_text}</a>"
|
| 1723 |
-
|
| 1724 |
if weekly_info.get("qualified"):
|
| 1725 |
weekly_ranking_text = f"<a href='{weekly_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{weekly_ranking_text}</a>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1726 |
|
| 1727 |
st.markdown(
|
| 1728 |
f"""
|
| 1729 |
<div style="margin-top: 1rem; padding: 1rem; background: rgba(32, 212, 108, 0.1); text-align:center; border-radius: 0.5rem; border: 1px solid rgba(32, 212, 108, 0.3);">
|
| 1730 |
<strong style="color: #20d46c; filter: drop-shadow(1px 1px 1px #003);">๐ Leaderboard Rankings:</strong><br/>
|
| 1731 |
-
{daily_rankings_text} {weekly_ranking_text}
|
| 1732 |
</div>
|
| 1733 |
""",
|
| 1734 |
unsafe_allow_html=True
|
|
|
|
| 44 |
show_spinner,
|
| 45 |
get_effective_game_title,
|
| 46 |
custom_rising_text,
|
| 47 |
+
is_legendary_score,
|
| 48 |
)
|
| 49 |
|
| 50 |
# --- Spinner context manager for custom spinner ---
|
|
|
|
| 1246 |
else:
|
| 1247 |
_mount_background_audio(False, None, 0.0)
|
| 1248 |
|
| 1249 |
+
if is_legendary_score(state.score):
|
| 1250 |
custom_rising_text(["LEGENDARY!", "๐๏ธ HALL OF LEGENDS"], font_size=[34, 28], upward_speed=[4, 5], animation_length=4, color=["#ffd54d", "#ffffff"])
|
| 1251 |
|
| 1252 |
# Set end_time if not already set
|
|
|
|
| 1501 |
return results
|
| 1502 |
|
| 1503 |
def _save_legendary_hall_entry(username: str, score: int, time_secs: float, word_list: list, leaderboard_results: dict, challenge_id: str = None) -> None:
|
| 1504 |
+
if not is_legendary_score(score) or st.session_state.get("hall_of_legends_saved", False):
|
| 1505 |
return
|
| 1506 |
|
| 1507 |
daily_info = (leaderboard_results or {}).get("daily", {})
|
|
|
|
| 1721 |
|
| 1722 |
if daily_info.get("qualified"):
|
| 1723 |
daily_rankings_text = f"<a href='{daily_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{daily_rankings_text}</a>"
|
| 1724 |
+
|
| 1725 |
if weekly_info.get("qualified"):
|
| 1726 |
weekly_ranking_text = f"<a href='{weekly_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{weekly_ranking_text}</a>"
|
| 1727 |
+
|
| 1728 |
+
# If weekly qualified AND this run is Legendary, offer quick link to Hall of Legends
|
| 1729 |
+
hall_link_html = ""
|
| 1730 |
+
try:
|
| 1731 |
+
if weekly_info.get("qualified") and is_legendary_score(state.score):
|
| 1732 |
+
hall_link_html = f"<div style='margin-top:0.5rem;'><a href='/?page=hall' target='_self' style='color: #ffd54d; text-decoration: underline; font-size: 1.1rem;'>๐๏ธ Hall of Legends</a></div>"
|
| 1733 |
+
except Exception:
|
| 1734 |
+
hall_link_html = ""
|
| 1735 |
|
| 1736 |
st.markdown(
|
| 1737 |
f"""
|
| 1738 |
<div style="margin-top: 1rem; padding: 1rem; background: rgba(32, 212, 108, 0.1); text-align:center; border-radius: 0.5rem; border: 1px solid rgba(32, 212, 108, 0.3);">
|
| 1739 |
<strong style="color: #20d46c; filter: drop-shadow(1px 1px 1px #003);">๐ Leaderboard Rankings:</strong><br/>
|
| 1740 |
+
{daily_rankings_text} {weekly_ranking_text} {hall_link_html}
|
| 1741 |
</div>
|
| 1742 |
""",
|
| 1743 |
unsafe_allow_html=True
|
wrdler/ui_helpers.py
CHANGED
|
@@ -80,6 +80,14 @@ def custom_rising_text(
|
|
| 80 |
)
|
| 81 |
st.html(html, unsafe_allow_javascript=True)
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# --- Utility: Convert Matplotlib Figure to PIL RGBA Image ---
|
| 84 |
def fig_to_pil_rgba(fig):
|
| 85 |
"""
|
|
@@ -118,6 +126,12 @@ def get_effective_game_title() -> str:
|
|
| 118 |
# Fall back to APP_SETTINGS
|
| 119 |
return APP_SETTINGS.get("game_title", "Wrdler")
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
# --- PWA Service Worker JS (for Streamlit head injection) ---
|
| 123 |
pwa_service_worker = """
|
|
@@ -630,6 +644,7 @@ def _render_footer(current_page: str = "play"):
|
|
| 630 |
# Determine which link should be highlighted as active
|
| 631 |
play_active = "active" if current_page == "play" else ""
|
| 632 |
leaderboard_active = "active" if current_page in {"today", "daily", "weekly", "history"} else ""
|
|
|
|
| 633 |
settings_active = "active" if current_page == "settings" else ""
|
| 634 |
game_title = get_effective_game_title()
|
| 635 |
game_version = f"{game_title.lower()}-v{version.replace('.', '-')}"
|
|
@@ -651,11 +666,13 @@ def _render_footer(current_page: str = "play"):
|
|
| 651 |
if game_id:
|
| 652 |
today_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
|
| 653 |
leaderboard_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
|
|
|
|
| 654 |
play_url = f"?game_id={game_id}#{game_version}"
|
| 655 |
settings_url = f"?page=settings&game_id={game_id}#settings"
|
| 656 |
else:
|
| 657 |
today_url = "?page=today#wrdler-leaderboards"
|
| 658 |
leaderboard_url = "?page=today#wrdler-leaderboards"
|
|
|
|
| 659 |
play_url = f"/#{game_version}"
|
| 660 |
settings_url = "?page=settings#settings"
|
| 661 |
|
|
@@ -717,6 +734,7 @@ def _render_footer(current_page: str = "play"):
|
|
| 717 |
<div class="bw-footer">
|
| 718 |
<nav class="bw-footer-nav">
|
| 719 |
<a href="{leaderboard_url if not leaderboard_active else '#wrdler-leaderboards'}" title="View Leaderboards" target="_self" class="{leaderboard_active}">๐ Leaderboard</a>
|
|
|
|
| 720 |
<a href="{play_url if not play_active else f'#{game_version}'}" title="Play Wrdler" target="_self" class="{play_active}">๐ฎ Play</a>
|
| 721 |
<a href="{settings_url if not settings_active else '#settings'}" title="Settings" target="_self" class="{settings_active}">โ๏ธ Settings</a>
|
| 722 |
</nav>
|
|
|
|
| 80 |
)
|
| 81 |
st.html(html, unsafe_allow_javascript=True)
|
| 82 |
|
| 83 |
+
|
| 84 |
+
def is_legendary_score(score: int | float | str) -> bool:
|
| 85 |
+
"""Return True when a score qualifies for Hall of Legends."""
|
| 86 |
+
try:
|
| 87 |
+
return int(score) >= 46
|
| 88 |
+
except Exception:
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
# --- Utility: Convert Matplotlib Figure to PIL RGBA Image ---
|
| 92 |
def fig_to_pil_rgba(fig):
|
| 93 |
"""
|
|
|
|
| 126 |
# Fall back to APP_SETTINGS
|
| 127 |
return APP_SETTINGS.get("game_title", "Wrdler")
|
| 128 |
|
| 129 |
+
def hall_page_url() -> str:
|
| 130 |
+
return "?page=hall"
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def game_page_url() -> str:
|
| 134 |
+
return "?page=play&overlay=0"
|
| 135 |
|
| 136 |
# --- PWA Service Worker JS (for Streamlit head injection) ---
|
| 137 |
pwa_service_worker = """
|
|
|
|
| 644 |
# Determine which link should be highlighted as active
|
| 645 |
play_active = "active" if current_page == "play" else ""
|
| 646 |
leaderboard_active = "active" if current_page in {"today", "daily", "weekly", "history"} else ""
|
| 647 |
+
hall_active = "active" if current_page == "hall" else ""
|
| 648 |
settings_active = "active" if current_page == "settings" else ""
|
| 649 |
game_title = get_effective_game_title()
|
| 650 |
game_version = f"{game_title.lower()}-v{version.replace('.', '-')}"
|
|
|
|
| 666 |
if game_id:
|
| 667 |
today_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
|
| 668 |
leaderboard_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
|
| 669 |
+
hall_url = "?page=hall"
|
| 670 |
play_url = f"?game_id={game_id}#{game_version}"
|
| 671 |
settings_url = f"?page=settings&game_id={game_id}#settings"
|
| 672 |
else:
|
| 673 |
today_url = "?page=today#wrdler-leaderboards"
|
| 674 |
leaderboard_url = "?page=today#wrdler-leaderboards"
|
| 675 |
+
hall_url = "?page=hall"
|
| 676 |
play_url = f"/#{game_version}"
|
| 677 |
settings_url = "?page=settings#settings"
|
| 678 |
|
|
|
|
| 734 |
<div class="bw-footer">
|
| 735 |
<nav class="bw-footer-nav">
|
| 736 |
<a href="{leaderboard_url if not leaderboard_active else '#wrdler-leaderboards'}" title="View Leaderboards" target="_self" class="{leaderboard_active}">๐ Leaderboard</a>
|
| 737 |
+
<a href="{hall_url if not hall_active else '#wrdler-hall'}" title="View Hall of Legends" target="_self" class="{hall_active}">๐๏ธ Hall</a>
|
| 738 |
<a href="{play_url if not play_active else f'#{game_version}'}" title="Play Wrdler" target="_self" class="{play_active}">๐ฎ Play</a>
|
| 739 |
<a href="{settings_url if not settings_active else '#settings'}" title="Settings" target="_self" class="{settings_active}">โ๏ธ Settings</a>
|
| 740 |
</nav>
|