v0.2.9
Browse filesBump 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 +33 -31
- README.md +58 -42
- specs/specs.md +36 -26
- wrdler/__init__.py +1 -1
- wrdler/local_storage.py +48 -13
- wrdler/settings/classic-classic-1.json +1 -1
- wrdler/settings/{classic-classic-2.json → classic-classic-full_sound_free_letters.json} +0 -0
- wrdler/settings/settings.json +1 -1
- wrdler/settings_page.py +120 -39
- wrdler/ui.py +7 -7
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# CLAUDE
|
| 2 |
|
| 3 |
-
Wrdler v0.2.
|
| 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.
|
| 16 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 17 |
**Branch:** AI (working branch)
|
| 18 |
|
| 19 |
-
## Current Features (v0.2.
|
| 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.
|
| 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.
|
| 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
|
| 120 |
-
│ ├── settings_page.py # Settings page UI (
|
| 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
|
| 128 |
│ │ ├── __init__.py # Module exports
|
| 129 |
-
│ │ ├── storage.py # HuggingFace storage & URL shortener
|
| 130 |
│ │ ├── storage.md # Storage module documentation
|
| 131 |
-
│ │ ├── constants.py # Storage-related constants
|
| 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
|
| 141 |
-
├── requirements.txt
|
| 142 |
-
├── uv.lock
|
| 143 |
-
├── Dockerfile
|
| 144 |
-
├── README.md
|
| 145 |
-
├── CLAUDE.md
|
| 146 |
-
├── GAMEPLAY_GUIDE.md
|
| 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
|
| 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
|
| 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` -
|
| 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":
|
| 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 |
|
|
@@ -21,7 +21,7 @@ thumbnail: >-
|
|
| 21 |
|
| 22 |
# Wrdler
|
| 23 |
|
| 24 |
-
Version 0.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,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.
|
| 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 (
|
| 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.
|
| 248 |
-
|
| 249 |
-
-
|
| 250 |
-
-
|
| 251 |
-
-
|
| 252 |
-
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
### v0.2.1
|
| 261 |
-
|
| 262 |
-
-
|
| 263 |
-
-
|
| 264 |
-
-
|
| 265 |
-
-
|
| 266 |
-
-
|
| 267 |
-
-
|
| 268 |
-
-
|
| 269 |
-
-
|
| 270 |
-
-
|
| 271 |
-
-
|
| 272 |
-
-
|
| 273 |
-
-
|
| 274 |
|
| 275 |
### v0.1.1
|
| 276 |
-
-
|
| 277 |
-
-
|
| 278 |
-
-
|
| 279 |
-
-
|
| 280 |
-
-
|
| 281 |
-
-
|
| 282 |
-
-
|
| 283 |
|
| 284 |
### v0.1.0
|
| 285 |
-
-
|
| 286 |
-
-
|
| 287 |
-
-
|
| 288 |
-
-
|
| 289 |
-
-
|
| 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
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
# Wrdler Specifications
|
| 2 |
|
| 3 |
-
**Version:** 0.2.
|
| 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.
|
| 67 |
-
|
| 68 |
-
###
|
| 69 |
-
-
|
| 70 |
-
-
|
| 71 |
-
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
-
|
| 75 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
-
|
| 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 |
-
-
|
| 85 |
-
-
|
| 86 |
-
-
|
| 87 |
|
| 88 |
### Challenge Mode
|
| 89 |
-
-
|
| 90 |
-
-
|
| 91 |
-
-
|
| 92 |
-
-
|
| 93 |
-
-
|
| 94 |
-
-
|
| 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
|
| 101 |
-
- **Weekly Leaderboards:** Top
|
| 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 |
-
-
|
| 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
|
|
@@ -9,5 +9,5 @@ Key differences from BattleWords:
|
|
| 9 |
- Daily and weekly leaderboards
|
| 10 |
"""
|
| 11 |
|
| 12 |
-
__version__ = "0.2.
|
| 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"]
|
|
@@ -219,18 +219,10 @@ def generate_settings_filename(settings: dict) -> str:
|
|
| 219 |
|
| 220 |
return f"{safe_mode}-{safe_wordlist}-{spacer}.json"
|
| 221 |
|
| 222 |
-
def
|
| 223 |
"""
|
| 224 |
-
|
| 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 |
-
|
| 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/.
|
|
@@ -11,7 +11,7 @@
|
|
| 11 |
"music_enabled": false,
|
| 12 |
"music_volume": 15,
|
| 13 |
"effects_volume": 25,
|
| 14 |
-
"enable_sound_effects":
|
| 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": {
|
|
File without changes
|
|
@@ -11,7 +11,7 @@
|
|
| 11 |
"music_enabled": false,
|
| 12 |
"music_volume": 15,
|
| 13 |
"effects_volume": 25,
|
| 14 |
-
"enable_sound_effects":
|
| 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": {
|
|
@@ -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 |
-
#
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
st.session_state
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
st.
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
# ---
|
| 465 |
settings_snapshot = {key: st.session_state.get(key) for key in _PERSISTED_SETTING_KEYS}
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
if
|
| 473 |
-
|
| 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 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
st.session_state["_settings_apply_pending"] = True
|
| 492 |
st.session_state.pop("_settings_save_error", None)
|
| 493 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
@@ -2278,13 +2278,13 @@ def _render_footer(current_page: str = "play"):
|
|
| 2278 |
}}
|
| 2279 |
}}
|
| 2280 |
</style>
|
| 2281 |
-
|
| 2282 |
-
|
| 2283 |
-
|
| 2284 |
-
|
| 2285 |
-
|
| 2286 |
-
|
| 2287 |
-
|
| 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 |
)
|