Surn commited on
Commit
09427c9
·
1 Parent(s): 58999ac

Bump version to 0.2.9; enhance settings management

Refactored settings management:
- Added `_prepare_settings_for_save` and `save_active_settings` for better handling of `settings.json`.
- Introduced `edit_settings_configuration` for overwriting specific settings files.
- Enhanced `settings_page.py` with support for creating, updating, renaming, and deleting settings files.
- Split "Save Settings" into "Create Settings" and "Update Settings" buttons.
- Improved settings loading logic and user feedback.

Updated default configurations:
- Enabled sound effects by default in `settings.json` and `classic-classic-1.json`.
- Added `classic-classic-full_sound_free_letters.json`.
- Removed deprecated `classic-classic-2.json`.

Other changes:
- Updated footer navigation to prevent reloading active pages.
- Incremented version to `0.2.9` in `CLAUDE.md`, `specs.md`, and `__init__.py`.
- Improved maintainability and usability across the codebase.

CLAUDE.md CHANGED
@@ -1,6 +1,6 @@
1
  # CLAUDE
2
 
3
- Wrdler v0.2.8
4
 
5
  # Wrdler - Project Context
6
 
@@ -12,11 +12,11 @@ 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.8
16
  **Repository:** https://github.com/Oncorporation/Wrdler.git
17
  **Branch:** AI (working branch)
18
 
19
- ## Current Features (v0.2.7)
20
 
21
  ### Core Gameplay
22
  - 8x6 grid with 6 hidden words (one per row, horizontal only)
@@ -38,11 +38,17 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
38
  - **Good:** 35-38 points
39
  - **Keep practicing:** < 35 points
40
 
41
- ### Settings Page
42
  - All game settings moved from sidebar to a dedicated Settings page (`?page=settings`)
43
  - Accessible via footer navigation (`⚙️ Settings` link)
44
  - Local JSON-based settings persistence in `wrdler/settings/`
45
  - Latest settings auto-loaded on startup
 
 
 
 
 
 
46
  - Sidebar now focused on lightweight controls (if any) and navigation
47
 
48
  ### Word List Management
@@ -89,17 +95,20 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
89
  ### Audio & Visuals
90
  - Ocean-themed gradient background with wave animations
91
  - Toggleable background music with volume control (configured via Settings page, played globally)
92
- - Sound effects (hit/miss/correct/incorrect) with volume control (configured via Settings page)
93
 
94
  ### PWA Support
95
  - Installable as Progressive Web App on desktop and mobile
96
  - Service worker for offline caching of static assets
97
  - Works offline for basic functionality
98
 
 
 
 
99
  ## Technical Architecture
100
 
101
  ### Technology Stack
102
- - **Framework:** Streamlit 1.51.0
103
  - **Language:** Python 3.12.8 (requires >=3.12, <3.13)
104
  - **Remote Storage:** huggingface_hub (>=0.20.0)
105
  - **AI Generation:** transformers, gradio_client
@@ -111,24 +120,24 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
111
  wrdler/
112
  ├── app.py # Streamlit entry point
113
  ├── wrdler/ # Main package
114
- │ ├── __init__.py # Version: 0.2.7
115
  │ ├── models.py # Data models (Coord, Word, Puzzle, GameState)
116
  │ ├── generator.py # Puzzle generation with deterministic seeding
117
  │ ├── logic.py # Game mechanics (reveal, guess, scoring)
118
  │ ├── ui.py # Streamlit UI with query param routing
119
- │ ├── oauth.py # HuggingFace OAuth utilities (NEW)
120
- │ ├── settings_page.py # Settings page UI (implemented, query-param route ?page=settings)
121
  │ ├── leaderboard.py # Leaderboard system (daily/weekly)
122
  │ ├── leaderboard_page.py # Leaderboard UI page
123
  │ ├── word_loader.py # Word list management
124
  │ ├── word_loader_ai.py # AI word generation
125
  │ ├── game_storage.py # HF game storage wrapper
126
  │ ├── version_info.py # Version display
127
- │ ├── modules/ # Shared utility modules (from OpenBadge)
128
  │ │ ├── __init__.py # Module exports
129
- │ │ ├── storage.py # HuggingFace storage & URL shortener (with folder listing)
130
  │ │ ├── storage.md # Storage module documentation
131
- │ │ ├── constants.py # Storage-related constants (trimmed)
132
  │ │ └── file_utils.py # File utility functions
133
  │ └── words/ # Word list files
134
  │ ├── classic.txt # Default word list
@@ -137,26 +146,19 @@ wrdler/
137
  ├── specs/ # Documentation
138
  ├── static/ # PWA assets (manifest.json, service-worker.js)
139
  ├── .env # Environment variables (HF credentials)
140
- ├── pyproject.toml # Project metadata
141
- ├── requirements.txt # Dependencies
142
- ├── uv.lock # UV lock file
143
- ├── Dockerfile # Container deployment
144
- ├── README.md # User-facing documentation
145
- ├── CLAUDE.md # This file - project context for Claude
146
- ├── GAMEPLAY_GUIDE.md # User guide with tips and strategies
147
- ├── pyproject.toml # Project metadata
148
- ├── requirements.txt # Dependencies
149
- ├── uv.lock # UV lock file
150
- ├── Dockerfile # Container deployment
151
- ├── README.md # User-facing documentation
152
- ├── CLAUDE.md # This file - project context for Claude
153
- ├── GAMEPLAY_GUIDE.md # User guide with tips and strategies
154
  ```
155
 
156
  ### Page Navigation System
157
  Uses **query parameter-based routing** (NOT Streamlit multi-page):
158
  - `?page=today|daily|weekly|history` → Leaderboard pages
159
- - `?page=settings` → Settings page (no OAuth gating yet, local settings JSON persistence)
160
  - `?game_id=<sid>` → Challenge mode
161
  - No query params → Main game page
162
 
@@ -212,7 +214,7 @@ HF_REPO_ID=YourUsername/YourRepo # Dataset repo for challenge storage
212
  USE_HF_WORDS=false # Enable HF Space API for word generation
213
  HF_WORD_LIST_REPO_ID=YourUsername/WordRepo # Dataset repo for AI word lists
214
 
215
- # OAuth Admin Access (NEW)
216
  ADMIN_USERS=username1,username2 # Comma-separated list of admin usernames
217
  MAX_DISPLAY_ENTRIES=25 # Max leaderboard entries to display (default: 25)
218
  ```
@@ -279,8 +281,8 @@ Move all game settings from sidebar to a dedicated settings page at `?page=setti
279
 
280
  ### Files to Modify
281
  - `wrdler/ui.py` - Add settings page routing, remove sidebar settings
282
- - `wrdler/settings_page.py` - NEW FILE - Settings UI with OAuth protection
283
- - `wrdler/oauth.py` - Already created
284
 
285
  ### Key OAuth Functions (wrdler/oauth.py)
286
  ```python
@@ -310,7 +312,7 @@ if page in {"today", "daily", "weekly", "history"}:
310
  render_leaderboard_page(default_tab=page)
311
  return
312
 
313
- if page == "settings": # TO BE IMPLEMENTED
314
  render_settings_page()
315
  return
316
 
 
1
  # CLAUDE
2
 
3
+ Wrdler v0.2.9
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.9
16
  **Repository:** https://github.com/Oncorporation/Wrdler.git
17
  **Branch:** AI (working branch)
18
 
19
+ ## Current Features (v0.2.9)
20
 
21
  ### Core Gameplay
22
  - 8x6 grid with 6 hidden words (one per row, horizontal only)
 
38
  - **Good:** 35-38 points
39
  - **Keep practicing:** < 35 points
40
 
41
+ ### Settings Page & Management
42
  - All game settings moved from sidebar to a dedicated Settings page (`?page=settings`)
43
  - Accessible via footer navigation (`⚙️ Settings` link)
44
  - Local JSON-based settings persistence in `wrdler/settings/`
45
  - Latest settings auto-loaded on startup
46
+ - Enhanced settings management: create, update, rename, and delete settings files
47
+ - Split "Save Settings" into "Create Settings" and "Update Settings" actions
48
+ - Improved settings loading and user feedback
49
+ - Default sound effects enabled in settings
50
+ - New default configuration: `classic-classic-full_sound_free_letters.json`
51
+ - Deprecated configuration removed: `classic-classic-2.json`
52
  - Sidebar now focused on lightweight controls (if any) and navigation
53
 
54
  ### Word List Management
 
95
  ### Audio & Visuals
96
  - Ocean-themed gradient background with wave animations
97
  - Toggleable background music with volume control (configured via Settings page, played globally)
98
+ - Sound effects (hit/miss/correct/incorrect) with volume control (configured via Settings page, enabled by default)
99
 
100
  ### PWA Support
101
  - Installable as Progressive Web App on desktop and mobile
102
  - Service worker for offline caching of static assets
103
  - Works offline for basic functionality
104
 
105
+ ### Footer Navigation
106
+ - Updated to prevent reloading active pages
107
+
108
  ## Technical Architecture
109
 
110
  ### Technology Stack
111
+ - **Framework:** Streamlit 1.52.1
112
  - **Language:** Python 3.12.8 (requires >=3.12, <3.13)
113
  - **Remote Storage:** huggingface_hub (>=0.20.0)
114
  - **AI Generation:** transformers, gradio_client
 
120
  wrdler/
121
  ├── app.py # Streamlit entry point
122
  ├── wrdler/ # Main package
123
+ │ ├── __init__.py # Version: 0.2.9
124
  │ ├── models.py # Data models (Coord, Word, Puzzle, GameState)
125
  │ ├── generator.py # Puzzle generation with deterministic seeding
126
  │ ├── logic.py # Game mechanics (reveal, guess, scoring)
127
  │ ├── ui.py # Streamlit UI with query param routing
128
+ │ ├── oauth.py # HuggingFace OAuth utilities
129
+ │ ├── settings_page.py # Settings page UI (enhanced)
130
  │ ├── leaderboard.py # Leaderboard system (daily/weekly)
131
  │ ├── leaderboard_page.py # Leaderboard UI page
132
  │ ├── word_loader.py # Word list management
133
  │ ├── word_loader_ai.py # AI word generation
134
  │ ├── game_storage.py # HF game storage wrapper
135
  │ ├── version_info.py # Version display
136
+ │ ├── modules/ # Shared utility modules
137
  │ │ ├── __init__.py # Module exports
138
+ │ │ ├── storage.py # HuggingFace storage & URL shortener
139
  │ │ ├── storage.md # Storage module documentation
140
+ │ │ ├── constants.py # Storage-related constants
141
  │ │ └── file_utils.py # File utility functions
142
  │ └── words/ # Word list files
143
  │ ├── classic.txt # Default word list
 
146
  ├── specs/ # Documentation
147
  ├── static/ # PWA assets (manifest.json, service-worker.js)
148
  ├── .env # Environment variables (HF credentials)
149
+ ├── pyproject.toml # Project metadata
150
+ ├── requirements.txt # Dependencies
151
+ ├── uv.lock # UV lock file
152
+ ├── Dockerfile # Container deployment
153
+ ├── README.md # User-facing documentation
154
+ ├── CLAUDE.md # This file - project context for Claude
155
+ ├── GAMEPLAY_GUIDE.md # User guide with tips and strategies
 
 
 
 
 
 
 
156
  ```
157
 
158
  ### Page Navigation System
159
  Uses **query parameter-based routing** (NOT Streamlit multi-page):
160
  - `?page=today|daily|weekly|history` → Leaderboard pages
161
+ - `?page=settings` → Settings page
162
  - `?game_id=<sid>` → Challenge mode
163
  - No query params → Main game page
164
 
 
214
  USE_HF_WORDS=false # Enable HF Space API for word generation
215
  HF_WORD_LIST_REPO_ID=YourUsername/WordRepo # Dataset repo for AI word lists
216
 
217
+ # OAuth Admin Access
218
  ADMIN_USERS=username1,username2 # Comma-separated list of admin usernames
219
  MAX_DISPLAY_ENTRIES=25 # Max leaderboard entries to display (default: 25)
220
  ```
 
281
 
282
  ### Files to Modify
283
  - `wrdler/ui.py` - Add settings page routing, remove sidebar settings
284
+ - `wrdler/settings_page.py` - Settings UI with OAuth protection (enhanced)
285
+ - `wrdler/oauth.py` - Already created
286
 
287
  ### Key OAuth Functions (wrdler/oauth.py)
288
  ```python
 
312
  render_leaderboard_page(default_tab=page)
313
  return
314
 
315
+ if page == "settings":
316
  render_settings_page()
317
  return
318
 
README.md CHANGED
@@ -21,7 +21,7 @@ thumbnail: >-
21
 
22
  # Wrdler
23
 
24
- Version 0.2.8
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,7 +29,7 @@ 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.8
33
 
34
  ## Key Differences from BattleWords
35
 
@@ -54,7 +54,7 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
54
  ### Audio & Visuals
55
  - Ocean-themed gradient background with wave animations
56
  - Background music system (toggleable with volume control)
57
- - Sound effects for hits, misses, correct/incorrect guesses
58
  - Responsive UI built with Streamlit
59
 
60
  ### AI Word Generation
@@ -71,10 +71,16 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
71
  - **Fallback support**: Gracefully uses dictionary words if AI generation fails
72
  - **Guaranteed distribution**: Ensures exactly 25 words each of lengths 4, 5, and 6
73
 
74
- ### Customization
75
  - Multiple word lists (classic, fourth_grade, wordlist)
76
  - Dedicated Settings page (`⚙️ Settings` in footer) for wordlist selection, game mode, grid options, and audio
77
  - Audio volume controls (music and effects separate)
 
 
 
 
 
 
78
 
79
  ### ✅ Challenge Mode
80
  - **Shareable challenge links** via short URLs (`?game_id=<sid>`)
@@ -124,6 +130,7 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
124
  - **Dockerfile-based deployment** supported for Hugging Face Spaces and other container platforms
125
  - **Environment variables** for Challenge Mode (HF_API_TOKEN, HF_REPO_ID, SPACE_NAME)
126
  - Works offline without HF credentials (Challenge Mode features disabled gracefully)
 
127
 
128
  ### Progressive Web App (PWA)
129
  - Installable on desktop and mobile from your browser
@@ -218,7 +225,7 @@ MAX_DISPLAY_ENTRIES=25 # Max leaderboard entries to display (def
218
  - `logic.py` – game mechanics (reveal, guess, scoring, free letters)
219
  - `ui.py` – Streamlit UI composition (with query-param routing and Settings/Leaderboard pages)
220
  - `oauth.py` – HuggingFace OAuth utilities (NEW)
221
- - `settings_page.py` – Settings page UI (implemented)
222
  - `leaderboard.py` – Daily/weekly leaderboard system (v0.2.1)
223
  - `leaderboard_page.py` – Leaderboard UI page (v0.2.1)
224
  - `game_storage.py` – Hugging Face remote storage integration and challenge sharing
@@ -244,49 +251,58 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
244
 
245
  ## Changelog
246
 
247
- ### v0.2.8 (Current) ✅
248
- **Settings Page & Local Settings Persistence (Settings Update)**
249
- - All game settings moved from sidebar to dedicated Settings page (`?page=settings`)
250
- - Settings accessible from footer navigation (`⚙️ Settings` link)
251
- - Local JSON-based settings persistence in `wrdler/settings/`
252
- - Latest settings automatically loaded on startup
253
-
254
- ### v0.2.7 ✅
255
- **Word List Filtering**
256
- - Added "Filter Wordlist" button to sidebar
257
- - ✅ Filters words against `assets/filter.txt` blocklist
258
- - Displays dialog with count and list of removed words
 
 
 
 
 
 
 
 
 
259
 
260
  ### v0.2.1
261
- **Daily and Weekly Leaderboards Improved**
262
- - Settings-based leaderboard separation (unique leaderboards per settings combo)
263
- - Folder-based discovery system (no index.json)
264
- - Top 25 displayed entries per leaderboard
265
- - Four-tab leaderboard page (Today, Daily, Weekly, History)
266
- - Automatic score qualification and submission
267
- - Query parameter filtering for direct links (`?gidd=`, `?gidw=`)
268
- - Integration with challenge mode (source_challenge_id tracking)
269
- - Unified JSON format with entry_type field (daily/weekly/challenge)
270
- - Period-based organization: daily (YYYY-MM-DD), weekly (YYYY-Www)
271
- - Enhanced storage.py with folder listing capabilities
272
- - Updated scoring tiers with "Legendary" (45+)
273
- - Settings page planned (move from sidebar, OAuth login required)
274
 
275
  ### v0.1.1
276
- - Enhanced AI word generation with intelligent word saving
277
- - Automatic retry mechanism for insufficient word counts (up to 3 retries)
278
- - 1000-word file size limit to prevent dictionary bloat
279
- - Improved new word detection (separates existing vs. new words before saving)
280
- - Better HF Space API integration with graceful fallback to local models
281
- - Additional word generation when initial pass doesn't meet MIN_REQUIRED threshold
282
- - Enhanced logging for word generation pipeline visibility
283
 
284
  ### v0.1.0
285
- - AI word generation functionality added
286
- - Topic-based custom word list creation
287
- - Dual generation modes (HF Space API + local transformers)
288
- - Utility modules integration (storage, file_utils, constants)
289
- - Documentation synchronized across all files
290
 
291
  ### v0.0.8
292
  - remove background animation
 
21
 
22
  # Wrdler
23
 
24
+ Version 0.2.9
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.9
33
 
34
  ## Key Differences from BattleWords
35
 
 
54
  ### Audio & Visuals
55
  - Ocean-themed gradient background with wave animations
56
  - Background music system (toggleable with volume control)
57
+ - Sound effects for hits, misses, correct/incorrect guesses (enabled by default)
58
  - Responsive UI built with Streamlit
59
 
60
  ### AI Word Generation
 
71
  - **Fallback support**: Gracefully uses dictionary words if AI generation fails
72
  - **Guaranteed distribution**: Ensures exactly 25 words each of lengths 4, 5, and 6
73
 
74
+ ### Customization & Settings Management
75
  - Multiple word lists (classic, fourth_grade, wordlist)
76
  - Dedicated Settings page (`⚙️ Settings` in footer) for wordlist selection, game mode, grid options, and audio
77
  - Audio volume controls (music and effects separate)
78
+ - Enhanced settings management: create, update, rename, and delete settings files
79
+ - Split "Save Settings" into "Create Settings" and "Update Settings" actions
80
+ - Improved settings loading and user feedback
81
+ - Default sound effects enabled in settings
82
+ - New default configuration: `classic-classic-full_sound_free_letters.json`
83
+ - Deprecated configuration removed: `classic-classic-2.json`
84
 
85
  ### ✅ Challenge Mode
86
  - **Shareable challenge links** via short URLs (`?game_id=<sid>`)
 
130
  - **Dockerfile-based deployment** supported for Hugging Face Spaces and other container platforms
131
  - **Environment variables** for Challenge Mode (HF_API_TOKEN, HF_REPO_ID, SPACE_NAME)
132
  - Works offline without HF credentials (Challenge Mode features disabled gracefully)
133
+ - Footer navigation updated to prevent reloading active pages
134
 
135
  ### Progressive Web App (PWA)
136
  - Installable on desktop and mobile from your browser
 
225
  - `logic.py` – game mechanics (reveal, guess, scoring, free letters)
226
  - `ui.py` – Streamlit UI composition (with query-param routing and Settings/Leaderboard pages)
227
  - `oauth.py` – HuggingFace OAuth utilities (NEW)
228
+ - `settings_page.py` – Settings page UI (enhanced: create, update, rename, delete settings)
229
  - `leaderboard.py` – Daily/weekly leaderboard system (v0.2.1)
230
  - `leaderboard_page.py` – Leaderboard UI page (v0.2.1)
231
  - `game_storage.py` – Hugging Face remote storage integration and challenge sharing
 
251
 
252
  ## Changelog
253
 
254
+ ### v0.2.9 (Current) ✅
255
+ - Bump version to 0.2.9
256
+ - Enhanced settings management: create, update, rename, and delete settings files
257
+ - Split "Save Settings" into "Create Settings" and "Update Settings" actions
258
+ - Improved settings loading and user feedback
259
+ - Default sound effects enabled in settings
260
+ - Added `classic-classic-full_sound_free_letters.json`
261
+ - Removed deprecated `classic-classic-2.json`
262
+ - Footer navigation updated to prevent reloading active pages
263
+ - Improved maintainability and usability
264
+
265
+ ### v0.2.8 (Settings Page & Local Settings Persistence)
266
+ - All game settings moved from sidebar to dedicated Settings page (`?page=settings`)
267
+ - Settings accessible from footer navigation (`⚙️ Settings` link)
268
+ - Local JSON-based settings persistence in `wrdler/settings/`
269
+ - Latest settings automatically loaded on startup
270
+
271
+ ### v0.2.7
272
+ - Added "Filter Wordlist" button to sidebar
273
+ - Filters words against `assets/filter.txt` blocklist
274
+ - Displays dialog with count and list of removed words
275
 
276
  ### v0.2.1
277
+ - Daily and Weekly Leaderboards Improved
278
+ - Settings-based leaderboard separation (unique leaderboards per settings combo)
279
+ - Folder-based discovery system (no index.json)
280
+ - Top 25 displayed entries per leaderboard
281
+ - Four-tab leaderboard page (Today, Daily, Weekly, History)
282
+ - Automatic score qualification and submission
283
+ - Query parameter filtering for direct links (`?gidd=`, `?gidw=`)
284
+ - Integration with challenge mode (source_challenge_id tracking)
285
+ - Unified JSON format with entry_type field (daily/weekly/challenge)
286
+ - Period-based organization: daily (YYYY-MM-DD), weekly (YYYY-Www)
287
+ - Enhanced storage.py with folder listing capabilities
288
+ - Updated scoring tiers with "Legendary" (45+)
289
+ - Settings page planned (move from sidebar, OAuth login required)
290
 
291
  ### v0.1.1
292
+ - Enhanced AI word generation with intelligent word saving
293
+ - Automatic retry mechanism for insufficient word counts (up to 3 retries)
294
+ - 1000-word file size limit to prevent dictionary bloat
295
+ - Improved new word detection (separates existing vs. new words before saving)
296
+ - Better HF Space API integration with graceful fallback to local models
297
+ - Additional word generation when initial pass doesn't meet MIN_REQUIRED threshold
298
+ - Enhanced logging for word generation pipeline visibility
299
 
300
  ### v0.1.0
301
+ - AI word generation functionality added
302
+ - Topic-based custom word list creation
303
+ - Dual generation modes (HF Space API + local transformers)
304
+ - Utility modules integration (storage, file_utils, constants)
305
+ - Documentation synchronized across all files
306
 
307
  ### v0.0.8
308
  - remove background animation
specs/specs.md CHANGED
@@ -1,8 +1,8 @@
1
  # Wrdler Specifications
2
 
3
- **Version:** 0.2.8
4
 
5
- **Status:** Production Ready - Leaderboards & Settings Page Implemented
6
  **Last Updated:** 2025-12-09
7
 
8
  ## Overview
@@ -63,42 +63,51 @@ Wrdler is a Python/Streamlit vocabulary puzzle game based on BattleWords, but wi
63
  - ✅ 10 incorrect guess limit per game
64
  - ✅ Two game modes: Classic (chain guesses) and Too Easy (single guess per reveal)
65
 
66
- ## Implemented Features (v0.2.4)
67
-
68
- ### Word List Management (v0.2.4)
69
- - **Filter Wordlist:** Remove words found in `assets/filter.txt` from the selected word list
70
- - **Sort Wordlist:** Sort words by length and alphabetically
71
- - **Feedback:** Dialog showing count and list of removed words
72
-
73
- ### AI Word Generation (v0.1.0+)
74
- - **Topic-Based Generation:** Create custom word lists for any theme using AI
75
- - **Dual Generation Modes:**
 
 
 
 
 
 
 
 
 
76
  - HF Space API (primary): Uses Hugging Face Space when `USE_HF_WORDS=true`
77
  - Local transformers (fallback): Falls back to local models if HF unavailable
78
- - **Intelligent Word Management:**
79
  - Smart detection separates existing dictionary words from new AI-generated words
80
  - Only saves new words to prevent duplicates in word files
81
  - Automatic retry mechanism (up to 3 attempts) if insufficient words generated
82
  - 1000-word file size limit prevents dictionary bloat
83
  - Auto-sorted by length then alphabetically
84
- - **Guaranteed Distribution:** Ensures exactly 25 words each of lengths 4, 5, and 6
85
- - **Graceful Fallback:** Uses dictionary words if AI generation fails
86
- - **Enhanced Logging:** Detailed pipeline visibility for debugging
87
 
88
  ### Challenge Mode
89
- - **Game ID Sharing:** Each puzzle generates a shareable link with `?game_id=<sid>` to challenge others with the same word list
90
- - **Remote Storage:** Game results and leaderboards stored in Hugging Face dataset repos
91
- - **Leaderboards:** Multi-user leaderboards sorted by score (descending) then time (ascending)
92
- - **Word List Difficulty:** Calculated and displayed for each challenge
93
- - **Top 5 Display:** Leaderboard banner shows top 5 players
94
- - **Optional Sharing:** "Show Challenge Share Links" toggle (default OFF) controls URL visibility
95
 
96
  ### Leaderboard System (v0.2.1) ✅ IMPLEMENTED
97
  Wrdler features a comprehensive daily and weekly leaderboard system:
98
 
99
  **Core Features:**
100
- - **Daily Leaderboards:** Top 20 scores for each day (resets UTC midnight)
101
- - **Weekly Leaderboards:** Top 20 scores for each ISO week (resets Monday UTC 00:00)
102
  - **Settings-Based Separation:** Each unique combination of game-affecting settings creates a separate leaderboard:
103
  - `game_mode` (classic, easy, too easy)
104
  - `wordlist_source` (classic.txt, fourth_grade.txt, etc.)
@@ -157,14 +166,15 @@ HF_REPO_ID/games/
157
  **Access:** 'Leaderboard' link in the footer navigation at the bottom of the page
158
 
159
  ### PWA Support
160
- - **PWA Installation:** App is installable as a Progressive Web App on desktop and mobile
161
  - Added `service worker` and `manifest.json`
162
  - Basic offline caching of static assets
163
  - INSTALL_GUIDE.md added with platform-specific install steps
164
  - No gameplay logic changes
165
 
166
- ### Settings Page (v0.2.8)
167
  - All game settings moved from sidebar to a dedicated settings page (`?page=settings`)
168
  - Accessible via the footer navigation (`⚙️ Settings` link)
169
  - Controls game mode, word list selection, grid options (spacer, grid ticks), and audio (music and sound effects)
170
  - Settings are persisted to JSON files in `wrdler/settings/` and the latest settings are loaded on app startup
 
 
1
  # Wrdler Specifications
2
 
3
+ **Version:** 0.2.9
4
 
5
+ **Status:** Production Ready - Leaderboards & Enhanced Settings Page Implemented
6
  **Last Updated:** 2025-12-09
7
 
8
  ## Overview
 
63
  - ✅ 10 incorrect guess limit per game
64
  - ✅ Two game modes: Classic (chain guesses) and Too Easy (single guess per reveal)
65
 
66
+ ## Implemented Features (v0.2.9)
67
+
68
+ ### Settings Management (v0.2.9)
69
+ - Enhanced settings management: create, update, rename, and delete settings files
70
+ - Split "Save Settings" into "Create Settings" and "Update Settings" actions
71
+ - Improved settings loading and user feedback
72
+ - Default sound effects enabled in settings
73
+ - New default configuration: `classic-classic-full_sound_free_letters.json`
74
+ - Deprecated configuration removed: `classic-classic-2.json`
75
+ - Footer navigation updated to prevent reloading active pages
76
+
77
+ ### Word List Management
78
+ - **Filter Wordlist:** Remove words found in `assets/filter.txt` from the selected word list
79
+ - **Sort Wordlist:** Sort words by length and alphabetically
80
+ - **Feedback:** Dialog showing count and list of removed words
81
+
82
+ ### AI Word Generation
83
+ - **Topic-Based Generation:** Create custom word lists for any theme using AI
84
+ - **Dual Generation Modes:**
85
  - HF Space API (primary): Uses Hugging Face Space when `USE_HF_WORDS=true`
86
  - Local transformers (fallback): Falls back to local models if HF unavailable
87
+ - **Intelligent Word Management:**
88
  - Smart detection separates existing dictionary words from new AI-generated words
89
  - Only saves new words to prevent duplicates in word files
90
  - Automatic retry mechanism (up to 3 attempts) if insufficient words generated
91
  - 1000-word file size limit prevents dictionary bloat
92
  - Auto-sorted by length then alphabetically
93
+ - **Guaranteed Distribution:** Ensures exactly 25 words each of lengths 4, 5, and 6
94
+ - **Graceful Fallback:** Uses dictionary words if AI generation fails
95
+ - **Enhanced Logging:** Detailed pipeline visibility for debugging
96
 
97
  ### Challenge Mode
98
+ - **Game ID Sharing:** Each puzzle generates a shareable link with `?game_id=<sid>` to challenge others with the same word list
99
+ - **Remote Storage:** Game results and leaderboards stored in Hugging Face dataset repos
100
+ - **Leaderboards:** Multi-user leaderboards sorted by score (descending) then time (ascending)
101
+ - **Word List Difficulty:** Calculated and displayed for each challenge
102
+ - **Top 5 Display:** Leaderboard banner shows top 5 players
103
+ - **Optional Sharing:** "Show Challenge Share Links" toggle (default OFF) controls URL visibility
104
 
105
  ### Leaderboard System (v0.2.1) ✅ IMPLEMENTED
106
  Wrdler features a comprehensive daily and weekly leaderboard system:
107
 
108
  **Core Features:**
109
+ - **Daily Leaderboards:** Top 25 scores for each day (resets UTC midnight)
110
+ - **Weekly Leaderboards:** Top 25 scores for each ISO week (resets Monday UTC 00:00)
111
  - **Settings-Based Separation:** Each unique combination of game-affecting settings creates a separate leaderboard:
112
  - `game_mode` (classic, easy, too easy)
113
  - `wordlist_source` (classic.txt, fourth_grade.txt, etc.)
 
166
  **Access:** 'Leaderboard' link in the footer navigation at the bottom of the page
167
 
168
  ### PWA Support
169
+ - **PWA Installation:** App is installable as a Progressive Web App on desktop and mobile
170
  - Added `service worker` and `manifest.json`
171
  - Basic offline caching of static assets
172
  - INSTALL_GUIDE.md added with platform-specific install steps
173
  - No gameplay logic changes
174
 
175
+ ### Settings Page (v0.2.8+)
176
  - All game settings moved from sidebar to a dedicated settings page (`?page=settings`)
177
  - Accessible via the footer navigation (`⚙️ Settings` link)
178
  - Controls game mode, word list selection, grid options (spacer, grid ticks), and audio (music and sound effects)
179
  - Settings are persisted to JSON files in `wrdler/settings/` and the latest settings are loaded on app startup
180
+ - Enhanced management: create, update, rename, delete settings files
wrdler/__init__.py CHANGED
@@ -9,5 +9,5 @@ Key differences from BattleWords:
9
  - Daily and weekly leaderboards
10
  """
11
 
12
- __version__ = "0.2.8"
13
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
 
9
  - Daily and weekly leaderboards
10
  """
11
 
12
+ __version__ = "0.2.9"
13
  __all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
wrdler/local_storage.py CHANGED
@@ -219,18 +219,10 @@ def generate_settings_filename(settings: dict) -> str:
219
 
220
  return f"{safe_mode}-{safe_wordlist}-{spacer}.json"
221
 
222
- def save_settings_configuration(settings: dict) -> str:
223
  """
224
- Saves the provided settings dictionary to a JSON file in wrdler/settings/.
225
- Ensures compatibility with leaderboard settings format.
226
- Also writes a copy to settings/settings.json as the canonical latest settings.
227
- Returns the filename used.
228
  """
229
- ensure_settings_dir()
230
- filename = generate_settings_filename(settings)
231
- filepath = os.path.join(SETTINGS_DIR, filename)
232
-
233
- # Transform to compatible format
234
  compatible_settings = settings.copy()
235
 
236
  # Normalize music track path to be relative for portability
@@ -263,17 +255,60 @@ def save_settings_configuration(settings: dict) -> str:
263
  compatible_settings["victory_sound"] = _to_relative_music_path(compatible_settings["victory_sound"])
264
  if "defeat_sound" in compatible_settings:
265
  compatible_settings["defeat_sound"] = _to_relative_music_path(compatible_settings["defeat_sound"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
  with open(filepath, "w", encoding="utf-8") as f:
268
  json.dump(compatible_settings, f, indent=2, ensure_ascii=False)
269
 
270
  # --- Write to canonical settings.json ---
271
- canonical_path = os.path.join(SETTINGS_DIR, "settings.json")
272
- with open(canonical_path, "w", encoding="utf-8") as f:
273
- json.dump(compatible_settings, f, indent=2, ensure_ascii=False)
274
 
275
  return filename
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  def load_settings_configuration(filename: str) -> dict:
278
  """
279
  Loads settings from a specific file in wrdler/settings/.
 
219
 
220
  return f"{safe_mode}-{safe_wordlist}-{spacer}.json"
221
 
222
+ def _prepare_settings_for_save(settings: dict) -> dict:
223
  """
224
+ Helper to transform local settings into the compatible storage format.
 
 
 
225
  """
 
 
 
 
 
226
  compatible_settings = settings.copy()
227
 
228
  # Normalize music track path to be relative for portability
 
255
  compatible_settings["victory_sound"] = _to_relative_music_path(compatible_settings["victory_sound"])
256
  if "defeat_sound" in compatible_settings:
257
  compatible_settings["defeat_sound"] = _to_relative_music_path(compatible_settings["defeat_sound"])
258
+
259
+ return compatible_settings
260
+
261
+ def save_active_settings(settings: dict) -> None:
262
+ """
263
+ Saves the provided settings dictionary to settings/settings.json only.
264
+ This sets the current active settings without creating or updating a named configuration file.
265
+ """
266
+ ensure_settings_dir()
267
+ compatible_settings = _prepare_settings_for_save(settings)
268
+ canonical_path = os.path.join(SETTINGS_DIR, "settings.json")
269
+ with open(canonical_path, "w", encoding="utf-8") as f:
270
+ json.dump(compatible_settings, f, indent=2, ensure_ascii=False)
271
+
272
+ def save_settings_configuration(settings: dict) -> str:
273
+ """
274
+ Saves the provided settings dictionary to a JSON file in wrdler/settings/.
275
+ Ensures compatibility with leaderboard settings format.
276
+ Also writes a copy to settings/settings.json as the canonical latest settings.
277
+ Returns the filename used.
278
+ """
279
+ ensure_settings_dir()
280
+ filename = generate_settings_filename(settings)
281
+ filepath = os.path.join(SETTINGS_DIR, filename)
282
+
283
+ compatible_settings = _prepare_settings_for_save(settings)
284
 
285
  with open(filepath, "w", encoding="utf-8") as f:
286
  json.dump(compatible_settings, f, indent=2, ensure_ascii=False)
287
 
288
  # --- Write to canonical settings.json ---
289
+ save_active_settings(settings)
 
 
290
 
291
  return filename
292
 
293
+ def edit_settings_configuration(filename: str, settings: dict) -> str:
294
+ """
295
+ Overwrites the specified settings file in wrdler/settings/ with the provided settings dict.
296
+ Also updates settings/settings.json as the canonical latest settings.
297
+ Returns the filename used.
298
+ """
299
+ ensure_settings_dir()
300
+ filepath = os.path.join(SETTINGS_DIR, filename)
301
+
302
+ compatible_settings = _prepare_settings_for_save(settings)
303
+
304
+ with open(filepath, "w", encoding="utf-8") as f:
305
+ json.dump(compatible_settings, f, indent=2, ensure_ascii=False)
306
+
307
+ # --- Write to canonical settings.json ---
308
+ save_active_settings(settings)
309
+
310
+ return filename
311
+
312
  def load_settings_configuration(filename: str) -> dict:
313
  """
314
  Loads settings from a specific file in wrdler/settings/.
wrdler/settings/classic-classic-1.json CHANGED
@@ -11,7 +11,7 @@
11
  "music_enabled": false,
12
  "music_volume": 15,
13
  "effects_volume": 25,
14
- "enable_sound_effects": false,
15
  "music_track_path": "background.mp3",
16
  "wordlist_source": "classic.txt",
17
  "puzzle_options": {
 
11
  "music_enabled": false,
12
  "music_volume": 15,
13
  "effects_volume": 25,
14
+ "enable_sound_effects": true,
15
  "music_track_path": "background.mp3",
16
  "wordlist_source": "classic.txt",
17
  "puzzle_options": {
wrdler/settings/{classic-classic-2.json → classic-classic-full_sound_free_letters.json} RENAMED
File without changes
wrdler/settings/settings.json CHANGED
@@ -11,7 +11,7 @@
11
  "music_enabled": false,
12
  "music_volume": 15,
13
  "effects_volume": 25,
14
- "enable_sound_effects": false,
15
  "music_track_path": "background.mp3",
16
  "wordlist_source": "classic.txt",
17
  "puzzle_options": {
 
11
  "music_enabled": false,
12
  "music_volume": 15,
13
  "effects_volume": 25,
14
+ "enable_sound_effects": true,
15
  "music_track_path": "background.mp3",
16
  "wordlist_source": "classic.txt",
17
  "puzzle_options": {
wrdler/settings_page.py CHANGED
@@ -5,7 +5,8 @@ from .word_loader import get_wordlist_files, get_wordlist_info
5
  from .generator import sort_word_file, filter_word_file
6
  from .audio import get_audio_tracks, _inject_audio_control_sync
7
  from .version_info import versions_html
8
- from .local_storage import save_settings_configuration, load_settings_configuration, list_settings_configurations, load_latest_settings
 
9
 
10
  # Keys that should persist across sessions when saving settings
11
  _PERSISTED_SETTING_KEYS = (
@@ -117,16 +118,19 @@ def _filter_wordlist(filename):
117
  st.info(f"No words removed from {filename}.")
118
 
119
  def render_settings_page(new_game_callback):
120
- # --- Always load settings.json on entry and apply to session state ---
121
- latest_settings = load_latest_settings()
122
- rerun_needed = False
123
- if latest_settings:
124
- for key, value in latest_settings.items():
125
- if st.session_state.get(key) != value:
126
- st.session_state[key] = value
127
- rerun_needed = True
128
- if rerun_needed:
129
- st.rerun()
 
 
 
130
 
131
  st.header("SETTINGS")
132
 
@@ -177,11 +181,46 @@ def render_settings_page(new_game_callback):
177
  st.session_state["_settings_snapshot"] = loaded_settings
178
  st.session_state["_settings_apply_pending"] = True
179
  st.session_state["_settings_saved_notice"] = f"Loaded configuration: {selected_config}"
180
- # Overwrite settings.json with the loaded configuration
181
- save_settings_configuration(loaded_settings)
 
 
 
182
  st.rerun()
183
  else:
184
  st.caption("No saved configurations found.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  st.markdown("---")
186
 
187
  st.header("Game Mode")
@@ -461,36 +500,78 @@ def render_settings_page(new_game_callback):
461
  else:
462
  st.caption("Place .mp3 files in wrdler/assets/audio/music to enable music.")
463
 
464
- # --- Save Settings button (must be outside any form or column context) ---
465
  settings_snapshot = {key: st.session_state.get(key) for key in _PERSISTED_SETTING_KEYS}
466
- if st.button("Save Settings", key="save_settings_btn", help="Apply settings and start a new game with them"):
467
- # Check if settings file already exists
468
- existing_file = _settings_file_exists(settings_snapshot)
469
- if not existing_file:
470
- # Generate base name (e.g., classic-classic)
471
- mode = settings_snapshot.get("game_mode", "classic")
472
- if settings_snapshot.get("use_ai_wordlist"):
473
- topic = settings_snapshot.get("ai_topic", "English").strip().replace(" ", "_")
474
- topic = "".join(c for c in topic if c.isalnum() or c in ('-', '_'))
475
- wordlist_part = f"ai-{topic}"
476
- else:
477
- w_file = settings_snapshot.get("selected_wordlist", "classic.txt")
478
- wordlist_part = os.path.splitext(os.path.basename(w_file))[0]
479
- base_name = f"{mode}-{wordlist_part}"
480
- filename = _get_next_settings_filename(base_name)
481
- # Save to new file and always update settings.json
482
- save_settings_configuration(settings_snapshot)
483
- # Rename the just-written file to the new unique filename
484
- os.rename(os.path.join(SETTINGS_DIR, save_settings_configuration(settings_snapshot)), os.path.join(SETTINGS_DIR, filename))
485
- st.session_state["_settings_saved_notice"] = f"Settings saved to {filename}!"
486
  else:
487
- # Always update settings.json
488
- save_settings_configuration(settings_snapshot)
489
- st.session_state["_settings_saved_notice"] = f"Settings already exists as {existing_file}. settings.json updated."
490
- st.session_state["_settings_snapshot"] = settings_snapshot
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
  st.session_state["_settings_apply_pending"] = True
492
  st.session_state.pop("_settings_save_error", None)
493
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
 
495
  _inject_audio_control_sync()
496
  st.markdown(versions_html(), unsafe_allow_html=True)
 
5
  from .generator import sort_word_file, filter_word_file
6
  from .audio import get_audio_tracks, _inject_audio_control_sync
7
  from .version_info import versions_html
8
+ from .local_storage import save_settings_configuration, load_settings_configuration, list_settings_configurations, load_latest_settings, save_active_settings
9
+ import shutil
10
 
11
  # Keys that should persist across sessions when saving settings
12
  _PERSISTED_SETTING_KEYS = (
 
118
  st.info(f"No words removed from {filename}.")
119
 
120
  def render_settings_page(new_game_callback):
121
+ # Only load settings.json if forced or on first load
122
+ if st.session_state.get("_settings_force_reload", False) or not st.session_state.get("_settings_first_load", False):
123
+ latest_settings = load_latest_settings()
124
+ rerun_needed = False
125
+ if latest_settings:
126
+ for key, value in latest_settings.items():
127
+ if st.session_state.get(key) != value:
128
+ st.session_state[key] = value
129
+ rerun_needed = True
130
+ st.session_state["_settings_first_load"] = True
131
+ st.session_state["_settings_force_reload"] = False
132
+ if rerun_needed:
133
+ st.rerun()
134
 
135
  st.header("SETTINGS")
136
 
 
181
  st.session_state["_settings_snapshot"] = loaded_settings
182
  st.session_state["_settings_apply_pending"] = True
183
  st.session_state["_settings_saved_notice"] = f"Loaded configuration: {selected_config}"
184
+ # Update settings.json with the loaded configuration (active settings)
185
+ save_active_settings(loaded_settings)
186
+ st.session_state["_settings_force_reload"] = True
187
+ st.session_state["_settings_loaded_file"] = selected_config # Track loaded file for Edit/Delete/Rename
188
+ st.session_state["rename_settings_text"] = selected_config
189
  st.rerun()
190
  else:
191
  st.caption("No saved configurations found.")
192
+ # --- Rename and Delete Section ---
193
+ selected_file = st.session_state.get("_settings_loaded_file")
194
+ if not selected_file:
195
+ selected_file = "settings.json" if os.path.exists(os.path.join(SETTINGS_DIR, "settings.json")) else None
196
+ if selected_file and not st.session_state.get("rename_settings_text"):
197
+ st.session_state["rename_settings_text"] = selected_file
198
+ st.markdown("**Current Settings File:**")
199
+ file_to_rename = st.text_input("File Name", value=st.session_state.get("rename_settings_text", ""), key="rename_settings_text")
200
+ col_rename, col_delete = st.columns(2)
201
+ with col_rename:
202
+ rename_disabled = not selected_file or selected_file == "Select..." or not file_to_rename or file_to_rename == selected_file
203
+ if st.button("Rename Settings", key="rename_settings_btn", disabled=rename_disabled):
204
+ src = os.path.join(SETTINGS_DIR, selected_file)
205
+ dst = os.path.join(SETTINGS_DIR, file_to_rename)
206
+ if os.path.exists(src) and not os.path.exists(dst):
207
+ try:
208
+ shutil.move(src, dst)
209
+ st.session_state["_settings_saved_notice"] = f"Renamed {selected_file} to {file_to_rename}."
210
+ st.session_state["_settings_loaded_file"] = file_to_rename
211
+ except Exception as e:
212
+ st.session_state["_settings_save_error"] = f"Rename failed: {e}"
213
+ st.rerun()
214
+ with col_delete:
215
+ delete_disabled = not selected_file or selected_file == "Select..." or selected_file == "settings.json"
216
+ if st.button("Delete Settings", key="delete_settings_btn", disabled=delete_disabled):
217
+ try:
218
+ os.remove(os.path.join(SETTINGS_DIR, selected_file))
219
+ st.session_state["_settings_saved_notice"] = f"Deleted {selected_file}."
220
+ st.session_state["_settings_loaded_file"] = None
221
+ except Exception as e:
222
+ st.session_state["_settings_save_error"] = f"Delete failed: {e}"
223
+ st.rerun()
224
  st.markdown("---")
225
 
226
  st.header("Game Mode")
 
500
  else:
501
  st.caption("Place .mp3 files in wrdler/assets/audio/music to enable music.")
502
 
503
+ # --- Settings buttons (Create/Update) ---
504
  settings_snapshot = {key: st.session_state.get(key) for key in _PERSISTED_SETTING_KEYS}
505
+ existing_file = _settings_file_exists(settings_snapshot)
506
+
507
+ def _create_settings_callback(snapshot):
508
+ mode = snapshot.get("game_mode", "classic")
509
+ if snapshot.get("use_ai_wordlist"):
510
+ topic = snapshot.get("ai_topic", "English").strip().replace(" ", "_")
511
+ topic = "".join(c for c in topic if c.isalnum() or c in ('-', '_'))
512
+ wordlist_part = f"ai-{topic}"
 
 
 
 
 
 
 
 
 
 
 
 
513
  else:
514
+ w_file = snapshot.get("selected_wordlist", "classic.txt")
515
+ wordlist_part = os.path.splitext(os.path.basename(w_file))[0]
516
+ base_name = f"{mode}-{wordlist_part}"
517
+ filename = _get_next_settings_filename(base_name)
518
+
519
+ new_filename = save_settings_configuration(snapshot)
520
+ if new_filename != filename:
521
+ src = os.path.join(SETTINGS_DIR, new_filename)
522
+ dst = os.path.join(SETTINGS_DIR, filename)
523
+ try:
524
+ if os.path.exists(src) and not os.path.exists(dst):
525
+ with open(src, "r", encoding="utf-8") as fsrc, open(dst, "w", encoding="utf-8") as fdst:
526
+ fdst.write(fsrc.read())
527
+ except Exception:
528
+ pass
529
+
530
+ st.session_state["_settings_saved_notice"] = f"Settings created as {filename}!"
531
+ st.session_state["_settings_snapshot"] = snapshot
532
  st.session_state["_settings_apply_pending"] = True
533
  st.session_state.pop("_settings_save_error", None)
534
+ st.session_state["_settings_force_reload"] = True
535
+ st.session_state["_settings_loaded_file"] = filename
536
+ st.session_state["rename_settings_text"] = filename
537
+
538
+ def _update_settings_callback(file_name, snapshot):
539
+ from .local_storage import edit_settings_configuration
540
+ edit_settings_configuration(file_name, snapshot)
541
+ st.session_state["_settings_saved_notice"] = f"Settings updated in {file_name}!"
542
+ st.session_state["_settings_snapshot"] = snapshot
543
+ st.session_state["_settings_apply_pending"] = True
544
+ st.session_state.pop("_settings_save_error", None)
545
+ st.session_state["_settings_force_reload"] = True
546
+ st.session_state["_settings_loaded_file"] = file_name
547
+ st.session_state["rename_settings_text"] = file_name
548
+
549
+ col_create, col_update = st.columns(2)
550
+ with col_create:
551
+ st.button(
552
+ "Create Settings",
553
+ key="create_settings_btn",
554
+ help="Create a new settings file and set as latest",
555
+ on_click=_create_settings_callback,
556
+ args=(settings_snapshot,)
557
+ )
558
+
559
+ with col_update:
560
+ # Use loaded file for update if available, else fallback to config match
561
+ update_file = st.session_state.get("_settings_loaded_file") or existing_file
562
+ update_disabled = not update_file or update_file == "Select..."
563
+
564
+ st.button(
565
+ "Update Settings",
566
+ key="update_settings_btn",
567
+ help="Update the existing settings file and set as latest",
568
+ disabled=update_disabled,
569
+ on_click=_update_settings_callback,
570
+ args=(update_file, settings_snapshot)
571
+ )
572
+
573
+ if update_disabled:
574
+ st.caption("No matching settings file to update.")
575
 
576
  _inject_audio_control_sync()
577
  st.markdown(versions_html(), unsafe_allow_html=True)
wrdler/ui.py CHANGED
@@ -2278,13 +2278,13 @@ def _render_footer(current_page: str = "play"):
2278
  }}
2279
  }}
2280
  </style>
2281
- <div class="bw-footer">
2282
- <nav class="bw-footer-nav">
2283
- <a href="{leaderboard_url}" title="View Leaderboards" target="_self" class="{leaderboard_active}">🏆 Leaderboard</a>
2284
- <a href="{play_url}" title="Play Wrdler" target="_self" class="{play_active}">🎮 Play</a>
2285
- <a href="{settings_url}" title="Settings" target="_self" class="{settings_active}">⚙️ Settings</a>
2286
- </nav>
2287
- </div>
2288
  """,
2289
  unsafe_allow_html=True,
2290
  )
 
2278
  }}
2279
  }}
2280
  </style>
2281
+ <div class="bw-footer">
2282
+ <nav class="bw-footer-nav">
2283
+ <a href="{leaderboard_url if not leaderboard_active else '#'}" title="View Leaderboards" target="_self" class="{leaderboard_active}">🏆 Leaderboard</a>
2284
+ <a href="{play_url if not play_active else '#'}" title="Play Wrdler" target="_self" class="{play_active}">🎮 Play</a>
2285
+ <a href="{settings_url if not settings_active else '#'}" title="Settings" target="_self" class="{settings_active}">⚙️ Settings</a>
2286
+ </nav>
2287
+ </div>
2288
  """,
2289
  unsafe_allow_html=True,
2290
  )