Leaderboard Page Identity Update
Browse files- specs/leaderboard_spec.md +182 -140
- wrdler/leaderboard_page.py +89 -33
specs/leaderboard_spec.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
# Wrdler Leaderboard System Specification
|
| 2 |
|
| 3 |
-
**Document Version:** 1.3.
|
| 4 |
**Target Project Version:** 0.2.0
|
| 5 |
**Author:** GitHub Copilot
|
| 6 |
**Date:** 2025-01-27
|
|
@@ -22,6 +22,7 @@
|
|
| 22 |
10. [UI Components](#10-ui-components)
|
| 23 |
11. [Testing Requirements](#11-testing-requirements)
|
| 24 |
12. [Migration Notes](#12-migration-notes)
|
|
|
|
| 25 |
|
| 26 |
---
|
| 27 |
|
|
@@ -92,29 +93,29 @@ Each date/week has settings-based subfolders. The folder name (file_id) encodes
|
|
| 92 |
|
| 93 |
```
|
| 94 |
HF_REPO_ID/
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
```
|
| 119 |
|
| 120 |
### 3.2 File ID Format
|
|
@@ -153,52 +154,52 @@ Instead of maintaining an `index.json` file, leaderboards are discovered by:
|
|
| 153 |
### 3.4 Data Flow
|
| 154 |
|
| 155 |
```
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
```
|
| 203 |
|
| 204 |
---
|
|
@@ -524,7 +525,7 @@ Add to `_render_sidebar()` in `ui.py`:
|
|
| 524 |
|
| 525 |
```python
|
| 526 |
st.header("Navigation")
|
| 527 |
-
if st.button("
|
| 528 |
st.session_state["show_leaderboard_page"] = True
|
| 529 |
st.rerun()
|
| 530 |
```
|
|
@@ -539,9 +540,9 @@ Modify `_game_over_content()` in `ui.py` to:
|
|
| 539 |
```python
|
| 540 |
# After score submission
|
| 541 |
if results["daily"]["qualified"]:
|
| 542 |
-
st.success(f"
|
| 543 |
if results["weekly"]["qualified"]:
|
| 544 |
-
st.success(f"
|
| 545 |
```
|
| 546 |
|
| 547 |
### 10.3 Leaderboard Page Routing
|
|
@@ -553,7 +554,7 @@ In `run_app()`:
|
|
| 553 |
if st.session_state.get("show_leaderboard_page", False):
|
| 554 |
from wrdler.leaderboard_page import render_leaderboard_page
|
| 555 |
render_leaderboard_page()
|
| 556 |
-
if st.button("
|
| 557 |
st.session_state["show_leaderboard_page"] = False
|
| 558 |
st.rerun()
|
| 559 |
return # Don't render game UI
|
|
@@ -606,7 +607,7 @@ class TestDateIds:
|
|
| 606 |
|
| 607 |
### Integration Tests
|
| 608 |
|
| 609 |
-
- Test full flow: game completion
|
| 610 |
- Test with mock HuggingFace repository
|
| 611 |
- Test folder-based discovery logic
|
| 612 |
- Test concurrent submissions (edge case)
|
|
@@ -647,67 +648,108 @@ class TestDateIds:
|
|
| 647 |
|
| 648 |
---
|
| 649 |
|
| 650 |
-
##
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ο»Ώ# Wrdler Leaderboard System Specification
|
| 2 |
|
| 3 |
+
**Document Version:** 1.3.1
|
| 4 |
**Target Project Version:** 0.2.0
|
| 5 |
**Author:** GitHub Copilot
|
| 6 |
**Date:** 2025-01-27
|
|
|
|
| 22 |
10. [UI Components](#10-ui-components)
|
| 23 |
11. [Testing Requirements](#11-testing-requirements)
|
| 24 |
12. [Migration Notes](#12-migration-notes)
|
| 25 |
+
13. [Operational Considerations](#13-operational-considerations)
|
| 26 |
|
| 27 |
---
|
| 28 |
|
|
|
|
| 93 |
|
| 94 |
```
|
| 95 |
HF_REPO_ID/
|
| 96 |
+
βββ games/ # All game-related storage
|
| 97 |
+
β βββ {challenge_id}/ # Existing challenge storage
|
| 98 |
+
β β βββ settings.json # entry_type: "challenge"
|
| 99 |
+
β βββ leaderboards/
|
| 100 |
+
β βββ daily/
|
| 101 |
+
β β βββ 2025-01-27/
|
| 102 |
+
β β β βββ classic-classic-0/
|
| 103 |
+
β β β β βββ settings.json
|
| 104 |
+
β β β βββ easy-easy-0/
|
| 105 |
+
β β β βββ settings.json
|
| 106 |
+
β β βββ 2025-01-26/
|
| 107 |
+
β β βββ classic-classic-0/
|
| 108 |
+
β β βββ settings.json
|
| 109 |
+
β βββ weekly/
|
| 110 |
+
β βββ 2025-W04/
|
| 111 |
+
β β βββ classic-classic-0/
|
| 112 |
+
β β β βββ settings.json
|
| 113 |
+
β β βββ easy-too_easy-0/
|
| 114 |
+
β β βββ settings.json
|
| 115 |
+
β βββ 2025-W03/
|
| 116 |
+
β βββ classic-classic-0/
|
| 117 |
+
β βββ settings.json
|
| 118 |
+
βββ shortener.json # Existing URL shortener
|
| 119 |
```
|
| 120 |
|
| 121 |
### 3.2 File ID Format
|
|
|
|
| 154 |
### 3.4 Data Flow
|
| 155 |
|
| 156 |
```
|
| 157 |
+
ββββββββββββββββββββββ
|
| 158 |
+
β Game Completion β
|
| 159 |
+
ββββββββββββββββββββββ
|
| 160 |
+
β
|
| 161 |
+
βΌ
|
| 162 |
+
ββββββββββββββββββββββ
|
| 163 |
+
β Get current game β
|
| 164 |
+
β settings β
|
| 165 |
+
ββββββββββββββββββββββ
|
| 166 |
+
β
|
| 167 |
+
βΌ
|
| 168 |
+
ββββββββββββββββββββββ
|
| 169 |
+
β Build file_id β
|
| 170 |
+
β prefix from β
|
| 171 |
+
β settings β
|
| 172 |
+
ββββββββββββββββββββββ
|
| 173 |
+
β
|
| 174 |
+
βΌ
|
| 175 |
+
ββββββββββββββββββββββ
|
| 176 |
+
β Scan folders for β
|
| 177 |
+
β matching file_id β
|
| 178 |
+
β or create new β
|
| 179 |
+
ββββββββββββββββββββββ
|
| 180 |
+
β
|
| 181 |
+
βββββββ΄ββββββ
|
| 182 |
+
β β
|
| 183 |
+
βΌ βΌ
|
| 184 |
+
βββββββββ βββββββββββββ
|
| 185 |
+
β Daily β β Weekly β
|
| 186 |
+
β LB β β LB β
|
| 187 |
+
βββββββββ βββββββββββββ
|
| 188 |
+
β β
|
| 189 |
+
ββββββ¬ββββββ
|
| 190 |
+
β
|
| 191 |
+
βΌ
|
| 192 |
+
βββββββββββββββββββββββ
|
| 193 |
+
β Check if score β
|
| 194 |
+
β qualifies (top β
|
| 195 |
+
β 20 displayed) β
|
| 196 |
+
βββββββββββββββββββββββ
|
| 197 |
+
β
|
| 198 |
+
βΌ
|
| 199 |
+
βββββββββββββββββββββββ
|
| 200 |
+
β Update & Upload β
|
| 201 |
+
β to HF repo β
|
| 202 |
+
βββββββββββββββββββββββ
|
| 203 |
```
|
| 204 |
|
| 205 |
---
|
|
|
|
| 525 |
|
| 526 |
```python
|
| 527 |
st.header("Navigation")
|
| 528 |
+
if st.button("π Leaderboards", use_container_width=True):
|
| 529 |
st.session_state["show_leaderboard_page"] = True
|
| 530 |
st.rerun()
|
| 531 |
```
|
|
|
|
| 540 |
```python
|
| 541 |
# After score submission
|
| 542 |
if results["daily"]["qualified"]:
|
| 543 |
+
st.success(f"π You ranked #{results['daily']['rank']} on today's leaderboard!")
|
| 544 |
if results["weekly"]["qualified"]:
|
| 545 |
+
st.success(f"π You ranked #{results['weekly']['rank']} on this week's leaderboard!")
|
| 546 |
```
|
| 547 |
|
| 548 |
### 10.3 Leaderboard Page Routing
|
|
|
|
| 554 |
if st.session_state.get("show_leaderboard_page", False):
|
| 555 |
from wrdler.leaderboard_page import render_leaderboard_page
|
| 556 |
render_leaderboard_page()
|
| 557 |
+
if st.button("β Back to Game"):
|
| 558 |
st.session_state["show_leaderboard_page"] = False
|
| 559 |
st.rerun()
|
| 560 |
return # Don't render game UI
|
|
|
|
| 607 |
|
| 608 |
### Integration Tests
|
| 609 |
|
| 610 |
+
- Test full flow: game completion β leaderboard submission β retrieval
|
| 611 |
- Test with mock HuggingFace repository
|
| 612 |
- Test folder-based discovery logic
|
| 613 |
- Test concurrent submissions (edge case)
|
|
|
|
| 648 |
|
| 649 |
---
|
| 650 |
|
| 651 |
+
## 13. Operational Considerations
|
| 652 |
+
|
| 653 |
+
### 13.1 Concurrency and Consistency
|
| 654 |
+
|
| 655 |
+
- Write model:
|
| 656 |
+
- Use optimistic concurrency: read `settings.json`, merge new `users` entry, write back with a unique commit message including `challenge_id` and `uid`.
|
| 657 |
+
- Implement retry with exponential backoff on HTTP 409/5xx or checksum mismatch.
|
| 658 |
+
- Ensure atomicity per file write: do not split updates across multiple files per leaderboard.
|
| 659 |
+
- Simultaneous file creation:
|
| 660 |
+
- When no matching `file_id` folder exists, first attempt creation; if a concurrent process creates it, fallback to loading and merging.
|
| 661 |
+
- Always re-verify settings match by reading `settings.json` after folder discovery.
|
| 662 |
+
- Sequence collisions:
|
| 663 |
+
- If `{wordlist}-{mode}-{sequence}` collides but settings differ, increment `sequence` until a unique folder is found; verify match via file content, not only prefix.
|
| 664 |
+
|
| 665 |
+
### 13.2 Caching and Discovery Performance
|
| 666 |
+
|
| 667 |
+
- Cache tiers:
|
| 668 |
+
- In-memory (per app session):
|
| 669 |
+
- Period listings for `games/leaderboards/{type}/` (TTL 60s).
|
| 670 |
+
- `file_id` listings inside a period (TTL 30s).
|
| 671 |
+
- Loaded `settings.json` for leaderboards (TTL 15s or invalidated on write).
|
| 672 |
+
- Invalidation:
|
| 673 |
+
- On successful submission, invalidate the specific leaderboard cache (file content and directory listing for that period).
|
| 674 |
+
- Provide a manual refresh option in UI (leaderboard page).
|
| 675 |
+
- Discovery limits:
|
| 676 |
+
- Cap directory scans to the most recent N periods (configurable; default 30). UI uses explicit period selection for older data.
|
| 677 |
+
- Prefer prefix filtering client-side before loading file content.
|
| 678 |
+
|
| 679 |
+
### 13.3 Error Handling and Observability
|
| 680 |
+
|
| 681 |
+
- Error taxonomy:
|
| 682 |
+
- Storage errors: `HF_STORAGE_UNAVAILABLE`, `HF_WRITE_CONFLICT`, `HF_NOT_FOUND`.
|
| 683 |
+
- Validation errors: `LB_INVALID_INPUT`, `LB_SETTINGS_MISMATCH`.
|
| 684 |
+
- Operational errors: `LB_TIMEOUT`, `LB_RETRY_EXCEEDED`.
|
| 685 |
+
- User feedback:
|
| 686 |
+
- On non-critical failure (e.g., leaderboard write conflict), show non-blocking warning and retry silently up to 3 times.
|
| 687 |
+
- Logging:
|
| 688 |
+
- Log submission events with fields: `entry_type`, `period_id`, `file_id`, `uid`, `score`, `time`, `rank_result`, `repo_path`, `latency_ms`.
|
| 689 |
+
- Log error events with `code`, `message`, `attempt`, `backoff_ms`.
|
| 690 |
+
- Telemetry (optional):
|
| 691 |
+
- Count successful submissions per period and per settings combination for basic monitoring.
|
| 692 |
+
|
| 693 |
+
### 13.4 Security and Abuse Controls
|
| 694 |
+
|
| 695 |
+
- Input validation:
|
| 696 |
+
- `username`: max 40 chars, strip control chars, allow alphanumerics, spaces, basic punctuation; reject offensive content if possible.
|
| 697 |
+
- `word_list`: array of 6 uppercase AβZ strings, length 3β10; drop invalid entries.
|
| 698 |
+
- `score`: 0β999; `time`: 1β36000 (10 hours); `word_list_difficulty`: float if provided, clamp to 0β10000.
|
| 699 |
+
- Spam and duplicates:
|
| 700 |
+
- Rate limit per IP/session (e.g., max 10 submissions per hour).
|
| 701 |
+
- Detect duplicate entries by same `uid` + `timestamp` within 10 seconds window; deduplicate silently.
|
| 702 |
+
- Repository permissions:
|
| 703 |
+
- Submissions require HF write permissions for the space; ensure credentials are scoped to the specific repo.
|
| 704 |
+
- Do not expose write tokens in client logs; keep server-side commit operations.
|
| 705 |
+
|
| 706 |
+
### 13.5 Data Lifecycle and Retention
|
| 707 |
+
|
| 708 |
+
- Retention:
|
| 709 |
+
- Keep daily leaderboards for 365 days; weekly leaderboards for 156 weeks (3 years).
|
| 710 |
+
- Optional archival: move older periods to `games/leaderboards_archive/{type}/` or leave as-is with documented retention.
|
| 711 |
+
- Cleanup:
|
| 712 |
+
- Provide a maintenance script to prune old periods and reindex cache.
|
| 713 |
+
- Privacy:
|
| 714 |
+
- Store only display names and gameplay metrics; avoid PII.
|
| 715 |
+
- Users may choose βAnonymousβ; do not display IP or identifiers publicly.
|
| 716 |
+
|
| 717 |
+
### 13.6 Time and Period Boundaries
|
| 718 |
+
|
| 719 |
+
- Timezone:
|
| 720 |
+
- All operations use UTC. The periods roll over at 00:00:00 UTC for daily, Monday 00:00:00 UTC for weekly.
|
| 721 |
+
- ISO week:
|
| 722 |
+
- Use Pythonβs `isocalendar()` to derive `YYYY-Www` and handle year transitions (weeks spanning year boundaries).
|
| 723 |
+
- Clock source:
|
| 724 |
+
- Use server-side timestamp for submissions; do not trust client clock. If unavailable, fall back to Python `datetime.utcnow()`.
|
| 725 |
+
|
| 726 |
+
### 13.7 UI Reliability and UX
|
| 727 |
+
|
| 728 |
+
- Loading states:
|
| 729 |
+
- Show skeleton/loading indicators while scanning folders or reading leaderboard JSON.
|
| 730 |
+
- Empty states:
|
| 731 |
+
- Display βNo entries yetβ when a leaderboard exists without users or has not been created.
|
| 732 |
+
- Accessibility:
|
| 733 |
+
- Ensure sufficient color contrast, keyboard navigation for tabs/period selectors, and alt text for icons.
|
| 734 |
+
- Internationalization (future):
|
| 735 |
+
- Keep date/time ISO formatting and English labels; design UI to allow future localization.
|
| 736 |
+
|
| 737 |
+
### 13.8 Ranking and Tie-Breaks (Operational Clarification)
|
| 738 |
+
|
| 739 |
+
- Sort order:
|
| 740 |
+
- Primary: `score` desc; secondary: `time` asc; tertiary: `word_list_difficulty` desc; quaternary: stable by `timestamp` asc.
|
| 741 |
+
- Display limit:
|
| 742 |
+
- Always store full `users` list; apply `max_display_entries` at render time only.
|
| 743 |
+
- Rank reporting:
|
| 744 |
+
- Return rank based on full sorted list even if not displayed; if outside display limit, mark `qualified=False`.
|
| 745 |
+
|
| 746 |
+
### 13.9 Commit and Retry Strategy (HF)
|
| 747 |
+
|
| 748 |
+
- Commit messages:
|
| 749 |
+
- Format: `leaderboard:{entry_type}:{period_id}:{file_id} add uid={uid} score={score} time={time}`.
|
| 750 |
+
- Retries:
|
| 751 |
+
- Backoff sequence: 0.25s, 0.5s, 1s; max 3 attempts; abort on `LB_SETTINGS_MISMATCH`.
|
| 752 |
+
- Partial failures:
|
| 753 |
+
- If daily succeeds and weekly fails (or vice versa), return both statuses independently; UI reports partial success.
|
| 754 |
+
|
| 755 |
+
---
|
wrdler/leaderboard_page.py
CHANGED
|
@@ -9,7 +9,7 @@ __version__ = "0.2.0"
|
|
| 9 |
|
| 10 |
import streamlit as st
|
| 11 |
from datetime import datetime
|
| 12 |
-
from typing import Optional
|
| 13 |
import pandas as pd
|
| 14 |
|
| 15 |
from wrdler.leaderboard import (
|
|
@@ -44,6 +44,17 @@ def _get_rank_emoji(rank: int) -> str:
|
|
| 44 |
return f"{rank}."
|
| 45 |
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
|
| 48 |
"""Render a leaderboard as a styled table."""
|
| 49 |
if title:
|
|
@@ -61,7 +72,7 @@ def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title:
|
|
| 61 |
for i, user in enumerate(display_users, 1):
|
| 62 |
rank_display = _get_rank_emoji(i)
|
| 63 |
time_display = _format_time(user.time)
|
| 64 |
-
difficulty = f"{user.word_list_difficulty:.2f}" if user.word_list_difficulty else "-"
|
| 65 |
|
| 66 |
# Show challenge indicator if from a challenge
|
| 67 |
challenge_badge = " π―" if user.source_challenge_id else ""
|
|
@@ -118,6 +129,22 @@ def render_leaderboard_page(default_tab: str = "daily"):
|
|
| 118 |
game_title = APP_SETTINGS.get("game_title", "Wrdler")
|
| 119 |
st.title(f"π {game_title} Leaderboards")
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
# Tab selection - set default based on parameter
|
| 122 |
tab_names = ["π
Daily", "π Weekly", "π History"]
|
| 123 |
|
|
@@ -139,48 +166,73 @@ def _render_daily_tab():
|
|
| 139 |
st.header("π
Daily Leaderboards")
|
| 140 |
st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each day. Resets at UTC midnight.")
|
| 141 |
|
| 142 |
-
#
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
daily_boards = get_last_n_daily_leaderboards(7, settings)
|
| 148 |
|
| 149 |
-
for date_id
|
| 150 |
-
#
|
| 151 |
try:
|
| 152 |
date_obj = datetime.strptime(date_id, "%Y-%m-%d")
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
else:
|
| 156 |
-
title = date_obj.strftime("%A, %B %d, %Y")
|
| 157 |
except ValueError:
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
|
| 164 |
def _render_weekly_tab():
|
| 165 |
"""Render weekly leaderboard tab."""
|
| 166 |
-
st.header("π Weekly
|
| 167 |
-
st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for
|
| 168 |
-
|
| 169 |
-
# Get current game settings for filtering
|
| 170 |
-
settings = _get_current_game_settings()
|
| 171 |
-
st.info(f"Showing leaderboard for: **{settings.game_mode}** mode, **{settings.wordlist_source}**")
|
| 172 |
|
| 173 |
weekly_id = get_current_weekly_id()
|
| 174 |
-
_, leaderboard = find_matching_leaderboard("weekly", weekly_id, settings)
|
| 175 |
|
| 176 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
try:
|
| 178 |
year, week = weekly_id.split("-W")
|
| 179 |
-
|
| 180 |
except ValueError:
|
| 181 |
-
|
| 182 |
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
|
| 186 |
def _render_history_tab():
|
|
@@ -200,7 +252,6 @@ def _render_history_tab():
|
|
| 200 |
key="history_daily_select"
|
| 201 |
)
|
| 202 |
|
| 203 |
-
# Show available settings for this period
|
| 204 |
settings_list = list_settings_for_period("daily", selected_daily)
|
| 205 |
if settings_list:
|
| 206 |
file_id_options = [s["file_id"] for s in settings_list]
|
|
@@ -213,7 +264,10 @@ def _render_history_tab():
|
|
| 213 |
|
| 214 |
if st.button("Load Daily", key="load_daily"):
|
| 215 |
leaderboard = load_leaderboard("daily", selected_daily, selected_file_id)
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
| 217 |
else:
|
| 218 |
st.info("No leaderboards found for this date.")
|
| 219 |
else:
|
|
@@ -229,7 +283,6 @@ def _render_history_tab():
|
|
| 229 |
key="history_weekly_select"
|
| 230 |
)
|
| 231 |
|
| 232 |
-
# Show available settings for this period
|
| 233 |
settings_list = list_settings_for_period("weekly", selected_weekly)
|
| 234 |
if settings_list:
|
| 235 |
file_id_options = [s["file_id"] for s in settings_list]
|
|
@@ -242,7 +295,10 @@ def _render_history_tab():
|
|
| 242 |
|
| 243 |
if st.button("Load Weekly", key="load_weekly"):
|
| 244 |
leaderboard = load_leaderboard("weekly", selected_weekly, selected_file_id)
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
| 246 |
else:
|
| 247 |
st.info("No leaderboards found for this week.")
|
| 248 |
else:
|
|
|
|
| 9 |
|
| 10 |
import streamlit as st
|
| 11 |
from datetime import datetime
|
| 12 |
+
from typing import Optional, Tuple
|
| 13 |
import pandas as pd
|
| 14 |
|
| 15 |
from wrdler.leaderboard import (
|
|
|
|
| 44 |
return f"{rank}."
|
| 45 |
|
| 46 |
|
| 47 |
+
def _settings_badge(lb: LeaderboardSettings) -> str:
|
| 48 |
+
"""Build a compact settings badge for headers."""
|
| 49 |
+
mode = lb.game_mode
|
| 50 |
+
source = lb.wordlist_source
|
| 51 |
+
incorrect = "errors:on" if lb.show_incorrect_guesses else "errors:off"
|
| 52 |
+
free = "free:on" if lb.enable_free_letters else "free:off"
|
| 53 |
+
spacer = lb.puzzle_options.get("spacer", 0)
|
| 54 |
+
overlap = lb.puzzle_options.get("may_overlap", False)
|
| 55 |
+
return f"[{mode} β’ {source} β’ {incorrect} β’ {free} β’ spacer:{spacer} β’ overlap:{'yes' if overlap else 'no'}]"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
|
| 59 |
"""Render a leaderboard as a styled table."""
|
| 60 |
if title:
|
|
|
|
| 72 |
for i, user in enumerate(display_users, 1):
|
| 73 |
rank_display = _get_rank_emoji(i)
|
| 74 |
time_display = _format_time(user.time)
|
| 75 |
+
difficulty = f"{user.word_list_difficulty:.2f}" if user.word_list_difficulty is not None else "-"
|
| 76 |
|
| 77 |
# Show challenge indicator if from a challenge
|
| 78 |
challenge_badge = " π―" if user.source_challenge_id else ""
|
|
|
|
| 129 |
game_title = APP_SETTINGS.get("game_title", "Wrdler")
|
| 130 |
st.title(f"π {game_title} Leaderboards")
|
| 131 |
|
| 132 |
+
# Inject CSS to style the active tab
|
| 133 |
+
st.markdown(
|
| 134 |
+
"""
|
| 135 |
+
<style>
|
| 136 |
+
/* Target Streamlit tabs: pick the selected tab button/link */
|
| 137 |
+
div[role="tablist"] p {padding: 0.5em 1em !important;}
|
| 138 |
+
div[role="tablist"] [aria-selected="true"] {
|
| 139 |
+
color: #ffffff !important;
|
| 140 |
+
border: 1px solid #4CAF50 !important;
|
| 141 |
+
}
|
| 142 |
+
div[role="tablist"] [aria-selected="true"] p {font-weight: bold !important;}
|
| 143 |
+
</style>
|
| 144 |
+
""",
|
| 145 |
+
unsafe_allow_html=True
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
# Tab selection - set default based on parameter
|
| 149 |
tab_names = ["π
Daily", "π Weekly", "π History"]
|
| 150 |
|
|
|
|
| 166 |
st.header("π
Daily Leaderboards")
|
| 167 |
st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each day. Resets at UTC midnight.")
|
| 168 |
|
| 169 |
+
# Show last 7 days; for each day, show all leaderboards (settings combos)
|
| 170 |
+
daily_periods = list_available_periods("daily", limit=7)
|
| 171 |
+
if not daily_periods:
|
| 172 |
+
st.info("No daily leaderboards found.")
|
| 173 |
+
return
|
|
|
|
| 174 |
|
| 175 |
+
for date_id in daily_periods:
|
| 176 |
+
# Human friendly date title
|
| 177 |
try:
|
| 178 |
date_obj = datetime.strptime(date_id, "%Y-%m-%d")
|
| 179 |
+
is_today = date_id == get_current_daily_id()
|
| 180 |
+
date_title = ("π Today " if is_today else "") + date_obj.strftime("%A, %B %d, %Y")
|
|
|
|
|
|
|
| 181 |
except ValueError:
|
| 182 |
+
date_title = date_id
|
| 183 |
+
|
| 184 |
+
# List all settings folders (file_ids) for this period
|
| 185 |
+
settings_list = list_settings_for_period("daily", date_id)
|
| 186 |
+
if not settings_list:
|
| 187 |
+
with st.expander(date_title, expanded=False):
|
| 188 |
+
st.info("No leaderboards for this date.")
|
| 189 |
+
continue
|
| 190 |
+
|
| 191 |
+
# Render one expander per settings combination
|
| 192 |
+
for settings_info in settings_list:
|
| 193 |
+
file_id = settings_info["file_id"]
|
| 194 |
+
lb = load_leaderboard("daily", date_id, file_id)
|
| 195 |
+
header_suffix = ""
|
| 196 |
+
if lb is not None:
|
| 197 |
+
header_suffix = _settings_badge(lb)
|
| 198 |
+
expander_title = f"{date_title} {header_suffix}" if header_suffix else date_title
|
| 199 |
+
with st.expander(expander_title, expanded=(date_id == get_current_daily_id())):
|
| 200 |
+
# Also show a small settings summary above the table
|
| 201 |
+
if lb is not None:
|
| 202 |
+
st.caption(f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}")
|
| 203 |
+
_render_leaderboard_table(lb, "")
|
| 204 |
|
| 205 |
|
| 206 |
def _render_weekly_tab():
|
| 207 |
"""Render weekly leaderboard tab."""
|
| 208 |
+
st.header("π Weekly Leaderboards")
|
| 209 |
+
st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each ISO week. Resets Monday at UTC midnight.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
weekly_id = get_current_weekly_id()
|
|
|
|
| 212 |
|
| 213 |
+
# Show current week; render all settings combinations that exist
|
| 214 |
+
settings_list = list_settings_for_period("weekly", weekly_id)
|
| 215 |
+
if not settings_list:
|
| 216 |
+
st.info("No weekly leaderboards found for the current week.")
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
try:
|
| 220 |
year, week = weekly_id.split("-W")
|
| 221 |
+
week_title = f"Week {int(week)}, {year}"
|
| 222 |
except ValueError:
|
| 223 |
+
week_title = weekly_id
|
| 224 |
|
| 225 |
+
for settings_info in settings_list:
|
| 226 |
+
file_id = settings_info["file_id"]
|
| 227 |
+
lb = load_leaderboard("weekly", weekly_id, file_id)
|
| 228 |
+
header_suffix = ""
|
| 229 |
+
if lb is not None:
|
| 230 |
+
header_suffix = _settings_badge(lb)
|
| 231 |
+
expander_title = f"ποΈ {week_title} {header_suffix}" if header_suffix else f"ποΈ {week_title}"
|
| 232 |
+
with st.expander(expander_title, expanded=True):
|
| 233 |
+
if lb is not None:
|
| 234 |
+
st.caption(f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}")
|
| 235 |
+
_render_leaderboard_table(lb, "")
|
| 236 |
|
| 237 |
|
| 238 |
def _render_history_tab():
|
|
|
|
| 252 |
key="history_daily_select"
|
| 253 |
)
|
| 254 |
|
|
|
|
| 255 |
settings_list = list_settings_for_period("daily", selected_daily)
|
| 256 |
if settings_list:
|
| 257 |
file_id_options = [s["file_id"] for s in settings_list]
|
|
|
|
| 264 |
|
| 265 |
if st.button("Load Daily", key="load_daily"):
|
| 266 |
leaderboard = load_leaderboard("daily", selected_daily, selected_file_id)
|
| 267 |
+
header = f"Daily: {selected_daily} "
|
| 268 |
+
if leaderboard is not None:
|
| 269 |
+
header += _settings_badge(leaderboard)
|
| 270 |
+
_render_leaderboard_table(leaderboard, header)
|
| 271 |
else:
|
| 272 |
st.info("No leaderboards found for this date.")
|
| 273 |
else:
|
|
|
|
| 283 |
key="history_weekly_select"
|
| 284 |
)
|
| 285 |
|
|
|
|
| 286 |
settings_list = list_settings_for_period("weekly", selected_weekly)
|
| 287 |
if settings_list:
|
| 288 |
file_id_options = [s["file_id"] for s in settings_list]
|
|
|
|
| 295 |
|
| 296 |
if st.button("Load Weekly", key="load_weekly"):
|
| 297 |
leaderboard = load_leaderboard("weekly", selected_weekly, selected_file_id)
|
| 298 |
+
header = f"Weekly: {selected_weekly} "
|
| 299 |
+
if leaderboard is not None:
|
| 300 |
+
header += _settings_badge(leaderboard)
|
| 301 |
+
_render_leaderboard_table(leaderboard, header)
|
| 302 |
else:
|
| 303 |
st.info("No leaderboards found for this week.")
|
| 304 |
else:
|