v0.2.7
Browse filesBump version to 0.2.7; enhance leaderboard system
Updated project version to 0.2.7 across code and documentation.
Enhanced the leaderboard system with new features:
- Added last 5 weeks of weekly leaderboards in "Weekly Tab."
- Improved "History Tab" with dropdown selectors and styled UI.
- Refined daily leaderboard rendering for selected weeks.
- Introduced helper functions for week-based operations.
Improved leaderboard page UI:
- Added prominent headers and better layout for history tabs.
- Enhanced display of leaderboard settings and metadata.
Updated documentation to reflect new features and version bump.
Refactored `leaderboard_page.py` for better usability and edge case handling.
Improved code readability, fixed formatting issues, and removed redundancy.
- CLAUDE.md +4 -0
- GAMEPLAY_GUIDE.md +3 -1
- README.md +7 -4
- pyproject.toml +1 -1
- specs/leaderboard_spec.md +11 -5
- specs/requirements.md +3 -2
- specs/specs.md +4 -2
- wrdler/__init__.py +1 -1
- wrdler/leaderboard_page.py +172 -78
- wrdler/settings_page.py +2 -2
- wrdler/ui.py +42 -38
|
@@ -1,3 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Wrdler - Project Context
|
| 2 |
|
| 3 |
## Project Overview
|
|
|
|
| 1 |
+
# CLAUDE
|
| 2 |
+
|
| 3 |
+
Wrdler v0.2.7
|
| 4 |
+
|
| 5 |
# Wrdler - Project Context
|
| 6 |
|
| 7 |
## Project Overview
|
|
@@ -1,5 +1,7 @@
|
|
| 1 |
# Wrdler Gameplay Guide
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
**Last Updated:** 2025-01-31
|
| 4 |
|
| 5 |
## Welcome to Wrdler!
|
|
|
|
| 1 |
# Wrdler Gameplay Guide
|
| 2 |
+
|
| 3 |
+
Version 0.2.7
|
| 4 |
+
|
| 5 |
**Last Updated:** 2025-01-31
|
| 6 |
|
| 7 |
## Welcome to Wrdler!
|
|
@@ -21,6 +21,10 @@ thumbnail: >-
|
|
| 21 |
|
| 22 |
# Wrdler
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
> **Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, adapted for a simpler 8x6 grid, horizontal words only, and free letter guesses at the start.**
|
| 25 |
|
| 26 |
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.
|
|
@@ -104,10 +108,9 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
|
|
| 104 |
|
| 105 |
**Leaderboard Page Features:**
|
| 106 |
- **Today Tab:** Current daily and weekly leaderboards side-by-side
|
| 107 |
-
- Query param filtering: `?gidd={file_id}` and `?gidw={file_id}`
|
| 108 |
- **Daily Tab:** Last 7 days of daily leaderboards with expandable date groups
|
| 109 |
-
- **Weekly Tab:**
|
| 110 |
-
- **History Tab:**
|
| 111 |
|
| 112 |
**Integration:**
|
| 113 |
- Automatic submission after game completion (opt-in via game over popup)
|
|
@@ -432,4 +435,4 @@ Ensure you have the necessary permissions and API access (if required) to use th
|
|
| 432 |
|
| 433 |
For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
|
| 434 |
|
| 435 |
-
Happy gaming and sound designing!
|
|
|
|
| 21 |
|
| 22 |
# Wrdler
|
| 23 |
|
| 24 |
+
Version 0.2.7
|
| 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 |
+
|
| 28 |
> **Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, adapted for a simpler 8x6 grid, horizontal words only, and free letter guesses at the start.**
|
| 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.
|
|
|
|
| 108 |
|
| 109 |
**Leaderboard Page Features:**
|
| 110 |
- **Today Tab:** Current daily and weekly leaderboards side-by-side
|
|
|
|
| 111 |
- **Daily Tab:** Last 7 days of daily leaderboards with expandable date groups
|
| 112 |
+
- **Weekly Tab:** Last 5 weeks, each rendered as its own expander (current or `week=YYYY-Www` query selection opens by default)
|
| 113 |
+
- **History Tab:** Historical leaderboard browser with dropdown selectors
|
| 114 |
|
| 115 |
**Integration:**
|
| 116 |
- Automatic submission after game completion (opt-in via game over popup)
|
|
|
|
| 435 |
|
| 436 |
For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
|
| 437 |
|
| 438 |
+
Happy gaming and sound designing!Happy gaming and sound designing!
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "wrdler"
|
| 3 |
-
version = "0.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"
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "wrdler"
|
| 3 |
+
version = "0.2.7"
|
| 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,7 +1,7 @@
|
|
| 1 |
๏ปฟ# Wrdler Leaderboard System Specification
|
| 2 |
|
| 3 |
-
**Document Version:** 1.4.
|
| 4 |
-
**Project Version:** 0.2.
|
| 5 |
**Author:** GitHub Copilot
|
| 6 |
**Last Updated:** 2025-12-08
|
| 7 |
**Status:** โ
Implemented and Documented
|
|
@@ -57,7 +57,7 @@ This specification documents the implemented **Daily and Weekly Leaderboard Syst
|
|
| 57 |
|
| 58 |
5. **Leaderboard Page**: New Streamlit page displaying:
|
| 59 |
- Last 7 days of daily leaderboards (filtered by current settings)
|
| 60 |
-
-
|
| 61 |
- Historical lookup via dropdown
|
| 62 |
|
| 63 |
6. **Folder-Based Discovery**: No index.json file. Leaderboards are discovered by scanning folder names. Folder names include settings info for fast filtering.
|
|
@@ -527,7 +527,7 @@ Add to `_render_sidebar()` in `ui.py`:
|
|
| 527 |
|
| 528 |
```python
|
| 529 |
st.header("Navigation")
|
| 530 |
-
if st.button("๐ Leaderboards",
|
| 531 |
st.session_state["show_leaderboard_page"] = True
|
| 532 |
st.rerun()
|
| 533 |
```
|
|
@@ -883,4 +883,10 @@ HF_REPO_ID/games/
|
|
| 883 |
"YYYY-MM-DD HH:MM:SS PST to YYYY-MM-DD HH:MM:SS PST"
|
| 884 |
(PST is UTC-8; adjust for daylight saving as needed)
|
| 885 |
For example, a UTC file date of `2025-12-08` covers `2025-12-08 00:00:00 UTC` to `2025-12-08 23:59:59 UTC`, which is displayed as `2025-12-07 16:00:00 PST` to `2025-12-08 15:59:59 PST`.
|
| 886 |
-
The leaderboard expander label should show: `Mon, Dec 08, 2025 4:00 PM PST โ Tue, Dec 09, 2025 3:59:59 PM PST [settings badge]`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
๏ปฟ# Wrdler Leaderboard System Specification
|
| 2 |
|
| 3 |
+
**Document Version:** 1.4.1
|
| 4 |
+
**Project Version:** 0.2.7
|
| 5 |
**Author:** GitHub Copilot
|
| 6 |
**Last Updated:** 2025-12-08
|
| 7 |
**Status:** โ
Implemented and Documented
|
|
|
|
| 57 |
|
| 58 |
5. **Leaderboard Page**: New Streamlit page displaying:
|
| 59 |
- Last 7 days of daily leaderboards (filtered by current settings)
|
| 60 |
+
- Last 5 weeks of weekly leaderboards with per-week expanders (current week open unless `week=YYYY-Www` query overrides)
|
| 61 |
- Historical lookup via dropdown
|
| 62 |
|
| 63 |
6. **Folder-Based Discovery**: No index.json file. Leaderboards are discovered by scanning folder names. Folder names include settings info for fast filtering.
|
|
|
|
| 527 |
|
| 528 |
```python
|
| 529 |
st.header("Navigation")
|
| 530 |
+
if st.button("๐ Leaderboards", width="stretch"):
|
| 531 |
st.session_state["show_leaderboard_page"] = True
|
| 532 |
st.rerun()
|
| 533 |
```
|
|
|
|
| 883 |
"YYYY-MM-DD HH:MM:SS PST to YYYY-MM-DD HH:MM:SS PST"
|
| 884 |
(PST is UTC-8; adjust for daylight saving as needed)
|
| 885 |
For example, a UTC file date of `2025-12-08` covers `2025-12-08 00:00:00 UTC` to `2025-12-08 23:59:59 UTC`, which is displayed as `2025-12-07 16:00:00 PST` to `2025-12-08 15:59:59 PST`.
|
| 886 |
+
The leaderboard expander label should show: `Mon, Dec 08, 2025 4:00 PM PST โ Tue, Dec 09, 2025 3:59:59 PM PST [settings badge]`
|
| 887 |
+
|
| 888 |
+
**Leaderboard Page UI:**
|
| 889 |
+
- **Today Tab:** Current daily and weekly leaderboards
|
| 890 |
+
- **Daily Tab:** Last 7 days of daily leaderboards
|
| 891 |
+
- **Weekly Tab:** Last 5 weeks displayed as individual expanders (current week or `week=YYYY-Www` query opens by default)
|
| 892 |
+
- **History Tab:** Historical leaderboard browser
|
|
@@ -1,5 +1,6 @@
|
|
| 1 |
-
๏ปฟ# Wrdler
|
| 2 |
-
|
|
|
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
**Last Updated:** 2025-12-08
|
| 5 |
|
|
|
|
| 1 |
+
๏ปฟ# Wrdler Requirements
|
| 2 |
+
|
| 3 |
+
**Version:** 0.2.7
|
| 4 |
**Status:** Production Ready - Leaderboards Implemented
|
| 5 |
**Last Updated:** 2025-12-08
|
| 6 |
|
|
@@ -1,5 +1,7 @@
|
|
| 1 |
-
# Wrdler
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
**Last Updated:** 2025-12-08
|
| 5 |
|
|
|
|
| 1 |
+
# Wrdler Specifications
|
| 2 |
+
|
| 3 |
+
**Version:** 0.2.7
|
| 4 |
+
|
| 5 |
**Status:** Production Ready - Leaderboards Implemented
|
| 6 |
**Last Updated:** 2025-12-08
|
| 7 |
|
|
@@ -9,5 +9,5 @@ Key differences from BattleWords:
|
|
| 9 |
- Daily and weekly leaderboards
|
| 10 |
"""
|
| 11 |
|
| 12 |
-
__version__ = "0.2.
|
| 13 |
__all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
|
|
|
|
| 9 |
- Daily and weekly leaderboards
|
| 10 |
"""
|
| 11 |
|
| 12 |
+
__version__ = "0.2.7"
|
| 13 |
__all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
|
|
@@ -9,7 +9,7 @@ __version__ = "0.2.2"
|
|
| 9 |
|
| 10 |
import streamlit as st
|
| 11 |
from datetime import datetime, timedelta, timezone
|
| 12 |
-
from typing import Optional, Tuple
|
| 13 |
import pandas as pd
|
| 14 |
try:
|
| 15 |
from zoneinfo import ZoneInfo
|
|
@@ -93,6 +93,27 @@ def _settings_badge(lb: LeaderboardSettings) -> str:
|
|
| 93 |
return f"\n\n[{mode} โข {source} โข {incorrect} โข {free}]"
|
| 94 |
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
|
| 97 |
"""Render a leaderboard as a styled table."""
|
| 98 |
if title:
|
|
@@ -136,7 +157,7 @@ def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title:
|
|
| 136 |
|
| 137 |
# Calculate height to show all rows (approx 35px per row + 38px header)
|
| 138 |
height = (len(df) + 1) * 35 + 3
|
| 139 |
-
st.dataframe(styled_df,
|
| 140 |
|
| 141 |
# Show entry count and last updated
|
| 142 |
total_entries = len(leaderboard.users)
|
|
@@ -293,31 +314,52 @@ def _render_weekly_tab():
|
|
| 293 |
st.header("๐ Weekly Leaderboards")
|
| 294 |
st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each ISO week. Resets Monday at UTC midnight.")
|
| 295 |
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
st.info("No weekly leaderboards found for the current week.")
|
| 302 |
return
|
| 303 |
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
|
| 323 |
def _render_history_tab():
|
|
@@ -325,69 +367,121 @@ def _render_history_tab():
|
|
| 325 |
st.header("๐ Historical Leaderboards")
|
| 326 |
st.write("Look up past leaderboards.")
|
| 327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
col1, col2 = st.columns(2)
|
| 329 |
|
| 330 |
with col1:
|
| 331 |
st.subheader("Daily History")
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
selected_daily = st.selectbox(
|
| 335 |
-
"Select a date",
|
| 336 |
-
options=daily_periods,
|
| 337 |
-
key="history_daily_select"
|
| 338 |
-
)
|
| 339 |
-
|
| 340 |
-
settings_list = list_settings_for_period("daily", selected_daily)
|
| 341 |
-
if settings_list:
|
| 342 |
-
file_id_options = [s["file_id"] for s in settings_list]
|
| 343 |
-
selected_file_id = st.selectbox(
|
| 344 |
-
"Select settings",
|
| 345 |
-
options=file_id_options,
|
| 346 |
-
format_func=lambda x: x.replace("-", " / "),
|
| 347 |
-
key="history_daily_file_id"
|
| 348 |
-
)
|
| 349 |
-
|
| 350 |
-
if st.button("Load Daily", key="load_daily"):
|
| 351 |
-
leaderboard = load_leaderboard("daily", selected_daily, selected_file_id)
|
| 352 |
-
header = f"Daily: {_get_pst_range_str(selected_daily)} "
|
| 353 |
-
if leaderboard is not None:
|
| 354 |
-
header += _settings_badge(leaderboard)
|
| 355 |
-
_render_leaderboard_table(leaderboard, header)
|
| 356 |
-
else:
|
| 357 |
-
st.info("No leaderboards found for this date.")
|
| 358 |
else:
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
with col2:
|
| 362 |
st.subheader("Weekly History")
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
options=weekly_periods,
|
| 368 |
-
key="history_weekly_select"
|
| 369 |
-
)
|
| 370 |
-
|
| 371 |
-
settings_list = list_settings_for_period("weekly", selected_weekly)
|
| 372 |
-
if settings_list:
|
| 373 |
-
file_id_options = [s["file_id"] for s in settings_list]
|
| 374 |
-
selected_file_id = st.selectbox(
|
| 375 |
-
"Select settings",
|
| 376 |
-
options=file_id_options,
|
| 377 |
-
format_func=lambda x: x.replace("-", " / "),
|
| 378 |
-
key="history_weekly_file_id"
|
| 379 |
-
)
|
| 380 |
-
|
| 381 |
-
if st.button("Load Weekly", key="load_weekly"):
|
| 382 |
-
leaderboard = load_leaderboard("weekly", selected_weekly, selected_file_id)
|
| 383 |
-
header = f"Weekly: {selected_weekly} "
|
| 384 |
-
if leaderboard is not None:
|
| 385 |
-
header += _settings_badge(leaderboard)
|
| 386 |
-
_render_leaderboard_table(leaderboard, header)
|
| 387 |
-
else:
|
| 388 |
-
st.info("No leaderboards found for this week.")
|
| 389 |
else:
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
|
| 393 |
def _render_today_tab():
|
|
|
|
| 9 |
|
| 10 |
import streamlit as st
|
| 11 |
from datetime import datetime, timedelta, timezone
|
| 12 |
+
from typing import Optional, Tuple, List
|
| 13 |
import pandas as pd
|
| 14 |
try:
|
| 15 |
from zoneinfo import ZoneInfo
|
|
|
|
| 93 |
return f"\n\n[{mode} โข {source} โข {incorrect} โข {free}]"
|
| 94 |
|
| 95 |
|
| 96 |
+
def _format_week_title(week_id: str) -> str:
|
| 97 |
+
try:
|
| 98 |
+
year, week = week_id.split("-W")
|
| 99 |
+
return f"Week {int(week)}, {year}"
|
| 100 |
+
except ValueError:
|
| 101 |
+
return week_id
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _get_daily_ids_for_week(week_id: str) -> List[str]:
|
| 105 |
+
try:
|
| 106 |
+
year, week = week_id.split("-W")
|
| 107 |
+
year_int = int(year)
|
| 108 |
+
week_int = int(week)
|
| 109 |
+
return [
|
| 110 |
+
datetime.fromisocalendar(year_int, week_int, weekday).strftime("%Y-%m-%d")
|
| 111 |
+
for weekday in range(1, 8)
|
| 112 |
+
]
|
| 113 |
+
except ValueError:
|
| 114 |
+
return []
|
| 115 |
+
|
| 116 |
+
|
| 117 |
def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
|
| 118 |
"""Render a leaderboard as a styled table."""
|
| 119 |
if title:
|
|
|
|
| 157 |
|
| 158 |
# Calculate height to show all rows (approx 35px per row + 38px header)
|
| 159 |
height = (len(df) + 1) * 35 + 3
|
| 160 |
+
st.dataframe(styled_df, width="stretch", hide_index=True, height=height)
|
| 161 |
|
| 162 |
# Show entry count and last updated
|
| 163 |
total_entries = len(leaderboard.users)
|
|
|
|
| 314 |
st.header("๐ Weekly Leaderboards")
|
| 315 |
st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each ISO week. Resets Monday at UTC midnight.")
|
| 316 |
|
| 317 |
+
try:
|
| 318 |
+
params = st.query_params if hasattr(st, "query_params") else {}
|
| 319 |
+
except Exception:
|
| 320 |
+
params = {}
|
| 321 |
+
|
| 322 |
+
requested_week = params.get("week")
|
| 323 |
+
current_week = get_current_weekly_id()
|
| 324 |
|
| 325 |
+
weekly_periods = list_available_periods("weekly", limit=5)
|
| 326 |
+
if not weekly_periods:
|
| 327 |
+
st.info("No weekly leaderboards found.")
|
|
|
|
| 328 |
return
|
| 329 |
|
| 330 |
+
if requested_week in weekly_periods:
|
| 331 |
+
expanded_week = requested_week
|
| 332 |
+
elif current_week in weekly_periods:
|
| 333 |
+
expanded_week = current_week
|
| 334 |
+
else:
|
| 335 |
+
expanded_week = weekly_periods[0]
|
| 336 |
+
|
| 337 |
+
for week_id in weekly_periods:
|
| 338 |
+
try:
|
| 339 |
+
year, week = week_id.split("-W")
|
| 340 |
+
week_title = f"Week {int(week)}, {year}"
|
| 341 |
+
except ValueError:
|
| 342 |
+
week_title = week_id
|
| 343 |
+
|
| 344 |
+
settings_list = list_settings_for_period("weekly", week_id)
|
| 345 |
+
expander_title = f"๐๏ธ {week_title}"
|
| 346 |
+
with st.expander(expander_title, expanded=(week_id == expanded_week)):
|
| 347 |
+
if not settings_list:
|
| 348 |
+
st.info("No leaderboards found for this week.")
|
| 349 |
+
continue
|
| 350 |
+
|
| 351 |
+
for idx, settings_info in enumerate(settings_list):
|
| 352 |
+
file_id = settings_info["file_id"]
|
| 353 |
+
lb = load_leaderboard("weekly", week_id, file_id)
|
| 354 |
+
if lb is not None:
|
| 355 |
+
st.caption(
|
| 356 |
+
f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
|
| 357 |
+
f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
|
| 358 |
+
f"spacer={lb.puzzle_options.get("spacer", 0)}, overlap={lb.puzzle_options.get("may_overlap", False)}"
|
| 359 |
+
)
|
| 360 |
+
_render_leaderboard_table(lb, "")
|
| 361 |
+
if idx < len(settings_list) - 1:
|
| 362 |
+
st.divider()
|
| 363 |
|
| 364 |
|
| 365 |
def _render_history_tab():
|
|
|
|
| 367 |
st.header("๐ Historical Leaderboards")
|
| 368 |
st.write("Look up past leaderboards.")
|
| 369 |
|
| 370 |
+
weekly_periods = list_available_periods("weekly", limit=12)
|
| 371 |
+
if not weekly_periods:
|
| 372 |
+
st.info("No weekly leaderboards found.")
|
| 373 |
+
return
|
| 374 |
+
|
| 375 |
+
week_options = [(_format_week_title(week_id), week_id) for week_id in weekly_periods]
|
| 376 |
+
week_labels = [label for label, _ in week_options]
|
| 377 |
+
label_to_week = {label: week_id for label, week_id in week_options}
|
| 378 |
+
|
| 379 |
+
# Prominent, styled header for the week selector to make it more noticeable
|
| 380 |
+
st.markdown(
|
| 381 |
+
"""
|
| 382 |
+
<style>
|
| 383 |
+
.history-select-label {
|
| 384 |
+
display:inline-block;
|
| 385 |
+
font-size:1.15rem;
|
| 386 |
+
font-weight:800;
|
| 387 |
+
background: linear-gradient(90deg, rgba(32,212,108,0.12), rgba(29,100,200,0.06));
|
| 388 |
+
padding: 8px 12px;
|
| 389 |
+
border-radius: 8px;
|
| 390 |
+
border: 1px solid rgba(32,212,108,0.18);
|
| 391 |
+
box-shadow: 0 6px 18px rgba(11,42,74,0.12);
|
| 392 |
+
margin-bottom: 6px;
|
| 393 |
+
}
|
| 394 |
+
.history-select-help {
|
| 395 |
+
display:block;
|
| 396 |
+
color:#d7faff;
|
| 397 |
+
margin-bottom:8px;
|
| 398 |
+
opacity:0.95;
|
| 399 |
+
}
|
| 400 |
+
.st-key-history_week_select {
|
| 401 |
+
display: flex;
|
| 402 |
+
justify-content: center;
|
| 403 |
+
align-items: center;
|
| 404 |
+
flex-wrap: wrap;
|
| 405 |
+
flex-direction: row;
|
| 406 |
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
| 407 |
+
padding: 5px;
|
| 408 |
+
max-width: 75%;
|
| 409 |
+
margin: 0 auto;
|
| 410 |
+
align-content: center;
|
| 411 |
+
border: 1px solid rgba(255,255,255,0.5);
|
| 412 |
+
padding: 5px;
|
| 413 |
+
}
|
| 414 |
+
.st-key-history_week_select label { display:none;}
|
| 415 |
+
.history-selectbox-wrap { margin-bottom: 2px; }
|
| 416 |
+
</style>
|
| 417 |
+
<div class="history-selectbox-wrap">
|
| 418 |
+
<div class="history-select-label">๐ Select a week</div>
|
| 419 |
+
<div class="history-select-help">Pick a week to view daily and weekly leaderboards for that week.</div>
|
| 420 |
+
</div>
|
| 421 |
+
""",
|
| 422 |
+
unsafe_allow_html=True
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
# Render the selectbox without a label because we've already shown a styled header above
|
| 426 |
+
selected_label = st.selectbox(
|
| 427 |
+
"",
|
| 428 |
+
options=week_labels,
|
| 429 |
+
key="history_week_select",
|
| 430 |
+
index=0
|
| 431 |
+
)
|
| 432 |
+
selected_week = label_to_week.get(selected_label, weekly_periods[0])
|
| 433 |
+
|
| 434 |
+
available_daily = set(list_available_periods("daily", limit=90))
|
| 435 |
+
week_daily_ids = [d for d in _get_daily_ids_for_week(selected_week) if d in available_daily]
|
| 436 |
+
|
| 437 |
col1, col2 = st.columns(2)
|
| 438 |
|
| 439 |
with col1:
|
| 440 |
st.subheader("Daily History")
|
| 441 |
+
if not week_daily_ids:
|
| 442 |
+
st.info("No daily leaderboards found for this week.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
else:
|
| 444 |
+
for date_id in week_daily_ids:
|
| 445 |
+
date_title = _get_pst_range_str(date_id)
|
| 446 |
+
settings_list = list_settings_for_period("daily", date_id)
|
| 447 |
+
if not settings_list:
|
| 448 |
+
with st.expander(date_title, expanded=False):
|
| 449 |
+
st.info("No leaderboards found for this date.")
|
| 450 |
+
continue
|
| 451 |
+
|
| 452 |
+
for settings_info in settings_list:
|
| 453 |
+
file_id = settings_info["file_id"]
|
| 454 |
+
lb = load_leaderboard("daily", date_id, file_id)
|
| 455 |
+
header_suffix = _settings_badge(lb) if lb is not None else ""
|
| 456 |
+
expander_title = f"{date_title} {header_suffix}" if header_suffix else date_title
|
| 457 |
+
with st.expander(expander_title, expanded=False):
|
| 458 |
+
if lb is not None:
|
| 459 |
+
st.caption(
|
| 460 |
+
f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
|
| 461 |
+
f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
|
| 462 |
+
f"spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}"
|
| 463 |
+
)
|
| 464 |
+
_render_leaderboard_table(lb, "")
|
| 465 |
|
| 466 |
with col2:
|
| 467 |
st.subheader("Weekly History")
|
| 468 |
+
week_title = _format_week_title(selected_week)
|
| 469 |
+
settings_list = list_settings_for_period("weekly", selected_week)
|
| 470 |
+
if not settings_list:
|
| 471 |
+
st.info("No leaderboards found for this week.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
else:
|
| 473 |
+
for idx, settings_info in enumerate(settings_list):
|
| 474 |
+
file_id = settings_info["file_id"]
|
| 475 |
+
lb = load_leaderboard("weekly", selected_week, file_id)
|
| 476 |
+
if lb is not None:
|
| 477 |
+
st.caption(
|
| 478 |
+
f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
|
| 479 |
+
f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
|
| 480 |
+
f"spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}"
|
| 481 |
+
)
|
| 482 |
+
_render_leaderboard_table(lb, week_title if idx == 0 else "")
|
| 483 |
+
if idx < len(settings_list) - 1:
|
| 484 |
+
st.divider()
|
| 485 |
|
| 486 |
|
| 487 |
def _render_today_tab():
|
|
@@ -168,7 +168,7 @@ def render_settings_page(new_game_callback):
|
|
| 168 |
key="ai_generate_btn",
|
| 169 |
help="Generate wordlist from topic",
|
| 170 |
on_click=_on_ai_generate,
|
| 171 |
-
|
| 172 |
)
|
| 173 |
|
| 174 |
# Display wordlist info below topic input
|
|
@@ -236,7 +236,7 @@ def render_settings_page(new_game_callback):
|
|
| 236 |
key="ai_generate_btn",
|
| 237 |
help="Generate wordlist from topic",
|
| 238 |
on_click=_on_ai_generate,
|
| 239 |
-
|
| 240 |
)
|
| 241 |
|
| 242 |
# Display wordlist info below topic input
|
|
|
|
| 168 |
key="ai_generate_btn",
|
| 169 |
help="Generate wordlist from topic",
|
| 170 |
on_click=_on_ai_generate,
|
| 171 |
+
width="stretch",
|
| 172 |
)
|
| 173 |
|
| 174 |
# Display wordlist info below topic input
|
|
|
|
| 236 |
key="ai_generate_btn",
|
| 237 |
help="Generate wordlist from topic",
|
| 238 |
on_click=_on_ai_generate,
|
| 239 |
+
width="stretch",
|
| 240 |
)
|
| 241 |
|
| 242 |
# Display wordlist info below topic input
|
|
@@ -537,7 +537,7 @@ border-radius: 50% !important;
|
|
| 537 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 538 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { flex: 0 0 auto !important; }
|
| 539 |
.bw-grid-row-anchor { height: 0; margin: 0; padding: 0; }
|
| 540 |
-
.st-emotion-cache-1n6tfoc { background: linear-gradient(-45deg, #
|
| 541 |
.st-emotion-cache-1n6tfoc::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; margin: 5px;}
|
| 542 |
.st-key-guess_input, .st-key-guess_submit { flex-direction: row; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: flex-end; }
|
| 543 |
.st-key-guess_input [id^="text_input"] {max-width: 80px;}
|
|
@@ -624,7 +624,7 @@ border-radius: 50% !important;
|
|
| 624 |
max-width:100%;
|
| 625 |
}
|
| 626 |
.stImage {max-width:300px;}
|
| 627 |
-
[id^="text_input"] {
|
| 628 |
background-color:#fff;
|
| 629 |
color:#000;
|
| 630 |
caret-color:#333;}
|
|
@@ -1079,7 +1079,7 @@ def _render_free_letters(state: GameState):
|
|
| 1079 |
if st.button(
|
| 1080 |
letter,
|
| 1081 |
key=f"free_letter_{letter}",
|
| 1082 |
-
|
| 1083 |
type="primary",
|
| 1084 |
on_click=_on_free_letter_click,
|
| 1085 |
args=(letter, state),
|
|
@@ -1746,7 +1746,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1746 |
height=0,
|
| 1747 |
)
|
| 1748 |
|
| 1749 |
-
# Share Challenge
|
| 1750 |
st.markdown("---")
|
| 1751 |
|
| 1752 |
# Style the containing Streamlit block via CSS :has() using an anchor inside this container
|
|
@@ -1784,7 +1784,7 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1784 |
unsafe_allow_html=True,
|
| 1785 |
)
|
| 1786 |
|
| 1787 |
-
st.markdown("### ๐ฎ
|
| 1788 |
|
| 1789 |
# Check if this is a shared game being completed
|
| 1790 |
is_shared_game = st.session_state.get("loaded_game_sid") is not None
|
|
@@ -1860,13 +1860,47 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1860 |
|
| 1861 |
# Check if share URL already generated
|
| 1862 |
if "share_url" not in st.session_state or st.session_state.get("share_url") is None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1863 |
button_text = "๐ Submit Your Result" if is_shared_game else "๐ Generate Share Link"
|
| 1864 |
|
| 1865 |
if not can_submit:
|
| 1866 |
st.warning("โ ๏ธ Please enter your name to submit.")
|
| 1867 |
|
| 1868 |
-
if st.button(button_text, key="generate_share_link",
|
| 1869 |
try:
|
|
|
|
| 1870 |
# Extract game data
|
| 1871 |
word_list = [w.text for w in state.puzzle.words]
|
| 1872 |
spacer = state.puzzle.spacer
|
|
@@ -1941,36 +1975,6 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1941 |
|
| 1942 |
except Exception as e:
|
| 1943 |
st.error(f"Failed to save game: {e}")
|
| 1944 |
-
|
| 1945 |
-
# Show separate "Submit to Leaderboard" button when NOT in a challenge
|
| 1946 |
-
if not is_shared_game and not st.session_state.get("leaderboard_submitted", False):
|
| 1947 |
-
st.markdown("---")
|
| 1948 |
-
st.markdown("##### ๐ Or just submit to leaderboards")
|
| 1949 |
-
if st.button("๐ Submit to Leaderboards", key="submit_leaderboard_only", use_container_width=True, disabled=not can_submit):
|
| 1950 |
-
try:
|
| 1951 |
-
word_list = [w.text for w in state.puzzle.words]
|
| 1952 |
-
lb_results = _submit_to_leaderboards(
|
| 1953 |
-
username, state.score, elapsed_seconds, word_list
|
| 1954 |
-
)
|
| 1955 |
-
st.session_state["leaderboard_submitted"] = True
|
| 1956 |
-
st.session_state["leaderboard_results"] = lb_results
|
| 1957 |
-
|
| 1958 |
-
# Show results
|
| 1959 |
-
daily_info = lb_results.get("daily", {})
|
| 1960 |
-
weekly_info = lb_results.get("weekly", {})
|
| 1961 |
-
|
| 1962 |
-
if daily_info.get("qualified") or weekly_info.get("qualified"):
|
| 1963 |
-
msg_parts = []
|
| 1964 |
-
if daily_info.get("qualified"):
|
| 1965 |
-
msg_parts.append(f"Daily #{daily_info['rank']}")
|
| 1966 |
-
if weekly_info.get("qualified"):
|
| 1967 |
-
msg_parts.append(f"Weekly #{weekly_info['rank']}")
|
| 1968 |
-
st.success(f"โ
Submitted! Ranked: {', '.join(msg_parts)}")
|
| 1969 |
-
else:
|
| 1970 |
-
st.info("โ
Score submitted (not in top 20)")
|
| 1971 |
-
st.rerun()
|
| 1972 |
-
except Exception as e:
|
| 1973 |
-
st.error(f"Failed to submit to leaderboards: {e}")
|
| 1974 |
else:
|
| 1975 |
# Conditionally display the generated share URL
|
| 1976 |
if st.session_state.get("show_challenge_share_links", False):
|
|
@@ -2290,11 +2294,11 @@ def run_app():
|
|
| 2290 |
_render_footer(current_page="settings")
|
| 2291 |
return
|
| 2292 |
|
| 2293 |
-
if page in {"today", "daily", "weekly"}:
|
| 2294 |
from .leaderboard_page import render_leaderboard_page
|
| 2295 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 2296 |
inject_ocean_layers()
|
| 2297 |
-
default = page if page in {"daily", "weekly"} else "today"
|
| 2298 |
render_leaderboard_page(default_tab=default)
|
| 2299 |
_render_footer(current_page=page if page else "play")
|
| 2300 |
return
|
|
|
|
| 537 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 538 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { flex: 0 0 auto !important; }
|
| 539 |
.bw-grid-row-anchor { height: 0; margin: 0; padding: 0; }
|
| 540 |
+
.st-emotion-cache-1n6tfoc { background: linear-gradient(-45deg, #a1a1c1, #1d64c8, #a1a1c1, #666666) !important; gap: 0.1rem !important; color: white; border-radius:15px; padding: 10px 10px 10px 10px; }
|
| 541 |
.st-emotion-cache-1n6tfoc::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; margin: 5px;}
|
| 542 |
.st-key-guess_input, .st-key-guess_submit { flex-direction: row; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: flex-end; }
|
| 543 |
.st-key-guess_input [id^="text_input"] {max-width: 80px;}
|
|
|
|
| 624 |
max-width:100%;
|
| 625 |
}
|
| 626 |
.stImage {max-width:300px;}
|
| 627 |
+
[id^="text_input"], .st-bb [id^="text_input"] {
|
| 628 |
background-color:#fff;
|
| 629 |
color:#000;
|
| 630 |
caret-color:#333;}
|
|
|
|
| 1079 |
if st.button(
|
| 1080 |
letter,
|
| 1081 |
key=f"free_letter_{letter}",
|
| 1082 |
+
width="stretch",
|
| 1083 |
type="primary",
|
| 1084 |
on_click=_on_free_letter_click,
|
| 1085 |
args=(letter, state),
|
|
|
|
| 1746 |
height=0,
|
| 1747 |
)
|
| 1748 |
|
| 1749 |
+
# Submit and Share Challenge Buttons
|
| 1750 |
st.markdown("---")
|
| 1751 |
|
| 1752 |
# Style the containing Streamlit block via CSS :has() using an anchor inside this container
|
|
|
|
| 1784 |
unsafe_allow_html=True,
|
| 1785 |
)
|
| 1786 |
|
| 1787 |
+
st.markdown("### ๐ฎ Enter Name to submit to leaderboard")
|
| 1788 |
|
| 1789 |
# Check if this is a shared game being completed
|
| 1790 |
is_shared_game = st.session_state.get("loaded_game_sid") is not None
|
|
|
|
| 1860 |
|
| 1861 |
# Check if share URL already generated
|
| 1862 |
if "share_url" not in st.session_state or st.session_state.get("share_url") is None:
|
| 1863 |
+
|
| 1864 |
+
|
| 1865 |
+
# Show separate "Submit to Leaderboard" button when NOT in a challenge
|
| 1866 |
+
if not is_shared_game and not st.session_state.get("leaderboard_submitted", False):
|
| 1867 |
+
|
| 1868 |
+
#st.markdown("##### ๐ Or just submit to leaderboards")
|
| 1869 |
+
if st.button("๐ Submit to Leaderboards", key="submit_leaderboard_only", width="stretch", disabled=not can_submit):
|
| 1870 |
+
try:
|
| 1871 |
+
word_list = [w.text for w in state.puzzle.words]
|
| 1872 |
+
lb_results = _submit_to_leaderboards(
|
| 1873 |
+
username, state.score, elapsed_seconds, word_list
|
| 1874 |
+
)
|
| 1875 |
+
st.session_state["leaderboard_submitted"] = True
|
| 1876 |
+
st.session_state["leaderboard_results"] = lb_results
|
| 1877 |
+
|
| 1878 |
+
# Show results
|
| 1879 |
+
daily_info = lb_results.get("daily", {})
|
| 1880 |
+
weekly_info = lb_results.get("weekly", {})
|
| 1881 |
+
|
| 1882 |
+
if daily_info.get("qualified") or weekly_info.get("qualified"):
|
| 1883 |
+
msg_parts = []
|
| 1884 |
+
if daily_info.get("qualified"):
|
| 1885 |
+
msg_parts.append(f"Daily #{daily_info['rank']}")
|
| 1886 |
+
if weekly_info.get("qualified"):
|
| 1887 |
+
msg_parts.append(f"Weekly #{weekly_info['rank']}")
|
| 1888 |
+
st.success(f"โ
Submitted! Ranked: {', '.join(msg_parts)}")
|
| 1889 |
+
else:
|
| 1890 |
+
st.info("โ
Score submitted (not in top 20)")
|
| 1891 |
+
st.rerun()
|
| 1892 |
+
except Exception as e:
|
| 1893 |
+
st.error(f"Failed to submit to leaderboards: {e}")
|
| 1894 |
+
|
| 1895 |
+
|
| 1896 |
button_text = "๐ Submit Your Result" if is_shared_game else "๐ Generate Share Link"
|
| 1897 |
|
| 1898 |
if not can_submit:
|
| 1899 |
st.warning("โ ๏ธ Please enter your name to submit.")
|
| 1900 |
|
| 1901 |
+
if st.button(button_text, key="generate_share_link", width="stretch", disabled=not can_submit):
|
| 1902 |
try:
|
| 1903 |
+
st.markdown("---")
|
| 1904 |
# Extract game data
|
| 1905 |
word_list = [w.text for w in state.puzzle.words]
|
| 1906 |
spacer = state.puzzle.spacer
|
|
|
|
| 1975 |
|
| 1976 |
except Exception as e:
|
| 1977 |
st.error(f"Failed to save game: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1978 |
else:
|
| 1979 |
# Conditionally display the generated share URL
|
| 1980 |
if st.session_state.get("show_challenge_share_links", False):
|
|
|
|
| 2294 |
_render_footer(current_page="settings")
|
| 2295 |
return
|
| 2296 |
|
| 2297 |
+
if page in {"today", "daily", "weekly", "history"}:
|
| 2298 |
from .leaderboard_page import render_leaderboard_page
|
| 2299 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 2300 |
inject_ocean_layers()
|
| 2301 |
+
default = page if page in {"daily", "weekly", "history"} else "today"
|
| 2302 |
render_leaderboard_page(default_tab=default)
|
| 2303 |
_render_footer(current_page=page if page else "play")
|
| 2304 |
return
|