Surn commited on
Commit
d26f012
ยท
1 Parent(s): b079e13

Add Hall of Legends feature and update to v0.3.2

Browse files

Introduce Hall of Legends leaderboard for legendary scores (46+), accessible from the footer. Remove Hall of Fantastics. Refactor leaderboard logic to use is_legendary_score utility. Update UI, navigation, documentation, and version numbers to v0.3.2. Includes minor bug fixes and display improvements.

CLAUDE.md CHANGED
@@ -1,6 +1,6 @@
1
  # CLAUDE
2
 
3
- Wrdler v0.3.1
4
 
5
  # Wrdler - Project Context
6
 
@@ -12,20 +12,24 @@ 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.3.1
16
- **Last Updated:** 2026-03-10
17
  **Repository:** https://github.com/Oncorporation/Wrdler.git
18
  **Branch:** main
19
 
20
- ## Recent Changes (v0.3.1)
 
 
 
21
  - Fixed "Your Guess" targeting bug
22
  - Fixed mobile "incorrect guesses" link
23
- ## Recent Changes (v0.3.0)
 
24
  - Minor UI and documentation updates
25
  - Small fixes to footer navigation and game-over dialog layout
26
  - Synchronized documentation headers and version strings to 0.3.0
27
 
28
- ## Recent Changes (v0.2.16)
29
  - Version bump to 0.2.16
30
  - Documentation and version updates
31
  - Update documentation versions from .md to .mdx
 
1
  # CLAUDE
2
 
3
+ Wrdler v0.3.2
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.3.2
16
+ **Last Updated:** 2026-04-15
17
  **Repository:** https://github.com/Oncorporation/Wrdler.git
18
  **Branch:** main
19
 
20
+ ## Recent Changes (v0.3.2)
21
+ - Added Hall of Legends for legendary scores
22
+
23
+ ## v0.3.1
24
  - Fixed "Your Guess" targeting bug
25
  - Fixed mobile "incorrect guesses" link
26
+
27
+ ## v0.3.0
28
  - Minor UI and documentation updates
29
  - Small fixes to footer navigation and game-over dialog layout
30
  - Synchronized documentation headers and version strings to 0.3.0
31
 
32
+ ## v0.2.16
33
  - Version bump to 0.2.16
34
  - Documentation and version updates
35
  - Update documentation versions from .md to .mdx
GAMEPLAY_GUIDE.mdx CHANGED
@@ -1,8 +1,8 @@
1
  # Wrdler Gameplay Guide
2
 
3
- Version 0.3.1
4
 
5
- **Last Updated:** 2026-03-10
6
 
7
  ## Welcome to Wrdler!
8
 
@@ -11,8 +11,8 @@ Wrdler is a simplified vocabulary puzzle game where you discover 6 hidden words
11
  ---
12
 
13
 
14
- ## Recent Changes (v0.3.0)
15
- See `README.md` for the canonical release notes: [Recent Changes (v0.3.0)](README.md#recent-changes-v030)
16
 
17
  ---
18
 
 
1
  # Wrdler Gameplay Guide
2
 
3
+ Version 0.3.2
4
 
5
+ **Last Updated:** 2026-04-15
6
 
7
  ## Welcome to Wrdler!
8
 
 
11
  ---
12
 
13
 
14
+ ## Recent Changes (v0.3.2)
15
+ See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](README.md#recent-changes-v032)
16
 
17
  ---
18
 
README.md CHANGED
@@ -21,7 +21,7 @@ thumbnail: >-
21
 
22
  # Wrdler
23
 
24
- Version 0.3.1
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,16 +29,16 @@ 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.3.1
33
- **Last Updated:** 2026-03-10
34
 
35
- ## Recent Changes (v0.3.1)
36
- - Fixed "Your Guess" targeting bug
37
- - Fixed mobile "incorrect guesses" link
38
- ## Recent Changes (v0.3.0)
39
- - Comment out graphics modules to improve speed
40
- - Small fixes to footer navigation and game-over dialog layout
41
- - Synchronized documentation and spec headers to v0.3.0 (Last Updated 2026-03-04)
42
 
43
  ## Key Differences from BattleWords
44
 
@@ -268,7 +268,19 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
268
 
269
  ## Changelog
270
 
271
- ### v0.2.17 (Current) โœ…
 
 
 
 
 
 
 
 
 
 
 
 
272
  - Enhance UI/UX and update documentation
273
  - Updated README.md and documentation to reflect version bump to 0.2.17 and grid simplification
274
  - Refactored JavaScript in `ui.py` for improved input focus management with exponential backoff
 
21
 
22
  # Wrdler
23
 
24
+ Version 0.3.2
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.3.2
33
+ **Last Updated:** 2026-04-15
34
 
35
+ ## Recent Changes (v0.3.2)
36
+ A new Hall of Legends feature was added in v0.3.2 to celebrate and preserve outstanding player performances.
37
+
38
+ - Permanent "Hall of Legends" leaderboard that highlights all-time top scorers across challenges and daily/weekly boards
39
+ - Hall entries include player name, score, date, and settings snapshot; accessible from the footer navigation
40
+ - UI updates to leaderboard pages (icons/badges and compact entry layout) and integration with existing leaderboard submission flow
41
+ - Minor bug fixes and display improvements related to leaderboard sorting and tie-breaker presentation
42
 
43
  ## Key Differences from BattleWords
44
 
 
268
 
269
  ## Changelog
270
 
271
+ ## v0.3.2 (Current) โœ…
272
+ - Added Hall of Legends for legendary scores
273
+ -
274
+ ## v0.3.1
275
+ - Fixed "Your Guess" targeting bug
276
+ - Fixed mobile "incorrect guesses" link
277
+
278
+ ## v0.3.0
279
+ - Comment out graphics modules to improve speed
280
+ - Small fixes to footer navigation and game-over dialog layout
281
+ - Synchronized documentation and spec headers to v0.3.0 (Last Updated 2026-03-04)
282
+
283
+ ### v0.2.17 โœ…
284
  - Enhance UI/UX and update documentation
285
  - Updated README.md and documentation to reflect version bump to 0.2.17 and grid simplification
286
  - Refactored JavaScript in `ui.py` for improved input focus management with exponential backoff
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "wrdler"
3
- version = "0.3.1"
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.3.2"
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.mdx CHANGED
@@ -2,12 +2,12 @@
2
 
3
  **Document Version:** 1.4.7
4
  **Author:** GitHub Copilot
5
- **Project Version:** 0.3.1
6
- **Last Updated:** 2026-03-10
7
  **Status:** โœ… Implemented and Documented
8
 
9
- ## Recent Changes (v0.3.0)
10
- See `README.md` for the canonical release notes: [Recent Changes (v0.3.0)](../README.md#recent-changes-v030)
11
 
12
  ---
13
 
 
2
 
3
  **Document Version:** 1.4.7
4
  **Author:** GitHub Copilot
5
+ **Project Version:** 0.3.2
6
+ **Last Updated:** 2026-04-15
7
  **Status:** โœ… Implemented and Documented
8
 
9
+ ## Recent Changes (v0.3.2)
10
+ See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](../README.md#recent-changes-v032)
11
 
12
  ---
13
 
specs/requirements.mdx CHANGED
@@ -1,11 +1,11 @@
1
  ๏ปฟ# Wrdler Requirements
2
 
3
  **Status:** Production Ready - Leaderboards Implemented
4
- **Version:** 0.3.1
5
- **Last Updated:** 2026-03-10
6
 
7
- ## Recent Changes (v0.2.18)
8
- See `README.md` for the canonical release notes: [Recent Changes (v0.2.18)](../README.md#recent-changes-v0218)
9
 
10
  This document breaks down the implementation tasks for Wrdler using the game rules described in `specs.md`. Wrdler is a Python/Streamlit project based on BattleWords but with a simplified 8x6 grid, horizontal-only words, and free letter guesses at the start.
11
 
 
1
  ๏ปฟ# Wrdler Requirements
2
 
3
  **Status:** Production Ready - Leaderboards Implemented
4
+ **Version:** 0.3.2
5
+ **Last Updated:** 2026-04-15
6
 
7
+ ## Recent Changes (v0.3.2)
8
+ See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](../README.md#recent-changes-v032)
9
 
10
  This document breaks down the implementation tasks for Wrdler using the game rules described in `specs.md`. Wrdler is a Python/Streamlit project based on BattleWords but with a simplified 8x6 grid, horizontal-only words, and free letter guesses at the start.
11
 
specs/specs.mdx CHANGED
@@ -1,12 +1,12 @@
1
  # Wrdler Specifications
2
 
3
- **Version:** 0.3.1
4
- **Last Updated:** 2026-03-10
5
 
6
  **Status:** Production Ready - Leaderboards & Enhanced Settings Page Implemented
7
 
8
- ## Recent Changes (v0.2.18)
9
- See `README.md` for the canonical release notes: [Recent Changes (v0.2.18)](../README.md#recent-changes-v0218)
10
 
11
  ## Overview
12
  Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, but with key differences. The objective is to discover hidden words on a grid by making strategic guesses and using free letter reveals at the game start.
 
1
  # Wrdler Specifications
2
 
3
+ **Version:** 0.3.2
4
+ **Last Updated:** 2026-04-15
5
 
6
  **Status:** Production Ready - Leaderboards & Enhanced Settings Page Implemented
7
 
8
+ ## Recent Changes (v0.3.2)
9
+ See `README.md` for the canonical release notes: [Recent Changes (v0.3.2)](../README.md#recent-changes-v032)
10
 
11
  ## Overview
12
  Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, but with key differences. The objective is to discover hidden words on a grid by making strategic guesses and using free letter reveals at the game start.
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.3.1: Fixed Your Guess targeting bug; fixed mobile "incorrect guesses" link.
12
  """
13
 
14
- __version__ = "0.3.1"
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.3.2: Added Hall of Legends for legendary scores.
12
  """
13
 
14
+ __version__ = "0.3.2"
15
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
wrdler/game_storage.py CHANGED
@@ -581,7 +581,7 @@ if __name__ == "__main__":
581
  score=42,
582
  time_seconds=180.01,
583
  game_mode="classic",
584
- grid_size=8, # Wrdler default
585
  wordlist_source="classic.txt"
586
  )
587
  print(f"Challenge ID: {challenge_id}")
 
581
  score=42,
582
  time_seconds=180.01,
583
  game_mode="classic",
584
+ grid_size=6, # Wrdler default
585
  wordlist_source="classic.txt"
586
  )
587
  print(f"Challenge ID: {challenge_id}")
wrdler/hall_of_legends.py CHANGED
@@ -7,29 +7,12 @@ import streamlit as st
7
 
8
  from .modules.constants import HF_REPO_ID, HALL_OF_LEGENDS_FILE, APP_SETTINGS, MAX_DISPLAY_ENTRIES
9
  from .modules.storage import _get_json_from_repo, _upload_json_to_repo
10
- from .ui_helpers import game_page_url, hall_page_url, fantastic_page_url
11
 
12
 
13
  CURRENT_SETTINGS_VERSION = 0
14
 
15
 
16
- def _get_weekly_fantastic_metadata(game_mode: str | None = None) -> tuple[str, str, str, int, str]:
17
- current_time = datetime.now(timezone.utc)
18
- iso_year, iso_week, _ = current_time.isocalendar()
19
- week_id = f"{iso_year}-W{iso_week:02d}"
20
- mode = game_mode or st.session_state.get("game_mode", "classic")
21
- wordlist_source = st.session_state.get("selected_wordlist", "classic.txt")
22
- wordlist_stem = os.path.splitext(os.path.basename(wordlist_source))[0] or "classic"
23
- settings_version = CURRENT_SETTINGS_VERSION
24
- slug = f"{wordlist_stem}-{mode}-{settings_version}-fantastic"
25
- return week_id, mode, wordlist_source, settings_version, slug
26
-
27
-
28
- def _weekly_fantastic_settings_path(game_mode: str | None = None) -> str:
29
- week_id, _, _, _, slug = _get_weekly_fantastic_metadata(game_mode)
30
- return f"games/leaderboards/weekly/{week_id}/{slug}/settings.json"
31
-
32
-
33
  def _default_hall_document() -> dict[str, Any]:
34
  game_mode = st.session_state.get("game_mode", "classic")
35
  wordlist_source = st.session_state.get("selected_wordlist", "classic.txt")
@@ -40,7 +23,9 @@ def _default_hall_document() -> dict[str, Any]:
40
  "challenge_id": f"hall-of-legends/{game_mode}-{wordlist_stem}-{spacer}",
41
  "entry_type": "hall",
42
  "game_mode": game_mode,
43
- "grid_size": 12,
 
 
44
  "puzzle_options": {
45
  "spacer": spacer,
46
  "may_overlap": False,
@@ -51,7 +36,7 @@ def _default_hall_document() -> dict[str, Any]:
51
  "show_incorrect_guesses": bool(st.session_state.get("show_incorrect_guesses", True)),
52
  "enable_free_letters": bool(st.session_state.get("enable_free_letters", False)),
53
  "wordlist_source": wordlist_source,
54
- "game_title": APP_SETTINGS.get("game_title", "Wrdler"),
55
  "max_display_entries": int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES)),
56
  }
57
 
@@ -74,57 +59,12 @@ def load_hall_document() -> dict[str, Any]:
74
  return default_doc
75
 
76
 
77
- def _default_fantastic_document(game_mode: str | None = None) -> dict[str, Any]:
78
- week_id, mode, wordlist_source, settings_version, slug = _get_weekly_fantastic_metadata(game_mode)
79
- spacer = int(st.session_state.get("spacer", 1))
80
-
81
- return {
82
- "challenge_id": f"{week_id}/{slug}",
83
- "entry_type": "fantastic",
84
- "game_mode": mode,
85
- "grid_size": 12,
86
- "puzzle_options": {
87
- "spacer": spacer,
88
- "may_overlap": False,
89
- },
90
- "users": [],
91
- "created_at": datetime.now().astimezone().isoformat(),
92
- "version": st.session_state.get("app_version", ""),
93
- "show_incorrect_guesses": bool(st.session_state.get("show_incorrect_guesses", True)),
94
- "enable_free_letters": bool(st.session_state.get("enable_free_letters", False)),
95
- "wordlist_source": wordlist_source,
96
- "game_title": APP_SETTINGS.get("game_title", "Battlewords"),
97
- "max_display_entries": int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES)),
98
- "settings_version": settings_version,
99
- "leaderboard_scope": "weekly",
100
- "week_id": week_id,
101
- }
102
-
103
-
104
- def load_fantastic_document(game_mode: str | None = None) -> dict[str, Any]:
105
- default_doc = _default_fantastic_document(game_mode)
106
- if not HF_REPO_ID:
107
- return default_doc
108
-
109
- data = _get_json_from_repo(HF_REPO_ID, _weekly_fantastic_settings_path(game_mode), "dataset")
110
-
111
- if isinstance(data, dict):
112
- merged = {**default_doc, **data}
113
- if not isinstance(merged.get("users"), list):
114
- merged["users"] = []
115
- return merged
116
-
117
- if isinstance(data, list):
118
- default_doc["users"] = data
119
- return default_doc
120
-
121
-
122
  def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
123
  if not HF_REPO_ID:
124
  return False, "HF_REPO_ID is not configured."
125
 
126
  score = int(user_entry.get("score", 0))
127
- if score < 46:
128
  return False, "Only Legendary runs (46+) can be saved."
129
 
130
  hall_doc = load_hall_document()
@@ -144,7 +84,9 @@ def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
144
  hall_doc["challenge_id"] = f"hall-of-legends/{state.game_mode}-{wordlist_stem}-{spacer}"
145
  hall_doc["entry_type"] = "hall"
146
  hall_doc["game_mode"] = state.game_mode
147
- hall_doc["grid_size"] = int(getattr(state, "grid_rows", getattr(state, "grid_size", 6)) or 6)
 
 
148
  hall_doc["puzzle_options"] = {
149
  "spacer": spacer,
150
  "may_overlap": False,
@@ -163,52 +105,6 @@ def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
163
  return False, "Unable to save to Hall of Legends."
164
 
165
 
166
- def save_fantastic_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
167
- if not HF_REPO_ID:
168
- return False, "HF_REPO_ID is not configured."
169
-
170
- score = int(user_entry.get("score", 0))
171
- if score < 42 or score > 45:
172
- return False, "Only Fantastic runs (42-45) can be saved."
173
-
174
- fantastic_doc = load_fantastic_document(state.game_mode)
175
- users = fantastic_doc.setdefault("users", [])
176
- uid = str(user_entry.get("uid", "")).strip()
177
-
178
- if uid and any(str(u.get("uid", "")) == uid for u in users):
179
- return True, "Already saved to Hall of Fantastics."
180
-
181
- users.append(user_entry)
182
- users.sort(key=lambda x: (-int(x.get("score", 0)), float(x.get("time", 999999.0))))
183
-
184
- week_id, _, wordlist_source, settings_version, slug = _get_weekly_fantastic_metadata(state.game_mode)
185
- spacer = int(st.session_state.get("spacer", 1))
186
-
187
- fantastic_doc["challenge_id"] = f"{week_id}/{slug}"
188
- fantastic_doc["entry_type"] = "fantastic"
189
- fantastic_doc["game_mode"] = state.game_mode
190
- fantastic_doc["grid_size"] = state.grid_size
191
- fantastic_doc["puzzle_options"] = {
192
- "spacer": spacer,
193
- "may_overlap": False,
194
- }
195
- fantastic_doc["created_at"] = datetime.now().astimezone().isoformat()
196
- fantastic_doc["version"] = st.session_state.get("app_version", "")
197
- fantastic_doc["show_incorrect_guesses"] = bool(st.session_state.get("show_incorrect_guesses", True))
198
- fantastic_doc["enable_free_letters"] = bool(st.session_state.get("enable_free_letters", False))
199
- fantastic_doc["wordlist_source"] = wordlist_source
200
- fantastic_doc["game_title"] = APP_SETTINGS.get("game_title", "Battlewords")
201
- fantastic_doc["max_display_entries"] = int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES))
202
- fantastic_doc["settings_version"] = settings_version
203
- fantastic_doc["leaderboard_scope"] = "weekly"
204
- fantastic_doc["week_id"] = week_id
205
-
206
- ok = _upload_json_to_repo(fantastic_doc, HF_REPO_ID, _weekly_fantastic_settings_path(state.game_mode), "dataset")
207
- if ok:
208
- return True, "Saved to Hall of Fantastics."
209
- return False, "Unable to save to Hall of Fantastics."
210
-
211
-
212
  def render_hall_page() -> None:
213
  # Epic hero header
214
  st.markdown(
@@ -243,7 +139,7 @@ def render_hall_page() -> None:
243
  """,
244
  unsafe_allow_html=True,
245
  )
246
- st.markdown(f"<div style='text-align:center; margin-top:0.75rem;'><a href=\"{fantastic_page_url()}\" target=\"_self\">View Hall of Fantastics</a></div>", unsafe_allow_html=True)
247
 
248
  hall_doc = load_hall_document()
249
  users = hall_doc.get("users", []) if isinstance(hall_doc, dict) else []
@@ -255,7 +151,7 @@ def render_hall_page() -> None:
255
  total = len(unique_names)
256
  if total == 0:
257
  st.info("No Hall of Legends entries yet. Be the first to etch your name into legend!")
258
- st.markdown(f"<a href=\"{game_page_url()}\" target=\"_self\">Return to game</a>", unsafe_allow_html=True)
259
  return
260
 
261
  # Summary/top-stats
@@ -321,100 +217,5 @@ def render_hall_page() -> None:
321
  """
322
 
323
  st.markdown(table_html, unsafe_allow_html=True)
324
- st.markdown(f"<a class=\"bw-return\" href=\"{game_page_url()}\" target=\"_self\">โŸต Return to game</a>", unsafe_allow_html=True)
325
-
326
-
327
- def render_fantastic_page() -> None:
328
- week_id, _, _, _, _ = _get_weekly_fantastic_metadata()
329
- st.markdown(
330
- f"""
331
- <style>
332
- .bw-hall-hero {{
333
- background: linear-gradient(90deg,#5b21b6 0%, #7c3aed 50%, #2563eb 100%);
334
- color: #fff;
335
- padding: 1.25rem;
336
- border-radius: 1rem;
337
- box-shadow: 0 12px 40px rgba(37,99,235,0.4);
338
- text-align: center;
339
- margin-bottom: 1rem;
340
- }}
341
- .bw-hall-hero h1 {{ margin: 0; font-size: 2.1rem; letter-spacing: 1px; }}
342
- .bw-hall-hero p {{ margin: 0.25rem 0 0; opacity: 0.92; }}
343
- .bw-top-cards {{ display:flex; gap:12px; margin-top:1rem; justify-content:center; flex-wrap:wrap; }}
344
- .bw-card {{ background: rgba(255,255,255,0.05); padding: 0.75rem 1rem; border-radius: 0.75rem; min-width: 180px; text-align:center; box-shadow: 0 6px 18px rgba(37,99,235,0.28); }}
345
- .bw-card h3 {{ margin:0; font-size:1.1rem; }}
346
- .bw-card p {{ margin:0.25rem 0 0; font-size:0.95rem; color:#e9ddff; }}
347
- .bw-table {{ width:100%; border-collapse:collapse; margin-top:1rem; }}
348
- .bw-table th, .bw-table td {{ padding:8px 10px; border-bottom:1px solid rgba(255,255,255,0.08); }}
349
- .bw-table th {{ text-align:left; background: rgba(255,255,255,0.03); }}
350
- .bw-top-row {{ background: linear-gradient(90deg,#facc1540,#60a5fa40); }}
351
- .bw-return {{ display:block; text-align:center; margin-top:1rem; }}
352
- </style>
353
- <div class="bw-hall-hero">
354
- <div style="font-size:2.4rem;">โœจ HALL OF FANTASTICS</div>
355
- <h1>Fantastic Players โ€ข Weekly Honors</h1>
356
- <p>Runs scoring 42-45 are celebrated here for the current week: <strong>{week_id}</strong>.</p>
357
- </div>
358
- """,
359
- unsafe_allow_html=True,
360
- )
361
-
362
- fantastic_doc = load_fantastic_document()
363
- users = fantastic_doc.get("users", []) if isinstance(fantastic_doc, dict) else []
364
- entries = [u for u in users if 42 <= int(u.get("score", 0)) <= 45]
365
- entries.sort(key=lambda x: (-int(x.get("score", 0)), float(x.get("time", 999999.0))))
366
-
367
- unique_names = {(str(u.get("username", "Anonymous") or "Anonymous").strip().lower()) for u in entries}
368
- total = len(unique_names)
369
- if total == 0:
370
- st.info("No Hall of Fantastics entries yet for this week. Claim the first Fantastic finish!")
371
- st.markdown(f"<div style='text-align:center; margin-top:0.75rem;'><a href=\"{hall_page_url()}\" target=\"_self\">View Hall of Legends</a></div>", unsafe_allow_html=True)
372
- st.markdown(f"<a href=\"{game_page_url()}\" target=\"_self\">Return to game</a>", unsafe_allow_html=True)
373
- return
374
-
375
- best = entries[0]
376
- best_score = int(best.get("score", 0))
377
- best_player = best.get("username", "Anonymous")
378
- best_time = float(best.get("time", 0.0))
379
- wordlist_name = fantastic_doc.get("wordlist_source", "")
380
 
381
- st.markdown(
382
- f"""
383
- <div class="bw-top-cards">
384
- <div class="bw-card"><h3>๐ŸŒŸ Top Fantastic</h3><p><strong>{best_score}</strong> pts by <strong>{best_player}</strong></p></div>
385
- <div class="bw-card"><h3>โšก Fastest Fantastic</h3><p><strong>{best_time:.3f}s</strong></p></div>
386
- <div class="bw-card"><h3>๐Ÿ—“๏ธ Current Week</h3><p><strong>{week_id}</strong></p></div>
387
- <div class="bw-card"><h3>๐ŸŽฏ Wordlist</h3><p><strong>{wordlist_name}</strong></p></div>
388
- <div class="bw-card"><h3>๐Ÿ™Œ Total Fantastics</h3><p><strong>{total}</strong> saved runs</p></div>
389
- </div>
390
- """,
391
- unsafe_allow_html=True,
392
- )
393
- st.markdown(f"<div style='text-align:center; margin-top:0.75rem;'><a href=\"{hall_page_url()}\" target=\"_self\">View Hall of Legends</a></div>", unsafe_allow_html=True)
394
-
395
- display_limit = min(int(fantastic_doc.get("max_display_entries", APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES))), MAX_DISPLAY_ENTRIES)
396
- rows_html = []
397
- for idx, entry in enumerate(entries[:display_limit], 1):
398
- score = int(entry.get("score", 0))
399
- player = entry.get("username", "Anonymous")
400
- tier = entry.get("tier", "Fantastic")
401
- saved = entry.get("timestamp", "")
402
- tr_class = "bw-top-row" if idx <= 3 else ""
403
- medal = "๐Ÿฅ‡" if idx == 1 else ("๐Ÿฅˆ" if idx == 2 else ("๐Ÿฅ‰" if idx == 3 else "โœจ"))
404
- rows_html.append(f"<tr class=\"{tr_class}\"><td style='width:64px'>{medal} {idx}</td><td><strong>{player}</strong></td><td>{score}</td><td>{tier}</td><td>{wordlist_name}</td><td style='white-space:nowrap'>{saved}</td></tr>")
405
-
406
- table_html = f"""
407
- <div style="overflow:auto; max-height:520px;">
408
- <table class="bw-table shiny-border">
409
- <thead>
410
- <tr><th>Rank</th><th>Player</th><th>Score</th><th>Tier</th><th>Wordlist</th><th>Saved</th></tr>
411
- </thead>
412
- <tbody>
413
- {''.join(rows_html)}
414
- </tbody>
415
- </table>
416
- </div>
417
- """
418
-
419
- st.markdown(table_html, unsafe_allow_html=True)
420
- st.markdown(f"<a class=\"bw-return\" href=\"{game_page_url()}\" target=\"_self\">โŸต Return to game</a>", unsafe_allow_html=True)
 
7
 
8
  from .modules.constants import HF_REPO_ID, HALL_OF_LEGENDS_FILE, APP_SETTINGS, MAX_DISPLAY_ENTRIES
9
  from .modules.storage import _get_json_from_repo, _upload_json_to_repo
10
+ from .ui_helpers import hall_page_url, is_legendary_score
11
 
12
 
13
  CURRENT_SETTINGS_VERSION = 0
14
 
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  def _default_hall_document() -> dict[str, Any]:
17
  game_mode = st.session_state.get("game_mode", "classic")
18
  wordlist_source = st.session_state.get("selected_wordlist", "classic.txt")
 
23
  "challenge_id": f"hall-of-legends/{game_mode}-{wordlist_stem}-{spacer}",
24
  "entry_type": "hall",
25
  "game_mode": game_mode,
26
+ # Store rectangular grid dimensions explicitly for Wrdler (rows ร— cols)
27
+ "grid_rows": int(st.session_state.get("grid_rows", 6)),
28
+ "grid_cols": int(st.session_state.get("grid_cols", 8)),
29
  "puzzle_options": {
30
  "spacer": spacer,
31
  "may_overlap": False,
 
36
  "show_incorrect_guesses": bool(st.session_state.get("show_incorrect_guesses", True)),
37
  "enable_free_letters": bool(st.session_state.get("enable_free_letters", False)),
38
  "wordlist_source": wordlist_source,
39
+ "game_title": "Wrdler",
40
  "max_display_entries": int(APP_SETTINGS.get("max_display_entries", MAX_DISPLAY_ENTRIES)),
41
  }
42
 
 
59
  return default_doc
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def save_hall_entry(user_entry: dict[str, Any], state: Any) -> tuple[bool, str]:
63
  if not HF_REPO_ID:
64
  return False, "HF_REPO_ID is not configured."
65
 
66
  score = int(user_entry.get("score", 0))
67
+ if not is_legendary_score(score):
68
  return False, "Only Legendary runs (46+) can be saved."
69
 
70
  hall_doc = load_hall_document()
 
84
  hall_doc["challenge_id"] = f"hall-of-legends/{state.game_mode}-{wordlist_stem}-{spacer}"
85
  hall_doc["entry_type"] = "hall"
86
  hall_doc["game_mode"] = state.game_mode
87
+ # Ensure we persist rectangular grid metadata (rows and cols)
88
+ hall_doc["grid_rows"] = int(getattr(state, "grid_rows", 6) or 6)
89
+ hall_doc["grid_cols"] = int(getattr(state, "grid_cols", 8) or 8)
90
  hall_doc["puzzle_options"] = {
91
  "spacer": spacer,
92
  "may_overlap": False,
 
105
  return False, "Unable to save to Hall of Legends."
106
 
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  def render_hall_page() -> None:
109
  # Epic hero header
110
  st.markdown(
 
139
  """,
140
  unsafe_allow_html=True,
141
  )
142
+
143
 
144
  hall_doc = load_hall_document()
145
  users = hall_doc.get("users", []) if isinstance(hall_doc, dict) else []
 
151
  total = len(unique_names)
152
  if total == 0:
153
  st.info("No Hall of Legends entries yet. Be the first to etch your name into legend!")
154
+ st.markdown("<a href='/' target='_self'>Return to game</a>", unsafe_allow_html=True)
155
  return
156
 
157
  # Summary/top-stats
 
217
  """
218
 
219
  st.markdown(table_html, unsafe_allow_html=True)
220
+ st.markdown("<a class='bw-return' href='/' target='_self'>โŸต Return to game</a>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
wrdler/ui.py CHANGED
@@ -44,6 +44,7 @@ from .ui_helpers import (
44
  show_spinner,
45
  get_effective_game_title,
46
  custom_rising_text,
 
47
  )
48
 
49
  # --- Spinner context manager for custom spinner ---
@@ -1245,7 +1246,7 @@ def _game_over_content(state: GameState) -> None:
1245
  else:
1246
  _mount_background_audio(False, None, 0.0)
1247
 
1248
- if state.score >= 46:
1249
  custom_rising_text(["LEGENDARY!", "๐Ÿ›๏ธ HALL OF LEGENDS"], font_size=[34, 28], upward_speed=[4, 5], animation_length=4, color=["#ffd54d", "#ffffff"])
1250
 
1251
  # Set end_time if not already set
@@ -1500,7 +1501,7 @@ def _game_over_content(state: GameState) -> None:
1500
  return results
1501
 
1502
  def _save_legendary_hall_entry(username: str, score: int, time_secs: float, word_list: list, leaderboard_results: dict, challenge_id: str = None) -> None:
1503
- if score < 46 or st.session_state.get("hall_of_legends_saved", False):
1504
  return
1505
 
1506
  daily_info = (leaderboard_results or {}).get("daily", {})
@@ -1720,15 +1721,23 @@ def _game_over_content(state: GameState) -> None:
1720
 
1721
  if daily_info.get("qualified"):
1722
  daily_rankings_text = f"<a href='{daily_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{daily_rankings_text}</a>"
1723
-
1724
  if weekly_info.get("qualified"):
1725
  weekly_ranking_text = f"<a href='{weekly_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{weekly_ranking_text}</a>"
 
 
 
 
 
 
 
 
1726
 
1727
  st.markdown(
1728
  f"""
1729
  <div style="margin-top: 1rem; padding: 1rem; background: rgba(32, 212, 108, 0.1); text-align:center; border-radius: 0.5rem; border: 1px solid rgba(32, 212, 108, 0.3);">
1730
  <strong style="color: #20d46c; filter: drop-shadow(1px 1px 1px #003);">๐Ÿ† Leaderboard Rankings:</strong><br/>
1731
- {daily_rankings_text} {weekly_ranking_text}
1732
  </div>
1733
  """,
1734
  unsafe_allow_html=True
 
44
  show_spinner,
45
  get_effective_game_title,
46
  custom_rising_text,
47
+ is_legendary_score,
48
  )
49
 
50
  # --- Spinner context manager for custom spinner ---
 
1246
  else:
1247
  _mount_background_audio(False, None, 0.0)
1248
 
1249
+ if is_legendary_score(state.score):
1250
  custom_rising_text(["LEGENDARY!", "๐Ÿ›๏ธ HALL OF LEGENDS"], font_size=[34, 28], upward_speed=[4, 5], animation_length=4, color=["#ffd54d", "#ffffff"])
1251
 
1252
  # Set end_time if not already set
 
1501
  return results
1502
 
1503
  def _save_legendary_hall_entry(username: str, score: int, time_secs: float, word_list: list, leaderboard_results: dict, challenge_id: str = None) -> None:
1504
+ if not is_legendary_score(score) or st.session_state.get("hall_of_legends_saved", False):
1505
  return
1506
 
1507
  daily_info = (leaderboard_results or {}).get("daily", {})
 
1721
 
1722
  if daily_info.get("qualified"):
1723
  daily_rankings_text = f"<a href='{daily_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{daily_rankings_text}</a>"
1724
+
1725
  if weekly_info.get("qualified"):
1726
  weekly_ranking_text = f"<a href='{weekly_leaderboard_url}' target='_self' style='color: #20d46c; text-decoration: underline; font-size: 1.2rem; filter: drop-shadow(1px 1px 1px #003);'>{weekly_ranking_text}</a>"
1727
+
1728
+ # If weekly qualified AND this run is Legendary, offer quick link to Hall of Legends
1729
+ hall_link_html = ""
1730
+ try:
1731
+ if weekly_info.get("qualified") and is_legendary_score(state.score):
1732
+ hall_link_html = f"<div style='margin-top:0.5rem;'><a href='/?page=hall' target='_self' style='color: #ffd54d; text-decoration: underline; font-size: 1.1rem;'>๐Ÿ›๏ธ Hall of Legends</a></div>"
1733
+ except Exception:
1734
+ hall_link_html = ""
1735
 
1736
  st.markdown(
1737
  f"""
1738
  <div style="margin-top: 1rem; padding: 1rem; background: rgba(32, 212, 108, 0.1); text-align:center; border-radius: 0.5rem; border: 1px solid rgba(32, 212, 108, 0.3);">
1739
  <strong style="color: #20d46c; filter: drop-shadow(1px 1px 1px #003);">๐Ÿ† Leaderboard Rankings:</strong><br/>
1740
+ {daily_rankings_text} {weekly_ranking_text} {hall_link_html}
1741
  </div>
1742
  """,
1743
  unsafe_allow_html=True
wrdler/ui_helpers.py CHANGED
@@ -80,6 +80,14 @@ def custom_rising_text(
80
  )
81
  st.html(html, unsafe_allow_javascript=True)
82
 
 
 
 
 
 
 
 
 
83
  # --- Utility: Convert Matplotlib Figure to PIL RGBA Image ---
84
  def fig_to_pil_rgba(fig):
85
  """
@@ -118,6 +126,12 @@ def get_effective_game_title() -> str:
118
  # Fall back to APP_SETTINGS
119
  return APP_SETTINGS.get("game_title", "Wrdler")
120
 
 
 
 
 
 
 
121
 
122
  # --- PWA Service Worker JS (for Streamlit head injection) ---
123
  pwa_service_worker = """
@@ -630,6 +644,7 @@ def _render_footer(current_page: str = "play"):
630
  # Determine which link should be highlighted as active
631
  play_active = "active" if current_page == "play" else ""
632
  leaderboard_active = "active" if current_page in {"today", "daily", "weekly", "history"} else ""
 
633
  settings_active = "active" if current_page == "settings" else ""
634
  game_title = get_effective_game_title()
635
  game_version = f"{game_title.lower()}-v{version.replace('.', '-')}"
@@ -651,11 +666,13 @@ def _render_footer(current_page: str = "play"):
651
  if game_id:
652
  today_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
653
  leaderboard_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
 
654
  play_url = f"?game_id={game_id}#{game_version}"
655
  settings_url = f"?page=settings&game_id={game_id}#settings"
656
  else:
657
  today_url = "?page=today#wrdler-leaderboards"
658
  leaderboard_url = "?page=today#wrdler-leaderboards"
 
659
  play_url = f"/#{game_version}"
660
  settings_url = "?page=settings#settings"
661
 
@@ -717,6 +734,7 @@ def _render_footer(current_page: str = "play"):
717
  <div class="bw-footer">
718
  <nav class="bw-footer-nav">
719
  <a href="{leaderboard_url if not leaderboard_active else '#wrdler-leaderboards'}" title="View Leaderboards" target="_self" class="{leaderboard_active}">๐Ÿ† Leaderboard</a>
 
720
  <a href="{play_url if not play_active else f'#{game_version}'}" title="Play Wrdler" target="_self" class="{play_active}">๐ŸŽฎ Play</a>
721
  <a href="{settings_url if not settings_active else '#settings'}" title="Settings" target="_self" class="{settings_active}">โš™๏ธ Settings</a>
722
  </nav>
 
80
  )
81
  st.html(html, unsafe_allow_javascript=True)
82
 
83
+
84
+ def is_legendary_score(score: int | float | str) -> bool:
85
+ """Return True when a score qualifies for Hall of Legends."""
86
+ try:
87
+ return int(score) >= 46
88
+ except Exception:
89
+ return False
90
+
91
  # --- Utility: Convert Matplotlib Figure to PIL RGBA Image ---
92
  def fig_to_pil_rgba(fig):
93
  """
 
126
  # Fall back to APP_SETTINGS
127
  return APP_SETTINGS.get("game_title", "Wrdler")
128
 
129
+ def hall_page_url() -> str:
130
+ return "?page=hall"
131
+
132
+
133
+ def game_page_url() -> str:
134
+ return "?page=play&overlay=0"
135
 
136
  # --- PWA Service Worker JS (for Streamlit head injection) ---
137
  pwa_service_worker = """
 
644
  # Determine which link should be highlighted as active
645
  play_active = "active" if current_page == "play" else ""
646
  leaderboard_active = "active" if current_page in {"today", "daily", "weekly", "history"} else ""
647
+ hall_active = "active" if current_page == "hall" else ""
648
  settings_active = "active" if current_page == "settings" else ""
649
  game_title = get_effective_game_title()
650
  game_version = f"{game_title.lower()}-v{version.replace('.', '-')}"
 
666
  if game_id:
667
  today_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
668
  leaderboard_url = f"?page=today&game_id={game_id}#wrdler-leaderboards"
669
+ hall_url = "?page=hall"
670
  play_url = f"?game_id={game_id}#{game_version}"
671
  settings_url = f"?page=settings&game_id={game_id}#settings"
672
  else:
673
  today_url = "?page=today#wrdler-leaderboards"
674
  leaderboard_url = "?page=today#wrdler-leaderboards"
675
+ hall_url = "?page=hall"
676
  play_url = f"/#{game_version}"
677
  settings_url = "?page=settings#settings"
678
 
 
734
  <div class="bw-footer">
735
  <nav class="bw-footer-nav">
736
  <a href="{leaderboard_url if not leaderboard_active else '#wrdler-leaderboards'}" title="View Leaderboards" target="_self" class="{leaderboard_active}">๐Ÿ† Leaderboard</a>
737
+ <a href="{hall_url if not hall_active else '#wrdler-hall'}" title="View Hall of Legends" target="_self" class="{hall_active}">๐Ÿ›๏ธ Hall</a>
738
  <a href="{play_url if not play_active else f'#{game_version}'}" title="Play Wrdler" target="_self" class="{play_active}">๐ŸŽฎ Play</a>
739
  <a href="{settings_url if not settings_active else '#settings'}" title="Settings" target="_self" class="{settings_active}">โš™๏ธ Settings</a>
740
  </nav>