# Wrdler Leaderboard System Specification **Document Version:** 1.4.1 **Project Version:** 0.2.7 **Author:** GitHub Copilot **Last Updated:** 2025-12-08 **Status:** ✅ Implemented and Documented --- ## 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 **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 ``` ┌────────────────────┐ │ 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.""" ``` --- ## 10. UI Components ### 10.1 Sidebar Navigation Add to `_render_sidebar()` in `ui.py`: ```python st.header("Navigation") if st.button("🏆 Leaderboards", width="stretch"): st.session_state["show_leaderboard_page"] = True st.rerun() ``` ### 10.2 Game Over Integration Modify `_game_over_content()` in `ui.py` to: 1. Call `submit_score_to_all_leaderboards()` after generating share link 2. Display qualification results: ```python # After score submission if results["daily"]["qualified"]: st.success(f"🏆 You ranked #{results['daily']['rank']} on today's leaderboard!") if results["weekly"]["qualified"]: st.success(f"🏆 You ranked #{results['weekly']['rank']} on this week's leaderboard!") ``` ### 10.3 Leaderboard Page Routing In `run_app()`: ```python # Check if leaderboard page should be shown if st.session_state.get("show_leaderboard_page", False): from wrdler.leaderboard_page import render_leaderboard_page render_leaderboard_page() if st.button("← Back to Game"): st.session_state["show_leaderboard_page"] = False st.rerun() return # Don't render game UI ``` --- ## 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`. ### 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