Surn commited on
Commit
fb5148a
Β·
1 Parent(s): a71802e

v0.2.14: Leaderboard and UI Enhancements

Browse files

Updated leaderboard navigation to footer, improved time precision with millisecond support, and refined daily/weekly leaderboard structure. Simplified "Today" tab to focus on daily leaderboards. Enhanced game-over dialog with integrated leaderboard submission and updated timer display.

Updated `UserEntry` and related functions for `float` time handling, ensuring backward compatibility. Improved UI clarity, fixed typos, and adjusted leaderboard rendering logic. Updated documentation to reflect new features and API changes. Performed bug fixes and code cleanup for consistency.

CLAUDE.md CHANGED
@@ -1,6 +1,6 @@
1
  # CLAUDE
2
 
3
- Wrdler v0.2.13
4
 
5
  # Wrdler - Project Context
6
 
@@ -12,18 +12,18 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
12
  - **2 free letter guesses at game start** (all instances revealed)
13
  - **Word composition:** 2 four-letter, 2 five-letter, 2 six-letter words per puzzle
14
 
15
- **Current Version:** 0.2.13
16
- **Last Updated:** 2025-12-31
17
  **Repository:** https://github.com/Oncorporation/Wrdler.git
18
  **Branch:** main
19
 
20
- ## Recent Changes (v0.2.13)
21
  - Leaderboard navigation moved to footer menu (not sidebar)
22
  - Game over dialog integrates leaderboard submission and shows qualification results
23
  - Leaderboard page routing uses query parameters and custom navigation links
24
  - Footer navigation links to Leaderboard, Play, and Settings pages
25
 
26
- ## Current Features (v0.2.13)
27
 
28
  ### Core Gameplay
29
  - 8x6 grid with 6 hidden words (one per row, horizontal only)
 
1
  # CLAUDE
2
 
3
+ Wrdler v0.2.14
4
 
5
  # Wrdler - Project Context
6
 
 
12
  - **2 free letter guesses at game start** (all instances revealed)
13
  - **Word composition:** 2 four-letter, 2 five-letter, 2 six-letter words per puzzle
14
 
15
+ **Current Version:** 0.2.14
16
+ **Last Updated:** 2026-01-05
17
  **Repository:** https://github.com/Oncorporation/Wrdler.git
18
  **Branch:** main
19
 
20
+ ## Recent Changes (v0.2.14)
21
  - Leaderboard navigation moved to footer menu (not sidebar)
22
  - Game over dialog integrates leaderboard submission and shows qualification results
23
  - Leaderboard page routing uses query parameters and custom navigation links
24
  - Footer navigation links to Leaderboard, Play, and Settings pages
25
 
26
+ ## Current Features (v0.2.14)
27
 
28
  ### Core Gameplay
29
  - 8x6 grid with 6 hidden words (one per row, horizontal only)
GAMEPLAY_GUIDE.md CHANGED
@@ -1,8 +1,8 @@
1
  # Wrdler Gameplay Guide
2
 
3
- Version 0.2.13
4
 
5
- **Last Updated:** 2025-12-31
6
 
7
  ## Welcome to Wrdler!
8
 
@@ -10,7 +10,7 @@ Wrdler is a simplified vocabulary puzzle game where you discover 6 hidden words
10
 
11
  ---
12
 
13
- ## Recent Changes (v0.2.13)
14
  - Leaderboard navigation moved to footer menu (not the sidebar)
15
  - Footer navigation links to Leaderboard, Play, and Settings pages
16
  - Game over dialog integrates leaderboard submission and shows qualification results
 
1
  # Wrdler Gameplay Guide
2
 
3
+ Version 0.2.14
4
 
5
+ **Last Updated:** 2026-01-05
6
 
7
  ## Welcome to Wrdler!
8
 
 
10
 
11
  ---
12
 
13
+ ## Recent Changes (v0.2.14)
14
  - Leaderboard navigation moved to footer menu (not the sidebar)
15
  - Footer navigation links to Leaderboard, Play, and Settings pages
16
  - Game over dialog integrates leaderboard submission and shows qualification results
README.md CHANGED
@@ -21,7 +21,7 @@ thumbnail: >-
21
 
22
  # Wrdler
23
 
24
- Version 0.2.13
25
 
26
  Wrdler is a vocabulary puzzle game with daily and weekly leaderboards, 8x6 grid, and two free letter guesses at the start. See CHANGELOG or specs for details on the latest updates.
27
 
@@ -29,12 +29,12 @@ Wrdler is a vocabulary puzzle game with daily and weekly leaderboards, 8x6 grid,
29
 
30
  Wrdler is a vocabulary learning game with a simplified grid and strategic letter guessing. The objective is to discover hidden words on a grid by making smart guesses before all letters are revealed.
31
 
32
- **Current Version:** v0.2.13
33
- **Last Updated:** 2025-12-31
34
 
35
- ## Recent Changes (v0.2.13)
 
36
  - Footer navigation links to Leaderboard, Play, and Settings pages
37
- - Leaderboard navigation moved to footer menu (not sidebar)
38
  - Game over dialog integrates leaderboard submission and shows qualification results
39
  - Leaderboard page routing uses query parameters and custom navigation links
40
 
 
21
 
22
  # Wrdler
23
 
24
+ Version 0.2.14
25
 
26
  Wrdler is a vocabulary puzzle game with daily and weekly leaderboards, 8x6 grid, and two free letter guesses at the start. See CHANGELOG or specs for details on the latest updates.
27
 
 
29
 
30
  Wrdler is a vocabulary learning game with a simplified grid and strategic letter guessing. The objective is to discover hidden words on a grid by making smart guesses before all letters are revealed.
31
 
32
+ **Current Version:** v0.2.14
33
+ **Last Updated:** 2026-01-05
34
 
35
+ ## Recent Changes (v0.2.14)
36
+ - Leaderboard navigation moved to footer menu (not the sidebar)
37
  - Footer navigation links to Leaderboard, Play, and Settings pages
 
38
  - Game over dialog integrates leaderboard submission and shows qualification results
39
  - Leaderboard page routing uses query parameters and custom navigation links
40
 
specs/leaderboard_spec.md CHANGED
@@ -1,29 +1,16 @@
1
  ο»Ώ# Wrdler Leaderboard System Specification
2
 
3
- -**Document Version:** 1.4.4
4
- -**Project Version:** 0.2.13
5
- -**Author:** GitHub Copilot
6
- -**Last Updated:** 2025-12-31
7
- -**Status:** βœ… Implemented and Documented
8
- +**Document Version:** 1.4.5
9
- +**Project Version:** 0.2.14
10
- +**Author:** GitHub Copilot
11
- +**Last Updated:** 2026-01-01
12
- +**Status:** βœ… Implemented and Documented
13
-
14
- -## Recent Changes (v0.2.12)
15
- -- Layout changes for improved usability
16
- -- Fixed static spinner graphic and favicon
17
- -- Background enable/disable toggles improved
18
- -- Sidebar disabled for streamlined UI
19
- +## Recent Changes (v0.2.13)
20
- +- Leaderboard navigation moved to footer menu (not the sidebar)
21
- +- Footer navigation links to Leaderboard, Play, and Settings pages
22
- +- Game over dialog integrates leaderboard submission and shows qualification results
23
- +- Leaderboard page routing uses query parameters and custom navigation links
24
- +
25
- +## Planned (v0.2.14)
26
- +- Documented API for collecting submitted words and estimating per-word difficulty from leaderboard `UserEntry` data (time/6 and score/6)
27
 
28
  ---
29
 
@@ -303,10 +290,10 @@ Each user entry in the `users` array:
303
  "uid": "20251130T190249Z-0XLG5O",
304
  "username": "Charles",
305
  "word_list": ["CHEER", "PLENTY", "REAL", "ARRIVE", "DEAF", "DAILY"],
306
- "word_list_difficulty": 117.48,
307
  "score": 39,
308
  "time": 132,
309
  "timestamp": "2025-11-30T19:02:49.544933+00:00",
 
310
  "source_challenge_id": null
311
  }
312
  ```
@@ -316,11 +303,13 @@ Each user entry in the `users` array:
316
  | `uid` | string | Unique user entry ID |
317
  | `username` | string | Player display name |
318
  | `word_list` | array | 6 words played |
319
- | `word_list_difficulty` | float | Calculated difficulty score |
320
  | `score` | int | Final score |
321
- | `time` | int | Time in seconds |
322
  | `timestamp` | string | ISO 8601 when entry was recorded |
323
- | `source_challenge_id` | string\|null | If from a challenge, the original challenge_id |
 
 
 
324
 
325
  ### 4.5 Settings Matching
326
 
@@ -486,7 +475,7 @@ __version__ = "0.1.6" # Updated to add folder listing functions
486
  def submit_score_to_all_leaderboards(
487
  username: str,
488
  score: int,
489
- time_seconds: int,
490
  word_list: List[str],
491
  settings: GameSettings,
492
  word_list_difficulty: Optional[float] = None,
@@ -933,10 +922,4 @@ HF_REPO_ID/games/
933
  "YYYY-MM-DD HH:MM:SS PST to YYYY-MM-DD HH:MM:SS PST"
934
  (PST is UTC-8; adjust for daylight saving as needed)
935
  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`.
936
- 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]`
937
-
938
- **Leaderboard Page UI:**
939
- - **Today Tab:** Current daily and weekly leaderboards
940
- - **Daily Tab:** Last 7 days of daily leaderboards
941
- - **Weekly Tab:** Last 5 weeks displayed as individual expanders (current week or `week=YYYY-Www` query opens by default)
942
- - **History Tab:** Historical leaderboard browser
 
1
  ο»Ώ# Wrdler Leaderboard System Specification
2
 
3
+ **Document Version:** 1.4.6
4
+ **Project Version:** 0.2.14
5
+ **Author:** GitHub Copilot
6
+ **Last Updated:** 2026-01-05
7
+ **Status:** βœ… Implemented and Documented
8
+
9
+ ## Recent Changes (v0.2.14)
10
+ - Leaderboard navigation moved to footer menu (not the sidebar)
11
+ - Footer navigation links to Leaderboard, Play, and Settings pages
12
+ - Game over dialog integrates leaderboard submission and shows qualification results
13
+ - Leaderboard page routing uses query parameters and custom navigation links
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  ---
16
 
 
290
  "uid": "20251130T190249Z-0XLG5O",
291
  "username": "Charles",
292
  "word_list": ["CHEER", "PLENTY", "REAL", "ARRIVE", "DEAF", "DAILY"],
 
293
  "score": 39,
294
  "time": 132,
295
  "timestamp": "2025-11-30T19:02:49.544933+00:00",
296
+ "word_list_difficulty": 117.48,
297
  "source_challenge_id": null
298
  }
299
  ```
 
303
  | `uid` | string | Unique user entry ID |
304
  | `username` | string | Player display name |
305
  | `word_list` | array | 6 words played |
 
306
  | `score` | int | Final score |
307
+ | `time` | float | Time in seconds (stored as `time`; legacy reads may also accept `time_seconds`) |
308
  | `timestamp` | string | ISO 8601 when entry was recorded |
309
+ | `word_list_difficulty` | float\|null | Calculated difficulty score (optional) |
310
+ | `source_challenge_id` | string\|null | If from a challenge, the original `challenge_id` (optional) |
311
+
312
+ **Implementation note:** `UserEntry.from_dict()` supports both `time` and legacy `time_seconds` during load; serialization always writes `time`.
313
 
314
  ### 4.5 Settings Matching
315
 
 
475
  def submit_score_to_all_leaderboards(
476
  username: str,
477
  score: int,
478
+ time_seconds: float,
479
  word_list: List[str],
480
  settings: GameSettings,
481
  word_list_difficulty: Optional[float] = None,
 
922
  "YYYY-MM-DD HH:MM:SS PST to YYYY-MM-DD HH:MM:SS PST"
923
  (PST is UTC-8; adjust for daylight saving as needed)
924
  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`.
925
+ 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
@@ -1,10 +1,10 @@
1
  ο»Ώ# Wrdler Requirements
2
 
3
- **Version:** 0.2.13
4
  **Status:** Production Ready - Leaderboards Implemented
5
- **Last Updated:** 2025-12-31
6
 
7
- ## Recent Changes (v0.2.13)
8
  - Leaderboard navigation moved to footer menu (not sidebar)
9
  - Footer navigation links to Leaderboard, Play, and Settings pages
10
  - Game over dialog integrates leaderboard submission and shows qualification results
 
1
  ο»Ώ# Wrdler Requirements
2
 
3
+ **Version:** 0.2.14
4
  **Status:** Production Ready - Leaderboards Implemented
5
+ **Last Updated:** 2026-01-05
6
 
7
+ ## Recent Changes (v0.2.14)
8
  - Leaderboard navigation moved to footer menu (not sidebar)
9
  - Footer navigation links to Leaderboard, Play, and Settings pages
10
  - Game over dialog integrates leaderboard submission and shows qualification results
specs/specs.md CHANGED
@@ -1,11 +1,11 @@
1
  # Wrdler Specifications
2
 
3
- **Version:** 0.2.13
4
- **Last Updated:** 2025-12-31
5
 
6
  **Status:** Production Ready - Leaderboards & Enhanced Settings Page Implemented
7
 
8
- ## Recent Changes (v0.2.13)
9
  - Leaderboard navigation moved to footer menu (not sidebar)
10
  - Footer navigation links to Leaderboard, Play, and Settings pages
11
  - Game over dialog integrates leaderboard submission and shows qualification results
@@ -145,7 +145,7 @@ HF_REPO_ID/games/
145
  - **Daily Tab:** Last 7 days of daily leaderboards with expandable date groups
146
  - Expandable groups per date
147
  - All settings combinations shown
148
- - **Weekly Tab:** Last 5 weeks, each rendered as its own expander (current or `week=YYYY-Www` query selection opens by default)
149
  - All settings combinations displayed
150
  - **History Tab:** Historical leaderboard browser with dropdown selectors
151
  - Dropdown selectors for period and settings
 
1
  # Wrdler Specifications
2
 
3
+ **Version:** 0.2.14
4
+ **Last Updated:** 2026-01-05
5
 
6
  **Status:** Production Ready - Leaderboards & Enhanced Settings Page Implemented
7
 
8
+ ## Recent Changes (v0.2.14)
9
  - Leaderboard navigation moved to footer menu (not sidebar)
10
  - Footer navigation links to Leaderboard, Play, and Settings pages
11
  - Game over dialog integrates leaderboard submission and shows qualification results
 
145
  - **Daily Tab:** Last 7 days of daily leaderboards with expandable date groups
146
  - Expandable groups per date
147
  - All settings combinations shown
148
+ - **Weekly Tab:** Last 5 weeks, each rendered as its own expander (current or `week=YYYY-Ww` query selection opens by default)
149
  - All settings combinations displayed
150
  - **History Tab:** Historical leaderboard browser with dropdown selectors
151
  - Dropdown selectors for period and settings
wrdler/__init__.py CHANGED
@@ -8,8 +8,8 @@ Key differences from BattleWords:
8
  - 2 free letter guesses at game start
9
  - Daily and weekly leaderboards
10
 
11
- v0.2.13: Footer navigation updates, leaderboard routing via query params, integrated game-over leaderboard submission.
12
  """
13
 
14
- __version__ = "0.2.13"
15
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
 
8
  - 2 free letter guesses at game start
9
  - Daily and weekly leaderboards
10
 
11
+ v0.2.14: Leaderboard changes to reduce API calls, add millisecond precision to game time.
12
  """
13
 
14
+ __version__ = "0.2.14"
15
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
wrdler/game_storage.py CHANGED
@@ -12,7 +12,7 @@ Wrdler Specifications:
12
  - No word overlaps
13
  - 2 free letter guesses at game start
14
  """
15
- __version__ = "0.2.0"
16
 
17
  import json
18
  import tempfile
@@ -60,7 +60,7 @@ def serialize_game_settings(
60
  word_list: List[str],
61
  username: str,
62
  score: int,
63
- time_seconds: int,
64
  game_mode: str,
65
  grid_size: int = 8,
66
  spacer: int = 0,
@@ -160,7 +160,7 @@ def add_user_result_to_game(
160
  username: str,
161
  word_list: List[str],
162
  score: int,
163
- time_seconds: int,
164
  repo_id: Optional[str] = None
165
  ) -> bool:
166
  """
@@ -215,7 +215,7 @@ def add_user_result_to_game(
215
  if difficulty_value is not None:
216
  user_result["word_list_difficulty"] = difficulty_value
217
  user_result["score"] = score
218
- user_result["time"] = time_seconds
219
  user_result["timestamp"] = datetime.now(timezone.utc).isoformat()
220
 
221
  # Add to users array
@@ -275,7 +275,7 @@ def save_game_to_hf(
275
  word_list: List[str],
276
  username: str,
277
  score: int,
278
- time_seconds: int,
279
  game_mode: str,
280
  grid_size: int = 8,
281
  spacer: int = 0,
@@ -332,7 +332,7 @@ def save_game_to_hf(
332
  ... word_list=["WORD", "TEST", "GAME", "PLAY", "SCORE", "FINAL"],
333
  ... username="Alice",
334
  ... score=42,
335
- ... time_seconds=180,
336
  ... game_mode="classic",
337
  ... wordlist_source="classic.txt"
338
  ... )
@@ -349,7 +349,7 @@ def save_game_to_hf(
349
  word_list=word_list,
350
  username=username,
351
  score=score,
352
- time_seconds=time_seconds,
353
  game_mode=game_mode,
354
  grid_size=grid_size,
355
  spacer=spacer,
@@ -579,7 +579,7 @@ if __name__ == "__main__":
579
  word_list=["WORD", "TEST", "GAME", "PLAY", "SCORE", "FINAL"],
580
  username="Alice",
581
  score=42,
582
- time_seconds=180,
583
  game_mode="classic",
584
  grid_size=8, # Wrdler default
585
  wordlist_source="classic.txt"
 
12
  - No word overlaps
13
  - 2 free letter guesses at game start
14
  """
15
+ __version__ = "0.2.1"
16
 
17
  import json
18
  import tempfile
 
60
  word_list: List[str],
61
  username: str,
62
  score: int,
63
+ time_seconds: float,
64
  game_mode: str,
65
  grid_size: int = 8,
66
  spacer: int = 0,
 
160
  username: str,
161
  word_list: List[str],
162
  score: int,
163
+ time_seconds: float,
164
  repo_id: Optional[str] = None
165
  ) -> bool:
166
  """
 
215
  if difficulty_value is not None:
216
  user_result["word_list_difficulty"] = difficulty_value
217
  user_result["score"] = score
218
+ user_result["time"] = float(time_seconds)
219
  user_result["timestamp"] = datetime.now(timezone.utc).isoformat()
220
 
221
  # Add to users array
 
275
  word_list: List[str],
276
  username: str,
277
  score: int,
278
+ time_seconds: float,
279
  game_mode: str,
280
  grid_size: int = 8,
281
  spacer: int = 0,
 
332
  ... word_list=["WORD", "TEST", "GAME", "PLAY", "SCORE", "FINAL"],
333
  ... username="Alice",
334
  ... score=42,
335
+ ... time_seconds=180.1,
336
  ... game_mode="classic",
337
  ... wordlist_source="classic.txt"
338
  ... )
 
349
  word_list=word_list,
350
  username=username,
351
  score=score,
352
+ time_seconds=float(time_seconds),
353
  game_mode=game_mode,
354
  grid_size=grid_size,
355
  spacer=spacer,
 
579
  word_list=["WORD", "TEST", "GAME", "PLAY", "SCORE", "FINAL"],
580
  username="Alice",
581
  score=42,
582
+ time_seconds=180.01,
583
  game_mode="classic",
584
  grid_size=8, # Wrdler default
585
  wordlist_source="classic.txt"
wrdler/leaderboard.py CHANGED
@@ -198,7 +198,7 @@ class UserEntry:
198
  username: str
199
  word_list: List[str]
200
  score: int
201
- time: int # seconds (matches existing 'time' field, not 'time_seconds')
202
  timestamp: str
203
  word_list_difficulty: Optional[float] = None
204
  source_challenge_id: Optional[str] = None # If entry came from a challenge
@@ -227,7 +227,7 @@ class UserEntry:
227
  username=data["username"],
228
  word_list=data["word_list"],
229
  score=data["score"],
230
- time=data.get("time", data.get("time_seconds", 0)), # Handle both field names
231
  timestamp=data["timestamp"],
232
  word_list_difficulty=data.get("word_list_difficulty"),
233
  source_challenge_id=data.get("source_challenge_id"),
@@ -611,7 +611,7 @@ def save_leaderboard(
611
  def create_user_entry(
612
  username: str,
613
  score: int,
614
- time_seconds: int,
615
  word_list: List[str],
616
  word_list_difficulty: Optional[float] = None,
617
  source_challenge_id: Optional[str] = None
@@ -632,7 +632,7 @@ def create_user_entry(
632
  def check_qualification(
633
  leaderboard: Optional[LeaderboardSettings],
634
  score: int,
635
- time_seconds: int,
636
  word_list_difficulty: Optional[float] = None
637
  ) -> bool:
638
  """
@@ -793,7 +793,7 @@ def submit_to_leaderboard(
793
  def submit_score_to_all_leaderboards(
794
  username: str,
795
  score: int,
796
- time_seconds: int,
797
  word_list: List[str],
798
  settings: GameSettings,
799
  word_list_difficulty: Optional[float] = None,
 
198
  username: str
199
  word_list: List[str]
200
  score: int
201
+ time: float # seconds (matches existing 'time' field, not 'time_seconds')
202
  timestamp: str
203
  word_list_difficulty: Optional[float] = None
204
  source_challenge_id: Optional[str] = None # If entry came from a challenge
 
227
  username=data["username"],
228
  word_list=data["word_list"],
229
  score=data["score"],
230
+ time=data.get("time", data.get("time_seconds", 0.0)), # Handle both field names
231
  timestamp=data["timestamp"],
232
  word_list_difficulty=data.get("word_list_difficulty"),
233
  source_challenge_id=data.get("source_challenge_id"),
 
611
  def create_user_entry(
612
  username: str,
613
  score: int,
614
+ time_seconds: float,
615
  word_list: List[str],
616
  word_list_difficulty: Optional[float] = None,
617
  source_challenge_id: Optional[str] = None
 
632
  def check_qualification(
633
  leaderboard: Optional[LeaderboardSettings],
634
  score: int,
635
+ time_seconds: float,
636
  word_list_difficulty: Optional[float] = None
637
  ) -> bool:
638
  """
 
793
  def submit_score_to_all_leaderboards(
794
  username: str,
795
  score: int,
796
+ time_seconds: float,
797
  word_list: List[str],
798
  settings: GameSettings,
799
  word_list_difficulty: Optional[float] = None,
wrdler/leaderboard_page.py CHANGED
@@ -106,10 +106,10 @@ def _get_pst_range_str_from_utc_timestamp(utc_ts) -> str:
106
  except Exception:
107
  return str(utc_ts)
108
 
109
- def _format_time(seconds: int) -> str:
110
  """Format seconds as MM:SS."""
111
  mins, secs = divmod(seconds, 60)
112
- return f"{mins:02d}:{secs:02d}"
113
 
114
 
115
  def _get_rank_emoji(rank: int) -> str:
@@ -322,8 +322,8 @@ def render_leaderboard_page(default_tab: str = "daily"):
322
  f"""
323
  <div class="bw-tab-nav">
324
  <a href="?page=today" target="_self" class="{'active' if active_tab == 'today' else ''}">🌟 Today</a>
325
- <a href="?page=daily" target="_self" class="{'active' if active_tab == 'daily' else ''}">πŸ“… Daily</a>
326
- <a href="?page=weekly" target="_self" class="{'active' if active_tab == 'weekly' else ''}">πŸ“† Weekly</a>
327
  <a href="?page=history" target="_self" class="{'active' if active_tab == 'history' else ''}">πŸ“š History</a>
328
  </div>
329
  """,
@@ -352,7 +352,7 @@ def _render_daily_tab():
352
  # Show last 7 days; for each day, show all leaderboards (settings combos)
353
  daily_periods = list_available_periods("daily", limit=7)
354
  if not daily_periods:
355
- st.info("No daily leaderboards found.")
356
  return
357
 
358
  for date_id in daily_periods:
@@ -384,7 +384,7 @@ def _render_daily_tab():
384
 
385
  def _render_weekly_tab():
386
  """Render weekly leaderboard tab."""
387
- st.header("πŸ“† Weekly Leaderboards")
388
  st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each ISO week. Resets Monday at UTC midnight.")
389
 
390
  try:
@@ -395,9 +395,9 @@ def _render_weekly_tab():
395
  requested_week = params.get("week")
396
  current_week = get_current_weekly_id()
397
 
398
- weekly_periods = list_available_periods("weekly", limit=5)
399
  if not weekly_periods:
400
- st.info("No weekly leaderboards found.")
401
  return
402
 
403
  if requested_week in weekly_periods:
@@ -418,7 +418,7 @@ def _render_weekly_tab():
418
  expander_title = f"πŸ—“οΈ {week_title}"
419
  with st.expander(expander_title, expanded=(week_id == expanded_week)):
420
  if not settings_list:
421
- st.info("No leaderboards found for this week.")
422
  continue
423
 
424
  for idx, settings_info in enumerate(settings_list):
@@ -442,7 +442,7 @@ def _render_history_tab():
442
 
443
  weekly_periods = list_available_periods("weekly", limit=12)
444
  if not weekly_periods:
445
- st.info("No weekly leaderboards found.")
446
  return
447
 
448
  week_options = [(_format_week_title(week_id), week_id) for week_id in weekly_periods]
@@ -512,14 +512,14 @@ def _render_history_tab():
512
  with col1:
513
  st.subheader("Daily History")
514
  if not week_daily_ids:
515
- st.info("No daily leaderboards found for this week.")
516
  else:
517
  for date_id in week_daily_ids:
518
  date_title = _get_pst_range_str(date_id)
519
  settings_list = list_settings_for_period("daily", date_id)
520
  if not settings_list:
521
  with st.expander(date_title, expanded=False):
522
- st.info("No leaderboards found for this date.")
523
  continue
524
 
525
  for settings_info in settings_list:
@@ -541,7 +541,7 @@ def _render_history_tab():
541
  week_title = _format_week_title(selected_week)
542
  settings_list = list_settings_for_period("weekly", selected_week)
543
  if not settings_list:
544
- st.info("No leaderboards found for this week.")
545
  else:
546
  for idx, settings_info in enumerate(settings_list):
547
  file_id = settings_info["file_id"]
@@ -558,13 +558,12 @@ def _render_history_tab():
558
 
559
 
560
  def _render_today_tab():
561
- """Render today's leaderboards tab - shows current daily and weekly leaderboards.
562
 
563
  If query string parameters are present:
564
  - gidd: Show only the specified daily leaderboard (file_id)
565
- - gidw: Show only the specified weekly leaderboard (file_id)
566
 
567
- Otherwise, show all current daily and weekly leaderboards in two columns.
568
  """
569
  # Get query parameters
570
  try:
@@ -573,105 +572,52 @@ def _render_today_tab():
573
  params = {}
574
 
575
  gidd = params.get("gidd", None)
576
- gidw = params.get("gidw", None)
577
-
578
- # If both query params are present, show filtered view
579
- if gidd or gidw:
580
  st.header("🎯 Today's Leaderboards (Filtered)")
581
-
582
- # Use two columns for filtered view as well
583
- col1, col2 = st.columns(2)
584
-
585
- # Show daily leaderboard if gidd is specified
586
- with col1:
587
- if gidd:
588
- st.subheader("πŸ“… Daily")
589
- daily_id = get_current_daily_id()
590
- lb = load_leaderboard("daily", daily_id, gidd)
591
-
592
- if lb:
593
- date_title = "🌟 Today: " + _get_pst_range_str(daily_id)
594
-
595
- header_suffix = _settings_badge(lb)
596
- with st.expander(f"{date_title} {header_suffix}", expanded=True):
597
- st.caption(f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}")
598
- _render_leaderboard_table(lb, "", grid_key=f"today-daily-{daily_id}-{gidd}")
599
- else:
600
- st.warning(f"Daily leaderboard not found: {gidd}")
601
-
602
- # Show weekly leaderboard if gidw is specified
603
- with col2:
604
- if gidw:
605
- st.subheader("πŸ“† Weekly")
606
- weekly_id = get_current_weekly_id()
607
- lb = load_leaderboard("weekly", weekly_id, gidw)
608
-
609
- if lb:
610
- try:
611
- year, week = weekly_id.split("-W")
612
- week_title = f"Week {int(week)}, {year}"
613
- except ValueError:
614
- week_title = weekly_id
615
-
616
- header_suffix = _settings_badge(lb)
617
- with st.expander(f"{week_title} {header_suffix}", expanded=True):
618
- st.caption(f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}")
619
- _render_leaderboard_table(lb, "", grid_key=f"today-weekly-{weekly_id}-{gidw}")
620
- else:
621
- st.warning(f"Weekly leaderboard not found: {gidw}")
622
  else:
623
- # Show all current leaderboards (default view) in two columns
624
  st.header("🌟 Today's Leaderboards")
625
- st.write("Current daily and weekly leaderboards. Resets: Daily at UTC midnight, Weekly on Monday.")
626
-
627
- col1, col2 = st.columns(2)
628
-
629
- # Left column: Current Daily Leaderboards
630
- with col1:
631
- st.subheader("πŸ“… Daily")
632
- daily_id = get_current_daily_id()
633
-
634
- date_title = "Today " + _get_pst_range_str(daily_id)
635
-
636
- settings_list = list_settings_for_period("daily", daily_id)
637
- if not settings_list:
638
- st.info("No daily leaderboards found for today.")
639
- else:
640
- for settings_info in settings_list:
641
- file_id = settings_info["file_id"]
642
- lb = load_leaderboard("daily", daily_id, file_id)
643
- header_suffix = ""
644
- if lb is not None:
645
- header_suffix = _settings_badge(lb)
646
- expander_title = f"{date_title} {header_suffix}" if header_suffix else date_title
647
- with st.expander(expander_title, expanded=True):
648
- if lb is not None:
649
- st.caption(f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}")
650
- _render_leaderboard_table(lb, "", grid_key=f"today-daily-{daily_id}-{file_id}")
651
-
652
- # Right column: Current Weekly Leaderboards
653
- with col2:
654
- st.subheader("πŸ“† Weekly")
655
- weekly_id = get_current_weekly_id()
656
-
657
- try:
658
- year, week = weekly_id.split("-W")
659
- week_title = f"Week {int(week)}, {year}"
660
- except ValueError:
661
- week_title = weekly_id
662
-
663
- settings_list = list_settings_for_period("weekly", weekly_id)
664
- if not settings_list:
665
- st.info("No weekly leaderboards found for this week.")
666
- else:
667
- for settings_info in settings_list:
668
- file_id = settings_info["file_id"]
669
- lb = load_leaderboard("weekly", weekly_id, file_id)
670
- header_suffix = ""
671
  if lb is not None:
672
- header_suffix = _settings_badge(lb)
673
- expander_title = f"{week_title} {header_suffix}" if header_suffix else week_title
674
- with st.expander(expander_title, expanded=True):
675
- if lb is not None:
676
- st.caption(f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}")
677
- _render_leaderboard_table(lb, "", grid_key=f"today-weekly-{weekly_id}-{file_id}")
 
106
  except Exception:
107
  return str(utc_ts)
108
 
109
+ def _format_time(seconds: float) -> str:
110
  """Format seconds as MM:SS."""
111
  mins, secs = divmod(seconds, 60)
112
+ return f"{mins:02.0f}:{secs:06.3f}"
113
 
114
 
115
  def _get_rank_emoji(rank: int) -> str:
 
322
  f"""
323
  <div class="bw-tab-nav">
324
  <a href="?page=today" target="_self" class="{'active' if active_tab == 'today' else ''}">🌟 Today</a>
325
+ <!-- <a href="?page=daily" target="_self" class="{'active' if active_tab == 'daily' else ''}">πŸ“… Daily</a> -->
326
+ <a href="?page=weekly" target="_self" class="{'active' if active_tab == 'weekly' else ''}">πŸ“† This Week</a>
327
  <a href="?page=history" target="_self" class="{'active' if active_tab == 'history' else ''}">πŸ“š History</a>
328
  </div>
329
  """,
 
352
  # Show last 7 days; for each day, show all leaderboards (settings combos)
353
  daily_periods = list_available_periods("daily", limit=7)
354
  if not daily_periods:
355
+ st.info("No daily leaderboards avaialble.")
356
  return
357
 
358
  for date_id in daily_periods:
 
384
 
385
  def _render_weekly_tab():
386
  """Render weekly leaderboard tab."""
387
+ st.header("πŸ“† This Week")
388
  st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each ISO week. Resets Monday at UTC midnight.")
389
 
390
  try:
 
395
  requested_week = params.get("week")
396
  current_week = get_current_weekly_id()
397
 
398
+ weekly_periods = list_available_periods("weekly", limit=1)
399
  if not weekly_periods:
400
+ st.info("No weekly leaderboards available.")
401
  return
402
 
403
  if requested_week in weekly_periods:
 
418
  expander_title = f"πŸ—“οΈ {week_title}"
419
  with st.expander(expander_title, expanded=(week_id == expanded_week)):
420
  if not settings_list:
421
+ st.info("No leaderboards avaialble for this week.")
422
  continue
423
 
424
  for idx, settings_info in enumerate(settings_list):
 
442
 
443
  weekly_periods = list_available_periods("weekly", limit=12)
444
  if not weekly_periods:
445
+ st.info("No weekly leaderboards available.")
446
  return
447
 
448
  week_options = [(_format_week_title(week_id), week_id) for week_id in weekly_periods]
 
512
  with col1:
513
  st.subheader("Daily History")
514
  if not week_daily_ids:
515
+ st.info("No daily leaderboards available for this week.")
516
  else:
517
  for date_id in week_daily_ids:
518
  date_title = _get_pst_range_str(date_id)
519
  settings_list = list_settings_for_period("daily", date_id)
520
  if not settings_list:
521
  with st.expander(date_title, expanded=False):
522
+ st.info("No leaderboards available for this date.")
523
  continue
524
 
525
  for settings_info in settings_list:
 
541
  week_title = _format_week_title(selected_week)
542
  settings_list = list_settings_for_period("weekly", selected_week)
543
  if not settings_list:
544
+ st.info("No leaderboards available for this week.")
545
  else:
546
  for idx, settings_info in enumerate(settings_list):
547
  file_id = settings_info["file_id"]
 
558
 
559
 
560
  def _render_today_tab():
561
+ """Render today's leaderboards tab - shows current daily leaderboard.
562
 
563
  If query string parameters are present:
564
  - gidd: Show only the specified daily leaderboard (file_id)
 
565
 
566
+ Otherwise, show all current daily leaderboards.
567
  """
568
  # Get query parameters
569
  try:
 
572
  params = {}
573
 
574
  gidd = params.get("gidd", None)
575
+
576
+ if gidd:
 
 
577
  st.header("🎯 Today's Leaderboards (Filtered)")
578
+
579
+ st.subheader("πŸ“… Daily")
580
+ daily_id = get_current_daily_id()
581
+ lb = load_leaderboard("daily", daily_id, gidd)
582
+
583
+ if lb:
584
+ date_title = "🌟 Today: " + _get_pst_range_str(daily_id)
585
+
586
+ header_suffix = _settings_badge(lb)
587
+ with st.expander(f"{date_title} {header_suffix}", expanded=True):
588
+ st.caption(
589
+ f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
590
+ f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
591
+ f"spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}"
592
+ )
593
+ _render_leaderboard_table(lb, "", grid_key=f"today-daily-{daily_id}-{gidd}")
594
+ else:
595
+ st.warning(f"Daily leaderboard not available: {gidd}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
  else:
 
597
  st.header("🌟 Today's Leaderboards")
598
+ st.write("Current daily leaderboards. Resets at UTC midnight.")
599
+
600
+ st.subheader("πŸ“… Daily")
601
+ daily_id = get_current_daily_id()
602
+
603
+ date_title = "Today " + _get_pst_range_str(daily_id)
604
+
605
+ settings_list = list_settings_for_period("daily", daily_id)
606
+ if not settings_list:
607
+ st.info("No daily leaderboards available for today.")
608
+ else:
609
+ for settings_info in settings_list:
610
+ file_id = settings_info["file_id"]
611
+ lb = load_leaderboard("daily", daily_id, file_id)
612
+ header_suffix = ""
613
+ if lb is not None:
614
+ header_suffix = _settings_badge(lb)
615
+ expander_title = f"{date_title} {header_suffix}" if header_suffix else date_title
616
+ with st.expander(expander_title, expanded=True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  if lb is not None:
618
+ st.caption(
619
+ f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
620
+ f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
621
+ f"spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}"
622
+ )
623
+ _render_leaderboard_table(lb, "", grid_key=f"today-daily-{daily_id}-{file_id}")
wrdler/ui.py CHANGED
@@ -348,7 +348,7 @@ def _render_header():
348
  if st.session_state.get("enable_free_letters", False):
349
  st.subheader("Choose 2 free letters, then reveal cells and guess words on each line!")
350
  else:
351
- st.subheader("Reveal letters of words on the six lines. Guess the words before all letters are found to get extra points and improve your score!")
352
 
353
  # Only show Challenge Mode expander if in challenge mode and game_id is present
354
  params = st.query_params if hasattr(st, "query_params") else {}
@@ -403,13 +403,13 @@ def _render_header():
403
  best_score = best_user["score"]
404
  best_time = best_user["time"]
405
  mins, secs = divmod(best_time, 60)
406
- best_time_str = f"{mins:02d}:{secs:02d}"
407
 
408
  # Build leaderboard HTML
409
  leaderboard_rows = []
410
  for i, user in enumerate(sorted_users[:5], 1): # Top 5
411
  u_mins, uSecs = divmod(user["time"], 60)
412
- u_time_str = f"{u_mins:02d}:{uSecs:02d}"
413
  medal = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"][i-1] if i <= 3 else f"{i}."
414
  # show optional difficulty if present
415
  diff_str = ""
@@ -986,7 +986,7 @@ def _render_score_panel(state: GameState):
986
  end = state.end_time or (now if is_game_over(state) else None)
987
  elapsed = (end or now) - start
988
  mins, secs = divmod(int(elapsed.total_seconds()), 60)
989
- timer_str = f"{mins:02d}:{secs:02d}"
990
  start_ms = int(start.timestamp() * 1000)
991
  end_ms = int(end.timestamp() * 1000) if end else "null"
992
 
@@ -1114,9 +1114,9 @@ def _game_over_content(state: GameState) -> None:
1114
  start = state.start_time or state.end_time or datetime.now()
1115
  end = state.end_time or datetime.now()
1116
  elapsed = end - start
1117
- elapsed_seconds = int(elapsed.total_seconds())
1118
- mins, secs = divmod(int(elapsed.total_seconds()), 60)
1119
- timer_str = f"{mins:02d}:{secs:02d}"
1120
 
1121
  # Compute optional word list difficulty for current run
1122
  difficulty_value = None
@@ -1306,7 +1306,7 @@ def _game_over_content(state: GameState) -> None:
1306
  btn_flag = st.session_state.get("gameover_button_pressed", None)
1307
 
1308
  # Helper function to submit to leaderboards
1309
- def _submit_to_leaderboards(username: str, score: int, time_secs: int, word_list: list, challenge_id: str = None):
1310
  """Submit score to daily and weekly leaderboards (with fallback verification).
1311
  Uses main `submit_score_to_all_leaderboards` and ensures weekly entry is attempted
1312
  even if the main call fails to produce a weekly result.
 
348
  if st.session_state.get("enable_free_letters", False):
349
  st.subheader("Choose 2 free letters, then reveal cells and guess words on each line!")
350
  else:
351
+ st.subheader("Reveal letters of words on the six lines. Guess the words for extra points and a better score!")
352
 
353
  # Only show Challenge Mode expander if in challenge mode and game_id is present
354
  params = st.query_params if hasattr(st, "query_params") else {}
 
403
  best_score = best_user["score"]
404
  best_time = best_user["time"]
405
  mins, secs = divmod(best_time, 60)
406
+ best_time_str = f"{mins:02.0f}:{secs:06.3f}"
407
 
408
  # Build leaderboard HTML
409
  leaderboard_rows = []
410
  for i, user in enumerate(sorted_users[:5], 1): # Top 5
411
  u_mins, uSecs = divmod(user["time"], 60)
412
+ u_time_str = f"{mins:02.0f}:{secs:06.3f}"
413
  medal = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"][i-1] if i <= 3 else f"{i}."
414
  # show optional difficulty if present
415
  diff_str = ""
 
986
  end = state.end_time or (now if is_game_over(state) else None)
987
  elapsed = (end or now) - start
988
  mins, secs = divmod(int(elapsed.total_seconds()), 60)
989
+ timer_str = f"{mins:02.0f}:{secs:06.3f}"
990
  start_ms = int(start.timestamp() * 1000)
991
  end_ms = int(end.timestamp() * 1000) if end else "null"
992
 
 
1114
  start = state.start_time or state.end_time or datetime.now()
1115
  end = state.end_time or datetime.now()
1116
  elapsed = end - start
1117
+ elapsed_seconds = float(elapsed.total_seconds())
1118
+ mins, secs = divmod(elapsed.total_seconds(), 60)
1119
+ timer_str = f"{mins:02.0f}:{secs:06.3f}"
1120
 
1121
  # Compute optional word list difficulty for current run
1122
  difficulty_value = None
 
1306
  btn_flag = st.session_state.get("gameover_button_pressed", None)
1307
 
1308
  # Helper function to submit to leaderboards
1309
+ def _submit_to_leaderboards(username: str, score: int, time_secs: float, word_list: list, challenge_id: str = None):
1310
  """Submit score to daily and weekly leaderboards (with fallback verification).
1311
  Uses main `submit_score_to_all_leaderboards` and ensures weekly entry is attempted
1312
  even if the main call fails to produce a weekly result.