Surn commited on
Commit
0860b52
·
1 Parent(s): f055004

Add Gradio UI and AI Topic Generation Features

Browse files

Version updated to 0.1.2 with significant enhancements:
- Implemented Gradio UI as an alternative to Streamlit, featuring neon-styled topic display, settings persistence, and a real-time game timer.
- Added AI Topic Generation for custom word lists, integrated into the settings tab.
- Enhanced CSS styling for Gradio interface and ensured compatibility with Gradio 5.50.
- Introduced settings persistence for features like "Enable Free Letters" and "Show Challenge Links."
- Fixed Streamlit caching issues and improved word list loading.
- Updated documentation to reflect new features and UI options.
- Refactored code for better support of new functionalities.

CLAUDE.md CHANGED
@@ -7,20 +7,37 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, with these k
7
  - **No scope/radar visualization**
8
  - **2 free letter guesses at game start** (all instances of chosen letters are revealed)
9
 
10
- **Current Version:** 0.1.1
11
  **Repository:** https://github.com/Oncorporation/Wrdler.git
12
  **Live Demo:** [DEPLOYMENT_URL_HERE]
13
 
14
  ## Recent Changes
15
 
16
- **v0.1.1 (Current):**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  - ✅ Enhanced AI word generation logic with intelligent word saving
18
  - ✅ Automatic retry mechanism for insufficient word counts (up to 3 retries)
19
- - ✅1000-word file size limit to prevent dictionary bloat
20
  - ✅ Better new word detection (separates existing vs. new words before saving)
21
  - ✅ Improved HF Space API integration with graceful fallback to local models
22
  - ✅ Additional word generation when initial pass doesn't meet MIN_REQUIRED threshold
23
  - ✅ Enhanced logging for word generation pipeline visibility
 
 
 
24
 
25
  **v0.1.0 (Previous):**
26
  - ✅ Version updated to 0.1.0 across all files
@@ -70,7 +87,7 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, with these k
70
  ## Technical Architecture
71
 
72
  ### Technology Stack
73
- - **Framework:** Streamlit 1.51.0
74
  - **Language:** Python 3.12.8 (requires >=3.12, <3.13)
75
  - **Visualization:** Matplotlib (>=3.8)
76
  - **HTTP Requests:** requests (>=2.31.0)
@@ -85,12 +102,14 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, with these k
85
  wrdler/
86
  ├── app.py # Streamlit entry point
87
  ├── wrdler/ # Main package
88
- │ ├── __init__.py # Version: 0.1.0
89
  │ ├── models.py # Data models (Coord, Word, Puzzle, GameState)
90
  │ ├── generator.py # Puzzle generation with deterministic seeding
91
  │ ├── logic.py # Game mechanics (reveal, guess, scoring)
92
  │ ├── ui.py # Streamlit UI
 
93
  │ ├── word_loader.py # Word list management
 
94
  │ ├── audio.py # Background music system
95
  │ ├── sounds.py # Sound effects management
96
  │ ├── generate_sounds.py # Sound generation utilities
@@ -106,6 +125,7 @@ wrdler/
106
  │ ├── classic.txt # Default word list
107
  │ ├── fourth_grade.txt # Elementary word list
108
  │ └── wordlist.txt # Full word list
 
109
  ├── tests/ # Unit tests
110
  │ └── test_sprint6_integration.py # Comprehensive integration tests
111
  ├── specs/ # Documentation
@@ -228,7 +248,7 @@ wrdler/
228
 
229
  ### Development Status
230
 
231
- **Current Version:** 0.1.1 (Complete)
232
  - ✅ All 7 sprints complete
233
  - ✅ 100% test coverage (25/25 tests)
234
  - ✅ AI word generation implemented
@@ -290,10 +310,13 @@ class GameState:
290
  # Install dependencies
291
  uv pip install -r requirements.txt --link-mode=copy
292
 
293
- # Run app
294
  uv run streamlit run app.py
295
  # or
296
  streamlit run app.py
 
 
 
297
  ```
298
 
299
  ### Docker Deployment
@@ -365,6 +388,14 @@ The dataset repository will contain:
365
 
366
  ## Post-v0.0.2 Enhancements
367
 
 
 
 
 
 
 
 
 
368
  ### v0.1.1 (AI Word Generation Enhancement)
369
  - Enhanced AI word generation with intelligent word saving
370
  - Automatic retry mechanism for insufficient word counts (up to 3 retries)
@@ -373,6 +404,9 @@ The dataset repository will contain:
373
  - Better HF Space API integration with fallback to local models
374
  - Additional word generation when MIN_REQUIRED threshold not met
375
  - Enhanced logging for generation pipeline visibility
 
 
 
376
 
377
  ### v0.1.0 (AI Word Generation)
378
  - AI-powered word list generation using Hugging Face Spaces
@@ -427,6 +461,7 @@ The dataset repository will contain:
427
  - ✅ Project uses modern Python features (3.12.8)
428
  - ✅ Requires Python >=3.12, <3.13 per pyproject.toml
429
  - ✅ Heavy use of Streamlit session state for game state management
 
430
  - ✅ Client-side JavaScript for timer updates without page refresh
431
  - ✅ CSS heavily customized for ocean theme aesthetics
432
  - ✅ All file paths should be absolute when working in WSL environment
@@ -438,6 +473,11 @@ The dataset repository will contain:
438
  - ✅ PWA injection via Docker build script (`inject-pwa-head.sh`)
439
  - ✅ AI word generation via `word_loader_ai.py` with Hugging Face integration
440
  - ✅ Utility modules provide reusable functions for storage and file ops
 
 
 
 
 
441
 
442
  ### Key Implementation Details
443
  - **No radar field in Puzzle dataclass** - removed in Sprint 3
@@ -450,6 +490,13 @@ The dataset repository will contain:
450
  - **Incorrect guess limit** - maximum 10 per game
451
  - **AI word generation** - generates 75 words per topic, saves to local files
452
  - **Utility modules** - shared functions from OpenBadge project
 
 
 
 
 
 
 
453
 
454
  ### WSL Environment Python Versions
455
  The development environment is WSL (Windows Subsystem for Linux) with access to both native Linux and Windows Python installations:
@@ -475,6 +522,7 @@ This file (CLAUDE.md) serves as a **living context document** for AI-assisted de
475
  - **[README.md](README.md)** - User-facing documentation, installation guide, and complete changelog
476
  - **[Dockerfile](Dockerfile)** - Container deployment configuration with PWA injection
477
  - **[pyproject.toml](pyproject.toml)** - Python project metadata and dependencies
 
478
 
479
  **When to use each:**
480
  - **specs.md** - Understanding game rules and scoring system
@@ -484,6 +532,7 @@ This file (CLAUDE.md) serves as a **living context document** for AI-assisted de
484
  - **README.md** - Public-facing info, setup instructions, and complete changelog
485
  - **INSTALL_GUIDE.md** - Installing Wrdler as a PWA
486
  - **Dockerfile** - Deployment configuration and container setup
 
487
 
488
  ## Challenge Mode & Remote Storage
489
 
@@ -511,6 +560,7 @@ This file (CLAUDE.md) serves as a **living context document** for AI-assisted de
511
 
512
  From `requirements.txt`:
513
  - streamlit>=1.51.0 (primary framework)
 
514
  - matplotlib>=3.8 (visualization)
515
  - requests>=2.31.0 (HTTP requests)
516
  - huggingface_hub>=0.20.0 (remote storage)
@@ -523,7 +573,8 @@ From `pyproject.toml`:
523
 
524
  ## Version History Summary
525
 
526
- - **v0.1.1** (Current) - Enhanced AI word generation with intelligent saving, retry logic, file size limits
 
527
  - **v0.1.0** (Previous) - AI word generation, utility modules, version bump
528
  - **v0.0.4** - Documentation sync, version update
529
  - **v0.0.2-0.0.3** - All 7 sprints complete, core Wrdler features
@@ -532,6 +583,6 @@ From `pyproject.toml`:
532
 
533
  ---
534
 
535
- **Last Updated:** 2025-01-31
536
- **Current Version:** 0.1.1
537
- **Status:** Production Ready - AI Enhanced ✅
 
7
  - **No scope/radar visualization**
8
  - **2 free letter guesses at game start** (all instances of chosen letters are revealed)
9
 
10
+ **Current Version:** 0.1.2
11
  **Repository:** https://github.com/Oncorporation/Wrdler.git
12
  **Live Demo:** [DEPLOYMENT_URL_HERE]
13
 
14
  ## Recent Changes
15
 
16
+ **v0.1.2 (Current):**
17
+ - ✅ **Gradio UI Topic Display** - Prominent neon-styled badge at top of game area
18
+ - Glassmorphism background with glowing cyan border
19
+ - Pulsing neon animation effect
20
+ - Editable - type new topic and press Enter to generate/switch word lists
21
+ - Auto-matches existing wordlist files or uses input as AI topic
22
+ - ✅ **Settings Persistence** - Enable Free Letters and Show Challenge Links persist across new games
23
+ - ✅ **Timer Implementation** - Real-time game timer using gr.Timer component
24
+ - Timer resets properly on New Game
25
+ - Stops when game is over
26
+ - ✅ **AI Topic Generation UI** - Added to Settings tab with Generate button
27
+ - ✅ **Streamlit Cache Fix** - Conditional caching to avoid warnings in Gradio mode
28
+ - ✅ **Enhanced CSS Styling** - Improved grid container, topic display, and responsive design
29
+
30
+ **v0.1.1 (Previous):**
31
  - ✅ Enhanced AI word generation logic with intelligent word saving
32
  - ✅ Automatic retry mechanism for insufficient word counts (up to 3 retries)
33
+ - ✅ 1000-word file size limit to prevent dictionary bloat
34
  - ✅ Better new word detection (separates existing vs. new words before saving)
35
  - ✅ Improved HF Space API integration with graceful fallback to local models
36
  - ✅ Additional word generation when initial pass doesn't meet MIN_REQUIRED threshold
37
  - ✅ Enhanced logging for word generation pipeline visibility
38
+ - ✅ **Gradio UI implementation** (gradio 5.50) as alternative to Streamlit
39
+ - ✅ Custom CSS styling for Gradio interface (style_wrdler.css)
40
+ - ✅ Dual UI framework support - choose between Streamlit or Gradio
41
 
42
  **v0.1.0 (Previous):**
43
  - ✅ Version updated to 0.1.0 across all files
 
87
  ## Technical Architecture
88
 
89
  ### Technology Stack
90
+ - **Framework:** Streamlit 1.51.0 (primary), Gradio 5.50 (alternative)
91
  - **Language:** Python 3.12.8 (requires >=3.12, <3.13)
92
  - **Visualization:** Matplotlib (>=3.8)
93
  - **HTTP Requests:** requests (>=2.31.0)
 
102
  wrdler/
103
  ├── app.py # Streamlit entry point
104
  ├── wrdler/ # Main package
105
+ │ ├── __init__.py # Version: 0.1.2
106
  │ ├── models.py # Data models (Coord, Word, Puzzle, GameState)
107
  │ ├── generator.py # Puzzle generation with deterministic seeding
108
  │ ├── logic.py # Game mechanics (reveal, guess, scoring)
109
  │ ├── ui.py # Streamlit UI
110
+ │ ├── gradio_ui.py # Gradio UI (alternative interface)
111
  │ ├── word_loader.py # Word list management
112
+ │ ├── word_loader_ai.py # AI word generation with HF Space API
113
  │ ├── audio.py # Background music system
114
  │ ├── sounds.py # Sound effects management
115
  │ ├── generate_sounds.py # Sound generation utilities
 
125
  │ ├── classic.txt # Default word list
126
  │ ├── fourth_grade.txt # Elementary word list
127
  │ └── wordlist.txt # Full word list
128
+ ├── style_wrdler.css # Custom CSS styling for Gradio interface
129
  ├── tests/ # Unit tests
130
  │ └── test_sprint6_integration.py # Comprehensive integration tests
131
  ├── specs/ # Documentation
 
248
 
249
  ### Development Status
250
 
251
+ **Current Version:** 0.1.2 (Complete)
252
  - ✅ All 7 sprints complete
253
  - ✅ 100% test coverage (25/25 tests)
254
  - ✅ AI word generation implemented
 
310
  # Install dependencies
311
  uv pip install -r requirements.txt --link-mode=copy
312
 
313
+ # Run Streamlit app (default)
314
  uv run streamlit run app.py
315
  # or
316
  streamlit run app.py
317
+
318
+ # Run Gradio app (alternative)
319
+ python -m wrdler.gradio_ui
320
  ```
321
 
322
  ### Docker Deployment
 
388
 
389
  ## Post-v0.0.2 Enhancements
390
 
391
+ ### v0.1.2 (Gradio UI Enhancements)
392
+ - Gradio UI Topic Display with neon styling and animations
393
+ - Settings persistence across new games
394
+ - Real-time timer implementation with gr.Timer
395
+ - AI Topic Generation UI in Settings tab
396
+ - Streamlit cache fix for Gradio compatibility
397
+ - Enhanced CSS styling throughout
398
+
399
  ### v0.1.1 (AI Word Generation Enhancement)
400
  - Enhanced AI word generation with intelligent word saving
401
  - Automatic retry mechanism for insufficient word counts (up to 3 retries)
 
404
  - Better HF Space API integration with fallback to local models
405
  - Additional word generation when MIN_REQUIRED threshold not met
406
  - Enhanced logging for generation pipeline visibility
407
+ - **Gradio UI implementation** (gradio 5.50) as alternative interface
408
+ - Custom CSS styling for Gradio (style_wrdler.css)
409
+ - Dual UI framework support (Streamlit + Gradio)
410
 
411
  ### v0.1.0 (AI Word Generation)
412
  - AI-powered word list generation using Hugging Face Spaces
 
461
  - ✅ Project uses modern Python features (3.12.8)
462
  - ✅ Requires Python >=3.12, <3.13 per pyproject.toml
463
  - ✅ Heavy use of Streamlit session state for game state management
464
+ - ✅ Gradio 5.50 state management using gr.State for game persistence
465
  - ✅ Client-side JavaScript for timer updates without page refresh
466
  - ✅ CSS heavily customized for ocean theme aesthetics
467
  - ✅ All file paths should be absolute when working in WSL environment
 
473
  - ✅ PWA injection via Docker build script (`inject-pwa-head.sh`)
474
  - ✅ AI word generation via `word_loader_ai.py` with Hugging Face integration
475
  - ✅ Utility modules provide reusable functions for storage and file ops
476
+ - ✅ **Gradio 5.50 compatibility:** Using modern gr.Button, gr.State, and event handlers
477
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
478
+ - Uses `gr.update()` for component updates
479
+ - Event handlers use `.click()`, `.change()`, `.submit()` methods
480
+ - State management via `gr.State(value=...)` with deep copy for updates
481
 
482
  ### Key Implementation Details
483
  - **No radar field in Puzzle dataclass** - removed in Sprint 3
 
490
  - **Incorrect guess limit** - maximum 10 per game
491
  - **AI word generation** - generates 75 words per topic, saves to local files
492
  - **Utility modules** - shared functions from OpenBadge project
493
+ - **Gradio 5.50 UI implementation**:
494
+ - Modern component API with `gr.Button`, `gr.Textbox`, `gr.HTML`, etc.
495
+ - State updates use `copy.deepcopy()` to trigger Gradio reactivity
496
+ - Event handlers return tuples matching output component order
497
+ - Custom CSS via `style_wrdler.css` file
498
+ - Modal dialogs using `gr.Modal(visible=False)` pattern
499
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
500
 
501
  ### WSL Environment Python Versions
502
  The development environment is WSL (Windows Subsystem for Linux) with access to both native Linux and Windows Python installations:
 
522
  - **[README.md](README.md)** - User-facing documentation, installation guide, and complete changelog
523
  - **[Dockerfile](Dockerfile)** - Container deployment configuration with PWA injection
524
  - **[pyproject.toml](pyproject.toml)** - Python project metadata and dependencies
525
+ - **[Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)** - Official Gradio migration reference (external)
526
 
527
  **When to use each:**
528
  - **specs.md** - Understanding game rules and scoring system
 
532
  - **README.md** - Public-facing info, setup instructions, and complete changelog
533
  - **INSTALL_GUIDE.md** - Installing Wrdler as a PWA
534
  - **Dockerfile** - Deployment configuration and container setup
535
+ - **Gradio Migration Guide** - Gradio 5.x/6.x compatibility and best practices
536
 
537
  ## Challenge Mode & Remote Storage
538
 
 
560
 
561
  From `requirements.txt`:
562
  - streamlit>=1.51.0 (primary framework)
563
+ - gradio>=5.50 (alternative UI framework)
564
  - matplotlib>=3.8 (visualization)
565
  - requests>=2.31.0 (HTTP requests)
566
  - huggingface_hub>=0.20.0 (remote storage)
 
573
 
574
  ## Version History Summary
575
 
576
+ - **v0.1.2** (Current) - Gradio UI Topic Display, settings persistence, timer, enhanced CSS
577
+ - **v0.1.1** (Previous) - Enhanced AI word generation with intelligent saving, retry logic, file size limits
578
  - **v0.1.0** (Previous) - AI word generation, utility modules, version bump
579
  - **v0.0.4** - Documentation sync, version update
580
  - **v0.0.2-0.0.3** - All 7 sprints complete, core Wrdler features
 
583
 
584
  ---
585
 
586
+ **Last Updated:** 2025-11-29
587
+ **Current Version:** 0.1.2
588
+ **Status:** Production Ready - Gradio UI Enhanced ✅
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: Wrdler
3
- emoji: 🧩
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: streamlit
@@ -8,10 +8,13 @@ sdk_version: 1.51.0
8
  python_version: 3.12.8
9
  app_port: 8501
10
  app_file: app.py
 
 
11
  tags:
12
  - game
13
  - vocabulary
14
  - streamlit
 
15
  - education
16
  - ai
17
  short_description: Fast paced word guessing game with AI-generated word lists
@@ -25,6 +28,10 @@ thumbnail: >-
25
 
26
  Wrdler is a vocabulary learning game with a simplified grid and strategic letter guessing. The objective is to discover hidden words on a grid by making smart guesses before all letters are revealed.
27
 
 
 
 
 
28
  ## Key Differences from BattleWords
29
 
30
  - **8x6 grid** (instead of 12x12) with **6 words total** (one per row)
@@ -82,8 +89,13 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
82
 
83
  ### Deployment & Technical
84
  - **Dockerfile-based deployment** supported for Hugging Face Spaces and other container platforms
 
 
 
 
85
  - **Environment variables** for Challenge Mode (HF_API_TOKEN, HF_REPO_ID, SPACE_NAME)
86
  - Works offline without HF credentials (Challenge Mode features disabled gracefully)
 
87
 
88
  ### Progressive Web App (PWA)
89
  - Installable on desktop and mobile from your browser
@@ -121,15 +133,21 @@ When playing a shared challenge (via a `game_id` link), the leaderboard displays
121
 
122
  You can run the app locally using either [uv](https://github.com/astral-sh/uv) or Streamlit directly:
123
 
124
- ```
 
125
  uv run streamlit run app.py
 
 
126
  ```
127
 
128
- or
129
- ```
130
- streamlit run app.py
 
131
  ```
132
 
 
 
133
  ### Dockerfile Deployment (Hugging Face Spaces and more)
134
 
135
  Wrdler supports containerized deployment using a `Dockerfile`. This is the recommended method for deploying to [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces-sdks-docker) or any Docker-compatible environment.
@@ -172,13 +190,17 @@ CRYPTO_PK= # Reserved for future signing
172
  - `wrdler/` – Python package
173
  - `models.py` – data models and types
174
  - `word_loader.py` – word list loading and validation
 
175
  - `generator.py` – word placement logic (8x6, horizontal only)
176
  - `logic.py` – game mechanics (reveal, guess, scoring, free letters)
177
  - `ui.py` – Streamlit UI composition
 
178
  - `game_storage.py` – Hugging Face remote storage integration and challenge sharing
179
  - `local_storage.py` – local JSON storage for results and high scores
180
  - `storage.py` – (legacy) local storage and high scores
181
- - `words/wordlist.txt`candidate words
 
 
182
  - `specs/` – documentation (`specs.md`, `requirements.md`)
183
  - `tests/` – unit tests
184
 
@@ -196,7 +218,18 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
196
 
197
  ## Changelog
198
 
199
- ### v0.1.1 (Current)
 
 
 
 
 
 
 
 
 
 
 
200
  - ✅ Enhanced AI word generation with intelligent word saving
201
  - ✅ Automatic retry mechanism for insufficient word counts (up to 3 retries)
202
  - ✅ 1000-word file size limit to prevent dictionary bloat
@@ -204,6 +237,9 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
204
  - ✅ Better HF Space API integration with graceful fallback to local models
205
  - ✅ Additional word generation when initial pass doesn't meet MIN_REQUIRED threshold
206
  - ✅ Enhanced logging for word generation pipeline visibility
 
 
 
207
 
208
  ### v0.1.0
209
  - ✅ AI word generation functionality added
@@ -244,7 +280,7 @@ Note
244
 
245
  ## Known Issues / TODO
246
 
247
- - Word list loading bug: the app may not select the proper word lists in some environments. Investigate `word_loader.get_wordlist_files()` / `load_word_list()` and sidebar selection persistence to ensure the chosen file is correctly used by the generator.
248
 
249
  ## Development Phases
250
 
@@ -260,10 +296,11 @@ Wrdler is based on BattleWords. BattlewordsTM. All Rights Reserved. All content,
260
 
261
  ## Hugging Face Spaces Configuration
262
 
263
- Wrdler is deployable as a Hugging Face Space. You can use either the YAML config block or a Dockerfile for advanced/custom deployments.
264
 
265
  To configure your Space with the YAML block, add it at the top of your `README.md`:
266
 
 
267
  ```yaml
268
  ---
269
  title: Wrdler
@@ -274,128 +311,415 @@ sdk: streamlit
274
  sdk_version: 1.51.0
275
  python_version: 3.12.8
276
  app_file: app.py
 
277
  tags:
278
  - game
279
  - vocabulary
280
  - streamlit
281
  - education
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  ---
283
  ```
284
 
285
  **Key parameters:**
286
  - `title`, `emoji`, `colorFrom`, `colorTo`: Visuals for your Space.
287
- - `sdk`: Use `streamlit` for Streamlit apps.
288
- - `sdk_version`: Latest supported Streamlit version.
289
- - `python_version`: Python version (default is3.10).
290
- - `app_file`: Entry point for your app.
 
291
  - `tags`: List of descriptive tags.
292
 
293
- **Dependencies:**
294
- Add a `requirements.txt` with your Python dependencies (e.g., `streamlit`, etc.).
 
 
 
 
 
295
 
296
- **Port:**
297
- Streamlit Spaces use port `8501` by default.
298
 
299
- **Embedding:**
300
- Spaces can be embedded in other sites using an `<iframe>`:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
- ```html
303
- <iframe src="https://[YourUsername]-Wrdler.hf.space?embed=true" title="Wrdler"></iframe>
304
- ```
305
 
306
- For full configuration options, see [Spaces Config Reference](https://huggingface.co/docs/hub/spaces-config-reference) and [Streamlit SDK Guide](https://huggingface.co/docs/hub/spaces-sdks-streamlit).
307
 
308
- # Assets Setup
309
 
310
- To fully experience Wrdler, especially the audio elements, ensure you set up the following assets:
311
 
312
- - Place your background music `.mp3` files in `wrdler/assets/audio/music/` to enable music.
313
- - Place your sound effect files (`.mp3` or `.wav`) in `wrdler/assets/audio/effects/` for sound effects.
314
 
315
- Refer to the documentation for guidance on compatible audio formats and common troubleshooting tips.
316
 
317
- # Sound Asset Generation
 
 
 
318
 
319
- To generate and save custom sound effects for Wrdler, you can use the `generate_sound_effect` function.
320
 
321
- ## Function: `generate_sound_effect`
 
 
 
 
 
 
 
 
 
322
 
323
- ```python
324
- def generate_sound_effect(effect: str, save_to_assets: bool = False, use_api: str = "huggingface") -> str:
325
- """
326
- Generate a sound effect and save it as a file.
 
327
 
328
- Parameters:
329
- - `effect`: Name of the effect to generate.
330
- - `save_to_assets`: If `True`, saves the effect to the assets directory;
331
- if `False`, saves to a temporary location. Default is `False`.
332
- - `use_api`: API to use for generation. Options are "huggingface" or "replicate". Default is "huggingface".
 
 
 
 
 
 
 
 
333
 
334
- Returns:
335
- - File path to the saved sound effect.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  ```
337
 
338
- ## Parameters
339
 
340
- - `effect`: The name of the sound effect you want to generate (e.g., "explosion", "powerup").
341
- - `save_to_assets` (optional): Set to `True` to save the generated sound effect to the game's assets directory. If `False`, the effect is saved to a temporary location. Default is `False`.
342
- - `use_api` (optional): The API to use for generating the sound. Options are `"huggingface"` or `"replicate"`. Default is `"huggingface"`.
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
- ## Returns
345
 
346
- - The function returns the file path to the saved sound effect, whether it's in the assets directory or a temporary location.
347
 
348
- ## Usage Example
349
 
350
- To generate a sound effect and save it to the assets directory:
 
 
 
351
 
352
- ```python
353
- generate_sound_effect("your_effect_name", save_to_assets=True)
 
 
354
  ```
355
 
356
- To generate a sound effect and keep it in a temporary location:
 
 
357
 
358
- ```python
359
- temp_path = generate_sound_effect("your_effect_name", save_to_assets=False)
 
 
 
 
 
 
360
  ```
361
 
362
- ## Note
 
 
 
363
 
364
- Ensure you have the necessary permissions and API access (if required) to use the sound generation service. Generated sounds are subject to the terms of use of the respective API.
365
 
366
- For any issues or enhancements, please refer to the project documentation or contact the project maintainer.
367
 
368
- Happy gaming and sound designing!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- ## What's New in v0.2.20-0.2.27: Challenge Mode 🎯
 
371
 
372
- ### Remote Challenge Sharing 🔗
373
- - Share challenges with friends via short URLs (`?game_id=<sid>`)
374
- - Each player gets different random words from the same wordlist
375
- - Multi-user leaderboards sorted by score and time
376
- - Word list difficulty calculation and display
377
- - Compare your performance against others!
378
-
379
- ### Leaderboards 🏆
380
- - Top 5 players displayed in Challenge Mode banner
381
- - Results sorted by: highest score → fastest time → highest difficulty
382
- - Submit results to existing challenges or create new ones
383
- - Player names supported (optional, defaults to "Anonymous")
384
-
385
- ### Remote Storage 💾
386
- - Challenge data stored in Hugging Face dataset repositories
387
- - Automatic save on game completion (with user consent)
388
- - "Show Challenge Share Links" toggle for privacy control (default OFF)
389
- - Works offline when HF credentials not configured
390
-
391
- ## What's Planned for v0.3.0
392
-
393
- ### Local Player History (Coming Soon)
394
- - Personal game results saved locally in `~/.wrdler/data/`
395
- - Offline-capable high score tracking
396
- - Player statistics (games played, averages, bests)
397
- - Privacy-first: no cloud dependency for personal data
398
- - Easy data management (delete `~/.wrdler/data/` to reset)
399
-
400
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
401
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Wrdler
3
+ emoji: 🎲
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: streamlit
 
8
  python_version: 3.12.8
9
  app_port: 8501
10
  app_file: app.py
11
+ suggested_hardware: cpu-basic
12
+ pinned: false
13
  tags:
14
  - game
15
  - vocabulary
16
  - streamlit
17
+ - gradio
18
  - education
19
  - ai
20
  short_description: Fast paced word guessing game with AI-generated word lists
 
28
 
29
  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.
30
 
31
+ **Dual UI Support:** Play with Streamlit (default) or Gradio 5.50+ (alternative) - same great gameplay, your choice of framework!
32
+
33
+ **🎮 Live Demo:** [Play on Hugging Face Spaces](https://huggingface.co/spaces/Surn/Wrdler)
34
+
35
  ## Key Differences from BattleWords
36
 
37
  - **8x6 grid** (instead of 12x12) with **6 words total** (one per row)
 
89
 
90
  ### Deployment & Technical
91
  - **Dockerfile-based deployment** supported for Hugging Face Spaces and other container platforms
92
+ - **Dual UI frameworks**:
93
+ - **Streamlit 1.51.0** (default) - Feature-rich, session-state based
94
+ - **Gradio 5.50+** (alternative) - Modern, reactive components
95
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
96
  - **Environment variables** for Challenge Mode (HF_API_TOKEN, HF_REPO_ID, SPACE_NAME)
97
  - Works offline without HF credentials (Challenge Mode features disabled gracefully)
98
+ - Compatible with both local development and cloud deployment
99
 
100
  ### Progressive Web App (PWA)
101
  - Installable on desktop and mobile from your browser
 
133
 
134
  You can run the app locally using either [uv](https://github.com/astral-sh/uv) or Streamlit directly:
135
 
136
+ **Streamlit UI (default):**
137
+ ```bash
138
  uv run streamlit run app.py
139
+ # or
140
+ streamlit run app.py
141
  ```
142
 
143
+ **Gradio UI (alternative):**
144
+ ```bash
145
+ python -m wrdler.gradio_ui
146
+ # or run the Gradio demo file directly
147
  ```
148
 
149
+ Both interfaces provide the same gameplay experience with slightly different UI frameworks.
150
+
151
  ### Dockerfile Deployment (Hugging Face Spaces and more)
152
 
153
  Wrdler supports containerized deployment using a `Dockerfile`. This is the recommended method for deploying to [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces-sdks-docker) or any Docker-compatible environment.
 
190
  - `wrdler/` – Python package
191
  - `models.py` – data models and types
192
  - `word_loader.py` – word list loading and validation
193
+ - `word_loader_ai.py` – AI word generation with HF Space API and local transformers
194
  - `generator.py` – word placement logic (8x6, horizontal only)
195
  - `logic.py` – game mechanics (reveal, guess, scoring, free letters)
196
  - `ui.py` – Streamlit UI composition
197
+ - `gradio_ui.py` – Gradio UI implementation (alternative interface)
198
  - `game_storage.py` – Hugging Face remote storage integration and challenge sharing
199
  - `local_storage.py` – local JSON storage for results and high scores
200
  - `storage.py` – (legacy) local storage and high scores
201
+ - `modules/`shared utility modules (storage, constants, file_utils)
202
+ - `words/` – word list files (classic.txt, fourth_grade.txt, wordlist.txt, AI-generated)
203
+ - `style_wrdler.css` – Custom CSS styling for Gradio interface
204
  - `specs/` – documentation (`specs.md`, `requirements.md`)
205
  - `tests/` – unit tests
206
 
 
218
 
219
  ## Changelog
220
 
221
+ ### v0.1.2 (Current)
222
+ - ✅ **Gradio UI Topic Display** - Prominent neon-styled badge at top of game area
223
+ - Glassmorphism background with glowing cyan border
224
+ - Pulsing neon animation effect
225
+ - Editable - type new topic and press Enter to generate/switch word lists
226
+ - ✅ **Settings Persistence** - Enable Free Letters and Show Challenge Links persist across new games
227
+ - ✅ **Timer Implementation** - Real-time game timer using gr.Timer component
228
+ - ✅ **AI Topic Generation UI** - Added to Settings tab with Generate button
229
+ - ✅ **Streamlit Cache Fix** - Conditional caching to avoid warnings in Gradio mode
230
+ - ✅ **Enhanced CSS Styling** - Improved grid container, topic display, and responsive design
231
+
232
+ ### v0.1.1
233
  - ✅ Enhanced AI word generation with intelligent word saving
234
  - ✅ Automatic retry mechanism for insufficient word counts (up to 3 retries)
235
  - ✅ 1000-word file size limit to prevent dictionary bloat
 
237
  - ✅ Better HF Space API integration with graceful fallback to local models
238
  - ✅ Additional word generation when initial pass doesn't meet MIN_REQUIRED threshold
239
  - ✅ Enhanced logging for word generation pipeline visibility
240
+ - ✅ **Gradio UI implementation** (gradio>=5.50) as alternative to Streamlit
241
+ - ✅ Custom CSS styling for Gradio interface (style_wrdler.css)
242
+ - ✅ Dual UI framework support - choose between Streamlit or Gradio
243
 
244
  ### v0.1.0
245
  - ✅ AI word generation functionality added
 
280
 
281
  ## Known Issues / TODO
282
 
283
+ - Word list loading bug: the app may not select the proper word lists in some environments. Investigate `word_loader.get_wordlist_files()` / `load_word_list()` and sidebar selection to ensure the chosen file is correctly used by the generator.
284
 
285
  ## Development Phases
286
 
 
296
 
297
  ## Hugging Face Spaces Configuration
298
 
299
+ Wrdler is deployable as a Hugging Face Space with **dual UI support**: Streamlit (default) and Gradio (>=5.50). You can use either the YAML config block or a Dockerfile for advanced/custom deployments.
300
 
301
  To configure your Space with the YAML block, add it at the top of your `README.md`:
302
 
303
+ **Streamlit Configuration (default):**
304
  ```yaml
305
  ---
306
  title: Wrdler
 
311
  sdk_version: 1.51.0
312
  python_version: 3.12.8
313
  app_file: app.py
314
+ suggested_hardware: cpu-basic
315
  tags:
316
  - game
317
  - vocabulary
318
  - streamlit
319
  - education
320
+ - ai
321
+ ---
322
+ ```
323
+
324
+ **Gradio Configuration (alternative):**
325
+ ```yaml
326
+ ---
327
+ title: Wrdler Gradio
328
+ emoji: 🎲
329
+ colorFrom: blue
330
+ colorTo: indigo
331
+ sdk: gradio
332
+ sdk_version: 5.50
333
+ python_version: 3.12.8
334
+ app_file: wrdler/gradio_ui.py
335
+ suggested_hardware: cpu-basic
336
+ tags:
337
+ - game
338
+ - vocabulary
339
+ - gradio
340
+ - education
341
+ - ai
342
  ---
343
  ```
344
 
345
  **Key parameters:**
346
  - `title`, `emoji`, `colorFrom`, `colorTo`: Visuals for your Space.
347
+ - `sdk`: Use `streamlit` or `gradio` depending on your preferred UI framework.
348
+ - `sdk_version`: Latest supported version (Streamlit 1.51.0 or Gradio 5.50+).
349
+ - `python_version`: Python version (3.12.8 recommended).
350
+ - `app_file`: Entry point for your app (`app.py` for Streamlit, `wrdler/gradio_ui.py` for Gradio).
351
+ - `suggested_hardware`: Recommended hardware tier (cpu-basic is sufficient).
352
  - `tags`: List of descriptive tags.
353
 
354
+ **Gradio Implementation Notes:**
355
+ - Uses modern Gradio 5.50+ API (compatible with upcoming Gradio 6)
356
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
357
+ - Custom styling via `style_wrdler.css`
358
+ - State management with `gr.State` and deep copy updates
359
+ - Event handlers use `.click()`, `.change()`, `.submit()` methods
360
+ - Modal dialogs with `gr.Modal(visible=False)` pattern
361
 
362
+ ````````
 
363
 
364
+ # Response
365
+ ````````markdown
366
+ ---
367
+ title: Wrdler
368
+ emoji: 🎲
369
+ colorFrom: blue
370
+ colorTo: indigo
371
+ sdk: streamlit
372
+ sdk_version: 1.51.0
373
+ python_version: 3.12.8
374
+ app_port: 8501
375
+ app_file: app.py
376
+ suggested_hardware: cpu-basic
377
+ pinned: false
378
+ tags:
379
+ - game
380
+ - vocabulary
381
+ - streamlit
382
+ - gradio
383
+ - education
384
+ - ai
385
+ short_description: Fast paced word guessing game with AI-generated word lists
386
+ thumbnail: >-
387
+ https://cdn-uploads.huggingface.co/production/uploads/6346595c9e5f0fe83fc60444/6rWS4AIaozoNMCbx9F5Rv.png
388
+ ---
389
 
390
+ # Wrdler
 
 
391
 
392
+ > **This project is based on BattleWords, but adapted for a simpler word puzzle game with an 8x6 grid, horizontal words only, and free letter guesses at the start.**
393
 
394
+ 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.
395
 
396
+ **Dual UI Support:** Play with Streamlit (default) or Gradio 5.50+ (alternative) - same great gameplay, your choice of framework!
397
 
398
+ **🎮 Live Demo:** [Play on Hugging Face Spaces](https://huggingface.co/spaces/Surn/Wrdler)
 
399
 
400
+ ## Key Differences from BattleWords
401
 
402
+ - **8x6 grid** (instead of 12x12) with **6 words total** (one per row)
403
+ - **Horizontal words only** (no vertical placement)
404
+ - **No scope/radar visualization**
405
+ - **2 free letter guesses** at the start - choose letters to reveal all instances in the grid
406
 
407
+ ## Features
408
 
409
+ ### Core Gameplay
410
+ - 8x6 grid with six hidden words (one per row, all horizontal)
411
+ - **Word composition:** Each puzzle contains exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words
412
+ - Game starts with 2 free letter guesses; all instances of chosen letters are revealed
413
+ - Reveal grid cells and guess words for points
414
+ - Scoring tiers: Good (34–37), Great (38–41), Fantastic (42+)
415
+ - Game ends when all words are guessed or all word letters are revealed
416
+ - Incorrect guess history with tooltip and optional display (enabled by default)
417
+ - 10 incorrect guess limit per game
418
+ - Two game modes: Classic (chain guesses) and Too Easy (single guess per reveal)
419
 
420
+ ### Audio & Visuals
421
+ - Ocean-themed gradient background with wave animations
422
+ - Background music system (toggleable with volume control)
423
+ - Sound effects for hits, misses, correct/incorrect guesses
424
+ - Responsive UI built with Streamlit
425
 
426
+ ### AI Word Generation
427
+ - **Topic-based word lists**: Generate custom word lists using AI for any theme
428
+ - **Intelligent word expansion**: New AI-generated words automatically saved to local files
429
+ - Smart detection separates existing dictionary words from new AI words
430
+ - Only saves new words to prevent duplicates
431
+ - Automatic retry mechanism (up to 3 attempts) for insufficient word counts
432
+ - 1000-word file size limit prevents bloat
433
+ - Auto-sorted by length then alphabetically
434
+ - **Dual generation modes**:
435
+ - **HF Space API** (primary): Uses Hugging Face Space when `USE_HF_WORDS=true`
436
+ - **Local transformers** (fallback): Falls back to local models if HF unavailable
437
+ - **Fallback support**: Gracefully uses dictionary words if AI generation fails
438
+ - **Guaranteed distribution**: Ensures exactly 25 words each of lengths 4, 5, and 6
439
 
440
+ ### Customization
441
+ - Multiple word lists (classic, fourth_grade, wordlist)
442
+ - Wordlist sidebar controls (picker + one-click sort)
443
+ - Audio volume controls (music and effects separate)
444
+
445
+ ### ✅ Challenge Mode
446
+ - **Shareable challenge links** via short URLs (`?game_id=<sid>`)
447
+ - **Multi-user leaderboards** sorted by score and time
448
+ - **Remote storage** via Hugging Face datasets
449
+ - **Word list difficulty calculation** and display
450
+ - **Submit results** to existing challenges or create new ones
451
+ - **Top 5 leaderboard** display in Challenge Mode banner
452
+ - **"Show Challenge Share Links" toggle** (default OFF) to control URL visibility
453
+ - Each player gets different random words from the same wordlist
454
+
455
+ ### Deployment & Technical
456
+ - **Dockerfile-based deployment** supported for Hugging Face Spaces and other container platforms
457
+ - **Dual UI frameworks**:
458
+ - **Streamlit 1.51.0** (default) - Feature-rich, session-state based
459
+ - **Gradio 5.50+** (alternative) - Modern, reactive components
460
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
461
+ - **Environment variables** for Challenge Mode (HF_API_TOKEN, HF_REPO_ID, SPACE_NAME)
462
+ - Works offline without HF credentials (Challenge Mode features disabled gracefully)
463
+ - Compatible with both local development and cloud deployment
464
+
465
+ ### Progressive Web App (PWA)
466
+ - Installable on desktop and mobile from your browser
467
+ - Includes `service worker` and `manifest.json` with basic offline caching of static assets
468
+ - See `INSTALL_GUIDE.md` for platform-specific steps
469
+
470
+ ### Planned
471
+ - Local persistent storage for personal game history
472
+ - Personal high scores sidebar (offline-capable)
473
+ - Player statistics tracking
474
+ - Deterministic seed UI for custom puzzles
475
+
476
+ ## Challenge Mode & Leaderboard
477
+
478
+ When playing a shared challenge (via a `game_id` link), the leaderboard displays all submitted results for that challenge. The leaderboard is **sorted by highest score (descending), then by fastest time (ascending)**. This means players with the most points appear at the top, and ties are broken by the shortest completion time.
479
+
480
+ ## Installation
481
+ 1. Clone the repository:
482
+ ```
483
+ git clone https://github.com/Oncorporation/Wrdler.git
484
+ cd wrdler
485
+ ```
486
+ 2. (Optional) Create and activate a virtual environment:
487
+ ```
488
+ python -m venv venv
489
+ source venv/bin/activate # On Windows use `venv\Scripts\activate`
490
+ ```
491
+ 3. Install dependencies: ( add --system if not using a virutal environment)
492
+ ```
493
+ uv pip install -r requirements.txt --link-mode=copy
494
  ```
495
 
 
496
 
497
+ ## Running Wrdler
498
+
499
+ You can run the app locally using either [uv](https://github.com/astral-sh/uv) or Streamlit directly:
500
+
501
+ **Streamlit UI (default):**
502
+ ```bash
503
+ uv run streamlit run app.py
504
+ # or
505
+ streamlit run app.py
506
+ ```
507
+
508
+ **Gradio UI (alternative):**
509
+ ```bash
510
+ python -m wrdler.gradio_ui
511
+ # or run the Gradio demo file directly
512
+ ```
513
 
514
+ Both interfaces provide the same gameplay experience with slightly different UI frameworks.
515
 
516
+ ### Dockerfile Deployment (Hugging Face Spaces and more)
517
 
518
+ Wrdler supports containerized deployment using a `Dockerfile`. This is the recommended method for deploying to [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces-sdks-docker) or any Docker-compatible environment.
519
 
520
+ To deploy on Hugging Face Spaces:
521
+ 1. Add a `Dockerfile` to your repository root (see [Spaces Dockerfile guide](https://huggingface.co/docs/hub/spaces-sdks-docker)).
522
+ 2. Push your code to your Hugging Face Space.
523
+ 3. The platform will build and run your app automatically.
524
 
525
+ For local Docker runs:
526
+ ```sh
527
+ docker build -t wrdler .
528
+ docker run -p8501:8501 wrdler
529
  ```
530
 
531
+ ### Environment Variables (for Challenge Mode)
532
+
533
+ Challenge Mode requires a `.env` file in the project root with HuggingFace Hub credentials:
534
 
535
+ ```bash
536
+ # Required for Challenge Mode
537
+ HF_API_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxx # or HF_TOKEN
538
+ HF_REPO_ID=YourUsername/YourRepo # Target HF dataset repo
539
+ SPACE_NAME=YourUsername/Wrdler # Your HF Space name
540
+
541
+ # Optional
542
+ CRYPTO_PK= # Reserved for future signing
543
  ```
544
 
545
+ **How to get your HF_API_TOKEN:**
546
+ 1. Go to https://huggingface.co/settings/tokens
547
+ 2. Create a new token with `write` access
548
+ 3. Add to `.env` file as `HF_API_TOKEN=hf_...`
549
 
550
+ **Note:** The app works without these variables, but Challenge Mode features (sharing, leaderboards) will be disabled.
551
 
552
+ ## Folder Structure
553
 
554
+ - `app.py` Streamlit entry point
555
+ - `wrdler/` – Python package
556
+ - `models.py` – data models and types
557
+ - `word_loader.py` – word list loading and validation
558
+ - `word_loader_ai.py` – AI word generation with HF Space API and local transformers
559
+ - `generator.py` – word placement logic (8x6, horizontal only)
560
+ - `logic.py` – game mechanics (reveal, guess, scoring, free letters)
561
+ - `ui.py` – Streamlit UI composition
562
+ - `gradio_ui.py` – Gradio UI implementation (alternative interface)
563
+ - `game_storage.py` – Hugging Face remote storage integration and challenge sharing
564
+ - `local_storage.py` – local JSON storage for results and high scores
565
+ - `storage.py` – (legacy) local storage and high scores
566
+ - `modules/` – shared utility modules (storage, constants, file_utils)
567
+ - `words/` – word list files (classic.txt, fourth_grade.txt, wordlist.txt, AI-generated)
568
+ - `style_wrdler.css` – Custom CSS styling for Gradio interface
569
+ - `specs/` – documentation (`specs.md`, `requirements.md`)
570
+ - `tests/` – unit tests
571
 
572
+ ## Test File Location
573
+ All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
574
 
575
+ ## How to Play
576
+
577
+ 1. **Start with 2 free letter guesses** - choose two letters to reveal all their instances in the grid.
578
+ 2. Click grid squares to reveal letters or empty spaces.
579
+ 3. After revealing a letter, enter a guess for a word in the text box.
580
+ 4. Earn points for correct guesses and bonus points for unrevealed letters.
581
+ 5. **The game ends when all six words are found or all word letters are revealed. Your score tier is displayed.**
582
+ 6. **To play a shared challenge, use a link with `?game_id=<sid>`. Your result will be added to the challenge leaderboard.**
583
+
584
+ ## Changelog
585
+
586
+ ### v0.1.2 (Current)
587
+ - ✅ **Gradio UI Topic Display** - Prominent neon-styled badge at top of game area
588
+ - Glassmorphism background with glowing cyan border
589
+ - Pulsing neon animation effect
590
+ - Editable - type new topic and press Enter to generate/switch word lists
591
+ - ✅ **Settings Persistence** - Enable Free Letters and Show Challenge Links persist across new games
592
+ - **Timer Implementation** - Real-time game timer using gr.Timer component
593
+ - ✅ **AI Topic Generation UI** - Added to Settings tab with Generate button
594
+ - **Streamlit Cache Fix** - Conditional caching to avoid warnings in Gradio mode
595
+ - ✅ **Enhanced CSS Styling** - Improved grid container, topic display, and responsive design
596
+
597
+ ### v0.1.1
598
+ - Enhanced AI word generation with intelligent word saving
599
+ - Automatic retry mechanism for insufficient word counts (up to 3 retries)
600
+ - ✅ 1000-word file size limit to prevent dictionary bloat
601
+ - Improved new word detection (separates existing vs. new words before saving)
602
+ - ✅ Better HF Space API integration with graceful fallback to local models
603
+ - Additional word generation when initial pass doesn't meet MIN_REQUIRED threshold
604
+ - ✅ Enhanced logging for word generation pipeline visibility
605
+ - ✅ **Gradio UI implementation** (gradio>=5.50) as alternative to Streamlit
606
+ - ✅ Custom CSS styling for Gradio interface (style_wrdler.css)
607
+ - ✅ Dual UI framework support - choose between Streamlit or Gradio
608
+
609
+ ### v0.1.0
610
+ - ✅ AI word generation functionality added
611
+ - ✅ Topic-based custom word list creation
612
+ - ✅ Dual generation modes (HF Space API + local transformers)
613
+ - ✅ Utility modules integration (storage, file_utils, constants)
614
+ - ✅ Documentation synchronized across all files
615
+
616
+ ### v0.0.8
617
+ - remove background animation
618
+ - add "easy" mode (single guess per reveal)
619
+
620
+ ### v0.0.7
621
+ - fix guess bug - allowing guesses only after word guessed or letter revealed
622
+
623
+ ### v0.0.2 (Current - All Sprints Complete) 🎉
624
+ - **Sprint 1-3:** Core data models, generator refactor, radar removal
625
+ - **Sprint 4:** Implemented free letter selection UI with circular green gradient buttons
626
+ - **Sprint 5:** Updated grid UI rendering for 8×6 display
627
+ - **Sprint 6:** Comprehensive integration testing (7/7 tests passing)
628
+ - **Sprint 7:** Complete documentation update
629
+ - Sound effects integration for free letter selection
630
+ - Mobile-responsive free letter grid
631
+ - Fixed duplicate rendering call bug
632
+ - **All core Wrdler features complete and tested**
633
+
634
+ ### v0.0.1 (Initial Wrdler Release)
635
+ - Project renamed from BattleWords to Wrdler
636
+ - Grid resized from 12x12 to 8x6
637
+ - Changed to one word per row (6 total), horizontal only
638
+ - Removed vertical word placement
639
+ - Removed scope/radar visualization
640
+ - Core data models updated for rectangular grid
641
+ - Generator refactored for horizontal-only placement
642
+
643
+ Note
644
+ - `battlewords/storage.py` remains local-only storage; a separate HF integration wrapper is provided as `game_storage.py` for remote challenge mode.
645
+
646
+ ## Known Issues / TODO
647
+
648
+ - Word list loading bug: the app may not select the proper word lists in some environments. Investigate `word_loader.get_wordlist_files()` / `load_word_list()` and sidebar selection to ensure the chosen file is correctly used by the generator.
649
+
650
+ ## Development Phases
651
+
652
+ - **Proof of Concept (0.1.0):** No overlaps, basic UI, single session.
653
+ - **Beta (0.5.0):** Overlaps allowed on shared letters, responsive layout, keyboard support, deterministic seed.
654
+ - **Full (1.0.0):** Enhanced UX, persistence, leaderboards, daily/practice modes, advanced features.
655
+
656
+ See `specs/requirements.md` and `specs/specs.md` for full details and roadmap.
657
+
658
+ ## License
659
+
660
+ Wrdler is based on BattleWords. BattlewordsTM. All Rights Reserved. All content, trademarks and logos are copyrighted by the owner.
661
+
662
+ ## Hugging Face Spaces Configuration
663
+
664
+ Wrdler is deployable as a Hugging Face Space with **dual UI support**: Streamlit (default) and Gradio (>=5.50). You can use either the YAML config block or a Dockerfile for advanced/custom deployments.
665
+
666
+ To configure your Space with the YAML block, add it at the top of your `README.md`:
667
+
668
+ **Streamlit Configuration (default):**
669
+ ```yaml
670
+ ---
671
+ title: Wrdler
672
+ emoji: 🎲
673
+ colorFrom: blue
674
+ colorTo: indigo
675
+ sdk: streamlit
676
+ sdk_version: 1.51.0
677
+ python_version: 3.12.8
678
+ app_file: app.py
679
+ suggested_hardware: cpu-basic
680
+ tags:
681
+ - game
682
+ - vocabulary
683
+ - streamlit
684
+ - education
685
+ - ai
686
+ ---
687
+ ```
688
+
689
+ **Gradio Configuration (alternative):**
690
+ ```yaml
691
+ ---
692
+ title: Wrdler Gradio
693
+ emoji: 🎲
694
+ colorFrom: blue
695
+ colorTo: indigo
696
+ sdk: gradio
697
+ sdk_version: 5.50
698
+ python_version: 3.12.8
699
+ app_file: wrdler/gradio_ui.py
700
+ suggested_hardware: cpu-basic
701
+ tags:
702
+ - game
703
+ - vocabulary
704
+ - gradio
705
+ - education
706
+ - ai
707
+ ---
708
+ ```
709
+
710
+ **Key parameters:**
711
+ - `title`, `emoji`, `colorFrom`, `colorTo`: Visuals for your Space.
712
+ - `sdk`: Use `streamlit` or `gradio` depending on your preferred UI framework.
713
+ - `sdk_version`: Latest supported version (Streamlit 1.51.0 or Gradio 5.50+).
714
+ - `python_version`: Python version (3.12.8 recommended).
715
+ - `app_file`: Entry point for your app (`app.py` for Streamlit, `wrdler/gradio_ui.py` for Gradio).
716
+ - `suggested_hardware`: Recommended hardware tier (cpu-basic is sufficient).
717
+ - `tags`: List of descriptive tags.
718
+
719
+ **Gradio Implementation Notes:**
720
+ - Uses modern Gradio 5.50+ API (compatible with upcoming Gradio 6)
721
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
722
+ - Custom styling via `style_wrdler.css`
723
+ - State management with `gr.State` and deep copy updates
724
+ - Event handlers use `.click()`, `.change()`, `.submit()` methods
725
+ - Modal dialogs with `gr.Modal(visible=False)` pattern
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "wrdler"
3
- version = "0.1.0"
4
  description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, and 2 free letter guesses"
5
  readme = "README.md"
6
  requires-python = ">=3.12,<3.13"
 
1
  [project]
2
  name = "wrdler"
3
+ version = "0.1.2"
4
  description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, and 2 free letter guesses"
5
  readme = "README.md"
6
  requires-python = ">=3.12,<3.13"
requirements.txt CHANGED
@@ -13,4 +13,5 @@ requests
13
  huggingface_hub
14
  python-dotenv
15
  google-api-core
16
- gradio>=4.0.0
 
 
13
  huggingface_hub
14
  python-dotenv
15
  google-api-core
16
+ gradio>=5.50
17
+ gradio-modal
specs/requirements.md CHANGED
@@ -15,14 +15,21 @@ This document breaks down the implementation tasks for Wrdler using the game rul
15
  - 2 free letter guesses at game start
16
 
17
  ## Implementation Details (v0.1.1)
18
- - **Tech Stack:** Python 3.12.8, Streamlit 1.51.0, numpy, matplotlib, transformers, gradio_client
19
- - **Architecture:** Single-player, local state in Streamlit session state
20
  - **Grid:** 8 columns × 6 rows (48 cells) with exactly six words
21
  - **Word Placement:** Horizontal-only, one word per row, no overlaps
22
  - **AI Generation:** Topic-based word lists with intelligent saving and retry logic
23
- - **Entry Point:** `app.py`
 
 
24
  - **Testing:** pytest with 25/25 tests passing (100%)
25
  - **Development Time:** ~12.75 hours across 7 sprints (Phase 1) + AI enhancements
 
 
 
 
 
26
 
27
  ## Streamlit Components (Implemented in v0.0.2)
28
  - State & caching ✅
@@ -57,6 +64,44 @@ This document breaks down the implementation tasks for Wrdler using the game rul
57
  - App reruns on interaction using `st.rerun()`
58
  - Game over dialog with final score and tier display
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  ## Folder Structure (Implemented)
61
  - `app.py` – Streamlit entry point ✅
62
  - `wrdler/` – Python package ✅
@@ -290,3 +335,75 @@ Return 75 Words for Game
290
 
291
  ## Test File Location
292
  All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  - 2 free letter guesses at game start
16
 
17
  ## Implementation Details (v0.1.1)
18
+ - **Tech Stack:** Python 3.12.8, Streamlit 1.51.0, Gradio 5.50, numpy, matplotlib, transformers, gradio_client
19
+ - **Architecture:** Single-player, local state in Streamlit session state or Gradio gr.State
20
  - **Grid:** 8 columns × 6 rows (48 cells) with exactly six words
21
  - **Word Placement:** Horizontal-only, one word per row, no overlaps
22
  - **AI Generation:** Topic-based word lists with intelligent saving and retry logic
23
+ - **Entry Points:**
24
+ - Streamlit: `app.py`
25
+ - Gradio: `wrdler/gradio_ui.py` or `python -m wrdler.gradio_ui`
26
  - **Testing:** pytest with 25/25 tests passing (100%)
27
  - **Development Time:** ~12.75 hours across 7 sprints (Phase 1) + AI enhancements
28
+ - **Gradio Implementation:** Modern API compatible with Gradio 5.50+
29
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
30
+ - Uses gr.State for game persistence
31
+ - Event handlers with modern `.click()`, `.change()`, `.submit()` methods
32
+ - Custom CSS styling via external file
33
 
34
  ## Streamlit Components (Implemented in v0.0.2)
35
  - State & caching ✅
 
64
  - App reruns on interaction using `st.rerun()`
65
  - Game over dialog with final score and tier display
66
 
67
+ ## Gradio Components (Implemented in v0.1.1)
68
+ - State management ✅
69
+ - `gr.State(value=create_new_game_state)` for game persistence
70
+ - State updates use `copy.deepcopy()` to trigger reactivity
71
+ - Serializable state dict (no complex objects in state)
72
+
73
+ - Layout & structure ✅
74
+ - `gr.Blocks()` with custom theme (`theme="Surn/beeuty"`)
75
+ - `gr.Tabs()` for Game and Settings sections
76
+ - `gr.Row()` and `gr.Column()` for responsive layout
77
+ - `gr.Modal()` for share challenge dialog
78
+
79
+ - Widgets (interaction) ✅
80
+ - 48 `gr.Button` components for grid cells with dynamic updates
81
+ - 26 `gr.Button` components for letter selection (only puzzle letters visible)
82
+ - `gr.Textbox` for word guessing with `.submit()` handler
83
+ - `gr.Button` for actions (New Game, Guess, Share Challenge)
84
+ - `gr.Dropdown` for settings (wordlist, game mode)
85
+ - `gr.Checkbox` for toggles (audio, display options)
86
+ - `gr.Slider` for volume controls
87
+
88
+ - Visualization ✅
89
+ - `gr.HTML()` for score panel with custom styling
90
+ - `gr.HTML()` for audio player (sound effects)
91
+ - `gr.HTML()` for game over display
92
+ - Custom CSS via `style_wrdler.css` file
93
+ - Responsive grid layout with CSS Grid
94
+
95
+ - Control flow ✅
96
+ - Event handlers return component update tuples
97
+ - `.click()`, `.change()`, `.submit()` for events
98
+ - `gr.update()` for conditional component updates
99
+ - State mutations trigger UI updates
100
+
101
+ - Reference ✅
102
+ - [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
103
+ - Compatible with Gradio 5.50+ API
104
+
105
  ## Folder Structure (Implemented)
106
  - `app.py` – Streamlit entry point ✅
107
  - `wrdler/` – Python package ✅
 
335
 
336
  ## Test File Location
337
  All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
338
+
339
+ ## Gradio 5.50 Implementation Reference
340
+
341
+ ### Migration from Gradio 4.x to 5.x
342
+ The Gradio UI implementation follows modern Gradio 5.50+ patterns, compatible with the upcoming Gradio 6 release:
343
+
344
+ **Official Reference:** [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
345
+
346
+ ### Key Gradio Patterns Used
347
+
348
+ 1. **State Management**
349
+ ```python
350
+ game_state = gr.State(value=create_new_game_state)
351
+ # Updates use deep copy to trigger reactivity
352
+ new_state = copy.deepcopy(state)
353
+ new_state["score"] = 42
354
+ return new_state
355
+ ```
356
+
357
+ 2. **Component Updates**
358
+ ```python
359
+ # Dynamic button updates
360
+ gr.Button(value="X", variant="primary", interactive=True)
361
+ # Conditional visibility
362
+ gr.update(visible=True)
363
+ ```
364
+
365
+ 3. **Event Handlers**
366
+ ```python
367
+ button.click(
368
+ fn=handler_function,
369
+ inputs=[input1, input2, state],
370
+ outputs=[output1, output2, state]
371
+ )
372
+ ```
373
+
374
+ 4. **Modal Dialogs**
375
+ ```python
376
+ with gr.Modal(visible=False, elem_id="modal-id") as modal:
377
+ gr.Markdown("Modal content")
378
+ ```
379
+
380
+ 5. **Custom CSS**
381
+ ```python
382
+ with gr.Blocks(css=css_content, theme="Surn/beeuty") as demo:
383
+ # Components
384
+ ```
385
+
386
+ ### Component Compatibility
387
+ - ✅ `gr.Button` - Modern button API with variant, size, interactive params
388
+ - ✅ `gr.Textbox` - Text input with submit handler support
389
+ - ✅ `gr.HTML` - Custom HTML rendering for score panels and audio
390
+ - ✅ `gr.State` - Serializable state management
391
+ - ✅ `gr.Dropdown` - Selection widgets for settings
392
+ - ✅ `gr.Checkbox` - Boolean toggles
393
+ - ✅ `gr.Slider` - Numeric value controls
394
+ - ✅ `gr.Tabs` - Multi-section layout
395
+ - ✅ `gr.Modal` - Dialog overlays
396
+ - ✅ `gr.Row` / `gr.Column` - Responsive layout containers
397
+
398
+ ### Best Practices
399
+ 1. **State must be JSON-serializable** - Use dicts/lists, not complex objects
400
+ 2. **Deep copy for updates** - `copy.deepcopy(state)` triggers reactivity
401
+ 3. **Return tuples matching outputs** - Order matters for component updates
402
+ 4. **Use elem_id for CSS targeting** - Consistent IDs for custom styling
403
+ 5. **Handle None/undefined gracefully** - Check state initialization
404
+ 6. **Batch updates efficiently** - Return all component changes together
405
+
406
+ ### File Structure
407
+ - `wrdler/gradio_ui.py` - Main Gradio interface implementation
408
+ - `style_wrdler.css` - Custom CSS for Gradio components
409
+ - Both files use Gradio 5.50+ compatible patterns
specs/specs.md CHANGED
@@ -133,7 +133,10 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, but with key
133
 
134
  ## Entry Point
135
  - The Streamlit entry point is `app.py`
 
136
  - **A `Dockerfile` can be used for containerized deployment (recommended for Hugging Face Spaces)**
 
 
137
 
138
  ## Deployment Requirements
139
 
@@ -199,6 +202,11 @@ HF_REPO_ID/
199
  - 1000-word file size limit
200
  - Improved HF Space API integration
201
  - Enhanced logging and error handling
 
 
 
 
 
202
 
203
  - **v0.1.0:** AI word generation foundation
204
  - Topic-based word list creation
 
133
 
134
  ## Entry Point
135
  - The Streamlit entry point is `app.py`
136
+ - The Gradio entry point is `wrdler/gradio_ui.py` (alternative interface)
137
  - **A `Dockerfile` can be used for containerized deployment (recommended for Hugging Face Spaces)**
138
+ - **Gradio 5.50 compatibility**: Uses modern component API and event handlers
139
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
140
 
141
  ## Deployment Requirements
142
 
 
202
  - 1000-word file size limit
203
  - Improved HF Space API integration
204
  - Enhanced logging and error handling
205
+ - **Gradio 5.50 UI implementation** as alternative to Streamlit
206
+ - Modern component API (gr.Button, gr.State, gr.Modal)
207
+ - Custom CSS styling via style_wrdler.css
208
+ - Full feature parity with Streamlit version
209
+ - Reference: [Gradio 6 Migration Guide](https://www.gradio.app/guides/gradio-6-migration-guide)
210
 
211
  - **v0.1.0:** AI word generation foundation
212
  - Topic-based word list creation
style_wrdler.css CHANGED
@@ -1,6 +1,39 @@
1
  /* Wrdler - Custom CSS for Gradio App */
2
  /* Based on Surn/beeuty theme patterns */
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  .interface-wrapper {
5
  max-width: 1200px;
6
  margin: 0 auto;
@@ -20,9 +53,9 @@
20
  border-radius: 1.25rem;
21
  overflow: hidden;
22
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
23
- max-width: 520px;
24
  margin: 0 auto 16px auto;
25
- font-weight: 600;
26
  }
27
 
28
  /* Inner container with original grid colors */
@@ -120,16 +153,36 @@
120
  padding: 0 !important;
121
  background: linear-gradient(145deg, #d4f1f9, #a8e6cf) !important;
122
  border: 2px solid #00bfa5 !important;
123
- color: #1a1a2e !important;
124
  cursor: default !important;
 
125
  }
126
 
127
- /* Empty revealed cells (middle dot) */
128
  .grid-cell-revealed button:has(span:empty),
129
- .grid-cell-revealed button[value="·"] {
 
130
  background: #2d2d44 !important;
131
  border: 2px solid #3d3d5c !important;
132
- color: #5d5d7c !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
 
135
  /* Legacy CSS for HTML grid (kept for compatibility) */
@@ -632,7 +685,7 @@
632
  }
633
 
634
  .tabitem {
635
- background: linear-gradient(145deg, rgba(26, 26, 46, 0.5), rgba(22, 33, 62, 0.5)) !important;
636
  border-radius: 0 0 12px 12px !important;
637
  padding: 16px !important;
638
  }
@@ -649,3 +702,281 @@
649
  min-width: 200px !important;
650
  margin-top: 16px !important;
651
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /* Wrdler - Custom CSS for Gradio App */
2
  /* Based on Surn/beeuty theme patterns */
3
 
4
+ /* ============================================
5
+ Override Gradio Theme Disabled Button Styling
6
+ Lighten up disabled buttons - reduce grayscale
7
+ ============================================ */
8
+ button[disabled],
9
+ button[disabled].svelte-o34uqh,
10
+ a.disabled,
11
+ a.disabled.svelte-o34uqh,
12
+ button:disabled,
13
+ button[aria-disabled="true"] {
14
+ opacity: 0.65 !important; /* Increased from 0.5 for lighter appearance */
15
+ filter: grayscale(30%) !important; /* Reduced from 30% for less graying */
16
+ cursor: not-allowed !important;
17
+ transform: none !important;
18
+ color: rgba(0,0,0,0.90);
19
+ }
20
+
21
+ /* Specific override for letter buttons when used */
22
+ .letter-btn-used button,
23
+ .letter-btn-used button[disabled],
24
+ .letter-btn-used button:disabled {
25
+ opacity: 0.5 !important; /* Even lighter for better visibility */
26
+ filter: grayscale(25%) !important; /* Minimal grayscale */
27
+ }
28
+
29
+ /* Specific override for grid cell buttons when revealed */
30
+ .grid-cell-revealed button,
31
+ .grid-cell-revealed button[disabled],
32
+ .grid-cell-revealed button:disabled {
33
+ opacity: 1 !important; /* Full opacity for revealed cells */
34
+ filter: none !important; /* No filter at all for revealed cells */
35
+ }
36
+
37
  .interface-wrapper {
38
  max-width: 1200px;
39
  margin: 0 auto;
 
53
  border-radius: 1.25rem;
54
  overflow: hidden;
55
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
56
+ max-width: 92.5%;
57
  margin: 0 auto 16px auto;
58
+ font-weight: 700;
59
  }
60
 
61
  /* Inner container with original grid colors */
 
153
  padding: 0 !important;
154
  background: linear-gradient(145deg, #d4f1f9, #a8e6cf) !important;
155
  border: 2px solid #00bfa5 !important;
156
+ color: #000000 !important; /* Changed from #1a1a2e to pure black for maximum contrast */
157
  cursor: default !important;
158
+ text-shadow: 0 0 1px rgba(255, 255, 255, 0.3) !important; /* Subtle white shadow for readability */
159
  }
160
 
161
+ /* Empty revealed cells - solid dark grey fill */
162
  .grid-cell-revealed button:has(span:empty),
163
+ .grid-cell-revealed button[value="·"],
164
+ .grid-cell-revealed button[value=""] {
165
  background: #2d2d44 !important;
166
  border: 2px solid #3d3d5c !important;
167
+ color: transparent !important; /* Hide any text content */
168
+ text-shadow: none !important;
169
+ font-size: 0 !important; /* Collapse any text rendering */
170
+ position: relative !important;
171
+ overflow: hidden !important;
172
+ }
173
+
174
+ /* Ensure empty cells are completely filled with dark grey */
175
+ .grid-cell-revealed button:has(span:empty)::before,
176
+ .grid-cell-revealed button[value="·"]::before,
177
+ .grid-cell-revealed button[value=""]::before {
178
+ content: '' !important;
179
+ position: absolute !important;
180
+ top: 0 !important;
181
+ left: 0 !important;
182
+ width: 100% !important;
183
+ height: 100% !important;
184
+ background: #2d2d44 !important;
185
+ z-index: 1 !important;
186
  }
187
 
188
  /* Legacy CSS for HTML grid (kept for compatibility) */
 
685
  }
686
 
687
  .tabitem {
688
+ background: linear-gradient(145deg, rgba(26, 26, 46, 0.3), rgba(22, 33, 62, 0.5)) !important;
689
  border-radius: 0 0 12px 12px !important;
690
  padding: 16px !important;
691
  }
 
702
  min-width: 200px !important;
703
  margin-top: 16px !important;
704
  }
705
+
706
+ /* Share Challenge Section */
707
+ .share-challenge-section {
708
+ margin-top: 16px;
709
+ padding: 16px;
710
+ background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666);
711
+ border-radius: 1rem;
712
+ box-shadow: 0 0 32px rgba(29, 100, 200, 0.5);
713
+ }
714
+
715
+ .share-challenge-section h3 {
716
+ color: #fff !important;
717
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
718
+ }
719
+
720
+ .share-challenge-section label {
721
+ color: #fff !important;
722
+ }
723
+
724
+ .share-challenge-section input {
725
+ background: rgba(0,0,0,0.2) !important;
726
+ color: #fff !important;
727
+ border: 1px solid rgba(255,255,255,0.3) !important;
728
+ }
729
+
730
+ .share-challenge-section button {
731
+ background: linear-gradient(145deg, #00bfa5, #00d2ff) !important;
732
+ color: #fff !important;
733
+ }
734
+
735
+ .share-challenge-section button:hover {
736
+ filter: brightness(1.1);
737
+ }
738
+
739
+ .share-challenge-result {
740
+ margin-top: 16px;
741
+ padding: 12px;
742
+ background: rgba(0, 191, 165, 0.2);
743
+ border-radius: 8px;
744
+ text-align: center;
745
+ }
746
+
747
+ .share-challenge-result h3 {
748
+ color: #00d2ff;
749
+ margin-bottom: 8px;
750
+ }
751
+
752
+ /* Share Challenge Trigger Button */
753
+ .share-challenge-trigger button {
754
+ background: linear-gradient(-45deg, #1d64c8, #00bfa5) !important;
755
+ color: #fff !important;
756
+ font-weight: 700 !important;
757
+ padding: 12px 24px !important;
758
+ font-size: 1.1rem !important;
759
+ border: none !important;
760
+ box-shadow: 0 0 20px rgba(29, 100, 200, 0.4) !important;
761
+ }
762
+
763
+ .share-challenge-trigger button:hover {
764
+ filter: brightness(1.1);
765
+ box-shadow: 0 0 30px rgba(29, 100, 200, 0.6) !important;
766
+ }
767
+
768
+ /* Share Challenge Modal */
769
+ .share-challenge-modal {
770
+ background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666) !important;
771
+ border-radius: 1rem !important;
772
+ box-shadow: 0 0 50px rgba(29, 100, 200, 0.5) !important;
773
+ }
774
+
775
+ .share-challenge-modal h2 {
776
+ color: #fff !important;
777
+ text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
778
+ }
779
+
780
+ .share-challenge-modal p {
781
+ color: #fff !important;
782
+ }
783
+
784
+ .share-challenge-modal label {
785
+ color: #fff !important;
786
+ }
787
+
788
+ .share-challenge-modal input {
789
+ background: rgba(0,0,0,0.2) !important;
790
+ color: #fff !important;
791
+ border: 1px solid rgba(255,255,255,0.3) !important;
792
+ }
793
+
794
+ .share-challenge-modal button {
795
+ margin-top: 8px !important;
796
+ }
797
+
798
+ /* ============================================
799
+ Topic Display - Prominent Inline Badge Style
800
+ ============================================ */
801
+ div:has(> #topic-display) {
802
+ display: flex;
803
+ flex-direction: inherit;
804
+ flex-wrap: wrap;
805
+ gap: unset !important;
806
+ box-shadow: none !important;
807
+ border: none !important;
808
+ border-radius: var(--block-radius);
809
+ background: none !important;
810
+ }
811
+ /* Topic container - center and constrain width */
812
+ #topic-display {
813
+ max-width: 90% !important;
814
+ margin: 0 auto 16px auto !important;
815
+ border: none !important;
816
+ background: transparent !important;
817
+ }
818
+
819
+ /* Hide the label span but keep the label container */
820
+ #topic-display span.sr-only,
821
+ #topic-display span.hide {
822
+ display: none !important;
823
+ }
824
+
825
+ /* Style the label container */
826
+ #topic-display label.container {
827
+ background: transparent !important;
828
+ border: none !important;
829
+ padding: 0 !important;
830
+ }
831
+
832
+ /* Style the input container */
833
+ #topic-display .input-container {
834
+ background: transparent !important;
835
+ border: none !important;
836
+ }
837
+
838
+ /* The actual input - styled as a glowing badge with theme colors */
839
+ /* Using multiple selectors for maximum specificity */
840
+ #topic-display input,
841
+ #topic-display input[type="text"],
842
+ #topic-display input.scroll-hide,
843
+ #topic-display .input-container input,
844
+ div#topic-display input,
845
+ .topic-input input,
846
+ div.topic-input input,
847
+ #topic-display label input,
848
+ #topic-display label.container input {
849
+ /* Dark background matching grid container */
850
+ background: linear-gradient(145deg, #1a1a2e, #16213e) !important;
851
+
852
+ /* Glowing border with theme cyan */
853
+ border: 2px solid #00d2ff !important;
854
+ border-radius: 50px !important;
855
+
856
+ /* Typography - theme colors */
857
+ padding: 12px 32px !important;
858
+ font-size: 1.4rem !important;
859
+ font-weight: 700 !important;
860
+ color: #00d2ff !important; /* Theme cyan color */
861
+ text-align: center !important;
862
+ text-transform: uppercase !important;
863
+ letter-spacing: 2px !important;
864
+ -webkit-text-fill-color: #00d2ff !important;
865
+ caret-color: #00bfa5 !important; /* Theme teal for caret */
866
+
867
+ /* Neon glow effect with theme colors */
868
+ box-shadow:
869
+ 0 0 10px rgba(0, 210, 255, 0.5),
870
+ 0 0 20px rgba(0, 210, 255, 0.3),
871
+ 0 0 40px rgba(0, 191, 165, 0.2),
872
+ inset 0 0 30px rgba(0, 210, 255, 0.1) !important;
873
+ text-shadow:
874
+ 0 0 10px rgba(0, 210, 255, 0.8),
875
+ 0 0 20px rgba(0, 210, 255, 0.5),
876
+ 0 0 30px rgba(0, 191, 165, 0.3);
877
+
878
+ /* Sizing */
879
+ width: 100% !important;
880
+ min-height: 48px !important;
881
+ height: 48px !important;
882
+ line-height: 24px !important;
883
+
884
+ /* Ensure visibility */
885
+ opacity: 1 !important;
886
+ visibility: visible !important;
887
+
888
+ /* Animation */
889
+ transition: all 0.3s ease !important;
890
+ animation: topic-neon-pulse 2.5s ease-in-out infinite;
891
+ }
892
+
893
+ /* Neon pulse animation - theme colors */
894
+ @keyframes topic-neon-pulse {
895
+ 0%, 100% {
896
+ box-shadow:
897
+ 0 0 10px rgba(0, 210, 255, 0.4),
898
+ 0 0 20px rgba(0, 210, 255, 0.3),
899
+ 0 0 40px rgba(0, 191, 165, 0.2),
900
+ inset 0 0 15px rgba(0, 210, 255, 0.1);
901
+ border-color: #00d2ff;
902
+ }
903
+ 50% {
904
+ box-shadow:
905
+ 0 0 15px rgba(0, 210, 255, 0.6),
906
+ 0 0 30px rgba(0, 210, 255, 0.4),
907
+ 0 0 60px rgba(0, 191, 165, 0.3),
908
+ inset 0 0 20px rgba(0, 210, 255, 0.15);
909
+ border-color: #00bfa5; /* Alternates to teal */
910
+ }
911
+ }
912
+
913
+ /* Hover state - intensified glow with theme colors */
914
+ #topic-display input:hover,
915
+ div#topic-display input:hover,
916
+ .topic-input input:hover {
917
+ background: linear-gradient(145deg, #16213e, #1a1a2e) !important;
918
+ border-color: #00bfa5 !important; /* Theme teal */
919
+ color: #00bfa5 !important;
920
+ -webkit-text-fill-color: #00bfa5 !important;
921
+ box-shadow:
922
+ 0 0 15px rgba(0, 191, 165, 0.6),
923
+ 0 0 30px rgba(0, 210, 255, 0.4),
924
+ 0 0 50px rgba(0, 191, 165, 0.3),
925
+ inset 0 0 40px rgba(0, 210, 255, 0.15) !important;
926
+ cursor: text;
927
+ }
928
+
929
+ /* Focus state - editing mode with bright glow */
930
+ #topic-display input:focus,
931
+ div#topic-display input:focus,
932
+ .topic-input input:focus {
933
+ background: linear-gradient(145deg, #1a1a2e, #0d0d1a) !important;
934
+ outline: none !important;
935
+ border-color: #00bfa5 !important;
936
+ color: #a8e6cf !important; /* Lighter teal for active editing */
937
+ -webkit-text-fill-color: #a8e6cf !important;
938
+ box-shadow:
939
+ 0 0 20px rgba(0, 191, 165, 0.8),
940
+ 0 0 40px rgba(0, 210, 255, 0.5),
941
+ 0 0 60px rgba(0, 191, 165, 0.3),
942
+ inset 0 0 50px rgba(0, 210, 255, 0.2) !important;
943
+ animation: none !important;
944
+ }
945
+
946
+ /* Placeholder styling - theme colors */
947
+ #topic-display input::placeholder,
948
+ div#topic-display input::placeholder,
949
+ .topic-input input::placeholder {
950
+ color: rgba(0, 210, 255, 0.5) !important; /* Muted cyan */
951
+ -webkit-text-fill-color: rgba(0, 210, 255, 0.5) !important;
952
+ font-style: italic;
953
+ text-transform: none !important;
954
+ letter-spacing: normal !important;
955
+ }
956
+
957
+ /* Responsive adjustments */
958
+ @media (max-width: 768px) {
959
+ #topic-display {
960
+ max-width: 350px !important;
961
+ }
962
+
963
+ #topic-display textarea,
964
+ #topic-display input {
965
+ font-size: 1.2rem !important;
966
+ padding: 10px 24px !important;
967
+ letter-spacing: 1px !important;
968
+ }
969
+ }
970
+
971
+ @media (max-width: 480px) {
972
+ #topic-display {
973
+ max-width: 280px !important;
974
+ }
975
+
976
+ #topic-display textarea,
977
+ #topic-display input {
978
+ font-size: 1rem !important;
979
+ padding: 8px 20px !important;
980
+ letter-spacing: 0.5px !important;
981
+ }
982
+ }
wrdler/__init__.py CHANGED
@@ -8,5 +8,5 @@ Key differences from BattleWords:
8
  - 2 free letter guesses at game start
9
  """
10
 
11
- __version__ = "0.1.1"
12
  __all__ = ["models", "generator", "logic", "ui", "word_loader"]
 
8
  - 2 free letter guesses at game start
9
  """
10
 
11
+ __version__ = "0.1.2"
12
  __all__ = ["models", "generator", "logic", "ui", "word_loader"]
wrdler/gradio_ui.py CHANGED
@@ -17,6 +17,7 @@ import base64
17
  import mimetypes
18
 
19
  import gradio as gr
 
20
 
21
  from .models import Coord, Word, Puzzle, GameState
22
  from .logic import (
@@ -29,6 +30,13 @@ from .logic import (
29
  compute_tier,
30
  )
31
  from .generator import generate_puzzle
 
 
 
 
 
 
 
32
 
33
  # Version info
34
  from . import __version__
@@ -215,10 +223,51 @@ def load_word_list_gradio(selected_file: Optional[str] = None) -> Dict[int, List
215
  def create_new_game_state(
216
  wordlist: str = "classic.txt",
217
  game_mode: str = "classic",
218
- seed: Optional[int] = None
 
219
  ) -> Dict[str, Any]:
220
  """Create a new game state dictionary for gr.State."""
221
- words_by_len = load_word_list_gradio(wordlist)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  puzzle = generate_puzzle(
223
  grid_rows=GRID_ROWS,
224
  grid_cols=GRID_COLS,
@@ -260,6 +309,10 @@ def create_new_game_state(
260
  "music_enabled": False,
261
  "music_volume": 30,
262
  "pending_sound": None, # Sound effect to play on next render
 
 
 
 
263
  # Challenge mode
264
  "challenge_mode": False,
265
  "challenge_sid": None,
@@ -441,6 +494,10 @@ def get_all_letter_button_updates(state: Dict[str, Any], all_puzzle_letters: Lis
441
 
442
  def render_free_letters_status(state: Dict[str, Any]) -> str:
443
  """Generate status message for free letter selection."""
 
 
 
 
444
  letters_used = state.get("free_letters_used", 0)
445
 
446
  if letters_used >= MAX_FREE_LETTERS:
@@ -458,6 +515,8 @@ def render_score_panel_html(state: Dict[str, Any]) -> str:
458
  start_time = state["start_time"]
459
  end_time = state["end_time"]
460
  game_over = state["game_over"]
 
 
461
 
462
  # Calculate elapsed time
463
  if end_time:
@@ -500,34 +559,33 @@ def render_score_panel_html(state: Dict[str, Any]) -> str:
500
 
501
  table_inner = "\n".join(table_rows)
502
 
503
- # Timer HTML
504
- if not game_over:
505
- timer_html = f'''
506
- <div class="timer" id="wrdler-timer" data-start="{start_time}">
507
- Time: <span class="time-value">{time_str}</span>
508
- </div>
509
- <script>
510
- (function() {{
511
- const timerEl = document.getElementById('wrdler-timer');
512
- if (!timerEl) return;
513
- const startTime = new Date(timerEl.dataset.start);
514
- function updateTimer() {{
515
- const now = new Date();
516
- const elapsed = Math.floor((now - startTime) / 1000);
517
- const mins = Math.floor(elapsed / 60);
518
- const secs = elapsed % 60;
519
- const timeSpan = timerEl.querySelector('.time-value');
520
- if (timeSpan) {{
521
- timeSpan.textContent = String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
522
- }}
523
- }}
524
- setInterval(updateTimer, 1000);
525
- updateTimer();
526
- }})();
527
- </script>
528
- '''
529
- else:
530
- timer_html = f'<div class="timer">Time: <span class="time-value">{time_str}</span></div>'
531
 
532
  html = f'''
533
  <div class='wrdler-score-panel-container'>
@@ -550,12 +608,18 @@ def render_score_panel_html(state: Dict[str, Any]) -> str:
550
  .wrdler-score-panel-container tr {{ border-bottom: 1px solid rgba(0, 0, 0, 0.1); background: #000;}}
551
  .wrdler-score-panel-container tr.found td {{ font-weight: 600; background: #000;}}
552
  .wrdler-score-panel-container tr.hidden td {{ color: #333; }}
 
 
 
 
 
553
  </style>
554
  <div class="score-header">Score: <span class="score-value">{score}</span></div>
555
- {timer_html}
556
  <table class='shiny-border' style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing: 0;\">
557
  {table_inner}
558
  </table>
 
559
  </div>
560
  '''
561
 
@@ -651,6 +715,20 @@ def render_game_over_html(state: Dict[str, Any]) -> str:
651
  html.append(f'<tr><td>{text}</td><td>{base}</td><td>+{bonus}</td><td>{points}</td></tr>')
652
 
653
  html.append('</table></div>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  html.append('</div>')
655
 
656
  return "\n".join(html)
@@ -707,11 +785,19 @@ def get_letter_button_updates(state: Dict[str, Any]) -> List[gr.Button]:
707
  def build_ui_outputs(state: Dict[str, Any], audio_html: str = "") -> tuple:
708
  """Build the complete UI output tuple for all handlers.
709
 
710
- Returns: (48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, state)
711
  """
712
  grid_button_updates = get_all_button_updates(state)
713
  letter_button_updates = get_letter_button_updates(state)
714
 
 
 
 
 
 
 
 
 
715
  return (
716
  *grid_button_updates, # 48 grid button updates
717
  *letter_button_updates, # 26 letter button updates
@@ -719,7 +805,9 @@ def build_ui_outputs(state: Dict[str, Any], audio_html: str = "") -> tuple:
719
  state.get("last_action", ""),
720
  audio_html,
721
  render_game_over_html(state),
722
- render_free_letters_status(state),
 
 
723
  state
724
  )
725
 
@@ -737,8 +825,9 @@ def handle_cell_click(row: int, col: int, state: Dict[str, Any]) -> tuple:
737
  if state.get("game_over"):
738
  return build_ui_outputs(state)
739
 
740
- # Check if free letters required first
741
- if state["free_letters_used"] < MAX_FREE_LETTERS:
 
742
  state["last_action"] = f"Please choose {MAX_FREE_LETTERS - state['free_letters_used']} more free letter(s) first."
743
  return build_ui_outputs(state)
744
 
@@ -809,6 +898,14 @@ def build_guess_outputs(state: Dict[str, Any], audio_html: str = "", clear_input
809
  grid_button_updates = get_all_button_updates(state)
810
  letter_button_updates = get_letter_button_updates(state)
811
 
 
 
 
 
 
 
 
 
812
  return (
813
  *grid_button_updates, # 48 grid button updates
814
  *letter_button_updates, # 26 letter button updates
@@ -817,7 +914,9 @@ def build_guess_outputs(state: Dict[str, Any], audio_html: str = "", clear_input
817
  "" if clear_input else gr.update(), # guess input - clear or no change
818
  audio_html,
819
  render_game_over_html(state),
820
- render_free_letters_status(state),
 
 
821
  state
822
  )
823
 
@@ -865,16 +964,20 @@ def handle_guess(guess_text: str, state: Dict[str, Any]) -> tuple:
865
 
866
  def handle_new_game(
867
  wordlist: str,
 
868
  game_mode: str,
869
  sfx_enabled: bool,
870
  sfx_volume: int,
871
  music_enabled: bool,
872
  music_volume: int,
 
 
 
873
  state: Dict[str, Any]
874
  ) -> tuple:
875
  """Handle new game creation."""
876
  state = ensure_state(state)
877
- new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode)
878
 
879
  # Preserve audio settings from UI components
880
  new_state["sound_effects_enabled"] = sfx_enabled
@@ -882,6 +985,15 @@ def handle_new_game(
882
  new_state["music_enabled"] = music_enabled
883
  new_state["music_volume"] = music_volume
884
 
 
 
 
 
 
 
 
 
 
885
  return build_ui_outputs(new_state)
886
 
887
 
@@ -904,6 +1016,86 @@ def handle_audio_settings_change(
904
  return new_state
905
 
906
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  # ---------------------------------------------------------------------------
908
  # Main App Creation
909
  # ---------------------------------------------------------------------------
@@ -916,11 +1108,18 @@ def create_app() -> gr.Blocks:
916
  if not wordlist_files:
917
  wordlist_files = ["classic.txt"]
918
 
919
- # CSS file path
920
  css_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "style_wrdler.css")
 
 
 
 
 
 
 
921
 
922
  with gr.Blocks(
923
- css=css_path if os.path.exists(css_path) else None,
924
  theme="Surn/beeuty",
925
  title=f"Wrdler v{__version__}"
926
  ) as demo:
@@ -933,37 +1132,54 @@ def create_app() -> gr.Blocks:
933
  gr.Markdown("Find all 6 hidden words in the 8×6 grid!")
934
 
935
  # Tab layout
936
- with gr.Tabs():
937
  # Game Tab
938
  with gr.TabItem("Game"):
939
  with gr.Row():
940
- # Left column - Game area
941
- with gr.Column(scale=5):
 
 
 
 
 
 
 
 
 
 
 
 
 
942
  # Challenge mode leaderboard (if in challenge mode)
943
  challenge_leaderboard_html = gr.HTML(
944
  value="",
945
  elem_id="challenge-leaderboard-container"
946
  )
947
-
948
- # Free letter selection using button grid
949
- free_letter_status = gr.Markdown(
950
- value="Choose 2 free letters:",
951
- elem_id="free-letter-status"
952
- )
953
- # Create 26 letter buttons (only puzzle letters will be visible)
954
- letter_buttons: List[gr.Button] = []
955
- with gr.Row(elem_classes=["letter-buttons-row"]):
956
- for letter in ALL_LETTERS:
957
- btn = gr.Button(
958
- value=letter,
959
- variant="primary",
960
- size="sm",
961
- min_width=36,
962
- visible=False, # Will be shown for letters in puzzle
963
- elem_classes=["letter-btn-available"],
964
- elem_id=f"letter-{letter}"
965
  )
966
- letter_buttons.append(btn)
 
 
 
 
 
 
 
 
 
 
 
 
 
967
 
968
  # Game grid using native Gradio buttons
969
  # Create 48 buttons (6 rows x 8 cols) in a CSS grid
@@ -976,7 +1192,7 @@ def create_app() -> gr.Blocks:
976
  value="?",
977
  variant="primary",
978
  size="sm",
979
- min_width=50,
980
  elem_classes=["grid-cell-unrevealed"],
981
  elem_id=f"cell-{row}-{col}"
982
  )
@@ -984,45 +1200,86 @@ def create_app() -> gr.Blocks:
984
 
985
  # Guess form
986
  with gr.Row():
987
- guess_input = gr.Textbox(
988
- label="Your Guess",
989
- placeholder="Enter a word...",
990
- max_lines=1,
991
- elem_id="guess-input"
992
- )
993
- guess_btn = gr.Button("Guess", variant="primary", elem_id="guess-btn")
994
 
995
  # Status message
996
- status_msg = gr.Markdown(value="Welcome! Choose your 2 free letters to start.")
997
 
998
  # Game over display
999
  game_over_html = gr.HTML(value="", elem_id="game-over-container")
1000
 
 
 
 
 
 
 
 
 
 
1001
  # Right column - Score panel and New Game
1002
  with gr.Column(scale=3):
 
 
 
 
 
 
 
1003
  # Score panel
1004
  score_panel_html = gr.HTML(
1005
  value=lambda: render_score_panel_html(create_new_game_state()),
1006
  elem_id="score-panel-container"
1007
  )
1008
- # New Game button
1009
- new_game_btn = gr.Button("New Game", variant="primary", size="lg")
1010
 
1011
  # Settings Tab
1012
  with gr.TabItem("Settings"):
1013
  with gr.Row():
1014
  with gr.Column():
1015
  gr.Markdown("### Game Settings")
 
 
1016
  wordlist_dropdown = gr.Dropdown(
1017
- choices=wordlist_files,
1018
  value=wordlist_files[0] if wordlist_files else "classic.txt",
1019
  label="Word List"
1020
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1021
  game_mode_dropdown = gr.Dropdown(
1022
  choices=["classic", "easy", "too easy"],
1023
  value="classic",
1024
  label="Game Mode"
1025
  )
 
 
 
 
 
 
 
 
 
 
 
 
1026
 
1027
  with gr.Column():
1028
  gr.Markdown("### Audio Settings")
@@ -1052,8 +1309,32 @@ def create_app() -> gr.Blocks:
1052
  # Hidden audio player for sound effects
1053
  audio_player_html = gr.HTML(value="", elem_id="audio-player", visible=False)
1054
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1055
  # Define common outputs for handlers
1056
- # Order: 48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, state
1057
  common_outputs = [
1058
  *grid_buttons,
1059
  *letter_buttons,
@@ -1062,6 +1343,8 @@ def create_app() -> gr.Blocks:
1062
  audio_player_html,
1063
  game_over_html,
1064
  free_letter_status,
 
 
1065
  game_state
1066
  ]
1067
 
@@ -1075,6 +1358,8 @@ def create_app() -> gr.Blocks:
1075
  audio_player_html,
1076
  game_over_html,
1077
  free_letter_status,
 
 
1078
  game_state
1079
  ]
1080
 
@@ -1110,11 +1395,131 @@ def create_app() -> gr.Blocks:
1110
  outputs=guess_outputs
1111
  )
1112
 
 
 
 
 
 
 
 
 
1113
  # New game button
1114
  new_game_btn.click(
1115
  fn=handle_new_game,
1116
- inputs=[wordlist_dropdown, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, game_state],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1117
  outputs=common_outputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1118
  )
1119
 
1120
  # Audio settings change handlers
@@ -1139,6 +1544,96 @@ def create_app() -> gr.Blocks:
1139
  outputs=[game_state]
1140
  )
1141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1142
  # Initialize UI on app load
1143
  def initialize_ui(state):
1144
  """Initialize UI components on app load."""
 
17
  import mimetypes
18
 
19
  import gradio as gr
20
+ from gradio_modal import Modal
21
 
22
  from .models import Coord, Word, Puzzle, GameState
23
  from .logic import (
 
30
  compute_tier,
31
  )
32
  from .generator import generate_puzzle
33
+ from .game_storage import (
34
+ save_game_to_hf,
35
+ add_user_result_to_game,
36
+ get_shareable_url,
37
+ load_game_from_sid
38
+ )
39
+ from .word_loader_ai import generate_ai_words
40
 
41
  # Version info
42
  from . import __version__
 
223
  def create_new_game_state(
224
  wordlist: str = "classic.txt",
225
  game_mode: str = "classic",
226
+ seed: Optional[int] = None,
227
+ ai_topic: str = ""
228
  ) -> Dict[str, Any]:
229
  """Create a new game state dictionary for gr.State."""
230
+ # Handle AI Generated word list
231
+ if wordlist == "AI Generated" and ai_topic.strip():
232
+ import re as re_module
233
+ # Convert topic to filename (same logic as _save_ai_words_to_file)
234
+ safe_topic = re_module.sub(r'[^\w\s-]', '', ai_topic.strip().lower()).strip()
235
+ safe_topic = re_module.sub(r'[-\s]+', '_', safe_topic)
236
+ topic_filename = f"{safe_topic}.txt"
237
+
238
+ # First try to load existing words from the topic file
239
+ words_by_len = None
240
+ try:
241
+ existing_words = load_word_list_gradio(topic_filename)
242
+ # Check if we have enough words of each length
243
+ has_enough = all(len(existing_words.get(length, [])) >= MIN_REQUIRED for length in (4, 5, 6))
244
+ if has_enough:
245
+ words_by_len = existing_words
246
+ except Exception:
247
+ pass
248
+
249
+ # If no existing file or not enough words, generate new AI words
250
+ if words_by_len is None:
251
+ try:
252
+ # Generate AI words - returns list of 75 words
253
+ ai_words, _, _ = generate_ai_words(topic=ai_topic.strip())
254
+ # Convert to words_by_len format
255
+ words_by_len = {4: [], 5: [], 6: []}
256
+ for word in ai_words:
257
+ word_len = len(word)
258
+ if word_len in words_by_len:
259
+ words_by_len[word_len].append(word.upper())
260
+ # Ensure we have enough words of each length
261
+ for length in (4, 5, 6):
262
+ if len(words_by_len[length]) < MIN_REQUIRED:
263
+ # Fall back to default words
264
+ words_by_len[length] = FALLBACK_WORDS[length].copy()
265
+ except Exception as e:
266
+ # Fall back to default word list on error
267
+ words_by_len = load_word_list_gradio("classic.txt")
268
+ else:
269
+ words_by_len = load_word_list_gradio(wordlist if wordlist != "AI Generated" else "classic.txt")
270
+
271
  puzzle = generate_puzzle(
272
  grid_rows=GRID_ROWS,
273
  grid_cols=GRID_COLS,
 
309
  "music_enabled": False,
310
  "music_volume": 30,
311
  "pending_sound": None, # Sound effect to play on next render
312
+ # Display settings
313
+ "show_incorrect_guesses": True,
314
+ "enable_free_letters": True,
315
+ "show_challenge_links": True,
316
  # Challenge mode
317
  "challenge_mode": False,
318
  "challenge_sid": None,
 
494
 
495
  def render_free_letters_status(state: Dict[str, Any]) -> str:
496
  """Generate status message for free letter selection."""
497
+ # Check if free letters are enabled
498
+ if not state.get("enable_free_letters", True):
499
+ return ""
500
+
501
  letters_used = state.get("free_letters_used", 0)
502
 
503
  if letters_used >= MAX_FREE_LETTERS:
 
515
  start_time = state["start_time"]
516
  end_time = state["end_time"]
517
  game_over = state["game_over"]
518
+ incorrect_guesses = state.get("incorrect_guesses", [])
519
+ show_incorrect = state.get("show_incorrect_guesses", True)
520
 
521
  # Calculate elapsed time
522
  if end_time:
 
559
 
560
  table_inner = "\n".join(table_rows)
561
 
562
+ # Timer HTML - calculate current elapsed time
563
+ if not game_over and start_time:
564
+ try:
565
+ start = datetime.fromisoformat(start_time)
566
+ now = datetime.now()
567
+ elapsed = (now - start).total_seconds()
568
+ minutes = int(elapsed // 60)
569
+ seconds = int(elapsed % 60)
570
+ time_str = f"{minutes:02d}:{seconds:02d}"
571
+ except Exception:
572
+ pass
573
+
574
+ timer_html = f'<div class="timer">Time: <span class="time-value">{time_str}</span></div>'
575
+
576
+ # Incorrect guesses HTML
577
+ incorrect_html = ""
578
+ if show_incorrect and incorrect_guesses:
579
+ recent = incorrect_guesses[-10:] # Show last 10
580
+ guess_items = "".join(f'<div class="guess-item">• {g}</div>' for g in recent)
581
+ incorrect_html = f'''
582
+ <div class="wrdler-incorrect-guesses">
583
+ <details>
584
+ <summary>Incorrect guesses ({len(incorrect_guesses)})</summary>
585
+ <div class="guess-list">{guess_items}</div>
586
+ </details>
587
+ </div>
588
+ '''
 
589
 
590
  html = f'''
591
  <div class='wrdler-score-panel-container'>
 
608
  .wrdler-score-panel-container tr {{ border-bottom: 1px solid rgba(0, 0, 0, 0.1); background: #000;}}
609
  .wrdler-score-panel-container tr.found td {{ font-weight: 600; background: #000;}}
610
  .wrdler-score-panel-container tr.hidden td {{ color: #333; }}
611
+ .wrdler-incorrect-guesses {{ margin-top: 12px; padding: 8px; background: rgba(255, 100, 100, 0.15); border-radius: 8px; }}
612
+ .wrdler-incorrect-guesses summary {{ color: #ff9999; font-size: 0.85rem; cursor: pointer; font-weight: 600; }}
613
+ .wrdler-incorrect-guesses summary:hover {{ color: #ffaaaa; }}
614
+ .wrdler-incorrect-guesses .guess-list {{ color: #ff9999; font-size: 0.8rem; margin-top: 6px; padding-left: 8px; }}
615
+ .wrdler-incorrect-guesses .guess-item {{ margin: 2px 0; }}
616
  </style>
617
  <div class="score-header">Score: <span class="score-value">{score}</span></div>
618
+ {timer_html}
619
  <table class='shiny-border' style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing: 0;\">
620
  {table_inner}
621
  </table>
622
+ {incorrect_html}
623
  </div>
624
  '''
625
 
 
715
  html.append(f'<tr><td>{text}</td><td>{base}</td><td>+{bonus}</td><td>{points}</td></tr>')
716
 
717
  html.append('</table></div>')
718
+
719
+ # Show share URL if already generated
720
+ share_url = state.get("share_url")
721
+ if share_url:
722
+ html.append(f'''
723
+ <div class="share-challenge-result">
724
+ <h3>Share Your Challenge</h3>
725
+ <p style="color: #00bfa5; font-weight: 600;">Share link generated!</p>
726
+ <div class="share-url-display">
727
+ <code style="background: rgba(0,0,0,0.3); padding: 8px 12px; border-radius: 6px; display: block; word-break: break-all; color: #fff;">{share_url}</code>
728
+ </div>
729
+ </div>
730
+ ''')
731
+
732
  html.append('</div>')
733
 
734
  return "\n".join(html)
 
785
  def build_ui_outputs(state: Dict[str, Any], audio_html: str = "") -> tuple:
786
  """Build the complete UI output tuple for all handlers.
787
 
788
+ Returns: (48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, free_letter_row, share_btn, state)
789
  """
790
  grid_button_updates = get_all_button_updates(state)
791
  letter_button_updates = get_letter_button_updates(state)
792
 
793
+ # Show share button only when game is over AND show_challenge_links is enabled
794
+ show_share = state.get("game_over", False) and state.get("show_challenge_links", True)
795
+ share_btn_visible = gr.Button(visible=show_share)
796
+
797
+ # Show free letter section only if enabled
798
+ show_free_letters = state.get("enable_free_letters", True)
799
+ free_letter_row_visible = gr.Row(visible=show_free_letters)
800
+
801
  return (
802
  *grid_button_updates, # 48 grid button updates
803
  *letter_button_updates, # 26 letter button updates
 
805
  state.get("last_action", ""),
806
  audio_html,
807
  render_game_over_html(state),
808
+ render_free_letters_status(state) if show_free_letters else "",
809
+ free_letter_row_visible,
810
+ share_btn_visible,
811
  state
812
  )
813
 
 
825
  if state.get("game_over"):
826
  return build_ui_outputs(state)
827
 
828
+ # Check if free letters required first (only if free letters are enabled)
829
+ free_letters_enabled = state.get("enable_free_letters", True)
830
+ if free_letters_enabled and state["free_letters_used"] < MAX_FREE_LETTERS:
831
  state["last_action"] = f"Please choose {MAX_FREE_LETTERS - state['free_letters_used']} more free letter(s) first."
832
  return build_ui_outputs(state)
833
 
 
898
  grid_button_updates = get_all_button_updates(state)
899
  letter_button_updates = get_letter_button_updates(state)
900
 
901
+ # Show share button only when game is over AND show_challenge_links is enabled
902
+ show_share = state.get("game_over", False) and state.get("show_challenge_links", True)
903
+ share_btn_visible = gr.Button(visible=show_share)
904
+
905
+ # Show free letter section only if enabled
906
+ show_free_letters = state.get("enable_free_letters", True)
907
+ free_letter_row_visible = gr.Row(visible=show_free_letters)
908
+
909
  return (
910
  *grid_button_updates, # 48 grid button updates
911
  *letter_button_updates, # 26 letter button updates
 
914
  "" if clear_input else gr.update(), # guess input - clear or no change
915
  audio_html,
916
  render_game_over_html(state),
917
+ render_free_letters_status(state) if show_free_letters else "",
918
+ free_letter_row_visible,
919
+ share_btn_visible,
920
  state
921
  )
922
 
 
964
 
965
  def handle_new_game(
966
  wordlist: str,
967
+ ai_topic: str,
968
  game_mode: str,
969
  sfx_enabled: bool,
970
  sfx_volume: int,
971
  music_enabled: bool,
972
  music_volume: int,
973
+ show_incorrect: bool,
974
+ enable_free: bool,
975
+ show_challenge: bool,
976
  state: Dict[str, Any]
977
  ) -> tuple:
978
  """Handle new game creation."""
979
  state = ensure_state(state)
980
+ new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode, ai_topic=ai_topic)
981
 
982
  # Preserve audio settings from UI components
983
  new_state["sound_effects_enabled"] = sfx_enabled
 
985
  new_state["music_enabled"] = music_enabled
986
  new_state["music_volume"] = music_volume
987
 
988
+ # Preserve display settings from UI components
989
+ new_state["show_incorrect_guesses"] = show_incorrect
990
+ new_state["enable_free_letters"] = enable_free
991
+ new_state["show_challenge_links"] = show_challenge
992
+
993
+ # Update welcome message based on free letters setting
994
+ if not enable_free:
995
+ new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
996
+
997
  return build_ui_outputs(new_state)
998
 
999
 
 
1016
  return new_state
1017
 
1018
 
1019
+ def handle_share_challenge(
1020
+ username: str,
1021
+ state: Dict[str, Any]
1022
+ ) -> Tuple[str, str, Dict[str, Any]]:
1023
+ """Handle share challenge button click."""
1024
+ state = ensure_state(state)
1025
+
1026
+ if not state.get("game_over"):
1027
+ return "Game not finished yet.", "", state
1028
+
1029
+ username = username.strip() if username else "Anonymous"
1030
+ if not username:
1031
+ username = "Anonymous"
1032
+
1033
+ # Get game data
1034
+ word_list = [text for text, x, y, direction in state["puzzle_words"]]
1035
+ wordlist_source = state.get("wordlist", "classic.txt")
1036
+ game_mode = state.get("game_mode", "classic")
1037
+ score = state.get("score", 0)
1038
+
1039
+ # Calculate elapsed time
1040
+ start_time = state.get("start_time")
1041
+ end_time = state.get("end_time")
1042
+ if end_time and start_time:
1043
+ start = datetime.fromisoformat(start_time)
1044
+ end = datetime.fromisoformat(end_time)
1045
+ elapsed_seconds = int((end - start).total_seconds())
1046
+ else:
1047
+ elapsed_seconds = 0
1048
+
1049
+ # Check if this is a shared challenge being completed
1050
+ is_shared_game = state.get("challenge_sid") is not None
1051
+ existing_sid = state.get("challenge_sid")
1052
+
1053
+ try:
1054
+ if is_shared_game and existing_sid:
1055
+ # Add result to existing challenge
1056
+ success = add_user_result_to_game(
1057
+ sid=existing_sid,
1058
+ username=username,
1059
+ word_list=word_list,
1060
+ score=score,
1061
+ time_seconds=elapsed_seconds
1062
+ )
1063
+
1064
+ if success:
1065
+ share_url = get_shareable_url(existing_sid)
1066
+ new_state = copy.deepcopy(state)
1067
+ new_state["share_url"] = share_url
1068
+ new_state["share_sid"] = existing_sid
1069
+ return f"Result submitted for {username}!", share_url, new_state
1070
+ else:
1071
+ return "Failed to submit result.", "", state
1072
+ else:
1073
+ # Create new challenge
1074
+ challenge_id, full_url, sid = save_game_to_hf(
1075
+ word_list=word_list,
1076
+ username=username,
1077
+ score=score,
1078
+ time_seconds=elapsed_seconds,
1079
+ game_mode=game_mode,
1080
+ grid_size=6, # Wrdler: 6 rows
1081
+ spacer=0,
1082
+ may_overlap=False,
1083
+ wordlist_source=wordlist_source
1084
+ )
1085
+
1086
+ if sid:
1087
+ share_url = get_shareable_url(sid)
1088
+ new_state = copy.deepcopy(state)
1089
+ new_state["share_url"] = share_url
1090
+ new_state["share_sid"] = sid
1091
+ return f"Share link generated!", share_url, new_state
1092
+ else:
1093
+ return "Failed to generate share link.", "", state
1094
+
1095
+ except Exception as e:
1096
+ return f"Error: {str(e)}", "", state
1097
+
1098
+
1099
  # ---------------------------------------------------------------------------
1100
  # Main App Creation
1101
  # ---------------------------------------------------------------------------
 
1108
  if not wordlist_files:
1109
  wordlist_files = ["classic.txt"]
1110
 
1111
+ # Load CSS content from file
1112
  css_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "style_wrdler.css")
1113
+ css_content = None
1114
+ if os.path.exists(css_path):
1115
+ try:
1116
+ with open(css_path, "r", encoding="utf-8") as f:
1117
+ css_content = f.read()
1118
+ except Exception:
1119
+ css_content = None
1120
 
1121
  with gr.Blocks(
1122
+ css=css_content,
1123
  theme="Surn/beeuty",
1124
  title=f"Wrdler v{__version__}"
1125
  ) as demo:
 
1132
  gr.Markdown("Find all 6 hidden words in the 8×6 grid!")
1133
 
1134
  # Tab layout
1135
+ with gr.Tabs():
1136
  # Game Tab
1137
  with gr.TabItem("Game"):
1138
  with gr.Row():
1139
+ with gr.Column():
1140
+ # Topic input at top - styled as a prominent glowing badge
1141
+ # Get initial topic value
1142
+ initial_topic = wordlist_files[0].replace(".txt", "") if wordlist_files else "classic"
1143
+ topic_display = gr.Textbox(
1144
+ value=initial_topic,
1145
+ placeholder="Enter topic...",
1146
+ show_label=False,
1147
+ max_lines=1,
1148
+ lines=1,
1149
+ elem_id="topic-display",
1150
+ elem_classes=["topic-input"],
1151
+ interactive=True
1152
+ )
1153
+
1154
  # Challenge mode leaderboard (if in challenge mode)
1155
  challenge_leaderboard_html = gr.HTML(
1156
  value="",
1157
  elem_id="challenge-leaderboard-container"
1158
  )
1159
+ with gr.Row():
1160
+ # Left column - Game area
1161
+ with gr.Column(scale=5):
1162
+ # Free letter selection using button grid (wrapped in Row for visibility control)
1163
+ with gr.Row(elem_classes=["free-letter-section"], visible=True) as free_letter_row:
1164
+ with gr.Column():
1165
+ free_letter_status = gr.Markdown(
1166
+ value="Choose 2 free letters:",
1167
+ elem_id="free-letter-status"
 
 
 
 
 
 
 
 
 
1168
  )
1169
+ # Create letter buttons (only puzzle letters will be visible)
1170
+ letter_buttons: List[gr.Button] = []
1171
+ with gr.Row(elem_classes=["letter-buttons-row"]):
1172
+ for letter in ALL_LETTERS:
1173
+ btn = gr.Button(
1174
+ value=letter,
1175
+ variant="primary",
1176
+ size="sm",
1177
+ min_width=36,
1178
+ visible=False, # Will be shown for letters in puzzle
1179
+ elem_classes=["letter-btn-available"],
1180
+ elem_id=f"letter-{letter}"
1181
+ )
1182
+ letter_buttons.append(btn)
1183
 
1184
  # Game grid using native Gradio buttons
1185
  # Create 48 buttons (6 rows x 8 cols) in a CSS grid
 
1192
  value="?",
1193
  variant="primary",
1194
  size="sm",
1195
+ min_width=20,
1196
  elem_classes=["grid-cell-unrevealed"],
1197
  elem_id=f"cell-{row}-{col}"
1198
  )
 
1200
 
1201
  # Guess form
1202
  with gr.Row():
1203
+ # New Game button
1204
+ new_game_btn = gr.Button("New Game", variant="primary", size="lg")
1205
+
 
 
 
 
1206
 
1207
  # Status message
1208
+ status_msg = gr.Markdown(value="Welcome!")
1209
 
1210
  # Game over display
1211
  game_over_html = gr.HTML(value="", elem_id="game-over-container")
1212
 
1213
+ # Share Challenge Button (visible after game over)
1214
+ share_challenge_btn = gr.Button(
1215
+ "🎮 Share Your Challenge",
1216
+ variant="primary",
1217
+ visible=False,
1218
+ elem_id="share-challenge-btn",
1219
+ elem_classes=["share-challenge-trigger"]
1220
+ )
1221
+
1222
  # Right column - Score panel and New Game
1223
  with gr.Column(scale=3):
1224
+ guess_input = gr.Textbox(
1225
+ label="Your Guess",
1226
+ placeholder="Enter a word...",
1227
+ max_lines=1,
1228
+ elem_id="guess-input"
1229
+ )
1230
+ guess_btn = gr.Button("Guess", variant="primary", elem_id="guess-btn")
1231
  # Score panel
1232
  score_panel_html = gr.HTML(
1233
  value=lambda: render_score_panel_html(create_new_game_state()),
1234
  elem_id="score-panel-container"
1235
  )
1236
+
1237
+
1238
 
1239
  # Settings Tab
1240
  with gr.TabItem("Settings"):
1241
  with gr.Row():
1242
  with gr.Column():
1243
  gr.Markdown("### Game Settings")
1244
+ # Add "AI Generated" option to wordlist choices
1245
+ wordlist_choices = ["AI Generated"] + wordlist_files
1246
  wordlist_dropdown = gr.Dropdown(
1247
+ choices=wordlist_choices,
1248
  value=wordlist_files[0] if wordlist_files else "classic.txt",
1249
  label="Word List"
1250
  )
1251
+ # AI Topic textbox and submit button (visible only when AI Generated is selected)
1252
+ with gr.Row(visible=False) as ai_topic_row:
1253
+ ai_topic_input = gr.Textbox(
1254
+ label="AI Topic",
1255
+ placeholder="Enter a topic (e.g., Ocean Life, Space, Cooking)",
1256
+ value="",
1257
+ scale=3,
1258
+ elem_id="ai-topic-input"
1259
+ )
1260
+ ai_topic_submit = gr.Button(
1261
+ "Generate",
1262
+ variant="primary",
1263
+ scale=1,
1264
+ elem_id="ai-topic-submit"
1265
+ )
1266
  game_mode_dropdown = gr.Dropdown(
1267
  choices=["classic", "easy", "too easy"],
1268
  value="classic",
1269
  label="Game Mode"
1270
  )
1271
+ show_incorrect_guesses = gr.Checkbox(
1272
+ value=True,
1273
+ label="Show Incorrect Guesses"
1274
+ )
1275
+ enable_free_letters = gr.Checkbox(
1276
+ value=True,
1277
+ label="Enable Free Letters"
1278
+ )
1279
+ show_challenge_links = gr.Checkbox(
1280
+ value=True,
1281
+ label="Show Challenge Share Links"
1282
+ )
1283
 
1284
  with gr.Column():
1285
  gr.Markdown("### Audio Settings")
 
1309
  # Hidden audio player for sound effects
1310
  audio_player_html = gr.HTML(value="", elem_id="audio-player", visible=False)
1311
 
1312
+ # Timer for updating elapsed time (ticks every second)
1313
+ game_timer = gr.Timer(value=1, active=True)
1314
+
1315
+ # Share Challenge Modal Dialog (outside tabs)
1316
+ with Modal(visible=False) as share_modal:
1317
+ gr.Markdown("## 🎮 Share Your Challenge")
1318
+ gr.Markdown("Challenge your friends to beat your score!")
1319
+ username_input = gr.Textbox(
1320
+ label="Your Name (optional)",
1321
+ placeholder="Anonymous",
1322
+ max_lines=1,
1323
+ elem_id="username-input"
1324
+ )
1325
+ share_btn = gr.Button("Generate Share Link", variant="primary", size="lg")
1326
+ share_status = gr.Markdown(value="", elem_id="share-status")
1327
+ share_url_display = gr.Textbox(
1328
+ label="Share URL (click to copy)",
1329
+ value="",
1330
+ interactive=False,
1331
+ visible=False,
1332
+ elem_id="share-url-display"
1333
+ )
1334
+ close_modal_btn = gr.Button("Close", variant="secondary")
1335
+
1336
  # Define common outputs for handlers
1337
+ # Order: 48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, free_letter_row, share_btn_visible, state
1338
  common_outputs = [
1339
  *grid_buttons,
1340
  *letter_buttons,
 
1343
  audio_player_html,
1344
  game_over_html,
1345
  free_letter_status,
1346
+ free_letter_row,
1347
+ share_challenge_btn,
1348
  game_state
1349
  ]
1350
 
 
1358
  audio_player_html,
1359
  game_over_html,
1360
  free_letter_status,
1361
+ free_letter_row,
1362
+ share_challenge_btn,
1363
  game_state
1364
  ]
1365
 
 
1395
  outputs=guess_outputs
1396
  )
1397
 
1398
+ # Helper to get topic display value
1399
+ def get_topic_display_value(wordlist, ai_topic):
1400
+ """Get the display value for the topic textbox."""
1401
+ if wordlist == "AI Generated" and ai_topic.strip():
1402
+ return ai_topic.strip()
1403
+ else:
1404
+ return wordlist.replace(".txt", "") if wordlist else "classic"
1405
+
1406
  # New game button
1407
  new_game_btn.click(
1408
  fn=handle_new_game,
1409
+ inputs=[wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, show_challenge_links, game_state],
1410
+ outputs=common_outputs
1411
+ ).then(
1412
+ fn=lambda: gr.Timer(active=True),
1413
+ inputs=None,
1414
+ outputs=game_timer
1415
+ ).then(
1416
+ fn=get_topic_display_value,
1417
+ inputs=[wordlist_dropdown, ai_topic_input],
1418
+ outputs=topic_display
1419
+ )
1420
+
1421
+ # Wordlist dropdown change handler - show/hide AI topic input row and update topic display
1422
+ def handle_wordlist_change(wordlist, ai_topic):
1423
+ """Show AI topic input when AI Generated is selected and update topic display."""
1424
+ is_ai = wordlist == "AI Generated"
1425
+ topic_value = ai_topic.strip() if is_ai and ai_topic.strip() else wordlist.replace(".txt", "")
1426
+ return gr.Row(visible=is_ai), gr.Textbox(value=topic_value)
1427
+
1428
+ wordlist_dropdown.change(
1429
+ fn=handle_wordlist_change,
1430
+ inputs=[wordlist_dropdown, ai_topic_input],
1431
+ outputs=[ai_topic_row, topic_display]
1432
+ )
1433
+
1434
+ # AI Topic submit button - generates new game with AI words
1435
+ ai_topic_submit.click(
1436
+ fn=handle_new_game,
1437
+ inputs=[wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, show_challenge_links, game_state],
1438
  outputs=common_outputs
1439
+ ).then(
1440
+ fn=lambda: gr.Timer(active=True),
1441
+ inputs=None,
1442
+ outputs=game_timer
1443
+ ).then(
1444
+ fn=lambda topic: topic.strip() if topic.strip() else "AI Generated",
1445
+ inputs=[ai_topic_input],
1446
+ outputs=topic_display
1447
+ )
1448
+
1449
+ # AI Topic textbox submit (pressing Enter)
1450
+ ai_topic_input.submit(
1451
+ fn=handle_new_game,
1452
+ inputs=[wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, show_challenge_links, game_state],
1453
+ outputs=common_outputs
1454
+ ).then(
1455
+ fn=lambda: gr.Timer(active=True),
1456
+ inputs=None,
1457
+ outputs=game_timer
1458
+ ).then(
1459
+ fn=lambda topic: topic.strip() if topic.strip() else "AI Generated",
1460
+ inputs=[ai_topic_input],
1461
+ outputs=topic_display
1462
+ )
1463
+
1464
+ # Topic display submit handler - check if matches wordlist or use as AI topic
1465
+ def handle_topic_submit(topic_input, game_mode, sfx_en, sfx_vol, music_en, music_vol, show_inc, enable_fl, show_chal, state):
1466
+ """Handle topic input - match wordlist or use as AI topic."""
1467
+ state = ensure_state(state)
1468
+ topic_input = topic_input.strip()
1469
+
1470
+ # Check if input matches a wordlist file (with or without .txt)
1471
+ matched_wordlist = None
1472
+ for wf in wordlist_files:
1473
+ name_without_ext = wf.replace(".txt", "")
1474
+ if topic_input.lower() == wf.lower() or topic_input.lower() == name_without_ext.lower():
1475
+ matched_wordlist = wf
1476
+ break
1477
+
1478
+ if matched_wordlist:
1479
+ # Use existing wordlist
1480
+ new_state = create_new_game_state(wordlist=matched_wordlist, game_mode=game_mode, ai_topic="")
1481
+ wordlist_value = matched_wordlist
1482
+ ai_topic_value = ""
1483
+ ai_row_visible = False
1484
+ else:
1485
+ # Use as AI topic
1486
+ new_state = create_new_game_state(wordlist="AI Generated", game_mode=game_mode, ai_topic=topic_input)
1487
+ wordlist_value = "AI Generated"
1488
+ ai_topic_value = topic_input
1489
+ ai_row_visible = True
1490
+
1491
+ # Preserve settings
1492
+ new_state["sound_effects_enabled"] = sfx_en
1493
+ new_state["sound_effects_volume"] = sfx_vol
1494
+ new_state["music_enabled"] = music_en
1495
+ new_state["music_volume"] = music_vol
1496
+ new_state["show_incorrect_guesses"] = show_inc
1497
+ new_state["enable_free_letters"] = enable_fl
1498
+ new_state["show_challenge_links"] = show_chal
1499
+
1500
+ if not enable_fl:
1501
+ new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
1502
+
1503
+ # Build outputs + settings updates
1504
+ base_outputs = build_ui_outputs(new_state)
1505
+ return (
1506
+ *base_outputs,
1507
+ gr.Dropdown(value=wordlist_value), # wordlist_dropdown
1508
+ gr.Textbox(value=ai_topic_value), # ai_topic_input
1509
+ gr.Row(visible=ai_row_visible), # ai_topic_row
1510
+ )
1511
+
1512
+ # Extended outputs for topic submit (includes settings updates)
1513
+ topic_submit_outputs = common_outputs + [wordlist_dropdown, ai_topic_input, ai_topic_row]
1514
+
1515
+ topic_display.submit(
1516
+ fn=handle_topic_submit,
1517
+ inputs=[topic_display, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, show_challenge_links, game_state],
1518
+ outputs=topic_submit_outputs
1519
+ ).then(
1520
+ fn=lambda: gr.Timer(active=True),
1521
+ inputs=None,
1522
+ outputs=game_timer
1523
  )
1524
 
1525
  # Audio settings change handlers
 
1544
  outputs=[game_state]
1545
  )
1546
 
1547
+ # Show incorrect guesses toggle handler
1548
+ def handle_show_incorrect_change(show_incorrect, state):
1549
+ state = ensure_state(state)
1550
+ new_state = copy.deepcopy(state)
1551
+ new_state["show_incorrect_guesses"] = show_incorrect
1552
+ return new_state
1553
+
1554
+ show_incorrect_guesses.change(
1555
+ fn=handle_show_incorrect_change,
1556
+ inputs=[show_incorrect_guesses, game_state],
1557
+ outputs=[game_state]
1558
+ )
1559
+
1560
+ # Enable free letters toggle handler
1561
+ def handle_enable_free_letters_change(enabled, state):
1562
+ state = ensure_state(state)
1563
+ new_state = copy.deepcopy(state)
1564
+ new_state["enable_free_letters"] = enabled
1565
+ # Update message based on setting
1566
+ if not enabled:
1567
+ new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
1568
+ elif new_state.get("free_letters_used", 0) < MAX_FREE_LETTERS:
1569
+ remaining = MAX_FREE_LETTERS - new_state.get("free_letters_used", 0)
1570
+ new_state["last_action"] = f"Choose {remaining} free letter{'s' if remaining > 1 else ''} to start."
1571
+ return build_ui_outputs(new_state)
1572
+
1573
+ enable_free_letters.change(
1574
+ fn=handle_enable_free_letters_change,
1575
+ inputs=[enable_free_letters, game_state],
1576
+ outputs=common_outputs
1577
+ )
1578
+
1579
+ # Show challenge links toggle handler
1580
+ def handle_show_challenge_links_change(enabled, state):
1581
+ state = ensure_state(state)
1582
+ new_state = copy.deepcopy(state)
1583
+ new_state["show_challenge_links"] = enabled
1584
+ return build_ui_outputs(new_state)
1585
+
1586
+ show_challenge_links.change(
1587
+ fn=handle_show_challenge_links_change,
1588
+ inputs=[show_challenge_links, game_state],
1589
+ outputs=common_outputs
1590
+ )
1591
+
1592
+ # Share challenge button handler
1593
+ def on_share_click(username, state):
1594
+ """Handle share button click."""
1595
+ status_text, url, new_state = handle_share_challenge(username, state)
1596
+ url_visible = bool(url)
1597
+ return (
1598
+ status_text,
1599
+ gr.Textbox(value=url, visible=url_visible),
1600
+ new_state
1601
+ )
1602
+
1603
+ share_btn.click(
1604
+ fn=on_share_click,
1605
+ inputs=[username_input, game_state],
1606
+ outputs=[share_status, share_url_display, game_state]
1607
+ )
1608
+
1609
+ # Modal open/close handlers
1610
+ share_challenge_btn.click(
1611
+ fn=lambda: Modal(visible=True),
1612
+ inputs=None,
1613
+ outputs=share_modal
1614
+ )
1615
+
1616
+ close_modal_btn.click(
1617
+ fn=lambda: Modal(visible=False),
1618
+ inputs=None,
1619
+ outputs=share_modal
1620
+ )
1621
+
1622
+ # Timer tick handler - updates score panel with current elapsed time
1623
+ def handle_timer_tick(state):
1624
+ """Update score panel on timer tick."""
1625
+ state = ensure_state(state)
1626
+ if state.get("game_over"):
1627
+ # Stop updating when game is over
1628
+ return render_score_panel_html(state), gr.Timer(active=False)
1629
+ return render_score_panel_html(state), gr.Timer(active=True)
1630
+
1631
+ game_timer.tick(
1632
+ fn=handle_timer_tick,
1633
+ inputs=[game_state],
1634
+ outputs=[score_panel_html, game_timer]
1635
+ )
1636
+
1637
  # Initialize UI on app load
1638
  def initialize_ui(state):
1639
  """Initialize UI components on app load."""
wrdler/word_loader.py CHANGED
@@ -3,11 +3,25 @@ from __future__ import annotations
3
  import re
4
  import os
5
  import string
 
 
6
  from typing import Dict, List, Optional
7
-
8
- import streamlit as st
9
  from importlib import resources
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  # Minimal built-ins used if the external file is missing or too small
13
  FALLBACK_WORDS: Dict[int, List[str]] = {
@@ -33,7 +47,15 @@ def get_wordlist_files() -> list[str]:
33
  return sorted(files)
34
 
35
 
36
- @st.cache_data(show_spinner=False)
 
 
 
 
 
 
 
 
37
  def load_word_list(selected_file: Optional[str] = None) -> Dict[int, List[str]]:
38
  """
39
  Load a word list, filter to uppercase A–Z, lengths in {4,5,6}, and dedupe while preserving order.
@@ -389,7 +411,7 @@ def compute_word_difficulties(file_path, words_array=None):
389
  return total_difficulty, difficulties
390
 
391
 
392
- @st.cache_data(show_spinner=False)
393
  def load_word_list_or_ai(
394
  use_ai: bool = False,
395
  topic: str = "English",
 
3
  import re
4
  import os
5
  import string
6
+ import logging
7
+ from functools import lru_cache
8
  from typing import Dict, List, Optional
 
 
9
  from importlib import resources
10
 
11
+ # Configure logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Streamlit caching - only use if we're actually in a Streamlit runtime
15
+ _USE_STREAMLIT_CACHE = False
16
+ try:
17
+ import streamlit as st
18
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
19
+ # Check if we're actually in a Streamlit runtime
20
+ if get_script_run_ctx() is not None:
21
+ _USE_STREAMLIT_CACHE = True
22
+ except Exception:
23
+ pass
24
+
25
 
26
  # Minimal built-ins used if the external file is missing or too small
27
  FALLBACK_WORDS: Dict[int, List[str]] = {
 
47
  return sorted(files)
48
 
49
 
50
+ def _cache_decorator(func):
51
+ """Apply appropriate caching based on runtime environment."""
52
+ if _USE_STREAMLIT_CACHE:
53
+ return st.cache_data(show_spinner=False)(func)
54
+ else:
55
+ return lru_cache(maxsize=32)(func)
56
+
57
+
58
+ @_cache_decorator
59
  def load_word_list(selected_file: Optional[str] = None) -> Dict[int, List[str]]:
60
  """
61
  Load a word list, filter to uppercase A–Z, lengths in {4,5,6}, and dedupe while preserving order.
 
411
  return total_difficulty, difficulties
412
 
413
 
414
+ @_cache_decorator
415
  def load_word_list_or_ai(
416
  use_ai: bool = False,
417
  topic: str = "English",
wrdler/word_loader_ai.py CHANGED
@@ -6,16 +6,23 @@ import re
6
  import string
7
  import logging
8
  from datetime import datetime
 
9
  from typing import Dict, List, Optional, Tuple
10
 
11
  # Configure logging
12
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
13
  logger = logging.getLogger(__name__)
14
 
15
- # Streamlit is optional; degrade gracefully if absent
 
16
  try:
17
  import streamlit as st
 
 
 
 
18
  except Exception: # pragma: no cover
 
19
  class _Stub:
20
  def cache_resource(self, **_): # type: ignore
21
  def deco(fn):
@@ -127,7 +134,15 @@ def _generate_via_hf_space(topic: str) -> Tuple[str, str]:
127
  # Model Loading
128
  # ---------------------------------------------------------------------------
129
 
130
- @st.cache_resource(show_spinner=False)
 
 
 
 
 
 
 
 
131
  def _load_model(model_name: str = DEFAULT_MODEL_NAME):
132
  """
133
  Try to load the requested model first, then fall back through AI_MODELS in order.
 
6
  import string
7
  import logging
8
  from datetime import datetime
9
+ from functools import lru_cache
10
  from typing import Dict, List, Optional, Tuple
11
 
12
  # Configure logging
13
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
14
  logger = logging.getLogger(__name__)
15
 
16
+ # Streamlit caching - only use if we're actually in a Streamlit runtime
17
+ _USE_STREAMLIT_CACHE = False
18
  try:
19
  import streamlit as st
20
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
21
+ # Check if we're actually in a Streamlit runtime
22
+ if get_script_run_ctx() is not None:
23
+ _USE_STREAMLIT_CACHE = True
24
  except Exception: # pragma: no cover
25
+ # Create stub for when streamlit isn't installed
26
  class _Stub:
27
  def cache_resource(self, **_): # type: ignore
28
  def deco(fn):
 
134
  # Model Loading
135
  # ---------------------------------------------------------------------------
136
 
137
+ def _resource_cache_decorator(func):
138
+ """Apply appropriate caching for resource loading based on runtime environment."""
139
+ if _USE_STREAMLIT_CACHE:
140
+ return st.cache_resource(show_spinner=False)(func)
141
+ else:
142
+ return lru_cache(maxsize=4)(func)
143
+
144
+
145
+ @_resource_cache_decorator
146
  def _load_model(model_name: str = DEFAULT_MODEL_NAME):
147
  """
148
  Try to load the requested model first, then fall back through AI_MODELS in order.
wrdler/words/cooking.txt CHANGED
@@ -1,24 +1,35 @@
1
  # AI-generated word list
2
- # Topic: COOKing
3
- # Last updated: 2025-11-28 12:16:45
4
- # Total words: 174
5
  # Format: one word per line, sorted by length then alphabetically
6
  #
7
  BAKE
 
8
  BEEN
9
  BLUE
10
  BOIL
 
 
11
  BUFF
12
  CAKE
13
  CALV
14
  CARD
 
15
  COOK
 
 
 
 
 
 
16
  FARM
17
  FAST
18
  FILL
19
  FIRE
20
  FISH
21
  FIZZ
 
22
  FOOD
23
  FRYD
24
  HARV
@@ -27,75 +38,121 @@ JUST
27
  KEEN
28
  KERN
29
  KING
 
 
30
  MAKE
31
  MANU
32
  MASH
33
  MEAT
 
34
  MINI
 
 
 
 
35
  PICK
36
  PLAN
 
37
  POTS
38
  POUR
39
  PREP
40
  PUFF
41
  RACK
 
42
  RECT
 
43
  ROLL
44
  SEAR
45
  SEEK
 
46
  SIFT
47
  SIMP
48
  SKIL
49
  SKIP
50
  SLOW
 
51
  SOFT
52
  SOUP
53
  STEW
54
  STIR
 
55
  TAKE
 
 
56
  THAW
 
 
57
  TURK
58
  VEAL
 
59
  WARM
60
  WASH
 
61
  WELL
62
  WHIP
63
  WRAP
 
64
  BACON
65
  BAKED
66
  BASTE
 
 
 
 
 
 
67
  BLANK
68
  BLEND
69
  BLTIG
70
  BOATS
71
  BOILD
72
  BREAD
 
 
73
  BROWN
74
  BUTTI
 
75
  CANDY
76
  CARBS
77
  CHICK
78
  CHILL
 
 
79
  CLEAN
 
80
  CRUMB
81
  CRUST
 
82
  DRIED
83
  FLAKY
 
84
  FRIED
85
  FRUIT
 
86
  FRYER
 
87
  GLAZE
88
  GRAIN
89
  GRASS
90
  GRILL
 
 
91
  HARVE
92
  HERBS
93
  HOUSE
 
 
94
  KITCH
95
  KNEAD
 
96
  MARIN
 
97
  MIXED
98
  MIXER
 
 
 
 
99
  PASTA
100
  PLATE
101
  PREPS
@@ -107,19 +164,29 @@ SALAD
107
  SALTY
108
  SAUTE
109
  SCONE
 
 
110
  SHAVE
111
  SHRIL
112
  SLICE
113
  SLICK
114
  SMAKE
 
 
 
 
115
  SPICE
116
  SPICY
117
  SPIRE
 
118
  START
119
  STEAK
 
120
  STICK
121
  STIRD
122
  STIRP
 
 
123
  STOVE
124
  SWEET
125
  TASTE
@@ -136,45 +203,74 @@ WHISK
136
  WIELD
137
  YIELD
138
  ZESTY
 
139
  ASSERT
 
 
140
  BARELY
141
  BARKED
142
  BAROBA
143
  BATTER
144
  BAUGHT
145
  BAZAAR
 
146
  BITTER
147
  BOILED
 
148
  BROWNY
 
149
  CARROT
150
  CHEESE
 
151
  COOKED
152
  COOKER
153
  COOKIN
154
  DETAIL
155
  DILUTE
 
156
  EATING
 
157
  FLOURD
158
  FLYING
159
  FROZEN
 
 
160
  GRATED
161
  GRILLD
 
162
  KNEADD
 
163
  LITTLE
164
  MARINA
165
  MASHED
166
  METHOD
 
167
  MUFFIN
168
  NOODLE
 
 
169
  PACKED
 
170
  PICKED
 
171
  PIERCE
172
  RECIPE
 
 
 
 
 
173
  SHIELD
174
  SMELLY
175
  SMOOTH
176
  SNAKES
177
  SPICED
 
 
 
178
  TASTES
 
 
179
  TOUCHS
180
  VEGGIE
 
 
1
  # AI-generated word list
2
+ # Topic: Cooking
3
+ # Last updated: 2025-11-29 15:33:26
4
+ # Total words: 270
5
  # Format: one word per line, sorted by length then alphabetically
6
  #
7
  BAKE
8
+ BEAN
9
  BEEN
10
  BLUE
11
  BOIL
12
+ BOWE
13
+ BUCK
14
  BUFF
15
  CAKE
16
  CALV
17
  CARD
18
+ CHOP
19
  COOK
20
+ CUBE
21
+ CURE
22
+ DASH
23
+ DINE
24
+ EGGS
25
+ EGGY
26
  FARM
27
  FAST
28
  FILL
29
  FIRE
30
  FISH
31
  FIZZ
32
+ FOIL
33
  FOOD
34
  FRYD
35
  HARV
 
38
  KEEN
39
  KERN
40
  KING
41
+ KITT
42
+ LICK
43
  MAKE
44
  MANU
45
  MASH
46
  MEAT
47
+ MEGA
48
  MINI
49
+ OVEN
50
+ PANE
51
+ PEEK
52
+ PEEL
53
  PICK
54
  PLAN
55
+ PLUM
56
  POTS
57
  POUR
58
  PREP
59
  PUFF
60
  RACK
61
+ RAWY
62
  RECT
63
+ RIPE
64
  ROLL
65
  SEAR
66
  SEEK
67
+ SHAK
68
  SIFT
69
  SIMP
70
  SKIL
71
  SKIP
72
  SLOW
73
+ SMOK
74
  SOFT
75
  SOUP
76
  STEW
77
  STIR
78
+ STOV
79
  TAKE
80
+ TART
81
+ TASI
82
  THAW
83
+ TRIM
84
+ TUBE
85
  TURK
86
  VEAL
87
+ VINE
88
  WARM
89
  WASH
90
+ WELD
91
  WELL
92
  WHIP
93
  WRAP
94
+ ZOOM
95
  BACON
96
  BAKED
97
  BASTE
98
+ BATCH
99
+ BERRY
100
+ BESTE
101
+ BEVAP
102
+ BITCH
103
+ BLACK
104
  BLANK
105
  BLEND
106
  BLTIG
107
  BOATS
108
  BOILD
109
  BREAD
110
+ BREAK
111
+ BRINE
112
  BROWN
113
  BUTTI
114
+ CANDS
115
  CANDY
116
  CARBS
117
  CHICK
118
  CHILL
119
+ CHOPS
120
+ CHUTE
121
  CLEAN
122
+ CRACK
123
  CRUMB
124
  CRUST
125
+ DINER
126
  DRIED
127
  FLAKY
128
+ FRICK
129
  FRIED
130
  FRUIT
131
+ FRYED
132
  FRYER
133
+ GAMES
134
  GLAZE
135
  GRAIN
136
  GRASS
137
  GRILL
138
+ GRIME
139
+ GUESS
140
  HARVE
141
  HERBS
142
  HOUSE
143
+ JELLY
144
+ JUICE
145
  KITCH
146
  KNEAD
147
+ KNIFE
148
  MARIN
149
+ METAL
150
  MIXED
151
  MIXER
152
+ OVENS
153
+ OVENY
154
+ PANDA
155
+ PANGS
156
  PASTA
157
  PLATE
158
  PREPS
 
164
  SALTY
165
  SAUTE
166
  SCONE
167
+ SCRAP
168
+ SHARP
169
  SHAVE
170
  SHRIL
171
  SLICE
172
  SLICK
173
  SMAKE
174
+ SMALL
175
+ SMOKE
176
+ SNACK
177
+ SPECK
178
  SPICE
179
  SPICY
180
  SPIRE
181
+ SPLAT
182
  START
183
  STEAK
184
+ STEAM
185
  STICK
186
  STIRD
187
  STIRP
188
+ STIRR
189
+ STOCK
190
  STOVE
191
  SWEET
192
  TASTE
 
203
  WIELD
204
  YIELD
205
  ZESTY
206
+ AROMAS
207
  ASSERT
208
+ BAKERY
209
+ BAKING
210
  BARELY
211
  BARKED
212
  BAROBA
213
  BATTER
214
  BAUGHT
215
  BAZAAR
216
+ BITOUT
217
  BITTER
218
  BOILED
219
+ BREADS
220
  BROWNY
221
+ BUCKET
222
  CARROT
223
  CHEESE
224
+ CHEFES
225
  COOKED
226
  COOKER
227
  COOKIN
228
  DETAIL
229
  DILUTE
230
+ DINING
231
  EATING
232
+ FLAVOR
233
  FLOURD
234
  FLYING
235
  FROZEN
236
+ FRYING
237
+ FRYOUT
238
  GRATED
239
  GRILLD
240
+ KITTEN
241
  KNEADD
242
+ LIQUID
243
  LITTLE
244
  MARINA
245
  MASHED
246
  METHOD
247
+ MIXOUT
248
  MUFFIN
249
  NOODLE
250
+ OVENED
251
+ OVENER
252
  PACKED
253
+ PANELS
254
  PICKED
255
+ PICKLE
256
  PIERCE
257
  RECIPE
258
+ REMAIN
259
+ REPEAT
260
+ SALAMI
261
+ SALTED
262
+ SEASON
263
  SHIELD
264
  SMELLY
265
  SMOOTH
266
  SNAKES
267
  SPICED
268
+ SPICES
269
+ SPLEND
270
+ STOVEL
271
  TASTES
272
+ THAWED
273
+ TOMATO
274
  TOUCHS
275
  VEGGIE
276
+ WRIGHT