# Wrdler Leaderboard System Specification -**Document Version:** 1.4.4 -**Project Version:** 0.2.13 -**Author:** GitHub Copilot -**Last Updated:** 2025-12-31 -**Status:** ✅ Implemented and Documented +**Document Version:** 1.4.5 +**Project Version:** 0.2.14 +**Author:** GitHub Copilot +**Last Updated:** 2026-01-01 +**Status:** ✅ Implemented and Documented -## Recent Changes (v0.2.12) -- Layout changes for improved usability -- Fixed static spinner graphic and favicon -- Background enable/disable toggles improved -- Sidebar disabled for streamlined UI +## Recent Changes (v0.2.13) +- Leaderboard navigation moved to footer menu (not the sidebar) +- Footer navigation links to Leaderboard, Play, and Settings pages +- Game over dialog integrates leaderboard submission and shows qualification results +- Leaderboard page routing uses query parameters and custom navigation links + +## Planned (v0.2.14) +- Documented API for collecting submitted words and estimating per-word difficulty from leaderboard `UserEntry` data (time/6 and score/6) --- ## Table of Contents 1. [Executive Summary](#1-executive-summary) 2. [Goals and Objectives](#2-goals-and-objectives) 3. [System Architecture](#3-system-architecture) 4. [Data Models](#4-data-models) 5. [New Python Modules](#5-new-python-modules) 6. [Implementation Steps](#6-implementation-steps) 7. [Version Changes](#7-version-changes) 8. [File Changes Summary](#8-file-changes-summary) 9. [API Reference](#9-api-reference) 10. [UI Components](#10-ui-components) 11. [Testing Requirements](#11-testing-requirements) 12. [Migration Notes](#12-migration-notes) 13. [Operational Considerations](#13-operational-considerations) --- ## 1. Executive Summary This specification documents the implemented **Daily and Weekly Leaderboard System** for Wrdler. The system: - ✅ Tracks top 25 scores for daily leaderboards (resets at UTC midnight) - ✅ Tracks top 25 scores for weekly leaderboards (resets at UTC Monday 00:00) - ✅ Creates separate leaderboards for each unique combination of game-affecting settings - ✅ Automatically adds qualifying scores from any game completion (including challenge mode) - ✅ Provides a dedicated leaderboard page with historical lookup capabilities - ✅ Stores leaderboard data in HuggingFace repository using existing storage infrastructure - ✅ Uses folder-based discovery (no index.json) with descriptive folder names - ✅ Uses a unified JSON format consistent with existing challenge settings.json files - **All leaderboard and challenge submissions use the latest st.session_state, including challenge overrides.** **Implementation Status:** All features complete and deployed as of version 0.2.0 --- ## 2. Goals and Objectives ### Primary Goals 1. **Settings-Based Leaderboards**: Each unique combination of game-affecting settings creates a separate leaderboard. Players using different settings compete on different leaderboards. 2. **Daily Leaderboards**: Create and maintain daily leaderboards with top 25 entries displayed (can store more), organized by date folders (e.g., `games/leaderboards/daily/2025-01-27/`) 3. **Weekly Leaderboards**: Create and maintain weekly leaderboards with top 25 entries displayed (can store more), organized by ISO week folders (e.g., `games/leaderboards/weekly/2025-W04/`) 4. **Automatic Qualification**: Every game completion (challenge or solo) checks if score qualifies for the matching daily/weekly leaderboard based on current settings 5. **Leaderboard Page**: New Streamlit page displaying: - Last 7 days of daily leaderboards (filtered by current settings) - Last 5 weeks of weekly leaderboards with per-week expanders (current week open unless `week=YYYY-Www` query overrides) - Historical lookup via dropdown 6. **Folder-Based Discovery**: No index.json file. Leaderboards are discovered by scanning folder names. Folder names include settings info for fast filtering. 7. **Unified File Format**: Leaderboard files use the same structure as challenge settings.json with an `entry_type` field to distinguish between "daily", "weekly", and "challenge" entries ### Secondary Goals - Maintain backward compatibility with existing challenge system - Minimize HuggingFace API calls through caching - Support sorting by score (descending), then time (ascending), then difficulty (descending) - Use `challenge_id` as the primary identifier across all entry types ### Game-Affecting Settings The following settings define a unique leaderboard: | Setting | Type | Description | |---------|------|-------------| | `game_mode` | string | `"classic"`, `"easy"`, `"too easy"` | | `wordlist_source` | string | Wordlist file (e.g., `"classic.txt"`, `"easy.txt"`) | | `show_incorrect_guesses` | bool | Whether incorrect guesses are shown | | `enable_free_letters` | bool | Whether free letters feature is enabled | | `puzzle_options` | object | Puzzle configuration (`spacer`, `may_overlap`) | **Example:** A player using `game_mode: "easy"` with `wordlist_source: "easy.txt"` competes on a different leaderboard than a player using `game_mode: "classic"` with `wordlist_source: "classic.txt"`. --- ## 3. System Architecture ### 3.1 Storage Structure Each date/week has settings-based subfolders. The folder name (file_id) encodes the settings for fast discovery. All leaderboards use `settings.json` as the filename (consistent with challenges): ``` HF_REPO_ID/ ├── games/ # All game-related storage │ ├── {challenge_id}/ # Existing challenge storage │ │ └── settings.json # entry_type: "challenge" │ └── leaderboards/ │ ├── daily/ │ │ ├── 2025-01-27/ │ │ │ ├── classic-classic-0/ │ │ │ │ └── settings.json │ │ │ └── easy-easy-0/ │ │ │ └── settings.json │ │ └── 2025-01-26/ │ │ └── classic-classic-0/ │ │ └── settings.json │ └── weekly/ │ ├── 2025-W04/ │ │ ├── classic-classic-0/ │ │ │ └── settings.json │ │ └── easy-too_easy-0/ │ │ └── settings.json │ └── 2025-W03/ │ └── classic-classic-0/ │ └── settings.json └── shortener.json # Existing URL shortener ``` ### 3.2 File ID Format The `file_id` (folder name) encodes settings for discovery without an index: ``` {wordlist_source}-{game_mode}-{sequence} ``` **Examples:** - `classic-classic-0` - Classic wordlist, classic mode, first instance - `easy-easy-0` - Easy wordlist, easy mode, first instance - `classic-too_easy-1` - Classic wordlist, "too easy" mode, second instance **Sanitization Rules:** - `.txt` extension is removed from wordlist_source - Spaces are replaced with underscores - All lowercase ### 3.3 Folder-Based Discovery Instead of maintaining an `index.json` file, leaderboards are discovered by: 1. **List period folders**: Scan `games/leaderboards/daily/` or `games/leaderboards/weekly/` for date/week folders 2. **List file_id folders**: For each period, scan for settings folders 3. **Filter by prefix**: Match file_ids that start with `{wordlist_source}-{game_mode}-` 4. **Load and verify**: Load `settings.json` to verify full settings match **Benefits:** - No index synchronization issues - Self-documenting folder structure - Can browse folders directly - Reduced write operations (no index updates) ### 3.4 Data Flow - All leaderboard and challenge submissions use the latest st.session_state, including challenge overrides. ``` ┌────────────────────┐ │ Game Completion │ └────────────────────┘ │ ▼ ┌────────────────────┐ │ Get current game │ │ settings │ └────────────────────┘ │ ▼ ┌────────────────────┐ │ Build file_id │ │ prefix from │ │ settings │ └────────────────────┘ │ ▼ ┌────────────────────┐ │ Scan folders for │ │ matching file_id │ │ or create new │ └────────────────────┘ │ ┌─────┴─────┐ │ │ ▼ ▼ ┌───────┐ ┌───────────┐ │ Daily │ │ Weekly │ │ LB │ │ LB | └───────┘ └───────────┘ │ │ └────┬─────┘ │ ▼ ┌─────────────────────┐ │ Check if score │ │ qualifies (top │ │ 25 displayed) │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Update & Upload │ │ to HF repo │ └─────────────────────┘ ``` --- ## 4. Data Models ### 4.1 Entry Type Definition The `entry_type` field distinguishes between different types of game entries: | entry_type | Description | Storage Location | |------------|-------------|------------------| | `"challenge"` | Player-created challenge for others to compete | `games/{challenge_id}/settings.json` | | `"daily"` | Daily leaderboard entry | `games/leaderboards/daily/{date}/{file_id}/settings.json` | | `"weekly"` | Weekly leaderboard entry | `games/leaderboards/weekly/{week}/{file_id}/settings.json` | ### 4.2 Unified File Schema (Consistent with Challenge settings.json) Both leaderboard files and challenge files use the **same base structure**. The settings in the file define what makes this leaderboard unique: ```json { "challenge_id": "2025-01-27/classic-classic-0", "entry_type": "daily", "game_mode": "classic", "grid_size": 8, "puzzle_options": { "spacer": 0, "may_overlap": false }, "users": [ { "uid": "20251130T190249Z-0XLG5O", "username": "Charles", "word_list": ["CHEER", "PLENTY", "REAL", "ARRIVE", "DEAF", "DAILY"], "word_list_difficulty": 117.48, "score": 39, "time": 132, "timestamp": "2025-11-30T19:02:49.544933+00:00", "source_challenge_id": null } ], "created_at": "2025-11-30T19:02:49.544933+00:00", "version": "0.2.0", "show_incorrect_guesses": true, "enable_free_letters": true, "wordlist_source": "classic.txt", "game_title": "Wrdler Gradio AI", "max_display_entries": 25 } ``` ### 4.3 Field Descriptions | Field | Type | Description | |-------|------|-------------| | `challenge_id` | string | Unique identifier. For daily: `"2025-01-27/classic-classic-0"`, weekly: `"2025-W04/easy-easy-0"`, challenge: `"20251130T190249Z-ABCDEF"` | | `entry_type` | string | One of: `"daily"`, `"weekly"`, `"challenge"` | | `game_mode` | string | Game difficulty: `"classic"`, `"easy"`, `"too easy"` | | `grid_size` | int | Grid width (8 for Wrdler) | | `puzzle_options` | object | Puzzle configuration (defines leaderboard uniqueness) | | `users` | array | Array of user entries (sorted by score desc, time asc, difficulty desc) | | `created_at` | string | ISO 8601 timestamp when entry was created | | `version` | string | Schema version | | `show_incorrect_guesses` | bool | Display setting (defines leaderboard uniqueness) | | `enable_free_letters` | bool | Free letters feature toggle (defines leaderboard uniqueness) | | `wordlist_source` | string | Source wordlist file (defines leaderboard uniqueness) | | `game_title` | string | Game title for display | | `max_display_entries` | int | Maximum entries to display (default 25, configurable via MAX_DISPLAY_ENTRIES env var) | ### 4.4 User Entry Schema Each user entry in the `users` array: ```json { "uid": "20251130T190249Z-0XLG5O", "username": "Charles", "word_list": ["CHEER", "PLENTY", "REAL", "ARRIVE", "DEAF", "DAILY"], "word_list_difficulty": 117.48, "score": 39, "time": 132, "timestamp": "2025-11-30T19:02:49.544933+00:00", "source_challenge_id": null } ``` | Field | Type | Description | |-------|------|-------------| | `uid` | string | Unique user entry ID | | `username` | string | Player display name | | `word_list` | array | 6 words played | | `word_list_difficulty` | float | Calculated difficulty score | | `score` | int | Final score | | `time` | int | Time in seconds | | `timestamp` | string | ISO 8601 when entry was recorded | | `source_challenge_id` | string\|null | If from a challenge, the original challenge_id | ### 4.5 Settings Matching Two leaderboards are considered the same if ALL of the following match: - `game_mode` - `wordlist_source` (after sanitization - .txt removed, lowercase) - `show_incorrect_guesses` - `enable_free_letters` - `puzzle_options.spacer` - `puzzle_options.may_overlap` ### 4.6 Weekly Leaderboard Naming Uses ISO 8601 week numbering: - Format: `YYYY-Www` (e.g., `2025-W04`) - Week starts on Monday - Week 1 is the week containing the first Thursday of the year --- ## 5. New Python Modules ### 5.1 `wrdler/leaderboard.py` (NEW FILE) **Purpose:** Core leaderboard logic for managing daily and weekly leaderboards with settings-based separation and folder-based discovery. Key classes: - `GameSettings` - Settings that define a unique leaderboard - `UserEntry` - Single user entry in a leaderboard - `LeaderboardSettings` - Unified leaderboard/challenge settings format Key functions: - `_sanitize_wordlist_source()` - Remove .txt extension and normalize - `_build_file_id()` - Create file_id from settings - `_parse_file_id()` - Parse file_id into components - `find_matching_leaderboard()` - Find leaderboard by scanning folders - `create_or_get_leaderboard()` - Get or create a leaderboard - `submit_score_to_all_leaderboards()` - Main entry point for submissions ### 5.2 `wrdler/modules/storage.py` (UPDATED) Added functions: - `_list_repo_folders()` - List folder names under a path in HuggingFace repo - `_list_repo_files_in_folder()` - List files in a folder --- ## 6. Implementation Steps ### Phase 1: Core Leaderboard Module (v0.2.0-alpha) ✅ COMPLETE | Step | Task | Files | Status | |------|------|-------|--------| | 1.1 | Create `wrdler/leaderboard.py` with `GameSettings` and data models | NEW | ✅ Complete | | 1.2 | Implement folder listing in storage.py | storage.py | ✅ Complete (_list_repo_folders) | | 1.3 | Implement `find_matching_leaderboard()` with folder scanning | leaderboard.py | ✅ Complete | | 1.4 | Implement `check_qualification()` and sorting | leaderboard.py | ✅ Complete (_sort_users) | | 1.5 | Implement `submit_to_leaderboard()` and `submit_score_to_all_leaderboards()` | leaderboard.py | ✅ Complete | | 1.6 | Write unit tests for leaderboard logic including settings matching | tests/test_leaderboard.py | ⏳ Recommended | ### Phase 2: UI Integration (v0.2.0-beta) ✅ COMPLETE | Step | Task | Files | Status | |------|------|-------|--------| | 2.1 | Create `wrdler/leaderboard_page.py` with settings filtering | NEW | ✅ Complete | | 2.2 | Add leaderboard navigation to `ui.py` sidebar | ui.py | ✅ Complete (footer menu) | | 2.3 | Integrate score submission in `_game_over_content()` with current settings | ui.py | ✅ Complete | | 2.4 | Add leaderboard results display in game over dialog | ui.py | ✅ Complete | | 2.5 | Style leaderboard tables to match ocean theme | leaderboard_page.py | ✅ Complete (pandas dataframe styling) | ### Phase 3: Challenge Format Migration (v0.2.0-beta) ✅ COMPLETE | Step | Task | Files | Status | |------|------|-------|--------| | 3.1 | Add `entry_type` field to existing challenge saves | game_storage.py | ✅ Complete | | 3.2 | Update challenge loading to handle `entry_type` | game_storage.py | ✅ Complete (defaults to "challenge") | | 3.3 | Test backward compatibility with old challenges | Manual | ✅ Verified | ### Phase 4: Testing & Polish (v0.2.0-rc) ✅ COMPLETE | Step | Task | Files | Status | |------|------|-------|--------| | 4.1 | Integration testing with HuggingFace | Manual | ✅ Verified | | 4.2 | Add caching for leaderboard data | leaderboard.py | ⏳ Future optimization | | 4.3 | Add error handling and retry logic | leaderboard.py | ✅ Complete (logging) | | 4.4 | Update documentation | README.md, specs/, CLAUDE.md | ✅ Complete | | 4.5 | Version bump and release notes | pyproject.toml, __init__.py | ✅ Complete (v0.2.0) --- ## 7. Version Changes ### pyproject.toml ```toml [project] name = "wrdler" version = "0.2.0" # Updated from 0.1.0 description = "Wrdler vocabulary puzzle game with daily/weekly leaderboards" ``` ### wrdler/__init__.py ```python __version__ = "0.2.0" # Updated from existing version ``` ### wrdler/game_storage.py ```python __version__ = "0.2.0" # Updated from 0.1.5 ``` ### wrdler/leaderboard.py (NEW) ```python __version__ = "0.2.0" ``` ### wrdler/leaderboard_page.py (NEW) ```python __version__ = "0.2.0" ``` ### wrdler/modules/storage.py ```python __version__ = "0.1.6" # Updated to add folder listing functions ``` --- ## 8. File Changes Summary ### New Files | File | Purpose | |------|---------| | `wrdler/leaderboard.py` | Core leaderboard logic with folder-based discovery | | `wrdler/leaderboard_page.py` | Streamlit leaderboard page | | `tests/test_leaderboard.py` | Unit tests for leaderboard | | `specs/leaderboard_spec.md` | This specification | ### Modified Files | File | Changes | |------|---------| | `pyproject.toml` | Version bump to 0.2.0 | | `wrdler/__init__.py` | Version bump, add leaderboard exports | | `wrdler/modules/storage.py` | Add `_list_repo_folders()` and `_list_repo_files_in_folder()` | | `wrdler/game_storage.py` | Version bump, add `entry_type` field, integrate leaderboard submission | | `wrdler/ui.py` | Add leaderboard nav, integrate submission in game over | | `wrdler/modules/__init__.py` | Export new functions if needed | --- ## 9. API Reference ### Public Functions in `wrdler/leaderboard.py` ```python def submit_score_to_all_leaderboards( username: str, score: int, time_seconds: int, word_list: List[str], settings: GameSettings, word_list_difficulty: Optional[float] = None, source_challenge_id: Optional[str] = None, repo_id: Optional[str] = None ) -> Dict[str, Any]: """Main entry point for submitting scores after game completion.""" def load_leaderboard( entry_type: EntryType, period_id: str, file_id: str, repo_id: Optional[str] = None ) -> Optional[LeaderboardSettings]: """Load a specific leaderboard by file ID.""" def find_matching_leaderboard( entry_type: EntryType, period_id: str, settings: GameSettings, repo_id: Optional[str] = None ) -> Tuple[Optional[str], Optional[LeaderboardSettings]]: """Find a leaderboard matching given settings.""" def get_last_n_daily_leaderboards( n: int = 7, settings: Optional[GameSettings] = None, repo_id: Optional[str] = None ) -> List[Tuple[str, Optional[LeaderboardSettings]]]: """Get recent daily leaderboards for display.""" def list_available_periods( entry_type: EntryType, limit: int = 30, repo_id: Optional[str] = None ) -> List[str]: """List available period IDs from folder structure.""" def list_settings_for_period( entry_type: EntryType, period_id: str, repo_id: Optional[str] = None ) -> List[Dict[str, Any]]: """List all settings combinations for a period.""" def get_current_daily_id() -> str: """Get today's period ID.""" def get_current_weekly_id() -> str: """Get this week's period ID.""" + +def collect_submitted_words_in_timeframe( + entry_type: EntryType, + start_utc: datetime, + end_utc: datetime, + repo_id: Optional[str] = None, + include_challenges: bool = False +) -> List[Dict[str, Any]]: + """ + Collect all words submitted via `UserEntry.word_list` across daily or weekly leaderboards + within a UTC timeframe. + + Purpose: + - Provide a corpus of played words for analytics (frequency, difficulty, etc.) + + Notes: + - Leaderboard period boundaries are UTC-based. + - Each `UserEntry.word_list` contains 6 words. + - This function is intended to scan leaderboard folders (`games/leaderboards/{daily|weekly}/...`) + for matching periods, load `settings.json`, then iterate `LeaderboardSettings.users`. + - If `include_challenges=True`, optionally include challenge `settings.json` user entries + if/when challenge submissions are stored in the same `users` schema. + + Difficulty calculation: + - The intent is to estimate per-word difficulty using per-entry averages: + - `time_per_word = user.time / 6` + - `score_per_word = user.score / 6` + - The returned records should include these derived fields per word. + + Args: + entry_type: "daily" or "weekly" (source of leaderboard entries). + start_utc: Inclusive start of timeframe in UTC. + end_utc: Exclusive end of timeframe in UTC. + repo_id: Optional HuggingFace repo override. + include_challenges: Whether to include challenge entries (future behavior). + + Returns: + A list of records shaped like: + { + "timestamp": str, # original `UserEntry.timestamp` (ISO 8601 UTC) + "uid": str, + "username": str, + "entry_type": str, # "daily" or "weekly" + "period_id": str, # YYYY-MM-DD or YYYY-Www + "file_id": str, + "word": str, + "word_index": int, # 0..5 + "time_per_word": float, # user.time / 6 + "score_per_word": float, # user.score / 6 + "word_list_difficulty": float|None, + "source_challenge_id": str|None + } + """ + ... ``` --- ## 10. UI Components ### 10.1 Footer Navigation (Updated) Leaderboard navigation is now accessed via the footer menu at the bottom of the page, not the sidebar. The footer contains links to Leaderboard, Play, and Settings pages. This replaces the previous sidebar navigation. ### 10.2 Game Over Integration (Updated) The game over dialog now integrates leaderboard submission and displays qualification results (rankings). After submitting your score, the dialog will show if you qualified for the daily or weekly leaderboard and your rank. ### 10.3 Leaderboard Page Routing (Updated) Leaderboard page routing uses query parameters and custom navigation links for tab selection (e.g., `?page=today`, `?page=weekly`). Tabs are not implemented with Streamlit's native tabs but with custom links for better URL support. --- ## 11. Testing Requirements ### Unit Tests (`tests/test_leaderboard.py`) ```python class TestUserEntry: def test_create_entry(self): ... def test_to_dict_roundtrip(self): ... def test_from_legacy_time_seconds_field(self): ... class TestLeaderboardSettings: def test_create_leaderboard(self): ... def test_entry_type_values(self): ... def test_get_display_users_limit(self): ... def test_format_matches_challenge(self): ... class TestGameSettings: def test_settings_matching_same(self): ... def test_settings_matching_different_mode(self): ... def test_settings_matching_txt_extension_ignored(self): ... def test_get_file_id_prefix(self): ... class TestFileIdFunctions: def test_sanitize_wordlist_source_removes_txt(self): ... def test_build_file_id(self): ... def test_parse_file_id(self): ... class TestQualification: def test_qualify_empty_leaderboard(self): ... def test_qualify_not_full(self): ... def test_qualify_by_score(self): ... def test_qualify_by_time_tiebreaker(self): ... def test_qualify_by_difficulty_tiebreaker(self): ... def test_not_qualify_lower_score(self): ... class TestDateIds: def test_daily_id_format(self): ... def test_weekly_id_format(self): ... def test_daily_path(self): ... # Tests new folder structure def test_weekly_path(self): ... # Tests new folder structure ``` ### Integration Tests - Test full flow: game completion → leaderboard submission → retrieval - Test with mock HuggingFace repository - Test folder-based discovery logic - Test concurrent submissions (edge case) - Test backward compatibility with legacy challenge files (no entry_type) --- ## 12. Migration Notes ### Backward Compatibility - Existing challenges continue to work unchanged (entry_type defaults to "challenge") - No changes to `shortener.json` format - Challenge `settings.json` format is extended (new fields are optional) - **No index.json migration needed** - folder-based discovery is self-contained ### Schema Evolution | Version | Changes | |---------|---------| | 0.1.x | Original challenge format | | 0.2.0 | Added `entry_type`, `max_display_entries`, `source_challenge_id` fields | | 0.2.0 | Changed to folder-based discovery (no index.json) | | 0.2.0 | New folder structure: `games/leaderboards/{type}/{period}/{file_id}/settings.json` | ### Data Migration - No migration required for existing challenges - New leaderboard files use folder-based storage from start - Legacy challenges without `entry_type` default to `"challenge"` ### Rollback Plan 1. Remove leaderboard imports from `ui.py` 2. Remove sidebar navigation button 3. Remove game over submission calls 4. Optionally: delete `games/leaderboards/` directory from HF repo --- ## 13. Implementation Notes ### 13.1 Actual Implementation Details The following represents the actual implementation as of v0.2.0: #### Core Modules Implemented **`wrdler/leaderboard.py` (v0.2.0):** - ✅ `GameSettings` dataclass with settings matching logic - ✅ `UserEntry` dataclass for individual scores - ✅ `LeaderboardSettings` dataclass (unified format) - ✅ File ID sanitization and parsing functions - ✅ Folder-based discovery with `_list_period_folders()` and `_list_file_ids_for_period()` - ✅ `find_matching_leaderboard()` with prefix filtering and full verification - ✅ `create_or_get_leaderboard()` with automatic sequence management - ✅ `submit_score_to_all_leaderboards()` as main entry point - ✅ `check_qualification()` for top 25 filtering - ✅ Period ID generators: `get_current_daily_id()`, `get_current_weekly_id()` - ✅ Historical lookup functions: `list_available_periods()`, `list_settings_for_period()` - ✅ URL generation with `get_leaderboard_url()` **`wrdler/leaderboard_page.py` (v0.2.0):** - ✅ Four-tab navigation system (Today, Daily, Weekly, History) - ✅ Query parameter routing (`?page=`, `?gidd=`, `?gidw=`) - ✅ Settings badge display with full configuration info - ✅ Styled pandas dataframes with rank emojis (🥇🥈🥉) - ✅ Challenge indicator badges (🎯) - ✅ Expandable leaderboard groups per settings combination - ✅ Last updated timestamps and entry counts **`wrdler/modules/storage.py` (Updated):** - ✅ `_list_repo_folders()` for folder discovery - ✅ Seamless integration with existing HF dataset functions **`wrdler/ui.py` (Updated):** - ✅ Footer menu integration for leaderboard page navigation - ✅ Automatic submission call in game over flow - ✅ Current settings extraction via `GameSettings` - ✅ Display qualification results with rank notifications #### Storage Implementation **Folder Structure (as built):** ``` HF_REPO_ID/games/ ├── leaderboards/ │ ├── daily/ │ │ └── {YYYY-MM-DD}/ │ │ └── {wordlist_source}-{game_mode}-{sequence}/ │ │ └── settings.json │ └── weekly/ │ └── {YYYY-Www}/ │ └── {wordlist_source}-{game_mode}-{sequence}/ │ └── settings.json └── {challenge_id}/ └── settings.json ``` **File ID Sanitization:** - `.txt` extension removed from wordlist_source - Spaces replaced with underscores - All lowercase - Regex: `r'[^\w\-]'` replaced with `_` #### UI/UX Features Implemented **Today Tab:** - Displays current daily and weekly leaderboards side-by-side in two columns - Query parameter filtering: `?gidd={file_id}` and `?gidw={file_id}` show specific leaderboards - Expandable settings groups with full configuration captions **Daily Tab:** - Shows last 7 days of daily leaderboards - One expander per date with all settings combinations nested - Today's date auto-expanded by default **Weekly Tab:** - Shows current ISO week leaderboard - All settings combinations displayed in expandable groups **History Tab:** - Two-column layout: Daily (left) and Weekly (right) - Dropdown selectors for period and settings combination - "Load Daily" and "Load Weekly" buttons for explicit loading **Table Styling:** - Pandas DataFrame with custom CSS styling - Rank column: large bold text with emojis - Score column: green color (#20d46c) with bold font - Challenge indicator: 🎯 badge appended to username - Last updated timestamp and entry count displayed below table #### Key Differences from Spec 1. **Navigation:** Implemented as 'Leaderboard' link in the footer menu instead of sidebar button 2. **Caching:** Not implemented in v0.2.0 (deferred to v0.3.0 for optimization) 3. **Tab Implementation:** Used query parameters with custom nav links instead of Streamlit native tabs for better URL support 4. **Table Rendering:** Used pandas DataFrames with styling instead of custom HTML tables #### Known Limitations (as of v0.2.0) 1. **No caching:** Each page load fetches from HF repository (can be slow) 2. **No pagination:** Displays top 25 only (additional entries stored but not shown) 3. **Limited error handling:** Basic logging, could benefit from retry logic 4. **No rate limiting:** Submission frequency not constrained 5. **No archival:** Old leaderboards remain indefinitely (no cleanup script) #### Future Enhancements (Planned for v0.3.0+) - ⏳ In-memory caching with TTL (60s for periods, 15s for leaderboards) - ⏳ Pagination for >25 entries - ⏳ Retry logic with exponential backoff - ⏳ Rate limiting per IP/session - ⏳ Archival script for old periods (>365 days daily, >156 weeks weekly) - ⏳ Manual refresh button in UI --- ## 14. Operational Considerations ### 14.1 Concurrency and Consistency - Write model: - Use optimistic concurrency: read `settings.json`, merge new `users` entry, write back with a unique commit message including `challenge_id` and `uid`. - Implement retry with exponential backoff on HTTP 409/5xx or checksum mismatch. - Ensure atomicity per file write: do not split updates across multiple files per leaderboard. - Simultaneous file creation: - When no matching `file_id` folder exists, first attempt creation; if a concurrent process creates it, fallback to loading and merging. - Always re-verify settings match by reading `settings.json` after folder discovery. - Sequence collisions: - If `{wordlist}-{mode}-{sequence}` collides but settings differ, increment `sequence` until a unique folder is found; verify match via file content, not only prefix. ### 14.2 Caching and Discovery Performance - Cache tiers: - In-memory (per app session): - Period listings for `games/leaderboards/{type}/` (TTL 60s). - `file_id` listings inside a period (TTL 30s). - Loaded `settings.json` for leaderboards (TTL 15s or invalidated on write). - Invalidation: - On successful submission, invalidate the specific leaderboard cache (file content and directory listing for that period). - Provide a manual refresh option in UI (leaderboard page). - Discovery limits: - Cap directory scans to the most recent N periods (configurable; default 30). UI uses explicit period selection for older data. - Prefer prefix filtering client-side before loading file content. ### 14.3 Error Handling and Observability - Error taxonomy: - Storage errors: `HF_STORAGE_UNAVAILABLE`, `HF_WRITE_CONFLICT`, `HF_NOT_FOUND`. - Validation errors: `LB_INVALID_INPUT`, `LB_SETTINGS_MISMATCH`. - Operational errors: `LB_TIMEOUT`, `LB_RETRY_EXCEEDED`. - User feedback: - On non-critical failure (e.g., leaderboard write conflict), show non-blocking warning and retry silently up to 3 times. - Logging: - Log submission events with fields: `entry_type`, `period_id`, `file_id`, `uid`, `score`, `time`, `rank_result`, `repo_path`, `latency_ms`. - Log error events with `code`, `message`, `attempt`, `backoff_ms`. - Telemetry (optional): - Count successful submissions per period and per settings combination for basic monitoring. ### 14.4 Security and Abuse Controls - Input validation: - `username`: max 40 chars, strip control chars, allow alphanumerics, spaces, basic punctuation; reject offensive content if possible. - `word_list`: array of 6 uppercase A–Z strings, length 3–10; drop invalid entries. - `score`: 0–999; `time`: 1–36000 (10 hours); `word_list_difficulty`: float if provided, clamp to 0–10000. - Spam and duplicates: - Rate limit per IP/session (e.g., max 10 submissions per hour). - Detect duplicate entries by same `uid` + `timestamp` within 10 seconds window; deduplicate silently. - Repository permissions: - Submissions require HF write permissions for the space; ensure credentials are scoped to the specific repo. - Do not expose write tokens in client logs; keep server-side commit operations. ### 14.5 Data Lifecycle and Retention - Retention: - Keep daily leaderboards for 365 days; weekly leaderboards for 156 weeks (3 years). - Optional archival: move older periods to `games/leaderboards_archive/{type}/` or leave as-is with documented retention. - Cleanup: - Provide a maintenance script to prune old periods and reindex cache. - Privacy: - Store only display names and gameplay metrics; avoid PII. - Users must enter a name (Anonymous not allowed); do not display IP or identifiers publicly. ### 14.6 Time and Period Boundaries - Timezone: - All operations use UTC. The periods roll over at 00:00:00 UTC for daily, Monday 00:00:00 UTC for weekly. - ISO week: - Use Python’s `isocalendar()` to derive `YYYY-Www` and handle year transitions (weeks spanning year boundaries). - Clock source: - Use server-side timestamp for submissions; do not trust client clock. If unavailable, fall back to Python `datetime.utcnow()`. ### 14.7 UI Reliability and UX - Loading states: - Show skeleton/loading indicators while scanning folders or reading leaderboard JSON. - Empty states: - Display “No entries yet” when a leaderboard exists without users or has not been created. - Accessibility: - Ensure sufficient color contrast, keyboard navigation for tabs/period selectors, and alt text for icons. - Internationalization (future): - Keep date/time ISO formatting and English labels; design UI to allow future localization. ### 14.8 Ranking and Tie-Breaks (Operational Clarification) - Sort order: - Primary: `score` desc; secondary: `time` asc; tertiary: `word_list_difficulty` desc; quaternary: stable by `timestamp` asc. - Display limit: - Always store full `users` list; apply `max_display_entries` at render time only. - Rank reporting: - Return rank based on full sorted list even if not displayed; if outside display limit, mark `qualified=False`. - **Duplicate removal:** - If multiple entries exist with identical `username`, `word_list`, `score`, `time`, `timestamp`, and `word_list_difficulty`, only keep one entry. - Prefer the entry with a non-null `source_challenge_id` if duplicates are found. ### 14.9 Commit and Retry Strategy (HF) - Commit messages: - Format: `leaderboard:{entry_type}:{period_id}:{file_id} add uid={uid} score={score} time={time}`. - Retries: - Backoff sequence: 0.25s, 0.5s, 1s; max 3 attempts; abort on `LB_SETTINGS_MISMATCH`. - Partial failures: - If daily succeeds and weekly fails (or vice versa), return both statuses independently; UI reports partial success. ### 14.10 Timezone Handling - Daily leaderboard files use UTC for period boundaries. - When displaying, show the UTC period as a PST date range: For daily leaderboards, display the period as: "YYYY-MM-DD 00:00:00 UTC to YYYY-MM-DD 23:59:59 UTC" and "YYYY-MM-DD HH:MM:SS PST to YYYY-MM-DD HH:MM:SS PST" (PST is UTC-8; adjust for daylight saving as needed) 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`. 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]` **Leaderboard Page UI:** - **Today Tab:** Current daily and weekly leaderboards - **Daily Tab:** Last 7 days of daily leaderboards - **Weekly Tab:** Last 5 weeks displayed as individual expanders (current week or `week=YYYY-Www` query opens by default) - **History Tab:** Historical leaderboard browser