Surn commited on
Commit
b7b95df
ยท
1 Parent(s): 583f150

v0.2.3 Update Leaderboard titles to daterange

Browse files
CLAUDE.md CHANGED
@@ -41,6 +41,13 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
41
  - Remote storage via HuggingFace datasets
42
  - Word list difficulty calculation
43
  - "Show Challenge Share Links" toggle (default OFF)
 
 
 
 
 
 
 
44
 
45
  ### Daily & Weekly Leaderboards
46
  - **Settings-Based Separation:** Each unique settings combo creates separate leaderboard
@@ -55,7 +62,7 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
55
  2025-12-08 00:00:00 UTC to 2025-12-08 23:59:59 UTC
56
  and
57
  2025-12-07 16:00:00 PST to 2025-12-08 15:59:59 PST
58
- The leaderboard expander label should show: `Monday, December 08, 2025 4:00 PM PST โ€“ Tuesday, December 09, 2025 3:59:59 PM PST [settings badge]`
59
 
60
  ### AI Word Generation
61
  - Topic-based word list generation via HuggingFace Spaces or local transformers
 
41
  - Remote storage via HuggingFace datasets
42
  - Word list difficulty calculation
43
  - "Show Challenge Share Links" toggle (default OFF)
44
+ - **Integration:**
45
+ - Automatic submission after game completion (opt-in via game over popup)
46
+ - Challenge scores also contribute to daily/weekly leaderboards
47
+ - Source tracking via `source_challenge_id` field
48
+ - Unified JSON format with `entry_type` field (daily/weekly/challenge)
49
+
50
+ **Access:** 'Leaderboard' link in the footer navigation at the bottom of the page
51
 
52
  ### Daily & Weekly Leaderboards
53
  - **Settings-Based Separation:** Each unique settings combo creates separate leaderboard
 
62
  2025-12-08 00:00:00 UTC to 2025-12-08 23:59:59 UTC
63
  and
64
  2025-12-07 16:00:00 PST to 2025-12-08 15:59:59 PST
65
+ 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]`
66
 
67
  ### AI Word Generation
68
  - Topic-based word list generation via HuggingFace Spaces or local transformers
README.md CHANGED
@@ -98,7 +98,7 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
98
  - Leaderboard files store UTC dates/times for each period.
99
  - When viewing daily leaderboards, the app displays the UTC period as a PST date range:
100
  - 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`.
101
- - The leaderboard expander label should show: `Monday, December 08, 2025 4:00 PM PST โ€“ Tuesday, December 09, 2025 3:59:59 PM PST [settings badge]`
102
  - Daily leaderboards display the date range in PST for easier local time understanding.
103
  - Weekly leaderboards use ISO week dates (e.g., `2025-W49`) and are available every Monday.
104
 
@@ -115,7 +115,7 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
115
  - Source tracking via `source_challenge_id` field
116
  - Unified JSON format with `entry_type` field (daily/weekly/challenge)
117
 
118
- **Access:** Footer navigation links at the bottom of the page
119
 
120
  ### Deployment & Technical
121
  - **Dockerfile-based deployment** supported for Hugging Face Spaces and other container platforms
@@ -237,7 +237,7 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
237
  4. Earn points for correct guesses and bonus points for unrevealed letters.
238
  5. **The game ends when all six words are found or all word letters are revealed. Your score tier is displayed.**
239
  6. **To play a shared challenge, use a link with `?game_id=<sid>`. Your result will be added to the challenge leaderboard.**
240
- 7. **Access leaderboards anytime using the footer navigation links at the bottom of the page.**
241
 
242
  ## Changelog
243
 
@@ -425,165 +425,4 @@ Ensure you have the necessary permissions and API access (if required) to use th
425
 
426
  For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
427
 
428
- Happy gaming and sound designing!
429
-
430
- ## What's New in v0.2.1: Daily & Weekly Leaderboards ๐Ÿ†
431
-
432
- ### Comprehensive Leaderboard System
433
- - **Settings-Based Competition:** Separate leaderboards for each unique settings combination
434
- - Game mode (classic, easy, too easy)
435
- - Wordlist source (classic.txt, fourth_grade.txt, etc.)
436
- - Display options (show incorrect guesses, enable free letters)
437
- - Puzzle options (spacer, may_overlap)
438
- - **Daily & Weekly Periods:** Top 20 scores per day and per ISO week
439
- - **Automatic Qualification:** Submit scores after any game completion
440
- - **Settings page planned:** Move from sidebar to dedicated page, login required
441
-
442
- ### Leaderboard Page Navigation
443
- - **Today Tab:** Current daily and weekly leaderboards with direct link filtering
444
- - **Daily Tab:** Last 7 days of history with expandable date groups
445
- - **Weekly Tab:** Current week with all settings combinations
446
- - **History Tab:** Browse any past period with dropdown selectors
447
-
448
- ### Storage & Discovery
449
- - **Folder-Based Architecture:** No index.json, scans period folders directly
450
- - **File ID Format:** `{wordlist_source}-{game_mode}-{sequence}` for fast filtering
451
- - **HuggingFace Storage:** `games/leaderboards/{daily|weekly}/{period}/{file_id}/settings.json`
452
- - **Unified Format:** Single JSON structure with `entry_type` field (daily/weekly/challenge)
453
-
454
- ### Integration Features
455
- - Challenge scores automatically contribute to daily/weekly leaderboards
456
- - Source tracking via `source_challenge_id` field
457
- - Query parameters for direct leaderboard links
458
- - Settings badges display full game configuration
459
-
460
- ### Challenge Mode ๐ŸŽฏ (Existing Features)
461
- - Share challenges with friends via short URLs (`?game_id=<sid>`)
462
- - Multi-user challenge leaderboards sorted by score and time
463
- - Top 5 players displayed in Challenge Mode banner
464
- - "Show Challenge Share Links" toggle for privacy control (default OFF)
465
-
466
- ## Known Issues / TODO
467
-
468
- - Word list loading bug: the app may not select the proper word lists in some environments. Investigate `word_loader.get_wordlist_files()` / `load_word_list()` and sidebar selection persistence to ensure the chosen file is correctly used by the generator.
469
-
470
- ## Development Phases
471
-
472
- - **Proof of Concept (0.1.0):** No overlaps, basic UI, single session.
473
- - **Beta (0.5.0):** Responsive layout, leaderboards, keyboard support, deterministic seed.
474
- - **Full (1.0.0):** Enhanced UX, Overlaps allowed on shared letters, persistence, daily/practice modes, advanced features.
475
-
476
- See `specs/requirements.md` and `specs/specs.md` for full details and roadmap.
477
-
478
- ## License
479
-
480
- Wrdler is based on BattleWords. BattlewordsTM. All Rights Reserved. All content, trademarks and logos are copyrighted by the owner.
481
-
482
- ## Hugging Face Spaces Configuration
483
-
484
- Wrdler is deployable as a Hugging Face Space. You can use either the YAML config block or a Dockerfile for advanced/custom deployments.
485
-
486
- To configure your Space with the YAML block, add it at the top of your `README.md`:
487
-
488
- ```yaml
489
- ---
490
- title: Wrdler
491
- emoji: ๐ŸŽฒ
492
- colorFrom: blue
493
- colorTo: indigo
494
- sdk: streamlit
495
- sdk_version: 1.51.0
496
- python_version: 3.12.8
497
- app_file: app.py
498
- tags:
499
- - game
500
- - vocabulary
501
- - streamlit
502
- - education
503
- ---
504
- ```
505
-
506
- **Key parameters:**
507
- - `title`, `emoji`, `colorFrom`, `colorTo`: Visuals for your Space.
508
- - `sdk`: Use `streamlit` for Streamlit apps.
509
- - `sdk_version`: Latest supported Streamlit version.
510
- - `python_version`: Python version (default is3.10).
511
- - `app_file`: Entry point for your app.
512
- - `tags`: List of descriptive tags.
513
-
514
- **Dependencies:**
515
- Add a `requirements.txt` with your Python dependencies (e.g., `streamlit`, etc.).
516
-
517
- **Port:**
518
- Streamlit Spaces use port `8501` by default.
519
-
520
- **Embedding:**
521
- Spaces can be embedded in other sites using an `<iframe>`:
522
-
523
- ```html
524
- <iframe src="https://[YourUsername]-Wrdler.hf.space?embed=true" title="Wrdler"></iframe>
525
- ```
526
-
527
- For full configuration options, see [Spaces Config Reference](https://huggingface.co/docs/hub/spaces-config-reference) and [Streamlit SDK Guide](https://huggingface.co/docs/hub/spaces-sdks-streamlit).
528
-
529
- # Assets Setup
530
-
531
- To fully experience Wrdler, especially the audio elements, ensure you set up the following assets:
532
-
533
- - Place your background music `.mp3` files in `wrdler/assets/audio/music/` to enable music.
534
- - Place your sound effect files (`.mp3` or `.wav`) in `wrdler/assets/audio/effects/` for sound effects.
535
-
536
- Refer to the documentation for guidance on compatible audio formats and common troubleshooting tips.
537
-
538
- # Sound Asset Generation
539
-
540
- To generate and save custom sound effects for Wrdler, you can use the `generate_sound_effect` function.
541
-
542
- ## Function: `generate_sound_effect`
543
-
544
- ```python
545
- def generate_sound_effect(effect: str, save_to_assets: bool = False, use_api: str = "huggingface") -> str:
546
- """
547
- Generate a sound effect and save it as a file.
548
-
549
- Parameters:
550
- - `effect`: Name of the effect to generate.
551
- - `save_to_assets`: If `True`, saves the effect to the assets directory;
552
- if `False`, saves to a temporary location. Default is `False`.
553
- - `use_api`: API to use for generation. Options are "huggingface" or "replicate". Default is "huggingface".
554
-
555
- Returns:
556
- - File path to the saved sound effect.
557
- ```
558
-
559
- ## Parameters
560
-
561
- - `effect`: The name of the sound effect you want to generate (e.g., "explosion", "powerup").
562
- - `save_to_assets` (optional): Set to `True` to save the generated sound effect to the game's assets directory. If `False`, the effect is saved to a temporary location. Default is `False`.
563
- - `use_api` (optional): The API to use for generating the sound. Options are `"huggingface"` or `"replicate"`. Default is `"huggingface"`.
564
-
565
- ## Returns
566
-
567
- - The function returns the file path to the saved sound effect, whether it's in the assets directory or a temporary location.
568
-
569
- ## Usage Example
570
-
571
- To generate a sound effect and save it to the assets directory:
572
-
573
- ```python
574
- generate_sound_effect("your_effect_name", save_to_assets=True)
575
- ```
576
-
577
- To generate a sound effect and keep it in a temporary location:
578
-
579
- ```python
580
- temp_path = generate_sound_effect("your_effect_name", save_to_assets=False)
581
- ```
582
-
583
- ## Note
584
-
585
- Ensure you have the necessary permissions and API access (if required) to use the sound generation service. Generated sounds are subject to the terms of use of the respective API.
586
-
587
- For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
588
-
589
  Happy gaming and sound designing!
 
98
  - Leaderboard files store UTC dates/times for each period.
99
  - When viewing daily leaderboards, the app displays the UTC period as a PST date range:
100
  - 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`.
101
+ - 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]`
102
  - Daily leaderboards display the date range in PST for easier local time understanding.
103
  - Weekly leaderboards use ISO week dates (e.g., `2025-W49`) and are available every Monday.
104
 
 
115
  - Source tracking via `source_challenge_id` field
116
  - Unified JSON format with `entry_type` field (daily/weekly/challenge)
117
 
118
+ **Access:** 'Leaderboard' link in the footer navigation at the bottom of the page
119
 
120
  ### Deployment & Technical
121
  - **Dockerfile-based deployment** supported for Hugging Face Spaces and other container platforms
 
237
  4. Earn points for correct guesses and bonus points for unrevealed letters.
238
  5. **The game ends when all six words are found or all word letters are revealed. Your score tier is displayed.**
239
  6. **To play a shared challenge, use a link with `?game_id=<sid>`. Your result will be added to the challenge leaderboard.**
240
+ 7. **Access leaderboards anytime using the 'Leaderboard' link in the footer navigation.**
241
 
242
  ## Changelog
243
 
 
425
 
426
  For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  Happy gaming and sound designing!
specs/leaderboard_spec.md CHANGED
@@ -185,7 +185,7 @@ Instead of maintaining an `index.json` file, leaderboards are discovered by:
185
  โ–ผ โ–ผ
186
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
187
  โ”‚ Daily โ”‚ โ”‚ Weekly โ”‚
188
- โ”‚ LB โ”‚ โ”‚ LB โ”‚
189
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
190
  โ”‚ โ”‚
191
  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
@@ -745,7 +745,7 @@ HF_REPO_ID/games/
745
 
746
  #### Key Differences from Spec
747
 
748
- 1. **Navigation:** Implemented as footer menu integration instead of sidebar button
749
  2. **Caching:** Not implemented in v0.2.0 (deferred to v0.3.0 for optimization)
750
  3. **Tab Implementation:** Used query parameters with custom nav links instead of Streamlit native tabs for better URL support
751
  4. **Table Rendering:** Used pandas DataFrames with styling instead of custom HTML tables
@@ -883,4 +883,4 @@ HF_REPO_ID/games/
883
  "YYYY-MM-DD HH:MM:SS PST to YYYY-MM-DD HH:MM:SS PST"
884
  (PST is UTC-8; adjust for daylight saving as needed)
885
  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`.
886
- The leaderboard expander label should show: `Monday, December 08, 2025 4:00 PM PST โ€“ Tuesday, December 09, 2025 3:59:59 PM PST [settings badge]`
 
185
  โ–ผ โ–ผ
186
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
187
  โ”‚ Daily โ”‚ โ”‚ Weekly โ”‚
188
+ โ”‚ LB โ”‚ โ”‚ LB |
189
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
190
  โ”‚ โ”‚
191
  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
 
745
 
746
  #### Key Differences from Spec
747
 
748
+ 1. **Navigation:** Implemented as 'Leaderboard' link in the footer menu instead of sidebar button
749
  2. **Caching:** Not implemented in v0.2.0 (deferred to v0.3.0 for optimization)
750
  3. **Tab Implementation:** Used query parameters with custom nav links instead of Streamlit native tabs for better URL support
751
  4. **Table Rendering:** Used pandas DataFrames with styling instead of custom HTML tables
 
883
  "YYYY-MM-DD HH:MM:SS PST to YYYY-MM-DD HH:MM:SS PST"
884
  (PST is UTC-8; adjust for daylight saving as needed)
885
  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`.
886
+ 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]`
specs/requirements.md CHANGED
@@ -197,7 +197,9 @@ This document breaks down the implementation tasks for Wrdler using the game rul
197
  2025-12-08 00:00:00 UTC to 2025-12-08 23:59:59 UTC
198
  and
199
  2025-12-07 16:00:00 PST to 2025-12-08 15:59:59 PST
200
- The leaderboard expander label should show: `Monday, December 08, 2025 4:00 PM PST โ€“ Tuesday, December 09, 2025 3:59:59 PM PST [settings badge]`
 
 
201
 
202
  ### Core Implementation
203
 
@@ -289,3 +291,6 @@ games/
289
 
290
  ## Test File Location
291
  All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
 
 
 
 
197
  2025-12-08 00:00:00 UTC to 2025-12-08 23:59:59 UTC
198
  and
199
  2025-12-07 16:00:00 PST to 2025-12-08 15:59:59 PST
200
+ 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]`
201
+
202
+ **Access:** 'Leaderboard' link in the footer navigation at the bottom of the page
203
 
204
  ### Core Implementation
205
 
 
291
 
292
  ## Test File Location
293
  All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
294
+
295
+ ### PWA Support
296
+ - โœ… **PWA Installation:** App is installable as a Progressive Web App on desktop and mobile
specs/specs.md CHANGED
@@ -138,6 +138,17 @@ HF_REPO_ID/games/
138
  - Filters by file_id prefix for matching settings
139
  - Loads and verifies full settings match
140
 
 
 
 
 
 
 
 
 
 
 
 
141
  ### PWA Support
142
  - โœ… **PWA Installation:** App is installable as a Progressive Web App on desktop and mobile
143
  - Added `service worker` and `manifest.json`
 
138
  - Filters by file_id prefix for matching settings
139
  - Loads and verifies full settings match
140
 
141
+ **Date Display Updates:**
142
+ - All leaderboard files use UTC for period boundaries.
143
+ - When displaying daily leaderboards, show the UTC period as a PST date range.
144
+ - Example: For UTC file date 2025-12-08, display:
145
+ 2025-12-08 00:00:00 UTC to 2025-12-08 23:59:59 UTC
146
+ and
147
+ 2025-12-07 16:00:00 PST to 2025-12-08 15:59:59 PST
148
+ 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]`
149
+
150
+ **Access:** 'Leaderboard' link in the footer navigation at the bottom of the page
151
+
152
  ### PWA Support
153
  - โœ… **PWA Installation:** App is installable as a Progressive Web App on desktop and mobile
154
  - Added `service worker` and `manifest.json`
wrdler/__init__.py CHANGED
@@ -9,5 +9,5 @@ Key differences from BattleWords:
9
  - Daily and weekly leaderboards
10
  """
11
 
12
- __version__ = "0.2.2"
13
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
 
9
  - Daily and weekly leaderboards
10
  """
11
 
12
+ __version__ = "0.2.3"
13
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
wrdler/leaderboard_page.py CHANGED
@@ -5,12 +5,16 @@ Wrdler Leaderboard Page
5
  Streamlit page component for displaying daily and weekly leaderboards
6
  with historical lookup capabilities.
7
  """
8
- __version__ = "0.2.0"
9
 
10
  import streamlit as st
11
- from datetime import datetime
12
  from typing import Optional, Tuple
13
  import pandas as pd
 
 
 
 
14
 
15
  from wrdler.leaderboard import (
16
  load_leaderboard,
@@ -27,6 +31,40 @@ from wrdler.leaderboard import (
27
  from wrdler.modules.constants import APP_SETTINGS
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def _format_time(seconds: int) -> str:
31
  """Format seconds as MM:SS."""
32
  mins, secs = divmod(seconds, 60)
@@ -50,9 +88,9 @@ def _settings_badge(lb: LeaderboardSettings) -> str:
50
  source = lb.wordlist_source
51
  incorrect = "errors:on" if lb.show_incorrect_guesses else "errors:off"
52
  free = "free:on" if lb.enable_free_letters else "free:off"
53
- spacer = lb.puzzle_options.get("spacer", 0)
54
- overlap = lb.puzzle_options.get("may_overlap", False)
55
- return f"[{mode} โ€ข {source} โ€ข {incorrect} โ€ข {free} โ€ข spacer:{spacer} โ€ข overlap:{'yes' if overlap else 'no'}]"
56
 
57
 
58
  def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
@@ -223,12 +261,8 @@ def _render_daily_tab():
223
 
224
  for date_id in daily_periods:
225
  # Human friendly date title
226
- try:
227
- date_obj = datetime.strptime(date_id, "%Y-%m-%d")
228
- is_today = date_id == get_current_daily_id()
229
- date_title = ("๐ŸŒŸ Today " if is_today else "") + date_obj.strftime("%A, %B %d, %Y")
230
- except ValueError:
231
- date_title = date_id
232
 
233
  # List all settings folders (file_id) for this period
234
  settings_list = list_settings_for_period("daily", date_id)
@@ -313,7 +347,7 @@ def _render_history_tab():
313
 
314
  if st.button("Load Daily", key="load_daily"):
315
  leaderboard = load_leaderboard("daily", selected_daily, selected_file_id)
316
- header = f"Daily: {selected_daily} "
317
  if leaderboard is not None:
318
  header += _settings_badge(leaderboard)
319
  _render_leaderboard_table(leaderboard, header)
@@ -387,11 +421,7 @@ def _render_today_tab():
387
  lb = load_leaderboard("daily", daily_id, gidd)
388
 
389
  if lb:
390
- try:
391
- date_obj = datetime.strptime(daily_id, "%Y-%m-%d")
392
- date_title = "๐ŸŒŸ Today " + date_obj.strftime("%A, %B %d, %Y")
393
- except ValueError:
394
- date_title = daily_id
395
 
396
  header_suffix = _settings_badge(lb)
397
  with st.expander(f"{date_title} {header_suffix}", expanded=True):
@@ -432,11 +462,7 @@ def _render_today_tab():
432
  st.subheader("๐Ÿ“… Daily")
433
  daily_id = get_current_daily_id()
434
 
435
- try:
436
- date_obj = datetime.strptime(daily_id, "%Y-%m-%d")
437
- date_title = "Today " + date_obj.strftime("%A, %B %d, %Y")
438
- except ValueError:
439
- date_title = daily_id
440
 
441
  settings_list = list_settings_for_period("daily", daily_id)
442
  if not settings_list:
 
5
  Streamlit page component for displaying daily and weekly leaderboards
6
  with historical lookup capabilities.
7
  """
8
+ __version__ = "0.2.2"
9
 
10
  import streamlit as st
11
+ from datetime import datetime, timedelta, timezone
12
  from typing import Optional, Tuple
13
  import pandas as pd
14
+ try:
15
+ from zoneinfo import ZoneInfo
16
+ except ImportError:
17
+ ZoneInfo = None
18
 
19
  from wrdler.leaderboard import (
20
  load_leaderboard,
 
31
  from wrdler.modules.constants import APP_SETTINGS
32
 
33
 
34
+ def _get_pst_range_str(date_id: str) -> str:
35
+ """
36
+ Convert UTC date string (YYYY-MM-DD) to a formatted PST date range string.
37
+ Example: "Mon, Dec 08, 2025 4:00 PM PST โ€“ Tue, Dec 09, 2025 3:59:59 PM PST"
38
+ """
39
+ try:
40
+ # UTC start and end
41
+ utc_start = datetime.strptime(date_id, "%Y-%m-%d").replace(tzinfo=timezone.utc)
42
+ utc_end = utc_start + timedelta(days=1) - timedelta(seconds=1)
43
+
44
+ tz = None
45
+ if ZoneInfo:
46
+ try:
47
+ tz = ZoneInfo("America/Los_Angeles")
48
+ except Exception:
49
+ pass
50
+
51
+ if tz is None:
52
+ # Fallback to fixed offset UTC-8 (PST)
53
+ tz = timezone(timedelta(hours=-8), name="PST")
54
+
55
+ pst_start = utc_start.astimezone(tz)
56
+ pst_end = utc_end.astimezone(tz)
57
+
58
+ def fmt(dt, show_seconds=False):
59
+ # %I is 01-12. lstrip("0") removes leading zero.
60
+ time_str = dt.strftime("%I:%M" + (":%S" if show_seconds else "") + " %p").lstrip("0")
61
+ return f"{dt.strftime('%a, %b %d, %Y')} {time_str} {dt.strftime('%Z')}"
62
+
63
+ return f"{fmt(pst_start)} โ€“ {fmt(pst_end, show_seconds=True)}"
64
+ except Exception:
65
+ return date_id
66
+
67
+
68
  def _format_time(seconds: int) -> str:
69
  """Format seconds as MM:SS."""
70
  mins, secs = divmod(seconds, 60)
 
88
  source = lb.wordlist_source
89
  incorrect = "errors:on" if lb.show_incorrect_guesses else "errors:off"
90
  free = "free:on" if lb.enable_free_letters else "free:off"
91
+ #spacer = lb.puzzle_options.get("spacer", 0)
92
+ #overlap = lb.puzzle_options.get("may_overlap", False)
93
+ return f"\n\n[{mode} โ€ข {source} โ€ข {incorrect} โ€ข {free}]"
94
 
95
 
96
  def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
 
261
 
262
  for date_id in daily_periods:
263
  # Human friendly date title
264
+ is_today = date_id == get_current_daily_id()
265
+ date_title = ("๐ŸŒŸ Today: " if is_today else "") + _get_pst_range_str(date_id)
 
 
 
 
266
 
267
  # List all settings folders (file_id) for this period
268
  settings_list = list_settings_for_period("daily", date_id)
 
347
 
348
  if st.button("Load Daily", key="load_daily"):
349
  leaderboard = load_leaderboard("daily", selected_daily, selected_file_id)
350
+ header = f"Daily: {_get_pst_range_str(selected_daily)} "
351
  if leaderboard is not None:
352
  header += _settings_badge(leaderboard)
353
  _render_leaderboard_table(leaderboard, header)
 
421
  lb = load_leaderboard("daily", daily_id, gidd)
422
 
423
  if lb:
424
+ date_title = "๐ŸŒŸ Today: " + _get_pst_range_str(daily_id)
 
 
 
 
425
 
426
  header_suffix = _settings_badge(lb)
427
  with st.expander(f"{date_title} {header_suffix}", expanded=True):
 
462
  st.subheader("๐Ÿ“… Daily")
463
  daily_id = get_current_daily_id()
464
 
465
+ date_title = "Today " + _get_pst_range_str(daily_id)
 
 
 
 
466
 
467
  settings_list = list_settings_for_period("daily", daily_id)
468
  if not settings_list:
wrdler/ui.py CHANGED
@@ -1618,7 +1618,7 @@ def _render_guess_form(state: GameState):
1618
  display: inline-flex;
1619
  align-items: center;
1620
  width: auto;
1621
- min-width: 100px;
1622
  }
1623
  .st-key-guess_input .stTooltipIcon .stTooltipHoverTarget::after {
1624
  content: "incorrect guesses";
@@ -2408,9 +2408,7 @@ def _render_footer(current_page: str = "play"):
2408
  """
2409
  # Determine which link should be highlighted as active
2410
  play_active = "active" if current_page == "play" else ""
2411
- today_active = "active" if current_page == "today" else ""
2412
- daily_active = "active" if current_page == "daily" else ""
2413
- weekly_active = "active" if current_page == "weekly" else ""
2414
 
2415
  # Check if we're in challenge mode and need to preserve game_id
2416
  game_id = None
@@ -2430,11 +2428,13 @@ def _render_footer(current_page: str = "play"):
2430
  today_url = f"?page=today&game_id={game_id}"
2431
  daily_url = f"?page=daily&game_id={game_id}"
2432
  weekly_url = f"?page=weekly&game_id={game_id}"
 
2433
  play_url = f"?game_id={game_id}"
2434
  else:
2435
  today_url = "?page=today"
2436
  daily_url = "?page=daily"
2437
  weekly_url = "?page=weekly"
 
2438
  play_url = "/"
2439
 
2440
  st.markdown(
@@ -2494,9 +2494,7 @@ def _render_footer(current_page: str = "play"):
2494
  </style>
2495
  <div class="bw-footer">
2496
  <nav class="bw-footer-nav">
2497
- <a href="{today_url}" title="View Today's Leaderboards" target="_self" class="{today_active}">๐ŸŒŸ Today</a>
2498
- <a href="{daily_url}" title="View Daily Leaderboards" target="_self" class="{daily_active}">๐Ÿ“… Daily</a>
2499
- <a href="{weekly_url}" title="View Weekly Leaderboards" target="_self" class="{weekly_active}">๐Ÿ“† Weekly</a>
2500
  <a href="{play_url}" title="Play Wrdler" target="_self" class="{play_active}">๐ŸŽฎ Play</a>
2501
  </nav>
2502
  </div>
 
1618
  display: inline-flex;
1619
  align-items: center;
1620
  width: auto;
1621
+ min-width: 100px;
1622
  }
1623
  .st-key-guess_input .stTooltipIcon .stTooltipHoverTarget::after {
1624
  content: "incorrect guesses";
 
2408
  """
2409
  # Determine which link should be highlighted as active
2410
  play_active = "active" if current_page == "play" else ""
2411
+ leaderboard_active = "active" if current_page in {"today", "daily", "weekly", "history"} else ""
 
 
2412
 
2413
  # Check if we're in challenge mode and need to preserve game_id
2414
  game_id = None
 
2428
  today_url = f"?page=today&game_id={game_id}"
2429
  daily_url = f"?page=daily&game_id={game_id}"
2430
  weekly_url = f"?page=weekly&game_id={game_id}"
2431
+ leaderboard_url = f"?page=today&game_id={game_id}"
2432
  play_url = f"?game_id={game_id}"
2433
  else:
2434
  today_url = "?page=today"
2435
  daily_url = "?page=daily"
2436
  weekly_url = "?page=weekly"
2437
+ leaderboard_url = "?page=today"
2438
  play_url = "/"
2439
 
2440
  st.markdown(
 
2494
  </style>
2495
  <div class="bw-footer">
2496
  <nav class="bw-footer-nav">
2497
+ <a href="{leaderboard_url}" title="View Leaderboards" target="_self" class="{leaderboard_active}">๐Ÿ† Leaderboard</a>
 
 
2498
  <a href="{play_url}" title="Play Wrdler" target="_self" class="{play_active}">๐ŸŽฎ Play</a>
2499
  </nav>
2500
  </div>