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