File size: 34,164 Bytes
fedb9b2 f1fd35c a16932a d786c85 f1fd35c fedb9b2 f1fd35c d786c85 f1fd35c 082223e d786c85 f1fd35c 082223e f1fd35c 082223e f1fd35c a16932a f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c fedb9b2 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c fedb9b2 b7b95df fedb9b2 082223e fedb9b2 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 082223e f1fd35c 9c7fde6 f1fd35c 082223e f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c d786c85 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c a16932a f1fd35c fedb9b2 f1fd35c fedb9b2 f1fd35c fedb9b2 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c fedb9b2 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c 9c7fde6 f1fd35c d786c85 082223e d786c85 b7b95df d786c85 082223e d786c85 082223e d786c85 fedb9b2 d786c85 fedb9b2 d786c85 fedb9b2 d786c85 fedb9b2 d786c85 fedb9b2 d786c85 fedb9b2 082223e fedb9b2 d786c85 fedb9b2 d786c85 fedb9b2 d786c85 fedb9b2 d786c85 fedb9b2 583f150 a16932a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 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 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 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 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 |
ο»Ώ# 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 |