Surn commited on
Commit
a16932a
ยท
1 Parent(s): 7bfeb28

Bump version to 0.2.7; enhance leaderboard system

Updated project version to 0.2.7 across code and documentation.
Enhanced the leaderboard system with new features:
- Added last 5 weeks of weekly leaderboards in "Weekly Tab."
- Improved "History Tab" with dropdown selectors and styled UI.
- Refined daily leaderboard rendering for selected weeks.
- Introduced helper functions for week-based operations.

Improved leaderboard page UI:
- Added prominent headers and better layout for history tabs.
- Enhanced display of leaderboard settings and metadata.

Updated documentation to reflect new features and version bump.
Refactored `leaderboard_page.py` for better usability and edge case handling.
Improved code readability, fixed formatting issues, and removed redundancy.

CLAUDE.md CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  # Wrdler - Project Context
2
 
3
  ## Project Overview
 
1
+ # CLAUDE
2
+
3
+ Wrdler v0.2.7
4
+
5
  # Wrdler - Project Context
6
 
7
  ## Project Overview
GAMEPLAY_GUIDE.md CHANGED
@@ -1,5 +1,7 @@
1
  # Wrdler Gameplay Guide
2
- **Version:** 0.2.6
 
 
3
  **Last Updated:** 2025-01-31
4
 
5
  ## Welcome to Wrdler!
 
1
  # Wrdler Gameplay Guide
2
+
3
+ Version 0.2.7
4
+
5
  **Last Updated:** 2025-01-31
6
 
7
  ## Welcome to Wrdler!
README.md CHANGED
@@ -21,6 +21,10 @@ thumbnail: >-
21
 
22
  # Wrdler
23
 
 
 
 
 
24
  > **Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, adapted for a simpler 8x6 grid, horizontal words only, and free letter guesses at the start.**
25
 
26
  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.
@@ -104,10 +108,9 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
104
 
105
  **Leaderboard Page Features:**
106
  - **Today Tab:** Current daily and weekly leaderboards side-by-side
107
- - Query param filtering: `?gidd={file_id}` and `?gidw={file_id}`
108
  - **Daily Tab:** Last 7 days of daily leaderboards with expandable date groups
109
- - **Weekly Tab:** Current ISO week leaderboard with all settings combinations
110
- - **History Tab:** Browse past leaderboards with date/week and settings selectors
111
 
112
  **Integration:**
113
  - Automatic submission after game completion (opt-in via game over popup)
@@ -432,4 +435,4 @@ Ensure you have the necessary permissions and API access (if required) to use th
432
 
433
  For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
434
 
435
- Happy gaming and sound designing!
 
21
 
22
  # Wrdler
23
 
24
+ Version 0.2.7
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
+
28
  > **Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, adapted for a simpler 8x6 grid, horizontal words only, and free letter guesses at the start.**
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.
 
108
 
109
  **Leaderboard Page Features:**
110
  - **Today Tab:** Current daily and weekly leaderboards side-by-side
 
111
  - **Daily Tab:** Last 7 days of daily leaderboards with expandable date groups
112
+ - **Weekly Tab:** Last 5 weeks, each rendered as its own expander (current or `week=YYYY-Www` query selection opens by default)
113
+ - **History Tab:** Historical leaderboard browser with dropdown selectors
114
 
115
  **Integration:**
116
  - Automatic submission after game completion (opt-in via game over popup)
 
435
 
436
  For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
437
 
438
+ Happy gaming and sound designing!Happy gaming and sound designing!
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "wrdler"
3
- version = "0.2.6"
4
  description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, 2 free letter guesses, and a settings-based daily/weekly leaderboard system. Features leaderboard UI, challenge sharing, and AI word lists."
5
  readme = "README.md"
6
  requires-python = ">=3.12,<3.13"
 
1
  [project]
2
  name = "wrdler"
3
+ version = "0.2.7"
4
  description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, 2 free letter guesses, and a settings-based daily/weekly leaderboard system. Features leaderboard UI, challenge sharing, and AI word lists."
5
  readme = "README.md"
6
  requires-python = ">=3.12,<3.13"
specs/leaderboard_spec.md CHANGED
@@ -1,7 +1,7 @@
1
  ๏ปฟ# Wrdler Leaderboard System Specification
2
 
3
- **Document Version:** 1.4.0
4
- **Project Version:** 0.2.6
5
  **Author:** GitHub Copilot
6
  **Last Updated:** 2025-12-08
7
  **Status:** โœ… Implemented and Documented
@@ -57,7 +57,7 @@ This specification documents the implemented **Daily and Weekly Leaderboard Syst
57
 
58
  5. **Leaderboard Page**: New Streamlit page displaying:
59
  - Last 7 days of daily leaderboards (filtered by current settings)
60
- - Current weekly leaderboard (filtered by current settings)
61
  - Historical lookup via dropdown
62
 
63
  6. **Folder-Based Discovery**: No index.json file. Leaderboards are discovered by scanning folder names. Folder names include settings info for fast filtering.
@@ -527,7 +527,7 @@ Add to `_render_sidebar()` in `ui.py`:
527
 
528
  ```python
529
  st.header("Navigation")
530
- if st.button("๐Ÿ† Leaderboards", use_container_width=True):
531
  st.session_state["show_leaderboard_page"] = True
532
  st.rerun()
533
  ```
@@ -883,4 +883,10 @@ 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: `Mon, Dec 08, 2025 4:00 PM PST โ€“ Tue, Dec 09, 2025 3:59:59 PM PST [settings badge]`
 
 
 
 
 
 
 
1
  ๏ปฟ# Wrdler Leaderboard System Specification
2
 
3
+ **Document Version:** 1.4.1
4
+ **Project Version:** 0.2.7
5
  **Author:** GitHub Copilot
6
  **Last Updated:** 2025-12-08
7
  **Status:** โœ… Implemented and Documented
 
57
 
58
  5. **Leaderboard Page**: New Streamlit page displaying:
59
  - Last 7 days of daily leaderboards (filtered by current settings)
60
+ - Last 5 weeks of weekly leaderboards with per-week expanders (current week open unless `week=YYYY-Www` query overrides)
61
  - Historical lookup via dropdown
62
 
63
  6. **Folder-Based Discovery**: No index.json file. Leaderboards are discovered by scanning folder names. Folder names include settings info for fast filtering.
 
527
 
528
  ```python
529
  st.header("Navigation")
530
+ if st.button("๐Ÿ† Leaderboards", width="stretch"):
531
  st.session_state["show_leaderboard_page"] = True
532
  st.rerun()
533
  ```
 
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]`
887
+
888
+ **Leaderboard Page UI:**
889
+ - **Today Tab:** Current daily and weekly leaderboards
890
+ - **Daily Tab:** Last 7 days of daily leaderboards
891
+ - **Weekly Tab:** Last 5 weeks displayed as individual expanders (current week or `week=YYYY-Www` query opens by default)
892
+ - **History Tab:** Historical leaderboard browser
specs/requirements.md CHANGED
@@ -1,5 +1,6 @@
1
- ๏ปฟ# Wrdler: Implementation Requirements
2
- **Version:** 0.2.6
 
3
  **Status:** Production Ready - Leaderboards Implemented
4
  **Last Updated:** 2025-12-08
5
 
 
1
+ ๏ปฟ# Wrdler Requirements
2
+
3
+ **Version:** 0.2.7
4
  **Status:** Production Ready - Leaderboards Implemented
5
  **Last Updated:** 2025-12-08
6
 
specs/specs.md CHANGED
@@ -1,5 +1,7 @@
1
- # Wrdler Game Specifications (specs.md)
2
- **Version:** 0.2.6
 
 
3
  **Status:** Production Ready - Leaderboards Implemented
4
  **Last Updated:** 2025-12-08
5
 
 
1
+ # Wrdler Specifications
2
+
3
+ **Version:** 0.2.7
4
+
5
  **Status:** Production Ready - Leaderboards Implemented
6
  **Last Updated:** 2025-12-08
7
 
wrdler/__init__.py CHANGED
@@ -9,5 +9,5 @@ Key differences from BattleWords:
9
  - Daily and weekly leaderboards
10
  """
11
 
12
- __version__ = "0.2.6"
13
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
 
9
  - Daily and weekly leaderboards
10
  """
11
 
12
+ __version__ = "0.2.7"
13
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
wrdler/leaderboard_page.py CHANGED
@@ -9,7 +9,7 @@ __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
@@ -93,6 +93,27 @@ def _settings_badge(lb: LeaderboardSettings) -> str:
93
  return f"\n\n[{mode} โ€ข {source} โ€ข {incorrect} โ€ข {free}]"
94
 
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
97
  """Render a leaderboard as a styled table."""
98
  if title:
@@ -136,7 +157,7 @@ def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title:
136
 
137
  # Calculate height to show all rows (approx 35px per row + 38px header)
138
  height = (len(df) + 1) * 35 + 3
139
- st.dataframe(styled_df, use_container_width=True, hide_index=True, height=height)
140
 
141
  # Show entry count and last updated
142
  total_entries = len(leaderboard.users)
@@ -293,31 +314,52 @@ def _render_weekly_tab():
293
  st.header("๐Ÿ“† Weekly Leaderboards")
294
  st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each ISO week. Resets Monday at UTC midnight.")
295
 
296
- weekly_id = get_current_weekly_id()
 
 
 
 
 
 
297
 
298
- # Show current week; render all settings combinations that exist
299
- settings_list = list_settings_for_period("weekly", weekly_id)
300
- if not settings_list:
301
- st.info("No weekly leaderboards found for the current week.")
302
  return
303
 
304
- try:
305
- year, week = weekly_id.split("-W")
306
- week_title = f"Week {int(week)}, {year}"
307
- except ValueError:
308
- week_title = weekly_id
309
-
310
- for settings_info in settings_list:
311
- file_id = settings_info["file_id"]
312
- lb = load_leaderboard("weekly", weekly_id, file_id)
313
- header_suffix = ""
314
- if lb is not None:
315
- header_suffix = _settings_badge(lb)
316
- expander_title = f"๐Ÿ—“๏ธ {week_title} {header_suffix}" if header_suffix else f"๐Ÿ—“๏ธ {week_title}"
317
- with st.expander(expander_title, expanded=True):
318
- if lb is not None:
319
- 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)}")
320
- _render_leaderboard_table(lb, "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
 
323
  def _render_history_tab():
@@ -325,69 +367,121 @@ def _render_history_tab():
325
  st.header("๐Ÿ“š Historical Leaderboards")
326
  st.write("Look up past leaderboards.")
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  col1, col2 = st.columns(2)
329
 
330
  with col1:
331
  st.subheader("Daily History")
332
- daily_periods = list_available_periods("daily", limit=30)
333
- if daily_periods:
334
- selected_daily = st.selectbox(
335
- "Select a date",
336
- options=daily_periods,
337
- key="history_daily_select"
338
- )
339
-
340
- settings_list = list_settings_for_period("daily", selected_daily)
341
- if settings_list:
342
- file_id_options = [s["file_id"] for s in settings_list]
343
- selected_file_id = st.selectbox(
344
- "Select settings",
345
- options=file_id_options,
346
- format_func=lambda x: x.replace("-", " / "),
347
- key="history_daily_file_id"
348
- )
349
-
350
- if st.button("Load Daily", key="load_daily"):
351
- leaderboard = load_leaderboard("daily", selected_daily, selected_file_id)
352
- header = f"Daily: {_get_pst_range_str(selected_daily)} "
353
- if leaderboard is not None:
354
- header += _settings_badge(leaderboard)
355
- _render_leaderboard_table(leaderboard, header)
356
- else:
357
- st.info("No leaderboards found for this date.")
358
  else:
359
- st.info("No daily leaderboards found.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
  with col2:
362
  st.subheader("Weekly History")
363
- weekly_periods = list_available_periods("weekly", limit=6)
364
- if weekly_periods:
365
- selected_weekly = st.selectbox(
366
- "Select a week",
367
- options=weekly_periods,
368
- key="history_weekly_select"
369
- )
370
-
371
- settings_list = list_settings_for_period("weekly", selected_weekly)
372
- if settings_list:
373
- file_id_options = [s["file_id"] for s in settings_list]
374
- selected_file_id = st.selectbox(
375
- "Select settings",
376
- options=file_id_options,
377
- format_func=lambda x: x.replace("-", " / "),
378
- key="history_weekly_file_id"
379
- )
380
-
381
- if st.button("Load Weekly", key="load_weekly"):
382
- leaderboard = load_leaderboard("weekly", selected_weekly, selected_file_id)
383
- header = f"Weekly: {selected_weekly} "
384
- if leaderboard is not None:
385
- header += _settings_badge(leaderboard)
386
- _render_leaderboard_table(leaderboard, header)
387
- else:
388
- st.info("No leaderboards found for this week.")
389
  else:
390
- st.info("No weekly leaderboards found.")
 
 
 
 
 
 
 
 
 
 
 
391
 
392
 
393
  def _render_today_tab():
 
9
 
10
  import streamlit as st
11
  from datetime import datetime, timedelta, timezone
12
+ from typing import Optional, Tuple, List
13
  import pandas as pd
14
  try:
15
  from zoneinfo import ZoneInfo
 
93
  return f"\n\n[{mode} โ€ข {source} โ€ข {incorrect} โ€ข {free}]"
94
 
95
 
96
+ def _format_week_title(week_id: str) -> str:
97
+ try:
98
+ year, week = week_id.split("-W")
99
+ return f"Week {int(week)}, {year}"
100
+ except ValueError:
101
+ return week_id
102
+
103
+
104
+ def _get_daily_ids_for_week(week_id: str) -> List[str]:
105
+ try:
106
+ year, week = week_id.split("-W")
107
+ year_int = int(year)
108
+ week_int = int(week)
109
+ return [
110
+ datetime.fromisocalendar(year_int, week_int, weekday).strftime("%Y-%m-%d")
111
+ for weekday in range(1, 8)
112
+ ]
113
+ except ValueError:
114
+ return []
115
+
116
+
117
  def _render_leaderboard_table(leaderboard: Optional[LeaderboardSettings], title: str):
118
  """Render a leaderboard as a styled table."""
119
  if title:
 
157
 
158
  # Calculate height to show all rows (approx 35px per row + 38px header)
159
  height = (len(df) + 1) * 35 + 3
160
+ st.dataframe(styled_df, width="stretch", hide_index=True, height=height)
161
 
162
  # Show entry count and last updated
163
  total_entries = len(leaderboard.users)
 
314
  st.header("๐Ÿ“† Weekly Leaderboards")
315
  st.write(f"Top {MAX_DISPLAY_ENTRIES} scores for each ISO week. Resets Monday at UTC midnight.")
316
 
317
+ try:
318
+ params = st.query_params if hasattr(st, "query_params") else {}
319
+ except Exception:
320
+ params = {}
321
+
322
+ requested_week = params.get("week")
323
+ current_week = get_current_weekly_id()
324
 
325
+ weekly_periods = list_available_periods("weekly", limit=5)
326
+ if not weekly_periods:
327
+ st.info("No weekly leaderboards found.")
 
328
  return
329
 
330
+ if requested_week in weekly_periods:
331
+ expanded_week = requested_week
332
+ elif current_week in weekly_periods:
333
+ expanded_week = current_week
334
+ else:
335
+ expanded_week = weekly_periods[0]
336
+
337
+ for week_id in weekly_periods:
338
+ try:
339
+ year, week = week_id.split("-W")
340
+ week_title = f"Week {int(week)}, {year}"
341
+ except ValueError:
342
+ week_title = week_id
343
+
344
+ settings_list = list_settings_for_period("weekly", week_id)
345
+ expander_title = f"๐Ÿ—“๏ธ {week_title}"
346
+ with st.expander(expander_title, expanded=(week_id == expanded_week)):
347
+ if not settings_list:
348
+ st.info("No leaderboards found for this week.")
349
+ continue
350
+
351
+ for idx, settings_info in enumerate(settings_list):
352
+ file_id = settings_info["file_id"]
353
+ lb = load_leaderboard("weekly", week_id, file_id)
354
+ if lb is not None:
355
+ st.caption(
356
+ f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
357
+ f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
358
+ f"spacer={lb.puzzle_options.get("spacer", 0)}, overlap={lb.puzzle_options.get("may_overlap", False)}"
359
+ )
360
+ _render_leaderboard_table(lb, "")
361
+ if idx < len(settings_list) - 1:
362
+ st.divider()
363
 
364
 
365
  def _render_history_tab():
 
367
  st.header("๐Ÿ“š Historical Leaderboards")
368
  st.write("Look up past leaderboards.")
369
 
370
+ weekly_periods = list_available_periods("weekly", limit=12)
371
+ if not weekly_periods:
372
+ st.info("No weekly leaderboards found.")
373
+ return
374
+
375
+ week_options = [(_format_week_title(week_id), week_id) for week_id in weekly_periods]
376
+ week_labels = [label for label, _ in week_options]
377
+ label_to_week = {label: week_id for label, week_id in week_options}
378
+
379
+ # Prominent, styled header for the week selector to make it more noticeable
380
+ st.markdown(
381
+ """
382
+ <style>
383
+ .history-select-label {
384
+ display:inline-block;
385
+ font-size:1.15rem;
386
+ font-weight:800;
387
+ background: linear-gradient(90deg, rgba(32,212,108,0.12), rgba(29,100,200,0.06));
388
+ padding: 8px 12px;
389
+ border-radius: 8px;
390
+ border: 1px solid rgba(32,212,108,0.18);
391
+ box-shadow: 0 6px 18px rgba(11,42,74,0.12);
392
+ margin-bottom: 6px;
393
+ }
394
+ .history-select-help {
395
+ display:block;
396
+ color:#d7faff;
397
+ margin-bottom:8px;
398
+ opacity:0.95;
399
+ }
400
+ .st-key-history_week_select {
401
+ display: flex;
402
+ justify-content: center;
403
+ align-items: center;
404
+ flex-wrap: wrap;
405
+ flex-direction: row;
406
+ border: 1px solid rgba(255, 255, 255, 0.5);
407
+ padding: 5px;
408
+ max-width: 75%;
409
+ margin: 0 auto;
410
+ align-content: center;
411
+ border: 1px solid rgba(255,255,255,0.5);
412
+ padding: 5px;
413
+ }
414
+ .st-key-history_week_select label { display:none;}
415
+ .history-selectbox-wrap { margin-bottom: 2px; }
416
+ </style>
417
+ <div class="history-selectbox-wrap">
418
+ <div class="history-select-label">๐Ÿ“š Select a week</div>
419
+ <div class="history-select-help">Pick a week to view daily and weekly leaderboards for that week.</div>
420
+ </div>
421
+ """,
422
+ unsafe_allow_html=True
423
+ )
424
+
425
+ # Render the selectbox without a label because we've already shown a styled header above
426
+ selected_label = st.selectbox(
427
+ "",
428
+ options=week_labels,
429
+ key="history_week_select",
430
+ index=0
431
+ )
432
+ selected_week = label_to_week.get(selected_label, weekly_periods[0])
433
+
434
+ available_daily = set(list_available_periods("daily", limit=90))
435
+ week_daily_ids = [d for d in _get_daily_ids_for_week(selected_week) if d in available_daily]
436
+
437
  col1, col2 = st.columns(2)
438
 
439
  with col1:
440
  st.subheader("Daily History")
441
+ if not week_daily_ids:
442
+ st.info("No daily leaderboards found for this week.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  else:
444
+ for date_id in week_daily_ids:
445
+ date_title = _get_pst_range_str(date_id)
446
+ settings_list = list_settings_for_period("daily", date_id)
447
+ if not settings_list:
448
+ with st.expander(date_title, expanded=False):
449
+ st.info("No leaderboards found for this date.")
450
+ continue
451
+
452
+ for settings_info in settings_list:
453
+ file_id = settings_info["file_id"]
454
+ lb = load_leaderboard("daily", date_id, file_id)
455
+ header_suffix = _settings_badge(lb) if lb is not None else ""
456
+ expander_title = f"{date_title} {header_suffix}" if header_suffix else date_title
457
+ with st.expander(expander_title, expanded=False):
458
+ if lb is not None:
459
+ st.caption(
460
+ f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
461
+ f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
462
+ f"spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}"
463
+ )
464
+ _render_leaderboard_table(lb, "")
465
 
466
  with col2:
467
  st.subheader("Weekly History")
468
+ week_title = _format_week_title(selected_week)
469
+ settings_list = list_settings_for_period("weekly", selected_week)
470
+ if not settings_list:
471
+ st.info("No leaderboards found for this week.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  else:
473
+ for idx, settings_info in enumerate(settings_list):
474
+ file_id = settings_info["file_id"]
475
+ lb = load_leaderboard("weekly", selected_week, file_id)
476
+ if lb is not None:
477
+ st.caption(
478
+ f"Settings: mode={lb.game_mode}, source={lb.wordlist_source}, "
479
+ f"show_incorrect={lb.show_incorrect_guesses}, free_letters={lb.enable_free_letters}, "
480
+ f"spacer={lb.puzzle_options.get('spacer', 0)}, overlap={lb.puzzle_options.get('may_overlap', False)}"
481
+ )
482
+ _render_leaderboard_table(lb, week_title if idx == 0 else "")
483
+ if idx < len(settings_list) - 1:
484
+ st.divider()
485
 
486
 
487
  def _render_today_tab():
wrdler/settings_page.py CHANGED
@@ -168,7 +168,7 @@ def render_settings_page(new_game_callback):
168
  key="ai_generate_btn",
169
  help="Generate wordlist from topic",
170
  on_click=_on_ai_generate,
171
- use_container_width=True,
172
  )
173
 
174
  # Display wordlist info below topic input
@@ -236,7 +236,7 @@ def render_settings_page(new_game_callback):
236
  key="ai_generate_btn",
237
  help="Generate wordlist from topic",
238
  on_click=_on_ai_generate,
239
- use_container_width=True,
240
  )
241
 
242
  # Display wordlist info below topic input
 
168
  key="ai_generate_btn",
169
  help="Generate wordlist from topic",
170
  on_click=_on_ai_generate,
171
+ width="stretch",
172
  )
173
 
174
  # Display wordlist info below topic input
 
236
  key="ai_generate_btn",
237
  help="Generate wordlist from topic",
238
  on_click=_on_ai_generate,
239
+ width="stretch",
240
  )
241
 
242
  # Display wordlist info below topic input
wrdler/ui.py CHANGED
@@ -537,7 +537,7 @@ border-radius: 50% !important;
537
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
538
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { flex: 0 0 auto !important; }
539
  .bw-grid-row-anchor { height: 0; margin: 0; padding: 0; }
540
- .st-emotion-cache-1n6tfoc { background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666); gap: 0.1rem !important; color: white; border-radius:15px; padding: 10px 10px 10px 10px; }
541
  .st-emotion-cache-1n6tfoc::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; margin: 5px;}
542
  .st-key-guess_input, .st-key-guess_submit { flex-direction: row; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: flex-end; }
543
  .st-key-guess_input [id^="text_input"] {max-width: 80px;}
@@ -624,7 +624,7 @@ border-radius: 50% !important;
624
  max-width:100%;
625
  }
626
  .stImage {max-width:300px;}
627
- [id^="text_input"] {
628
  background-color:#fff;
629
  color:#000;
630
  caret-color:#333;}
@@ -1079,7 +1079,7 @@ def _render_free_letters(state: GameState):
1079
  if st.button(
1080
  letter,
1081
  key=f"free_letter_{letter}",
1082
- use_container_width=True,
1083
  type="primary",
1084
  on_click=_on_free_letter_click,
1085
  args=(letter, state),
@@ -1746,7 +1746,7 @@ def _game_over_content(state: GameState) -> None:
1746
  height=0,
1747
  )
1748
 
1749
- # Share Challenge Button
1750
  st.markdown("---")
1751
 
1752
  # Style the containing Streamlit block via CSS :has() using an anchor inside this container
@@ -1784,7 +1784,7 @@ def _game_over_content(state: GameState) -> None:
1784
  unsafe_allow_html=True,
1785
  )
1786
 
1787
- st.markdown("### ๐ŸŽฎ Share Your Challenge")
1788
 
1789
  # Check if this is a shared game being completed
1790
  is_shared_game = st.session_state.get("loaded_game_sid") is not None
@@ -1860,13 +1860,47 @@ def _game_over_content(state: GameState) -> None:
1860
 
1861
  # Check if share URL already generated
1862
  if "share_url" not in st.session_state or st.session_state.get("share_url") is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1863
  button_text = "๐Ÿ“Š Submit Your Result" if is_shared_game else "๐Ÿ”— Generate Share Link"
1864
 
1865
  if not can_submit:
1866
  st.warning("โš ๏ธ Please enter your name to submit.")
1867
 
1868
- if st.button(button_text, key="generate_share_link", use_container_width=True, disabled=not can_submit):
1869
  try:
 
1870
  # Extract game data
1871
  word_list = [w.text for w in state.puzzle.words]
1872
  spacer = state.puzzle.spacer
@@ -1941,36 +1975,6 @@ def _game_over_content(state: GameState) -> None:
1941
 
1942
  except Exception as e:
1943
  st.error(f"Failed to save game: {e}")
1944
-
1945
- # Show separate "Submit to Leaderboard" button when NOT in a challenge
1946
- if not is_shared_game and not st.session_state.get("leaderboard_submitted", False):
1947
- st.markdown("---")
1948
- st.markdown("##### ๐Ÿ† Or just submit to leaderboards")
1949
- if st.button("๐Ÿ† Submit to Leaderboards", key="submit_leaderboard_only", use_container_width=True, disabled=not can_submit):
1950
- try:
1951
- word_list = [w.text for w in state.puzzle.words]
1952
- lb_results = _submit_to_leaderboards(
1953
- username, state.score, elapsed_seconds, word_list
1954
- )
1955
- st.session_state["leaderboard_submitted"] = True
1956
- st.session_state["leaderboard_results"] = lb_results
1957
-
1958
- # Show results
1959
- daily_info = lb_results.get("daily", {})
1960
- weekly_info = lb_results.get("weekly", {})
1961
-
1962
- if daily_info.get("qualified") or weekly_info.get("qualified"):
1963
- msg_parts = []
1964
- if daily_info.get("qualified"):
1965
- msg_parts.append(f"Daily #{daily_info['rank']}")
1966
- if weekly_info.get("qualified"):
1967
- msg_parts.append(f"Weekly #{weekly_info['rank']}")
1968
- st.success(f"โœ… Submitted! Ranked: {', '.join(msg_parts)}")
1969
- else:
1970
- st.info("โœ… Score submitted (not in top 20)")
1971
- st.rerun()
1972
- except Exception as e:
1973
- st.error(f"Failed to submit to leaderboards: {e}")
1974
  else:
1975
  # Conditionally display the generated share URL
1976
  if st.session_state.get("show_challenge_share_links", False):
@@ -2290,11 +2294,11 @@ def run_app():
2290
  _render_footer(current_page="settings")
2291
  return
2292
 
2293
- if page in {"today", "daily", "weekly"}:
2294
  from .leaderboard_page import render_leaderboard_page
2295
  st.markdown(ocean_background_css, unsafe_allow_html=True)
2296
  inject_ocean_layers()
2297
- default = page if page in {"daily", "weekly"} else "today"
2298
  render_leaderboard_page(default_tab=default)
2299
  _render_footer(current_page=page if page else "play")
2300
  return
 
537
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
538
  .bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { flex: 0 0 auto !important; }
539
  .bw-grid-row-anchor { height: 0; margin: 0; padding: 0; }
540
+ .st-emotion-cache-1n6tfoc { background: linear-gradient(-45deg, #a1a1c1, #1d64c8, #a1a1c1, #666666) !important; gap: 0.1rem !important; color: white; border-radius:15px; padding: 10px 10px 10px 10px; }
541
  .st-emotion-cache-1n6tfoc::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; margin: 5px;}
542
  .st-key-guess_input, .st-key-guess_submit { flex-direction: row; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: flex-end; }
543
  .st-key-guess_input [id^="text_input"] {max-width: 80px;}
 
624
  max-width:100%;
625
  }
626
  .stImage {max-width:300px;}
627
+ [id^="text_input"], .st-bb [id^="text_input"] {
628
  background-color:#fff;
629
  color:#000;
630
  caret-color:#333;}
 
1079
  if st.button(
1080
  letter,
1081
  key=f"free_letter_{letter}",
1082
+ width="stretch",
1083
  type="primary",
1084
  on_click=_on_free_letter_click,
1085
  args=(letter, state),
 
1746
  height=0,
1747
  )
1748
 
1749
+ # Submit and Share Challenge Buttons
1750
  st.markdown("---")
1751
 
1752
  # Style the containing Streamlit block via CSS :has() using an anchor inside this container
 
1784
  unsafe_allow_html=True,
1785
  )
1786
 
1787
+ st.markdown("### ๐ŸŽฎ Enter Name to submit to leaderboard")
1788
 
1789
  # Check if this is a shared game being completed
1790
  is_shared_game = st.session_state.get("loaded_game_sid") is not None
 
1860
 
1861
  # Check if share URL already generated
1862
  if "share_url" not in st.session_state or st.session_state.get("share_url") is None:
1863
+
1864
+
1865
+ # Show separate "Submit to Leaderboard" button when NOT in a challenge
1866
+ if not is_shared_game and not st.session_state.get("leaderboard_submitted", False):
1867
+
1868
+ #st.markdown("##### ๐Ÿ† Or just submit to leaderboards")
1869
+ if st.button("๐Ÿ† Submit to Leaderboards", key="submit_leaderboard_only", width="stretch", disabled=not can_submit):
1870
+ try:
1871
+ word_list = [w.text for w in state.puzzle.words]
1872
+ lb_results = _submit_to_leaderboards(
1873
+ username, state.score, elapsed_seconds, word_list
1874
+ )
1875
+ st.session_state["leaderboard_submitted"] = True
1876
+ st.session_state["leaderboard_results"] = lb_results
1877
+
1878
+ # Show results
1879
+ daily_info = lb_results.get("daily", {})
1880
+ weekly_info = lb_results.get("weekly", {})
1881
+
1882
+ if daily_info.get("qualified") or weekly_info.get("qualified"):
1883
+ msg_parts = []
1884
+ if daily_info.get("qualified"):
1885
+ msg_parts.append(f"Daily #{daily_info['rank']}")
1886
+ if weekly_info.get("qualified"):
1887
+ msg_parts.append(f"Weekly #{weekly_info['rank']}")
1888
+ st.success(f"โœ… Submitted! Ranked: {', '.join(msg_parts)}")
1889
+ else:
1890
+ st.info("โœ… Score submitted (not in top 20)")
1891
+ st.rerun()
1892
+ except Exception as e:
1893
+ st.error(f"Failed to submit to leaderboards: {e}")
1894
+
1895
+
1896
  button_text = "๐Ÿ“Š Submit Your Result" if is_shared_game else "๐Ÿ”— Generate Share Link"
1897
 
1898
  if not can_submit:
1899
  st.warning("โš ๏ธ Please enter your name to submit.")
1900
 
1901
+ if st.button(button_text, key="generate_share_link", width="stretch", disabled=not can_submit):
1902
  try:
1903
+ st.markdown("---")
1904
  # Extract game data
1905
  word_list = [w.text for w in state.puzzle.words]
1906
  spacer = state.puzzle.spacer
 
1975
 
1976
  except Exception as e:
1977
  st.error(f"Failed to save game: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1978
  else:
1979
  # Conditionally display the generated share URL
1980
  if st.session_state.get("show_challenge_share_links", False):
 
2294
  _render_footer(current_page="settings")
2295
  return
2296
 
2297
+ if page in {"today", "daily", "weekly", "history"}:
2298
  from .leaderboard_page import render_leaderboard_page
2299
  st.markdown(ocean_background_css, unsafe_allow_html=True)
2300
  inject_ocean_layers()
2301
+ default = page if page in {"daily", "weekly", "history"} else "today"
2302
  render_leaderboard_page(default_tab=default)
2303
  _render_footer(current_page=page if page else "play")
2304
  return