Spaces:
Running
Running
Add Gradio UI and AI Topic Generation Features
Browse filesVersion 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 +62 -11
- README.md +414 -90
- pyproject.toml +1 -1
- requirements.txt +2 -1
- specs/requirements.md +120 -3
- specs/specs.md +8 -0
- style_wrdler.css +338 -7
- wrdler/__init__.py +1 -1
- wrdler/gradio_ui.py +570 -75
- wrdler/word_loader.py +26 -4
- wrdler/word_loader_ai.py +17 -2
- wrdler/words/cooking.txt +99 -3
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.
|
| 11 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 12 |
**Live Demo:** [DEPLOYMENT_URL_HERE]
|
| 13 |
|
| 14 |
## Recent Changes
|
| 15 |
|
| 16 |
-
**v0.1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 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.
|
|
|
|
| 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-
|
| 536 |
-
**Current Version:** 0.1.
|
| 537 |
-
**Status:** Production Ready -
|
|
|
|
| 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 |
-
|
| 129 |
-
```
|
| 130 |
-
|
|
|
|
| 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 |
-
- `
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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`
|
| 288 |
-
- `sdk_version`: Latest supported Streamlit
|
| 289 |
-
- `python_version`: Python version (
|
| 290 |
-
- `app_file`: Entry point for your app.
|
|
|
|
| 291 |
- `tags`: List of descriptive tags.
|
| 292 |
|
| 293 |
-
**
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
-
|
| 297 |
-
Streamlit Spaces use port `8501` by default.
|
| 298 |
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
|
| 303 |
-
<iframe src="https://[YourUsername]-Wrdler.hf.space?embed=true" title="Wrdler"></iframe>
|
| 304 |
-
```
|
| 305 |
|
| 306 |
-
|
| 307 |
|
| 308 |
-
|
| 309 |
|
| 310 |
-
|
| 311 |
|
| 312 |
-
|
| 313 |
-
- Place your sound effect files (`.mp3` or `.wav`) in `wrdler/assets/audio/effects/` for sound effects.
|
| 314 |
|
| 315 |
-
|
| 316 |
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
-
|
| 320 |
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
|
|
|
| 327 |
|
| 328 |
-
|
| 329 |
-
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
```
|
| 337 |
|
| 338 |
-
## Parameters
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
-
|
| 345 |
|
| 346 |
-
|
| 347 |
|
| 348 |
-
|
| 349 |
|
| 350 |
-
To
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
-
|
| 353 |
-
|
|
|
|
|
|
|
| 354 |
```
|
| 355 |
|
| 356 |
-
|
|
|
|
|
|
|
| 357 |
|
| 358 |
-
```
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
```
|
| 361 |
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
-
|
| 365 |
|
| 366 |
-
|
| 367 |
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
-
##
|
|
|
|
| 371 |
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
-
|
| 387 |
-
-
|
| 388 |
-
-
|
| 389 |
-
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
-
|
| 396 |
-
-
|
| 397 |
-
-
|
| 398 |
-
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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>=
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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:
|
| 24 |
margin: 0 auto 16px auto;
|
| 25 |
-
font-weight:
|
| 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: #
|
| 124 |
cursor: default !important;
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
-
/* Empty revealed cells
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
(
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
}
|
| 527 |
-
</
|
| 528 |
-
|
| 529 |
-
|
| 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 |
-
|
|
|
|
| 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
|
| 920 |
css_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "style_wrdler.css")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 921 |
|
| 922 |
with gr.Blocks(
|
| 923 |
-
css=
|
| 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 |
-
|
| 941 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 |
-
|
| 988 |
-
|
| 989 |
-
|
| 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!
|
| 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 |
-
|
| 1009 |
-
|
| 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=
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
@
|
| 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
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
# Last updated: 2025-11-
|
| 4 |
-
# Total words:
|
| 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
|