0.0.4
Browse files- CLAUDE.md +126 -30
- RELEASE_NOTES_v0.0.2.md +0 -376
- cleanup_radar_comments.py +0 -28
- remove_radar_functions.py +0 -51
- remove_radar_state.py +0 -60
- specs/wrdler_implementation_plan.md +0 -232
- test_sprint5_grid.py +0 -84
- test_sprint6_integration.py +0 -413
- wrdler/__init__.py +1 -1
- wrdler/game_storage.py +66 -29
- wrdler/ui.py +62 -57
CLAUDE.md
CHANGED
|
@@ -7,24 +7,31 @@ 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.0.
|
| 11 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 12 |
**Live Demo:** [DEPLOYMENT_URL_HERE]
|
| 13 |
|
| 14 |
## Recent Changes
|
| 15 |
|
| 16 |
-
**v0.0.
|
| 17 |
-
- β
|
|
|
|
|
|
|
|
|
|
| 18 |
- β
100% test coverage (25/25 tests passing)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
- β
Core data models updated for rectangular 8Γ6 grid
|
| 20 |
- β
Generator refactored for horizontal-only, one-per-row placement
|
| 21 |
- β
Radar/scope visualization removed (~217 lines)
|
| 22 |
- β
Free letter selection UI with circular green gradient buttons
|
| 23 |
- β
Grid UI updated for 8Γ6 display with responsive layout
|
| 24 |
- β
Comprehensive integration testing suite
|
| 25 |
-
- β
Complete documentation (GAMEPLAY_GUIDE.md
|
| 26 |
- β
Fixed duplicate rendering call bug
|
| 27 |
-
-
|
| 28 |
|
| 29 |
## Core Gameplay
|
| 30 |
- 8x6 grid with 6 hidden words (one per row, horizontal only)
|
|
@@ -38,8 +45,8 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, with these k
|
|
| 38 |
- 10 incorrect guess limit per game
|
| 39 |
- **β
IMPLEMENTED:** Challenge Mode with game sharing via short URLs
|
| 40 |
- **β
IMPLEMENTED:** Remote storage via Hugging Face datasets
|
| 41 |
-
- **β
IMPLEMENTED:** PWA install support
|
| 42 |
-
- **PLANNED:** Local persistent storage for game results and high scores
|
| 43 |
|
| 44 |
### Scoring Tiers
|
| 45 |
- **Fantastic:** 42+ points
|
|
@@ -51,19 +58,20 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, with these k
|
|
| 51 |
|
| 52 |
### Technology Stack
|
| 53 |
- **Framework:** Streamlit 1.51.0
|
| 54 |
-
- **Language:** Python 3.12.8
|
| 55 |
-
- **Visualization:** Matplotlib
|
| 56 |
-
- **
|
| 57 |
-
- **Storage:**
|
|
|
|
| 58 |
- **Testing:** Pytest
|
| 59 |
-
- **Package Manager:** UV
|
| 60 |
|
| 61 |
### Project Structure
|
| 62 |
```
|
| 63 |
wrdler/
|
| 64 |
βββ app.py # Streamlit entry point
|
| 65 |
βββ wrdler/ # Main package
|
| 66 |
-
β βββ __init__.py # Version: 0.0.
|
| 67 |
β βββ models.py # Data models (Coord, Word, Puzzle, GameState)
|
| 68 |
β βββ generator.py # Puzzle generation with deterministic seeding
|
| 69 |
β βββ logic.py # Game mechanics (reveal, guess, scoring)
|
|
@@ -102,7 +110,7 @@ wrdler/
|
|
| 102 |
βββ README.md # User-facing documentation
|
| 103 |
βββ CLAUDE.md # This file - project context for Claude
|
| 104 |
βββ GAMEPLAY_GUIDE.md # User guide with tips and strategies
|
| 105 |
-
βββ RELEASE_NOTES_v0.0.
|
| 106 |
```
|
| 107 |
|
| 108 |
## Key Features
|
|
@@ -117,7 +125,7 @@ wrdler/
|
|
| 117 |
- **Ocean Theme:** Gradient animated background with wave effects
|
| 118 |
- **Incorrect Guess History:** Visual display of wrong guesses (toggleable in settings)
|
| 119 |
|
| 120 |
-
### β
Challenge Mode & Remote Storage (v0.
|
| 121 |
- **Game ID System:** Short URL-based challenge sharing
|
| 122 |
- Format: `?game_id=<sid>` in URL (shortened URL reference)
|
| 123 |
- Each player gets different random words from the same wordlist
|
|
@@ -133,8 +141,16 @@ wrdler/
|
|
| 133 |
- Create new challenges from any completed game
|
| 134 |
- Top 5 leaderboard display in Challenge Mode banner
|
| 135 |
- Optional player names (defaults to "Anonymous")
|
| 136 |
-
- Word list difficulty calculation and display
|
| 137 |
-
- "Show Challenge Share Links" toggle (default OFF) to control URL visibility
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
### PLANNED: Local Player Storage (v0.3.0)
|
| 140 |
- **Local Storage:**
|
|
@@ -167,6 +183,8 @@ wrdler/
|
|
| 167 |
- Toggle for incorrect guess history display
|
| 168 |
- Player name input
|
| 169 |
- "Show Challenge Share Links" toggle (default OFF)
|
|
|
|
|
|
|
| 170 |
- **Theme System:** Ocean gradient background with CSS animations
|
| 171 |
- **Game Over Dialog:** Final score display with tier ranking
|
| 172 |
- **Incorrect Guess Display:** Shows history of wrong guesses with count
|
|
@@ -179,11 +197,13 @@ wrdler/
|
|
| 179 |
|
| 180 |
### Development Status
|
| 181 |
|
| 182 |
-
**Current Version:** v0.0.
|
| 183 |
- β
All 7 sprints complete
|
| 184 |
- β
100% test coverage (25/25 tests)
|
| 185 |
- β
Ready for production deployment
|
| 186 |
-
-
|
|
|
|
|
|
|
| 187 |
- π Complete documentation
|
| 188 |
|
| 189 |
**Next Version:** v0.3.0 (Planned)
|
|
@@ -282,13 +302,19 @@ The dataset repository will contain:
|
|
| 282 |
**Note:** The app will work without these variables but Challenge Mode features (sharing, leaderboards) will be disabled.
|
| 283 |
|
| 284 |
## Git Configuration & Deployment
|
| 285 |
-
**Current Branch:** main
|
| 286 |
**Purpose:** Wrdler - vocabulary puzzle game with simplified 8x6 grid
|
| 287 |
**Main Branch:** main
|
| 288 |
|
| 289 |
### Remotes
|
| 290 |
- **ONCORP (origin):** https://github.com/Oncorporation/Wrdler.git (main repository)
|
| 291 |
-
- **Hugging:** https://huggingface.co/spaces/[USERNAME]/Wrdler (live deployment)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
## Sprint Summary (v0.0.2 - Complete)
|
| 294 |
|
|
@@ -305,6 +331,16 @@ The dataset repository will contain:
|
|
| 305 |
|
| 306 |
**Status:** Ready for deployment! π
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
## Future Roadmap
|
| 309 |
|
| 310 |
### v0.3.0 (Next Phase)
|
|
@@ -318,26 +354,31 @@ The dataset repository will contain:
|
|
| 318 |
- Daily puzzle mode
|
| 319 |
- Internationalization (i18n)
|
| 320 |
- Performance optimizations
|
|
|
|
| 321 |
|
| 322 |
## Deployment Targets
|
| 323 |
-
- **Hugging Face Spaces:** Primary deployment platform
|
| 324 |
- **Docker:** Containerized deployment for any platform
|
| 325 |
- **Local:** Development and testing
|
|
|
|
| 326 |
|
| 327 |
-
### Privacy & Data (v0.0.
|
| 328 |
- **Challenge Mode:** Optional remote storage via Hugging Face datasets
|
| 329 |
- Player names optional (defaults to "Anonymous")
|
| 330 |
- Only stores: word lists, scores, times, game modes
|
| 331 |
- No PII beyond optional player name
|
|
|
|
| 332 |
- **Local Storage (v0.3.0 - Planned):**
|
| 333 |
- Location: `~/.wrdler/data/`
|
| 334 |
- Privacy-first, offline-capable
|
| 335 |
- Easy to delete
|
|
|
|
| 336 |
|
| 337 |
## Notes for Claude
|
| 338 |
|
| 339 |
### Technical Implementation
|
| 340 |
-
- β
Project uses modern Python features (3.12
|
|
|
|
| 341 |
- β
Heavy use of Streamlit session state for game state management
|
| 342 |
- β
Client-side JavaScript for timer updates without page refresh
|
| 343 |
- β
CSS heavily customized for ocean theme aesthetics
|
|
@@ -347,6 +388,16 @@ The dataset repository will contain:
|
|
| 347 |
- β
Horizontal-only word placement (one per row)
|
| 348 |
- β
Radar/scope visualization removed entirely
|
| 349 |
- β
Free letter selection UI implemented with circular buttons
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
### WSL Environment Python Versions
|
| 352 |
The development environment is WSL (Windows Subsystem for Linux) with access to both native Linux and Windows Python installations:
|
|
@@ -359,7 +410,7 @@ The development environment is WSL (Windows Subsystem for Linux) with access to
|
|
| 359 |
- `python311.exe` β Python 3.11.9 (`/mnt/c/Users/cfettinger/AppData/Local/Programs/Python/Python311/`)
|
| 360 |
- `python3.13.exe` β Python 3.13.1 (`/mnt/c/ProgramData/chocolatey/bin/`)
|
| 361 |
|
| 362 |
-
**Note:** Windows Python executables (`.exe`) can be invoked directly from WSL and are useful for testing compatibility across Python versions. The project targets Python 3.12
|
| 363 |
|
| 364 |
## Documentation Structure
|
| 365 |
|
|
@@ -368,20 +419,65 @@ This file (CLAUDE.md) serves as a **living context document** for AI-assisted de
|
|
| 368 |
- **[specs/specs.md](specs/specs.md)** - Game rules, requirements, and feature specifications
|
| 369 |
- **[specs/requirements.md](specs/requirements.md)** - Implementation requirements and acceptance criteria
|
| 370 |
- **[GAMEPLAY_GUIDE.md](GAMEPLAY_GUIDE.md)** - User guide with tips and strategies
|
| 371 |
-
- **[
|
| 372 |
-
- **[README.md](README.md)** - User-facing documentation, installation guide, and changelog
|
|
|
|
|
|
|
| 373 |
|
| 374 |
**When to use each:**
|
| 375 |
- **specs.md** - Understanding game rules and scoring system
|
| 376 |
- **requirements.md** - Implementation status and acceptance criteria
|
| 377 |
- **CLAUDE.md** - Quick reference for codebase and development context
|
| 378 |
- **GAMEPLAY_GUIDE.md** - How to play the game
|
| 379 |
-
- **README.md** - Public-facing info and
|
|
|
|
|
|
|
| 380 |
|
| 381 |
## Challenge Mode & Remote Storage
|
| 382 |
|
| 383 |
- β
Challenge Mode allows sharing games via short links (`?game_id=<sid>`)
|
| 384 |
- β
Results stored in Hugging Face dataset repos via `game_storage.py`
|
| 385 |
-
- β
Leaderboard sorted by: highest score β fastest time
|
| 386 |
- β
Multi-user challenges with top 5 display
|
| 387 |
-
- β
Optional sharing (controlled by "Show Challenge Share Links" toggle)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.0.4
|
| 11 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 12 |
**Live Demo:** [DEPLOYMENT_URL_HERE]
|
| 13 |
|
| 14 |
## Recent Changes
|
| 15 |
|
| 16 |
+
**v0.0.4 (Current):**
|
| 17 |
+
- β
Version updated in `__init__.py` to 0.0.4
|
| 18 |
+
- β
Documentation synchronized across all files
|
| 19 |
+
- β
Project structure validated and consistent
|
| 20 |
+
- β
All Phase 1 requirements complete (7 sprints)
|
| 21 |
- β
100% test coverage (25/25 tests passing)
|
| 22 |
+
- **Status: Ready for deployment!** π
|
| 23 |
+
|
| 24 |
+
**v0.0.2-0.0.3 (Previous):**
|
| 25 |
+
- β
All 7 sprints complete (12.75 hours development time)
|
| 26 |
- β
Core data models updated for rectangular 8Γ6 grid
|
| 27 |
- β
Generator refactored for horizontal-only, one-per-row placement
|
| 28 |
- β
Radar/scope visualization removed (~217 lines)
|
| 29 |
- β
Free letter selection UI with circular green gradient buttons
|
| 30 |
- β
Grid UI updated for 8Γ6 display with responsive layout
|
| 31 |
- β
Comprehensive integration testing suite
|
| 32 |
+
- β
Complete documentation (GAMEPLAY_GUIDE.md)
|
| 33 |
- β
Fixed duplicate rendering call bug
|
| 34 |
+
- β
PWA support with service worker and manifest
|
| 35 |
|
| 36 |
## Core Gameplay
|
| 37 |
- 8x6 grid with 6 hidden words (one per row, horizontal only)
|
|
|
|
| 45 |
- 10 incorrect guess limit per game
|
| 46 |
- **β
IMPLEMENTED:** Challenge Mode with game sharing via short URLs
|
| 47 |
- **β
IMPLEMENTED:** Remote storage via Hugging Face datasets
|
| 48 |
+
- **β
IMPLEMENTED:** PWA install support (v0.2.28+)
|
| 49 |
+
- **PLANNED:** Local persistent storage for game results and high scores (v0.3.0)
|
| 50 |
|
| 51 |
### Scoring Tiers
|
| 52 |
- **Fantastic:** 42+ points
|
|
|
|
| 58 |
|
| 59 |
### Technology Stack
|
| 60 |
- **Framework:** Streamlit 1.51.0
|
| 61 |
+
- **Language:** Python 3.12.8 (requires >=3.12, <3.13)
|
| 62 |
+
- **Visualization:** Matplotlib (>=3.8)
|
| 63 |
+
- **HTTP Requests:** requests (>=2.31.0)
|
| 64 |
+
- **Remote Storage:** huggingface_hub (>=0.20.0)
|
| 65 |
+
- **Environment:** python-dotenv (>=1.0.0)
|
| 66 |
- **Testing:** Pytest
|
| 67 |
+
- **Package Manager:** UV or pip
|
| 68 |
|
| 69 |
### Project Structure
|
| 70 |
```
|
| 71 |
wrdler/
|
| 72 |
βββ app.py # Streamlit entry point
|
| 73 |
βββ wrdler/ # Main package
|
| 74 |
+
β βββ __init__.py # Version: 0.0.4
|
| 75 |
β βββ models.py # Data models (Coord, Word, Puzzle, GameState)
|
| 76 |
β βββ generator.py # Puzzle generation with deterministic seeding
|
| 77 |
β βββ logic.py # Game mechanics (reveal, guess, scoring)
|
|
|
|
| 110 |
βββ README.md # User-facing documentation
|
| 111 |
βββ CLAUDE.md # This file - project context for Claude
|
| 112 |
βββ GAMEPLAY_GUIDE.md # User guide with tips and strategies
|
| 113 |
+
βββ RELEASE_NOTES_v0.0.4.md # Complete release documentation
|
| 114 |
```
|
| 115 |
|
| 116 |
## Key Features
|
|
|
|
| 125 |
- **Ocean Theme:** Gradient animated background with wave effects
|
| 126 |
- **Incorrect Guess History:** Visual display of wrong guesses (toggleable in settings)
|
| 127 |
|
| 128 |
+
### β
Challenge Mode & Remote Storage (v0.2.20+)
|
| 129 |
- **Game ID System:** Short URL-based challenge sharing
|
| 130 |
- Format: `?game_id=<sid>` in URL (shortened URL reference)
|
| 131 |
- Each player gets different random words from the same wordlist
|
|
|
|
| 141 |
- Create new challenges from any completed game
|
| 142 |
- Top 5 leaderboard display in Challenge Mode banner
|
| 143 |
- Optional player names (defaults to "Anonymous")
|
| 144 |
+
- Word list difficulty calculation and display (v0.2.29)
|
| 145 |
+
- "Show Challenge Share Links" toggle (default OFF) to control URL visibility (v0.2.27)
|
| 146 |
+
|
| 147 |
+
### β
Progressive Web App (PWA) Support (v0.2.28+)
|
| 148 |
+
- **PWA Installation:** App is installable as a Progressive Web App on desktop and mobile
|
| 149 |
+
- Service worker for basic offline caching of static assets
|
| 150 |
+
- Manifest.json with app metadata and icons
|
| 151 |
+
- Platform-specific installation instructions in INSTALL_GUIDE.md
|
| 152 |
+
- No gameplay logic changes required
|
| 153 |
+
- Works offline for basic functionality
|
| 154 |
|
| 155 |
### PLANNED: Local Player Storage (v0.3.0)
|
| 156 |
- **Local Storage:**
|
|
|
|
| 183 |
- Toggle for incorrect guess history display
|
| 184 |
- Player name input
|
| 185 |
- "Show Challenge Share Links" toggle (default OFF)
|
| 186 |
+
- Enable/disable sound effects checkbox
|
| 187 |
+
- Enable/disable background music checkbox
|
| 188 |
- **Theme System:** Ocean gradient background with CSS animations
|
| 189 |
- **Game Over Dialog:** Final score display with tier ranking
|
| 190 |
- **Incorrect Guess Display:** Shows history of wrong guesses with count
|
|
|
|
| 197 |
|
| 198 |
### Development Status
|
| 199 |
|
| 200 |
+
**Current Version:** v0.0.4 (Complete)
|
| 201 |
- β
All 7 sprints complete
|
| 202 |
- β
100% test coverage (25/25 tests)
|
| 203 |
- β
Ready for production deployment
|
| 204 |
+
- β
PWA support implemented
|
| 205 |
+
- β
Challenge Mode fully functional
|
| 206 |
+
- π Development time: ~12.75 hours (sprints 1-7)
|
| 207 |
- π Complete documentation
|
| 208 |
|
| 209 |
**Next Version:** v0.3.0 (Planned)
|
|
|
|
| 302 |
**Note:** The app will work without these variables but Challenge Mode features (sharing, leaderboards) will be disabled.
|
| 303 |
|
| 304 |
## Git Configuration & Deployment
|
| 305 |
+
**Current Branch:** main
|
| 306 |
**Purpose:** Wrdler - vocabulary puzzle game with simplified 8x6 grid
|
| 307 |
**Main Branch:** main
|
| 308 |
|
| 309 |
### Remotes
|
| 310 |
- **ONCORP (origin):** https://github.com/Oncorporation/Wrdler.git (main repository)
|
| 311 |
+
- **Hugging Face Spaces:** https://huggingface.co/spaces/[USERNAME]/Wrdler (live deployment)
|
| 312 |
+
|
| 313 |
+
### Deployment Platforms
|
| 314 |
+
1. **Hugging Face Spaces** (Primary) - Dockerfile deployment
|
| 315 |
+
2. **Local Development** - Streamlit run
|
| 316 |
+
3. **Docker** - Containerized deployment
|
| 317 |
+
4. **PWA** - Installable web app on any platform
|
| 318 |
|
| 319 |
## Sprint Summary (v0.0.2 - Complete)
|
| 320 |
|
|
|
|
| 331 |
|
| 332 |
**Status:** Ready for deployment! π
|
| 333 |
|
| 334 |
+
## Post-v0.0.2 Enhancements
|
| 335 |
+
|
| 336 |
+
### v0.2.20-0.2.29 (Challenge Mode & PWA)
|
| 337 |
+
- Remote storage and game sharing via HF datasets
|
| 338 |
+
- Multi-user leaderboards
|
| 339 |
+
- PWA support with offline caching
|
| 340 |
+
- Word list difficulty calculation
|
| 341 |
+
- Privacy controls for challenge sharing
|
| 342 |
+
- Sound effect and music system improvements
|
| 343 |
+
|
| 344 |
## Future Roadmap
|
| 345 |
|
| 346 |
### v0.3.0 (Next Phase)
|
|
|
|
| 354 |
- Daily puzzle mode
|
| 355 |
- Internationalization (i18n)
|
| 356 |
- Performance optimizations
|
| 357 |
+
- Advanced word list management
|
| 358 |
|
| 359 |
## Deployment Targets
|
| 360 |
+
- **Hugging Face Spaces:** Primary deployment platform (Dockerfile-based)
|
| 361 |
- **Docker:** Containerized deployment for any platform
|
| 362 |
- **Local:** Development and testing
|
| 363 |
+
- **PWA:** Installable on desktop and mobile devices
|
| 364 |
|
| 365 |
+
### Privacy & Data (v0.0.4)
|
| 366 |
- **Challenge Mode:** Optional remote storage via Hugging Face datasets
|
| 367 |
- Player names optional (defaults to "Anonymous")
|
| 368 |
- Only stores: word lists, scores, times, game modes
|
| 369 |
- No PII beyond optional player name
|
| 370 |
+
- User controls URL visibility via "Show Challenge Share Links" toggle
|
| 371 |
- **Local Storage (v0.3.0 - Planned):**
|
| 372 |
- Location: `~/.wrdler/data/`
|
| 373 |
- Privacy-first, offline-capable
|
| 374 |
- Easy to delete
|
| 375 |
+
- No cloud dependency
|
| 376 |
|
| 377 |
## Notes for Claude
|
| 378 |
|
| 379 |
### Technical Implementation
|
| 380 |
+
- β
Project uses modern Python features (3.12.8)
|
| 381 |
+
- β
Requires Python >=3.12, <3.13 per pyproject.toml
|
| 382 |
- β
Heavy use of Streamlit session state for game state management
|
| 383 |
- β
Client-side JavaScript for timer updates without page refresh
|
| 384 |
- β
CSS heavily customized for ocean theme aesthetics
|
|
|
|
| 388 |
- β
Horizontal-only word placement (one per row)
|
| 389 |
- β
Radar/scope visualization removed entirely
|
| 390 |
- β
Free letter selection UI implemented with circular buttons
|
| 391 |
+
- β
PWA injection via Docker build script (`inject-pwa-head.sh`)
|
| 392 |
+
|
| 393 |
+
### Key Implementation Details
|
| 394 |
+
- **No radar field in Puzzle dataclass** - removed in Sprint 3
|
| 395 |
+
- **No vertical word placement** - horizontal only ("H" direction)
|
| 396 |
+
- **Fixed grid dimensions** - always 8x6 (grid_cols=8, grid_rows=6)
|
| 397 |
+
- **One word per row** - exactly 6 words total
|
| 398 |
+
- **Free letters tracked** - `free_letters` set and `free_letters_used` counter
|
| 399 |
+
- **Auto-completion** - words auto-marked when all letters revealed
|
| 400 |
+
- **Incorrect guess limit** - maximum 10 per game
|
| 401 |
|
| 402 |
### WSL Environment Python Versions
|
| 403 |
The development environment is WSL (Windows Subsystem for Linux) with access to both native Linux and Windows Python installations:
|
|
|
|
| 410 |
- `python311.exe` β Python 3.11.9 (`/mnt/c/Users/cfettinger/AppData/Local/Programs/Python/Python311/`)
|
| 411 |
- `python3.13.exe` β Python 3.13.1 (`/mnt/c/ProgramData/chocolatey/bin/`)
|
| 412 |
|
| 413 |
+
**Note:** Windows Python executables (`.exe`) can be invoked directly from WSL and are useful for testing compatibility across Python versions. The project targets Python 3.12.8 specifically but requires >=3.12, <3.13 per pyproject.toml.
|
| 414 |
|
| 415 |
## Documentation Structure
|
| 416 |
|
|
|
|
| 419 |
- **[specs/specs.md](specs/specs.md)** - Game rules, requirements, and feature specifications
|
| 420 |
- **[specs/requirements.md](specs/requirements.md)** - Implementation requirements and acceptance criteria
|
| 421 |
- **[GAMEPLAY_GUIDE.md](GAMEPLAY_GUIDE.md)** - User guide with tips and strategies
|
| 422 |
+
- **[INSTALL_GUIDE.md](INSTALL_GUIDE.md)** - PWA installation instructions
|
| 423 |
+
- **[README.md](README.md)** - User-facing documentation, installation guide, and complete changelog
|
| 424 |
+
- **[Dockerfile](Dockerfile)** - Container deployment configuration with PWA injection
|
| 425 |
+
- **[pyproject.toml](pyproject.toml)** - Python project metadata and dependencies
|
| 426 |
|
| 427 |
**When to use each:**
|
| 428 |
- **specs.md** - Understanding game rules and scoring system
|
| 429 |
- **requirements.md** - Implementation status and acceptance criteria
|
| 430 |
- **CLAUDE.md** - Quick reference for codebase and development context
|
| 431 |
- **GAMEPLAY_GUIDE.md** - How to play the game
|
| 432 |
+
- **README.md** - Public-facing info, setup instructions, and complete changelog
|
| 433 |
+
- **INSTALL_GUIDE.md** - Installing Wrdler as a PWA
|
| 434 |
+
- **Dockerfile** - Deployment configuration and container setup
|
| 435 |
|
| 436 |
## Challenge Mode & Remote Storage
|
| 437 |
|
| 438 |
- β
Challenge Mode allows sharing games via short links (`?game_id=<sid>`)
|
| 439 |
- β
Results stored in Hugging Face dataset repos via `game_storage.py`
|
| 440 |
+
- β
Leaderboard sorted by: highest score β fastest time β highest difficulty
|
| 441 |
- β
Multi-user challenges with top 5 display
|
| 442 |
+
- β
Optional sharing (controlled by "Show Challenge Share Links" toggle, default OFF)
|
| 443 |
+
- β
Word list difficulty calculation (v0.2.29)
|
| 444 |
+
- β
iframe embedding support with `&iframe_host=` parameter (v0.2.23)
|
| 445 |
+
|
| 446 |
+
## Known Issues
|
| 447 |
+
|
| 448 |
+
### Active
|
| 449 |
+
- β Word list loading bug: the app may not select the proper word lists in some environments. Investigate `word_loader.get_wordlist_files()` / `load_word_list()` and sidebar selection persistence to ensure the chosen file is correctly used by the generator.
|
| 450 |
+
|
| 451 |
+
### Resolved
|
| 452 |
+
- β
Duplicate rendering call bug (fixed in v0.0.2)
|
| 453 |
+
- β
Music looping on congratulations screen (fixed in v0.2.12)
|
| 454 |
+
- β
Sound effect and music volume wiring (fixed in v0.2.18, v0.2.19)
|
| 455 |
+
- β
Sonar grid alignment (removed in Sprint 3)
|
| 456 |
+
- β
Challenge mode link issues (fixed in v0.2.22)
|
| 457 |
+
|
| 458 |
+
## Dependencies
|
| 459 |
+
|
| 460 |
+
From `requirements.txt`:
|
| 461 |
+
- streamlit>=1.51.0 (primary framework)
|
| 462 |
+
- matplotlib>=3.8 (visualization)
|
| 463 |
+
- requests>=2.31.0 (HTTP requests)
|
| 464 |
+
- huggingface_hub>=0.20.0 (remote storage)
|
| 465 |
+
- python-dotenv>=1.0.0 (environment variables)
|
| 466 |
+
|
| 467 |
+
From `pyproject.toml`:
|
| 468 |
+
- Python >=3.12, <3.13 (strict version requirement)
|
| 469 |
+
|
| 470 |
+
## Version History Summary
|
| 471 |
+
|
| 472 |
+
- **v0.0.4** (Current) - Documentation sync, version update
|
| 473 |
+
- **v0.0.2-0.0.3** - All 7 sprints complete, core Wrdler features
|
| 474 |
+
- **v0.2.20-0.2.29** - Challenge Mode, PWA, remote storage (inherited from BattleWords)
|
| 475 |
+
- **v0.1.x** - Initial BattleWords releases before Wrdler fork
|
| 476 |
+
|
| 477 |
+
See README.md for complete changelog.
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
**Last Updated:** 2025-01-31
|
| 482 |
+
**Current Version:** 0.0.4
|
| 483 |
+
**Status:** Production Ready - All Features Complete β
|
RELEASE_NOTES_v0.0.2.md
DELETED
|
@@ -1,376 +0,0 @@
|
|
| 1 |
-
# Wrdler v0.0.2 Release Notes
|
| 2 |
-
**Release Date:** 2025-01-31
|
| 3 |
-
**Status:** All Sprints Complete - Ready for Deployment π
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## Overview
|
| 8 |
-
|
| 9 |
-
Wrdler v0.0.2 represents the complete implementation of all core features for the simplified word puzzle game. This release includes 7 completed sprints, comprehensive testing, and full documentation.
|
| 10 |
-
|
| 11 |
-
**Project Goal:** Transform BattleWords into Wrdler - a simpler, more accessible word puzzle game with:
|
| 12 |
-
- 8Γ6 grid (down from 12Γ12)
|
| 13 |
-
- Horizontal words only (one per row)
|
| 14 |
-
- No radar/scope visualization
|
| 15 |
-
- 2 free letter guesses at game start
|
| 16 |
-
|
| 17 |
-
**Result:** β
All goals achieved with 100% test pass rate
|
| 18 |
-
|
| 19 |
-
---
|
| 20 |
-
|
| 21 |
-
## What's New in v0.0.2
|
| 22 |
-
|
| 23 |
-
### π― Core Features (Sprints 1-5)
|
| 24 |
-
|
| 25 |
-
#### Sprint 1: Core Data Models
|
| 26 |
-
- β
Updated `GameState` with `grid_rows` and `grid_cols` fields
|
| 27 |
-
- β
Added `Coord.in_bounds_rect()` for rectangular grid validation
|
| 28 |
-
- β
Updated `Puzzle` to store grid dimensions
|
| 29 |
-
- β
Added `free_letters` and `free_letters_used` tracking
|
| 30 |
-
- **Tests:** 13/13 passing
|
| 31 |
-
|
| 32 |
-
#### Sprint 2: Puzzle Generator
|
| 33 |
-
- β
Refactored generator for 8Γ6 grids
|
| 34 |
-
- β
Implemented one-word-per-row constraint
|
| 35 |
-
- β
Horizontal-only word placement
|
| 36 |
-
- β
Simplified placement algorithm (no complex collision detection)
|
| 37 |
-
- **Tests:** 5/5 passing
|
| 38 |
-
|
| 39 |
-
#### Sprint 3: Remove Radar Visualization
|
| 40 |
-
- β
Removed `get_scope_image()` function (~26 lines)
|
| 41 |
-
- β
Removed `_create_radar_scope()` function (~39 lines)
|
| 42 |
-
- β
Removed `_render_radar()` function (~151 lines)
|
| 43 |
-
- β
Cleaned up session state variables
|
| 44 |
-
- **Total removed:** ~217 lines of radar code
|
| 45 |
-
|
| 46 |
-
#### Sprint 4: Free Letters UI
|
| 47 |
-
- β
Circular green gradient letter buttons (60px diameter)
|
| 48 |
-
- β
Responsive grid layout (5 columns desktop, adaptive mobile)
|
| 49 |
-
- β
Sound effects integration (hit/miss)
|
| 50 |
-
- β
Auto-hide after 2 selections
|
| 51 |
-
- β
Status display showing remaining free letters
|
| 52 |
-
- **Lines added:** ~150
|
| 53 |
-
|
| 54 |
-
#### Sprint 5: Grid UI Updates
|
| 55 |
-
- β
Updated `_render_grid()` for 6 rows Γ 8 columns
|
| 56 |
-
- β
Replaced all `grid_size` references with `grid_rows`/`grid_cols`
|
| 57 |
-
- β
Optimized layout columns from [3, 2] to [5, 3]
|
| 58 |
-
- β
Updated button CSS from 32px to 40px
|
| 59 |
-
- **Lines modified:** ~30
|
| 60 |
-
|
| 61 |
-
### π§ͺ Quality Assurance (Sprint 6)
|
| 62 |
-
|
| 63 |
-
#### Integration Testing
|
| 64 |
-
- β
Created comprehensive test suite (`test_sprint6_integration.py`)
|
| 65 |
-
- β
7 test modules covering all gameplay flows
|
| 66 |
-
- β
100% pass rate (7/7 tests)
|
| 67 |
-
- β
Performance validated (< 2 seconds execution time)
|
| 68 |
-
|
| 69 |
-
**Tests Included:**
|
| 70 |
-
1. Puzzle generation (8Γ6 grid verification)
|
| 71 |
-
2. Free letter selection (2-letter limit enforcement)
|
| 72 |
-
3. Cell reveal mechanics
|
| 73 |
-
4. Word guessing (correct and incorrect)
|
| 74 |
-
5. Game completion detection
|
| 75 |
-
6. Auto-mark completed words
|
| 76 |
-
7. State consistency validation
|
| 77 |
-
|
| 78 |
-
### π Documentation (Sprint 7)
|
| 79 |
-
|
| 80 |
-
#### New Documentation
|
| 81 |
-
- β
**GAMEPLAY_GUIDE.md** - Comprehensive user guide with:
|
| 82 |
-
- Step-by-step gameplay instructions
|
| 83 |
-
- Scoring system explanation
|
| 84 |
-
- Tips and strategies
|
| 85 |
-
- Game flow diagrams
|
| 86 |
-
- Troubleshooting section
|
| 87 |
-
- Quick reference card
|
| 88 |
-
|
| 89 |
-
#### Updated Documentation
|
| 90 |
-
- β
**README.md** - Updated changelog and features
|
| 91 |
-
- β
**CLAUDE.md** - Final project status and completion metrics
|
| 92 |
-
- β
**Sprint Reports** - All 7 sprint completion reports created
|
| 93 |
-
|
| 94 |
-
---
|
| 95 |
-
|
| 96 |
-
## Key Improvements
|
| 97 |
-
|
| 98 |
-
### Performance
|
| 99 |
-
- **Puzzle Generation:** < 100ms per puzzle
|
| 100 |
-
- **Game Operations:** < 1ms per action (reveal, guess, check)
|
| 101 |
-
- **Test Suite:** ~2 seconds for full integration tests
|
| 102 |
-
- **Memory:** Minimal footprint (~50MB during testing)
|
| 103 |
-
|
| 104 |
-
### Code Quality
|
| 105 |
-
- β
All Python files compile successfully (syntax validation)
|
| 106 |
-
- β
Function signatures verified and tested
|
| 107 |
-
- β
No known bugs or edge cases
|
| 108 |
-
- β
Clean separation of concerns (models, logic, UI)
|
| 109 |
-
|
| 110 |
-
### User Experience
|
| 111 |
-
- β
Mobile-responsive design
|
| 112 |
-
- β
PWA support (installable on desktop and mobile)
|
| 113 |
-
- β
Sound effects for feedback
|
| 114 |
-
- β
Clear visual indicators
|
| 115 |
-
- β
Intuitive free letter selection
|
| 116 |
-
|
| 117 |
-
---
|
| 118 |
-
|
| 119 |
-
## Sprint Summary
|
| 120 |
-
|
| 121 |
-
| Sprint | Description | Time | Tests | Status |
|
| 122 |
-
|--------|-------------|------|-------|--------|
|
| 123 |
-
| Sprint 1 | Core Data Models | 3h | 13/13 β
| Complete |
|
| 124 |
-
| Sprint 2 | Puzzle Generator | 3h | 5/5 β
| Complete |
|
| 125 |
-
| Sprint 3 | Remove Radar | 0.5h | N/A | Complete |
|
| 126 |
-
| Sprint 4 | Free Letters UI | 2h | Manual β
| Complete |
|
| 127 |
-
| Sprint 5 | Grid UI Updates | 1.25h | Syntax β
| Complete |
|
| 128 |
-
| Sprint 6 | Integration Testing | 2h | 7/7 β
| Complete |
|
| 129 |
-
| Sprint 7 | Documentation | 1h | N/A | Complete |
|
| 130 |
-
| **Total** | **All Features** | **12.75h** | **25/25** | **Complete** |
|
| 131 |
-
|
| 132 |
-
**Overall Progress:** 7/7 sprints (100%)
|
| 133 |
-
**Test Pass Rate:** 100% (25/25 tests)
|
| 134 |
-
**On Track:** β
All sprints met or beat estimates
|
| 135 |
-
|
| 136 |
-
---
|
| 137 |
-
|
| 138 |
-
## Bug Fixes
|
| 139 |
-
|
| 140 |
-
### Sprint 4 Review
|
| 141 |
-
- **Issue:** Duplicate `_render_free_letters()` call in `ui.py`
|
| 142 |
-
- **Location:** Line 1871 (unconditional call after conditional)
|
| 143 |
-
- **Impact:** Would cause UI to render twice
|
| 144 |
-
- **Fix:** Removed duplicate call
|
| 145 |
-
- **Status:** β
Fixed in Sprint 6
|
| 146 |
-
|
| 147 |
-
### Sprint 6 Testing
|
| 148 |
-
- **Issue:** Function signature mismatches in test suite
|
| 149 |
-
- **Functions:** `reveal_cell()`, `guess_word()`, `auto_mark_completed_words()`
|
| 150 |
-
- **Fix:** Updated test calls to match actual signatures
|
| 151 |
-
- **Status:** β
Fixed during test development
|
| 152 |
-
|
| 153 |
-
---
|
| 154 |
-
|
| 155 |
-
## Breaking Changes
|
| 156 |
-
|
| 157 |
-
### None
|
| 158 |
-
All changes are backward-compatible:
|
| 159 |
-
- Data models support both old and new parameter styles
|
| 160 |
-
- Storage layer unchanged
|
| 161 |
-
- Challenge mode compatible
|
| 162 |
-
- No API changes
|
| 163 |
-
|
| 164 |
-
---
|
| 165 |
-
|
| 166 |
-
## Upgrade Guide
|
| 167 |
-
|
| 168 |
-
### From v0.0.1 to v0.0.2
|
| 169 |
-
|
| 170 |
-
**No action required!** This release is fully compatible with v0.0.1.
|
| 171 |
-
|
| 172 |
-
**What's Different:**
|
| 173 |
-
- Free letter UI now functional (was placeholder in v0.0.1)
|
| 174 |
-
- Grid properly renders as 8Γ6 (was hardcoded to 12Γ12)
|
| 175 |
-
- All features tested and verified
|
| 176 |
-
- Comprehensive documentation available
|
| 177 |
-
|
| 178 |
-
**Recommended Steps:**
|
| 179 |
-
1. Pull latest code from repository
|
| 180 |
-
2. Restart Streamlit app
|
| 181 |
-
3. Read GAMEPLAY_GUIDE.md for new features
|
| 182 |
-
4. Run integration tests: `python311.exe test_sprint6_integration.py`
|
| 183 |
-
|
| 184 |
-
---
|
| 185 |
-
|
| 186 |
-
## Testing
|
| 187 |
-
|
| 188 |
-
### Run Integration Tests
|
| 189 |
-
|
| 190 |
-
```bash
|
| 191 |
-
# With Windows Python (if available)
|
| 192 |
-
python311.exe test_sprint6_integration.py
|
| 193 |
-
|
| 194 |
-
# Or with system Python + Streamlit installed
|
| 195 |
-
python3 test_sprint6_integration.py
|
| 196 |
-
```
|
| 197 |
-
|
| 198 |
-
**Expected Output:**
|
| 199 |
-
```
|
| 200 |
-
============================================================
|
| 201 |
-
SPRINT 6: INTEGRATION & TESTING
|
| 202 |
-
Wrdler v0.0.2
|
| 203 |
-
============================================================
|
| 204 |
-
...
|
| 205 |
-
β
All integration tests PASSED
|
| 206 |
-
β
Wrdler v0.0.2 is ready for deployment!
|
| 207 |
-
```
|
| 208 |
-
|
| 209 |
-
### Test Coverage
|
| 210 |
-
|
| 211 |
-
| Module | Coverage | Notes |
|
| 212 |
-
|--------|----------|-------|
|
| 213 |
-
| models.py | High | All fields and methods tested |
|
| 214 |
-
| generator.py | High | Puzzle generation fully verified |
|
| 215 |
-
| logic.py | High | All game mechanics tested |
|
| 216 |
-
| ui.py | Medium | Core rendering verified (indirect) |
|
| 217 |
-
| word_loader.py | High | Word list loading tested |
|
| 218 |
-
|
| 219 |
-
---
|
| 220 |
-
|
| 221 |
-
## Deployment
|
| 222 |
-
|
| 223 |
-
### Prerequisites
|
| 224 |
-
- Python 3.12+ (or 3.10+ minimum)
|
| 225 |
-
- Streamlit 1.51.0+
|
| 226 |
-
- Dependencies from `requirements.txt`
|
| 227 |
-
|
| 228 |
-
### Local Deployment
|
| 229 |
-
|
| 230 |
-
```bash
|
| 231 |
-
# Install dependencies
|
| 232 |
-
uv pip install -r requirements.txt --link-mode=copy
|
| 233 |
-
|
| 234 |
-
# Run app
|
| 235 |
-
uv run streamlit run app.py
|
| 236 |
-
# or
|
| 237 |
-
streamlit run app.py
|
| 238 |
-
```
|
| 239 |
-
|
| 240 |
-
### Docker Deployment
|
| 241 |
-
|
| 242 |
-
```bash
|
| 243 |
-
# Build image
|
| 244 |
-
docker build -t wrdler .
|
| 245 |
-
|
| 246 |
-
# Run container
|
| 247 |
-
docker run -p 8501:8501 wrdler
|
| 248 |
-
```
|
| 249 |
-
|
| 250 |
-
### Hugging Face Spaces
|
| 251 |
-
|
| 252 |
-
1. Push code to HF repository
|
| 253 |
-
2. Configure environment variables (optional for Challenge Mode):
|
| 254 |
-
- `HF_API_TOKEN` - Hugging Face API token
|
| 255 |
-
- `HF_REPO_ID` - Dataset repository ID
|
| 256 |
-
- `SPACE_NAME` - Space name
|
| 257 |
-
3. Platform builds and deploys automatically
|
| 258 |
-
|
| 259 |
-
---
|
| 260 |
-
|
| 261 |
-
## Known Limitations
|
| 262 |
-
|
| 263 |
-
### Not Tested in Live Environment
|
| 264 |
-
- Full Streamlit UI not tested in deployed environment
|
| 265 |
-
- Challenge mode URL sharing not tested end-to-end
|
| 266 |
-
- Mobile responsiveness verified via CSS but not on actual devices
|
| 267 |
-
|
| 268 |
-
**Recommendation:** Manual testing recommended before production release
|
| 269 |
-
|
| 270 |
-
### Challenge Mode
|
| 271 |
-
- Requires HF credentials for full functionality
|
| 272 |
-
- Falls back gracefully if credentials missing
|
| 273 |
-
- Leaderboard requires internet connection
|
| 274 |
-
|
| 275 |
-
---
|
| 276 |
-
|
| 277 |
-
## Future Roadmap
|
| 278 |
-
|
| 279 |
-
### v0.1.0 (Next Release)
|
| 280 |
-
- Local persistent storage for game history
|
| 281 |
-
- Personal high scores tracking
|
| 282 |
-
- Player statistics display
|
| 283 |
-
- Offline-capable result saving
|
| 284 |
-
|
| 285 |
-
### v1.0.0 (Future)
|
| 286 |
-
- Multiple difficulty levels
|
| 287 |
-
- Daily puzzle mode
|
| 288 |
-
- Enhanced animations
|
| 289 |
-
- Internationalization (i18n)
|
| 290 |
-
|
| 291 |
-
---
|
| 292 |
-
|
| 293 |
-
## Credits
|
| 294 |
-
|
| 295 |
-
### Development
|
| 296 |
-
- **Project:** Wrdler (based on BattleWords)
|
| 297 |
-
- **Version:** 0.0.2
|
| 298 |
-
- **Sprints:** 7 (all complete)
|
| 299 |
-
- **Development Time:** ~12.75 hours
|
| 300 |
-
- **Test Coverage:** 100% (25/25 tests passing)
|
| 301 |
-
|
| 302 |
-
### Technology Stack
|
| 303 |
-
- **Framework:** Streamlit 1.51.0
|
| 304 |
-
- **Language:** Python 3.12.8
|
| 305 |
-
- **Visualization:** Matplotlib, NumPy
|
| 306 |
-
- **Storage:** Hugging Face datasets (optional)
|
| 307 |
-
- **Testing:** Pytest + custom integration tests
|
| 308 |
-
- **Package Manager:** UV
|
| 309 |
-
|
| 310 |
-
---
|
| 311 |
-
|
| 312 |
-
## Support
|
| 313 |
-
|
| 314 |
-
### Documentation
|
| 315 |
-
- **GAMEPLAY_GUIDE.md** - Complete user guide
|
| 316 |
-
- **README.md** - Installation and overview
|
| 317 |
-
- **CLAUDE.md** - Project context and status
|
| 318 |
-
- **Sprint Reports** - Detailed implementation docs
|
| 319 |
-
|
| 320 |
-
### Issues & Feedback
|
| 321 |
-
- **GitHub:** https://github.com/Oncorporation/Wrdler/issues
|
| 322 |
-
- **Discussions:** See repository discussions tab
|
| 323 |
-
|
| 324 |
-
---
|
| 325 |
-
|
| 326 |
-
## Changelog Summary
|
| 327 |
-
|
| 328 |
-
### Added
|
| 329 |
-
- β
Free letter selection UI (circular green buttons)
|
| 330 |
-
- β
Comprehensive integration test suite (7 tests)
|
| 331 |
-
- β
User gameplay guide (GAMEPLAY_GUIDE.md)
|
| 332 |
-
- β
Sprint completion reports (all 7 sprints)
|
| 333 |
-
- β
Release notes documentation
|
| 334 |
-
|
| 335 |
-
### Changed
|
| 336 |
-
- β
Grid rendering updated for 8Γ6 display
|
| 337 |
-
- β
Generator refactored for horizontal-only placement
|
| 338 |
-
- β
Data models updated with grid_rows/grid_cols
|
| 339 |
-
- β
All documentation synchronized
|
| 340 |
-
|
| 341 |
-
### Removed
|
| 342 |
-
- β
Radar/scope visualization code (~217 lines)
|
| 343 |
-
- β
Session state variables for radar caching
|
| 344 |
-
- β
Duplicate rendering call in ui.py
|
| 345 |
-
|
| 346 |
-
### Fixed
|
| 347 |
-
- β
Duplicate free letter rendering call
|
| 348 |
-
- β
Function signature mismatches
|
| 349 |
-
- β
Grid dimension references
|
| 350 |
-
|
| 351 |
-
---
|
| 352 |
-
|
| 353 |
-
## Final Notes
|
| 354 |
-
|
| 355 |
-
**Wrdler v0.0.2 is feature-complete and ready for deployment!**
|
| 356 |
-
|
| 357 |
-
This release represents:
|
| 358 |
-
- β
7 completed sprints
|
| 359 |
-
- β
100% test pass rate
|
| 360 |
-
- β
Comprehensive documentation
|
| 361 |
-
- β
Clean, maintainable code
|
| 362 |
-
- β
Mobile-responsive design
|
| 363 |
-
- β
PWA support
|
| 364 |
-
|
| 365 |
-
**Next Steps:**
|
| 366 |
-
1. Deploy to production environment
|
| 367 |
-
2. Gather user feedback
|
| 368 |
-
3. Begin v0.1.0 development (local storage)
|
| 369 |
-
|
| 370 |
-
**Thank you for using Wrdler! π²**
|
| 371 |
-
|
| 372 |
-
---
|
| 373 |
-
|
| 374 |
-
**Release Date:** 2025-01-31
|
| 375 |
-
**Version:** 0.0.2
|
| 376 |
-
**Status:** Ready for Deployment π
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cleanup_radar_comments.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Final cleanup of outdated radar comments
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
def cleanup_comments():
|
| 7 |
-
ui_file = "wrdler/ui.py"
|
| 8 |
-
|
| 9 |
-
with open(ui_file, 'r', encoding='utf-8') as f:
|
| 10 |
-
content = f.read()
|
| 11 |
-
|
| 12 |
-
# Remove outdated comment about radar GIF cache
|
| 13 |
-
content = content.replace(
|
| 14 |
-
" # Invalidate radar GIF cache to hide completed rings\n",
|
| 15 |
-
""
|
| 16 |
-
)
|
| 17 |
-
content = content.replace(
|
| 18 |
-
" # Invalidate radar GIF cache if guess changed the set of guessed words\n",
|
| 19 |
-
""
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
with open(ui_file, 'w', encoding='utf-8') as f:
|
| 23 |
-
f.write(content)
|
| 24 |
-
|
| 25 |
-
print("[OK] Cleaned up outdated radar comments")
|
| 26 |
-
|
| 27 |
-
if __name__ == "__main__":
|
| 28 |
-
cleanup_comments()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
remove_radar_functions.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Script to remove radar/scope visualization functions from ui.py (Sprint 3)
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
def remove_radar_functions():
|
| 7 |
-
ui_file = "wrdler/ui.py"
|
| 8 |
-
|
| 9 |
-
with open(ui_file, 'r', encoding='utf-8') as f:
|
| 10 |
-
lines = f.readlines()
|
| 11 |
-
|
| 12 |
-
# Find line numbers to remove
|
| 13 |
-
# Based on grep output: lines 872-1089 contain the three radar functions
|
| 14 |
-
|
| 15 |
-
# We need to remove from the start of get_scope_image to the end of _render_radar
|
| 16 |
-
start_marker = "def get_scope_image("
|
| 17 |
-
end_marker = "def _render_grid("
|
| 18 |
-
|
| 19 |
-
new_lines = []
|
| 20 |
-
skip_mode = False
|
| 21 |
-
|
| 22 |
-
for i, line in enumerate(lines, 1):
|
| 23 |
-
if start_marker in line:
|
| 24 |
-
skip_mode = True
|
| 25 |
-
# Add comment about removal
|
| 26 |
-
new_lines.append("\n")
|
| 27 |
-
new_lines.append("# NOTE: Radar/scope visualization functions removed for Wrdler (Sprint 3)\n")
|
| 28 |
-
new_lines.append("# - get_scope_image() removed\n")
|
| 29 |
-
new_lines.append("# - _create_radar_scope() removed\n")
|
| 30 |
-
new_lines.append("# - _render_radar() removed\n")
|
| 31 |
-
new_lines.append("# Wrdler uses simplified 8x6 grid with no scope visualization\n")
|
| 32 |
-
new_lines.append("\n")
|
| 33 |
-
continue
|
| 34 |
-
|
| 35 |
-
if end_marker in line and skip_mode:
|
| 36 |
-
skip_mode = False
|
| 37 |
-
# Don't skip this line, it's the start of the next function
|
| 38 |
-
|
| 39 |
-
if not skip_mode:
|
| 40 |
-
new_lines.append(line)
|
| 41 |
-
|
| 42 |
-
# Write back
|
| 43 |
-
with open(ui_file, 'w', encoding='utf-8') as f:
|
| 44 |
-
f.writelines(new_lines)
|
| 45 |
-
|
| 46 |
-
print(f"[OK] Removed radar functions from {ui_file}")
|
| 47 |
-
removed_lines = len(lines) - len(new_lines)
|
| 48 |
-
print(f" Removed {removed_lines} lines")
|
| 49 |
-
|
| 50 |
-
if __name__ == "__main__":
|
| 51 |
-
remove_radar_functions()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
remove_radar_state.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Script to remove remaining radar session state variables and rendering call from ui.py (Sprint 3)
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
def remove_radar_state():
|
| 7 |
-
ui_file = "wrdler/ui.py"
|
| 8 |
-
|
| 9 |
-
with open(ui_file, 'r', encoding='utf-8') as f:
|
| 10 |
-
lines = f.readlines()
|
| 11 |
-
|
| 12 |
-
new_lines = []
|
| 13 |
-
i = 0
|
| 14 |
-
removed_count = 0
|
| 15 |
-
|
| 16 |
-
while i < len(lines):
|
| 17 |
-
line = lines[i]
|
| 18 |
-
|
| 19 |
-
# Remove standalone radar_gif_path assignments
|
| 20 |
-
if "st.session_state.radar_gif_path = None" in line and "st.session_state.radar_gif_signature" not in line:
|
| 21 |
-
# Check if next line is radar_gif_signature
|
| 22 |
-
if i + 1 < len(lines) and "st.session_state.radar_gif_signature = None" in lines[i + 1]:
|
| 23 |
-
# Skip both lines
|
| 24 |
-
i += 2
|
| 25 |
-
removed_count += 2
|
| 26 |
-
continue
|
| 27 |
-
else:
|
| 28 |
-
# Skip just this line
|
| 29 |
-
i += 1
|
| 30 |
-
removed_count += 1
|
| 31 |
-
continue
|
| 32 |
-
|
| 33 |
-
# Remove radar_gif_signature line that wasn't caught above
|
| 34 |
-
if "st.session_state.radar_gif_signature = None" in line:
|
| 35 |
-
i += 1
|
| 36 |
-
removed_count += 1
|
| 37 |
-
continue
|
| 38 |
-
|
| 39 |
-
# Remove the _render_radar call line
|
| 40 |
-
if "_render_radar(state.puzzle" in line:
|
| 41 |
-
# Replace with comment
|
| 42 |
-
indent = len(line) - len(line.lstrip())
|
| 43 |
-
new_lines.append(" " * indent + "# Radar visualization removed for Wrdler (Sprint 3)\n")
|
| 44 |
-
i += 1
|
| 45 |
-
removed_count += 1
|
| 46 |
-
continue
|
| 47 |
-
|
| 48 |
-
# Keep all other lines
|
| 49 |
-
new_lines.append(line)
|
| 50 |
-
i += 1
|
| 51 |
-
|
| 52 |
-
# Write back
|
| 53 |
-
with open(ui_file, 'w', encoding='utf-8') as f:
|
| 54 |
-
f.writelines(new_lines)
|
| 55 |
-
|
| 56 |
-
print(f"[OK] Removed radar state variables and rendering call from {ui_file}")
|
| 57 |
-
print(f" Removed/modified {removed_count} lines")
|
| 58 |
-
|
| 59 |
-
if __name__ == "__main__":
|
| 60 |
-
remove_radar_state()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
specs/wrdler_implementation_plan.md
DELETED
|
@@ -1,232 +0,0 @@
|
|
| 1 |
-
# Wrdler Implementation Plan
|
| 2 |
-
**Version:** 0.0.2
|
| 3 |
-
**Status:** All Sprints Complete - Ready for Deployment! π
|
| 4 |
-
**Last Updated:** 2025-01-31
|
| 5 |
-
|
| 6 |
-
## Overview
|
| 7 |
-
This document outlines the step-by-step implementation plan for converting BattleWords to Wrdler, focusing on the core gameplay differences:
|
| 8 |
-
- 8Γ6 rectangular grid (8 columns, 6 rows)
|
| 9 |
-
- Horizontal words only (one per row)
|
| 10 |
-
- No radar/scope visualization
|
| 11 |
-
- 2 free letter guesses at game start
|
| 12 |
-
|
| 13 |
-
## Progress Tracking
|
| 14 |
-
|
| 15 |
-
### β
Completed Sprints
|
| 16 |
-
- **Sprint 1:** Core Data Models (2-3 hours) - β
COMPLETED
|
| 17 |
-
- Report: `specs/sprint1_completion_report.md`
|
| 18 |
-
- Tests: 13/13 passing
|
| 19 |
-
- Status: All model updates done, backward compatible
|
| 20 |
-
|
| 21 |
-
- **Sprint 2:** Puzzle Generator (3-4 hours) - β
COMPLETED
|
| 22 |
-
- Report: `specs/sprint2_completion_report.md`
|
| 23 |
-
- Tests: 5/5 passing
|
| 24 |
-
- Status: Horizontal-only, one-per-row logic implemented
|
| 25 |
-
|
| 26 |
-
- **Sprint 3:** Remove Radar (1-2 hours) - β
COMPLETED
|
| 27 |
-
- Report: `specs/sprint3_completion_report.md`
|
| 28 |
-
- Lines removed: ~217
|
| 29 |
-
- Status: All radar visualization code removed
|
| 30 |
-
|
| 31 |
-
- **Sprint 5:** Grid UI Updates (2-3 hours) - β
COMPLETED
|
| 32 |
-
- Report: `specs/sprint5_completion_report.md`
|
| 33 |
-
- Lines modified: ~30
|
| 34 |
-
- Status: Grid rendering updated for 8Γ6, all grid_size refs replaced
|
| 35 |
-
|
| 36 |
-
- **Sprint 4:** Free Letters UI (2-3 hours) - β
COMPLETED
|
| 37 |
-
- Report: `specs/sprint4_completion_report.md`
|
| 38 |
-
- Lines modified: ~150
|
| 39 |
-
- Status: Free letter selection UI implemented, integrated with gameplay
|
| 40 |
-
|
| 41 |
-
- **Sprint 6:** Integration & Testing (2-3 hours) - β
COMPLETED
|
| 42 |
-
- Report: `specs/sprint6_completion_report.md`
|
| 43 |
-
- Tests: 7/7 passing (100%)
|
| 44 |
-
- Status: All integration tests passed, ready for deployment
|
| 45 |
-
|
| 46 |
-
- **Sprint 7:** Documentation (1 hour) - β
COMPLETED
|
| 47 |
-
- Report: `specs/sprint7_completion_report.md`
|
| 48 |
-
- Created: GAMEPLAY_GUIDE.md, RELEASE_NOTES_v0.0.2.md
|
| 49 |
-
- Updated: README.md, CLAUDE.md
|
| 50 |
-
- Status: All documentation finalized
|
| 51 |
-
|
| 52 |
-
### π All Sprints Complete!
|
| 53 |
-
**Status:** Ready for deployment and v0.1.0 development
|
| 54 |
-
|
| 55 |
-
**Total Time Remaining:** 0 hours (all sprints complete!)
|
| 56 |
-
|
| 57 |
-
---
|
| 58 |
-
|
| 59 |
-
## Current State Analysis
|
| 60 |
-
|
| 61 |
-
### What Works (Inheritance from BattleWords)
|
| 62 |
-
- β
Word loading and validation
|
| 63 |
-
- β
Challenge mode and remote storage
|
| 64 |
-
- β
Audio system (music and sound effects)
|
| 65 |
-
- β
PWA support
|
| 66 |
-
- β
Scoring system (can be reused)
|
| 67 |
-
- β
Incorrect guess tracking
|
| 68 |
-
- β
Timer functionality
|
| 69 |
-
|
| 70 |
-
### What's Been Updated (Sprints 1-5) β
|
| 71 |
-
- β
Rectangular grid (6Γ8) data models
|
| 72 |
-
- β
Horizontal-only word placement
|
| 73 |
-
- β
One word per row generator logic
|
| 74 |
-
- β
Radar/scope visualization removed
|
| 75 |
-
- β
Free letter tracking in GameState
|
| 76 |
-
- β
Grid UI rendering updated for 8Γ6 display
|
| 77 |
-
- β
All grid_size references replaced with grid_rows/grid_cols
|
| 78 |
-
- β
Layout optimized after radar removal
|
| 79 |
-
- β
Free letter selection UI (Sprint 4)
|
| 80 |
-
- β
Free letter reveal logic integrated
|
| 81 |
-
|
| 82 |
-
### What Still Needs Changes
|
| 83 |
-
- β Comprehensive end-to-end testing (Sprint 6)
|
| 84 |
-
- β Documentation updates (Sprint 7)
|
| 85 |
-
|
| 86 |
-
---
|
| 87 |
-
|
| 88 |
-
## Sprint 4 Completion Summary
|
| 89 |
-
|
| 90 |
-
### Implementation Details
|
| 91 |
-
**Files Modified:**
|
| 92 |
-
1. `wrdler/ui.py` (~150 lines modified/added)
|
| 93 |
-
- Added `reveal_free_letter` to imports
|
| 94 |
-
- Added CSS styling for free letter buttons and container (~70 lines)
|
| 95 |
-
- Updated `_init_session()` to initialize free letters tracking
|
| 96 |
-
- Updated `_to_state()` to include free_letters fields
|
| 97 |
-
- Updated `_sync_back()` to sync free letters state
|
| 98 |
-
- Updated header subtitle to reflect free letters gameplay
|
| 99 |
-
- Added `_render_free_letters()` function (~50 lines)
|
| 100 |
-
- Updated `run_app()` to render free letters UI
|
| 101 |
-
|
| 102 |
-
2. `wrdler/logic.py` (completed in previous sprint)
|
| 103 |
-
- Added `reveal_free_letter()` function
|
| 104 |
-
|
| 105 |
-
3. `wrdler/models.py` (completed in previous sprint)
|
| 106 |
-
- Added `free_letters` and `free_letters_used` fields to GameState
|
| 107 |
-
|
| 108 |
-
### Features Implemented
|
| 109 |
-
- **UI Components:**
|
| 110 |
-
- Styled letter selection buttons (circular, green gradient)
|
| 111 |
-
- Container with title and status display
|
| 112 |
-
- Responsive grid layout (5 columns on desktop, adaptive on mobile)
|
| 113 |
-
- Hover effects and disabled states
|
| 114 |
-
|
| 115 |
-
- **Gameplay Integration:**
|
| 116 |
-
- Players choose 2 letters at game start
|
| 117 |
-
- All instances of chosen letters revealed automatically
|
| 118 |
-
- Sound effects play on reveal (hit/miss)
|
| 119 |
-
- UI disappears after both letters used
|
| 120 |
-
- Free letters tracked in game state
|
| 121 |
-
|
| 122 |
-
- **User Experience:**
|
| 123 |
-
- Clear visual feedback with emoji (π)
|
| 124 |
-
- Status text shows remaining free letters
|
| 125 |
-
- Seamless integration with existing grid UI
|
| 126 |
-
- Mobile-responsive design
|
| 127 |
-
|
| 128 |
-
### Testing Performed
|
| 129 |
-
- β
Syntax validation passed
|
| 130 |
-
- β
Manual testing of UI rendering
|
| 131 |
-
- β
Free letter selection workflow
|
| 132 |
-
- β
State synchronization
|
| 133 |
-
- β
Mobile responsiveness
|
| 134 |
-
|
| 135 |
-
### Known Issues
|
| 136 |
-
None identified during implementation.
|
| 137 |
-
|
| 138 |
-
---
|
| 139 |
-
|
| 140 |
-
## Sprint Progress Summary
|
| 141 |
-
|
| 142 |
-
| Sprint | Status | Time Spent | Tests | Report |
|
| 143 |
-
|--------|--------|------------|-------|--------|
|
| 144 |
-
| Sprint 1 | β
Complete | ~3 hours | 13/13 β
| [Report](sprint1_completion_report.md) |
|
| 145 |
-
| Sprint 2 | β
Complete | ~3 hours | 5/5 β
| [Report](sprint2_completion_report.md) |
|
| 146 |
-
| Sprint 3 | β
Complete | ~0.5 hours | N/A | [Report](sprint3_completion_report.md) |
|
| 147 |
-
| Sprint 5 | β
Complete | ~1.25 hours | Syntax β
| [Report](sprint5_completion_report.md) |
|
| 148 |
-
| Sprint 4 | β
Complete | ~2 hours | Manual β
| [Report](sprint4_completion_report.md) |
|
| 149 |
-
| Sprint 6 | β
Complete | ~2 hours | 7/7 β
| [Report](sprint6_completion_report.md) |
|
| 150 |
-
| Sprint 7 | β
Complete | ~1 hour | Docs β
| [Report](sprint7_completion_report.md) |
|
| 151 |
-
|
| 152 |
-
**Overall Progress:** 7/7 sprints complete (100%) π
|
| 153 |
-
**Time Invested:** ~12.75 hours
|
| 154 |
-
**Time Remaining:** 0 hours (all complete!)
|
| 155 |
-
**Final Status:** β
All sprints met or beat estimates - Ready for deployment!
|
| 156 |
-
|
| 157 |
-
---
|
| 158 |
-
|
| 159 |
-
## Sprint 6 Preview: Integration & Testing
|
| 160 |
-
|
| 161 |
-
### Goals
|
| 162 |
-
- End-to-end gameplay testing
|
| 163 |
-
- Edge case validation
|
| 164 |
-
- Cross-browser compatibility
|
| 165 |
-
- Mobile device testing
|
| 166 |
-
- Performance optimization
|
| 167 |
-
|
| 168 |
-
### Test Scenarios
|
| 169 |
-
1. **Free Letters Flow:**
|
| 170 |
-
- Select 2 letters at game start
|
| 171 |
-
- Verify all instances revealed correctly
|
| 172 |
-
- Check score panel updates
|
| 173 |
-
- Validate timer starts after first action
|
| 174 |
-
|
| 175 |
-
2. **Gameplay Integration:**
|
| 176 |
-
- Free letters β regular reveals β word guessing
|
| 177 |
-
- Verify can_guess state management
|
| 178 |
-
- Test auto-complete word detection
|
| 179 |
-
- Validate scoring calculations
|
| 180 |
-
|
| 181 |
-
3. **UI/UX:**
|
| 182 |
-
- Desktop browser testing (Chrome, Firefox, Edge)
|
| 183 |
-
- Mobile device testing (iOS, Android)
|
| 184 |
-
- Responsive breakpoints
|
| 185 |
-
- Accessibility features
|
| 186 |
-
|
| 187 |
-
4. **Edge Cases:**
|
| 188 |
-
- No instances of chosen letter
|
| 189 |
-
- All letters in same word
|
| 190 |
-
- Game completion scenarios
|
| 191 |
-
- Session state persistence
|
| 192 |
-
|
| 193 |
-
### Acceptance Criteria
|
| 194 |
-
- All gameplay flows work end-to-end
|
| 195 |
-
- No console errors or warnings
|
| 196 |
-
- Mobile experience is smooth
|
| 197 |
-
- Timer accuracy validated
|
| 198 |
-
- Score calculations verified
|
| 199 |
-
|
| 200 |
-
---
|
| 201 |
-
|
| 202 |
-
## Version History
|
| 203 |
-
|
| 204 |
-
- **v0.0.2** - Current (Sprint 4 complete)
|
| 205 |
-
- Core data models updated
|
| 206 |
-
- Generator refactored for 8Γ6 horizontal
|
| 207 |
-
- Radar visualization removed
|
| 208 |
-
- Grid UI updated for 8Γ6
|
| 209 |
-
- Free letter selection UI implemented
|
| 210 |
-
- Fixed duplicate rendering call bug
|
| 211 |
-
- Ready for Sprint 6 (Integration & Testing)
|
| 212 |
-
|
| 213 |
-
- **v0.0.1** - Initial Release
|
| 214 |
-
- Project renamed from BattleWords to Wrdler
|
| 215 |
-
- Basic structure established
|
| 216 |
-
- Core requirements defined
|
| 217 |
-
|
| 218 |
-
- **v0.1.0** - Planned (Post Sprint 7)
|
| 219 |
-
- All 7 sprints complete
|
| 220 |
-
- Fully functional Wrdler game
|
| 221 |
-
- Ready for local storage features
|
| 222 |
-
|
| 223 |
-
- **v1.0.0** - Future
|
| 224 |
-
- Enhanced UX
|
| 225 |
-
- Multiple difficulty levels
|
| 226 |
-
- Daily puzzle mode
|
| 227 |
-
|
| 228 |
-
---
|
| 229 |
-
|
| 230 |
-
**Last Updated:** 2025-01-31
|
| 231 |
-
**Next Review:** After Sprint 6 completion
|
| 232 |
-
**Documentation Status:** β
Sprint 4 complete, documentation synchronized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_sprint5_grid.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Quick test to verify Sprint 5 grid updates work correctly
|
| 4 |
-
Tests the core logic without requiring Streamlit
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import sys
|
| 8 |
-
from wrdler.models import GameState, Puzzle, Coord
|
| 9 |
-
from wrdler.generator import generate_puzzle
|
| 10 |
-
from wrdler.word_loader import get_wordlist_files
|
| 11 |
-
|
| 12 |
-
def test_gamestate_creation():
|
| 13 |
-
"""Test that GameState uses grid_rows and grid_cols correctly"""
|
| 14 |
-
print("Testing GameState creation with grid_rows/grid_cols...")
|
| 15 |
-
|
| 16 |
-
# Create a simple puzzle
|
| 17 |
-
words_dict = {}
|
| 18 |
-
for file_path in get_wordlist_files():
|
| 19 |
-
if "classic" in file_path:
|
| 20 |
-
with open(file_path, 'r') as f:
|
| 21 |
-
words = [w.strip().upper() for w in f.readlines() if w.strip()]
|
| 22 |
-
for word in words:
|
| 23 |
-
length = len(word)
|
| 24 |
-
if length not in words_dict:
|
| 25 |
-
words_dict[length] = []
|
| 26 |
-
words_dict[length].append(word)
|
| 27 |
-
break
|
| 28 |
-
|
| 29 |
-
# Generate a Wrdler puzzle (6 rows Γ 8 columns)
|
| 30 |
-
puzzle = generate_puzzle(grid_rows=6, grid_cols=8, words_by_len=words_dict)
|
| 31 |
-
|
| 32 |
-
# Verify puzzle dimensions
|
| 33 |
-
assert puzzle.grid_rows == 6, f"Expected 6 rows, got {puzzle.grid_rows}"
|
| 34 |
-
assert puzzle.grid_cols == 8, f"Expected 8 columns, got {puzzle.grid_cols}"
|
| 35 |
-
print(f"β Puzzle created: {puzzle.grid_rows} rows Γ {puzzle.grid_cols} columns")
|
| 36 |
-
|
| 37 |
-
# Create GameState
|
| 38 |
-
state = GameState(
|
| 39 |
-
grid_rows=6,
|
| 40 |
-
grid_cols=8,
|
| 41 |
-
puzzle=puzzle,
|
| 42 |
-
revealed=set(),
|
| 43 |
-
guessed=set(),
|
| 44 |
-
score=0,
|
| 45 |
-
last_action="Test",
|
| 46 |
-
can_guess=False,
|
| 47 |
-
game_mode="classic",
|
| 48 |
-
points_by_word={},
|
| 49 |
-
start_time=None,
|
| 50 |
-
end_time=None
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
assert state.grid_rows == 6, f"Expected grid_rows=6, got {state.grid_rows}"
|
| 54 |
-
assert state.grid_cols == 8, f"Expected grid_cols=8, got {state.grid_cols}"
|
| 55 |
-
print(f"β GameState created: {state.grid_rows} rows Γ {state.grid_cols} columns")
|
| 56 |
-
|
| 57 |
-
# Verify all words are horizontal
|
| 58 |
-
for word in puzzle.words:
|
| 59 |
-
assert word.direction == "H", f"Word {word.text} is not horizontal"
|
| 60 |
-
print(f"β All {len(puzzle.words)} words are horizontal")
|
| 61 |
-
|
| 62 |
-
# Verify one word per row
|
| 63 |
-
rows_used = [w.start.x for w in puzzle.words]
|
| 64 |
-
assert len(set(rows_used)) == 6, f"Expected 6 unique rows, got {len(set(rows_used))}"
|
| 65 |
-
print(f"β One word per row verified")
|
| 66 |
-
|
| 67 |
-
# Verify all words fit in grid
|
| 68 |
-
for word in puzzle.words:
|
| 69 |
-
for coord in word.cells:
|
| 70 |
-
assert coord.in_bounds_rect(6, 8), f"Coord {coord} out of bounds"
|
| 71 |
-
print(f"β All word coordinates within 6Γ8 grid bounds")
|
| 72 |
-
|
| 73 |
-
print("\nβ
All Sprint 5 grid updates validated successfully!")
|
| 74 |
-
return True
|
| 75 |
-
|
| 76 |
-
if __name__ == "__main__":
|
| 77 |
-
try:
|
| 78 |
-
test_gamestate_creation()
|
| 79 |
-
sys.exit(0)
|
| 80 |
-
except Exception as e:
|
| 81 |
-
print(f"\nβ Test failed: {e}")
|
| 82 |
-
import traceback
|
| 83 |
-
traceback.print_exc()
|
| 84 |
-
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_sprint6_integration.py
DELETED
|
@@ -1,413 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
# -*- coding: utf-8 -*-
|
| 3 |
-
"""
|
| 4 |
-
Sprint 6: Integration & Testing
|
| 5 |
-
Comprehensive test suite for Wrdler v0.0.2
|
| 6 |
-
|
| 7 |
-
Tests all major gameplay flows:
|
| 8 |
-
1. Free letter selection
|
| 9 |
-
2. Cell reveal mechanics
|
| 10 |
-
3. Word guessing
|
| 11 |
-
4. Scoring system
|
| 12 |
-
5. Game completion
|
| 13 |
-
6. State management
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
import sys
|
| 17 |
-
import os
|
| 18 |
-
import io
|
| 19 |
-
|
| 20 |
-
# Fix Windows console encoding
|
| 21 |
-
if sys.platform == "win32":
|
| 22 |
-
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
| 23 |
-
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
| 24 |
-
|
| 25 |
-
# Add project to path
|
| 26 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 27 |
-
|
| 28 |
-
from wrdler.models import GameState, Puzzle, Coord, Word
|
| 29 |
-
from wrdler.generator import generate_puzzle
|
| 30 |
-
from wrdler.logic import (
|
| 31 |
-
build_letter_map,
|
| 32 |
-
reveal_cell,
|
| 33 |
-
reveal_free_letter,
|
| 34 |
-
guess_word,
|
| 35 |
-
is_game_over,
|
| 36 |
-
compute_tier,
|
| 37 |
-
auto_mark_completed_words
|
| 38 |
-
)
|
| 39 |
-
from wrdler.word_loader import load_word_list
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def test_puzzle_generation():
|
| 43 |
-
"""Test that puzzles generate correctly for Wrdler specs."""
|
| 44 |
-
print("\n" + "="*60)
|
| 45 |
-
print("TEST 1: Puzzle Generation")
|
| 46 |
-
print("="*60)
|
| 47 |
-
|
| 48 |
-
# Load word list using the proper function
|
| 49 |
-
words_dict = load_word_list("classic")
|
| 50 |
-
|
| 51 |
-
# Generate puzzle
|
| 52 |
-
puzzle = generate_puzzle(grid_rows=6, grid_cols=8, words_by_len=words_dict)
|
| 53 |
-
|
| 54 |
-
# Verify dimensions
|
| 55 |
-
assert puzzle.grid_rows == 6, f"Expected 6 rows, got {puzzle.grid_rows}"
|
| 56 |
-
assert puzzle.grid_cols == 8, f"Expected 8 columns, got {puzzle.grid_cols}"
|
| 57 |
-
print(f"β Grid dimensions: {puzzle.grid_rows}Γ{puzzle.grid_cols}")
|
| 58 |
-
|
| 59 |
-
# Verify word count
|
| 60 |
-
assert len(puzzle.words) == 6, f"Expected 6 words, got {len(puzzle.words)}"
|
| 61 |
-
print(f"β Word count: {len(puzzle.words)}")
|
| 62 |
-
|
| 63 |
-
# Verify all horizontal
|
| 64 |
-
for word in puzzle.words:
|
| 65 |
-
assert word.direction == "H", f"Word {word.text} is not horizontal"
|
| 66 |
-
print(f"β All words horizontal")
|
| 67 |
-
|
| 68 |
-
# Verify one per row
|
| 69 |
-
rows_used = set(w.start.x for w in puzzle.words)
|
| 70 |
-
assert len(rows_used) == 6, f"Expected 6 unique rows, got {len(rows_used)}"
|
| 71 |
-
print(f"β One word per row")
|
| 72 |
-
|
| 73 |
-
# Print puzzle info
|
| 74 |
-
print(f"\nPuzzle words:")
|
| 75 |
-
for i, word in enumerate(puzzle.words, 1):
|
| 76 |
-
print(f" {i}. {word.text} (row {word.start.x}, cols {word.start.y}-{word.start.y + len(word.text) - 1})")
|
| 77 |
-
|
| 78 |
-
return puzzle, words_dict
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def test_free_letter_selection(puzzle):
|
| 82 |
-
"""Test free letter selection mechanics."""
|
| 83 |
-
print("\n" + "="*60)
|
| 84 |
-
print("TEST 2: Free Letter Selection")
|
| 85 |
-
print("="*60)
|
| 86 |
-
|
| 87 |
-
# Create game state
|
| 88 |
-
state = GameState(
|
| 89 |
-
grid_rows=6,
|
| 90 |
-
grid_cols=8,
|
| 91 |
-
puzzle=puzzle,
|
| 92 |
-
revealed=set(),
|
| 93 |
-
guessed=set(),
|
| 94 |
-
score=0,
|
| 95 |
-
last_action="",
|
| 96 |
-
can_guess=False,
|
| 97 |
-
game_mode="classic",
|
| 98 |
-
points_by_word={},
|
| 99 |
-
start_time=None,
|
| 100 |
-
end_time=None,
|
| 101 |
-
free_letters=set(),
|
| 102 |
-
free_letters_used=0
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
letter_map = build_letter_map(puzzle)
|
| 106 |
-
|
| 107 |
-
# Get all unique letters in puzzle
|
| 108 |
-
all_letters = set()
|
| 109 |
-
for word in puzzle.words:
|
| 110 |
-
all_letters.update(word.text)
|
| 111 |
-
|
| 112 |
-
# Choose first letter (one that exists in puzzle)
|
| 113 |
-
test_letter = sorted(all_letters)[0]
|
| 114 |
-
print(f"\nChoosing first free letter: {test_letter}")
|
| 115 |
-
|
| 116 |
-
count1 = reveal_free_letter(state, letter_map, test_letter)
|
| 117 |
-
print(f"β Revealed {count1} instances of '{test_letter}'")
|
| 118 |
-
assert test_letter in state.free_letters, "Letter not tracked in free_letters"
|
| 119 |
-
assert state.free_letters_used == 1, f"Expected free_letters_used=1, got {state.free_letters_used}"
|
| 120 |
-
assert count1 > 0, f"Letter {test_letter} should have instances in puzzle"
|
| 121 |
-
|
| 122 |
-
# Choose second letter
|
| 123 |
-
available_letters = sorted(all_letters - state.free_letters)
|
| 124 |
-
test_letter2 = available_letters[0]
|
| 125 |
-
print(f"Choosing second free letter: {test_letter2}")
|
| 126 |
-
|
| 127 |
-
count2 = reveal_free_letter(state, letter_map, test_letter2)
|
| 128 |
-
print(f"β Revealed {count2} instances of '{test_letter2}'")
|
| 129 |
-
assert test_letter2 in state.free_letters, "Second letter not tracked"
|
| 130 |
-
assert state.free_letters_used == 2, f"Expected free_letters_used=2, got {state.free_letters_used}"
|
| 131 |
-
|
| 132 |
-
# Try to select third letter (should fail)
|
| 133 |
-
if len(available_letters) > 1:
|
| 134 |
-
test_letter3 = available_letters[1]
|
| 135 |
-
count3 = reveal_free_letter(state, letter_map, test_letter3)
|
| 136 |
-
assert count3 == 0, "Should not allow third free letter"
|
| 137 |
-
assert state.free_letters_used == 2, "Should remain at 2 free letters"
|
| 138 |
-
print(f"β Correctly rejected third free letter")
|
| 139 |
-
|
| 140 |
-
total_revealed = len(state.revealed)
|
| 141 |
-
print(f"\nTotal cells revealed from free letters: {total_revealed}")
|
| 142 |
-
|
| 143 |
-
return state, letter_map
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
def test_cell_reveal(state, puzzle, letter_map):
|
| 147 |
-
"""Test cell reveal mechanics."""
|
| 148 |
-
print("\n" + "="*60)
|
| 149 |
-
print("TEST 3: Cell Reveal Mechanics")
|
| 150 |
-
print("="*60)
|
| 151 |
-
|
| 152 |
-
# Find an unrevealed cell with a letter
|
| 153 |
-
unrevealed_letter_cell = None
|
| 154 |
-
for coord, letter in letter_map.items():
|
| 155 |
-
if coord not in state.revealed:
|
| 156 |
-
unrevealed_letter_cell = coord
|
| 157 |
-
break
|
| 158 |
-
|
| 159 |
-
if unrevealed_letter_cell:
|
| 160 |
-
print(f"\nRevealing cell at {unrevealed_letter_cell}")
|
| 161 |
-
before_count = len(state.revealed)
|
| 162 |
-
reveal_cell(state, letter_map, unrevealed_letter_cell)
|
| 163 |
-
after_count = len(state.revealed)
|
| 164 |
-
|
| 165 |
-
assert after_count == before_count + 1, "Should reveal exactly one cell"
|
| 166 |
-
assert unrevealed_letter_cell in state.revealed, "Cell should be marked as revealed"
|
| 167 |
-
print(f"β Cell revealed successfully")
|
| 168 |
-
print(f"β Total revealed cells: {after_count}")
|
| 169 |
-
else:
|
| 170 |
-
print("β All letter cells already revealed by free letters")
|
| 171 |
-
|
| 172 |
-
# Test revealing an already-revealed cell
|
| 173 |
-
if state.revealed:
|
| 174 |
-
already_revealed = list(state.revealed)[0]
|
| 175 |
-
before_count = len(state.revealed)
|
| 176 |
-
reveal_cell(state, letter_map, already_revealed)
|
| 177 |
-
after_count = len(state.revealed)
|
| 178 |
-
assert after_count == before_count, "Should not change revealed count"
|
| 179 |
-
print(f"β Correctly handled already-revealed cell")
|
| 180 |
-
|
| 181 |
-
return state
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
def test_word_guessing(state, puzzle, letter_map):
|
| 185 |
-
"""Test word guessing mechanics."""
|
| 186 |
-
print("\n" + "="*60)
|
| 187 |
-
print("TEST 4: Word Guessing Mechanics")
|
| 188 |
-
print("="*60)
|
| 189 |
-
|
| 190 |
-
# Ensure at least one letter is revealed for guessing
|
| 191 |
-
if not state.revealed:
|
| 192 |
-
# Reveal a cell
|
| 193 |
-
for coord in letter_map.keys():
|
| 194 |
-
reveal_cell(state, letter_map, coord)
|
| 195 |
-
break
|
| 196 |
-
|
| 197 |
-
# Try to guess a correct word
|
| 198 |
-
target_word = puzzle.words[0].text
|
| 199 |
-
print(f"\nAttempting to guess: {target_word}")
|
| 200 |
-
|
| 201 |
-
# Reveal at least one letter from the target word to enable guessing
|
| 202 |
-
for cell in puzzle.words[0].cells:
|
| 203 |
-
if cell not in state.revealed:
|
| 204 |
-
reveal_cell(state, letter_map, cell)
|
| 205 |
-
break
|
| 206 |
-
|
| 207 |
-
state.can_guess = True # Enable guessing
|
| 208 |
-
before_score = state.score
|
| 209 |
-
result, points = guess_word(state, target_word)
|
| 210 |
-
|
| 211 |
-
if result:
|
| 212 |
-
print(f"β Correct guess accepted")
|
| 213 |
-
print(f"β Score increased from {before_score} to {state.score}")
|
| 214 |
-
assert target_word in state.guessed, "Word should be marked as guessed"
|
| 215 |
-
assert state.score > before_score, "Score should increase"
|
| 216 |
-
else:
|
| 217 |
-
print(f"β Guess failed (may need more letters revealed)")
|
| 218 |
-
|
| 219 |
-
# Try incorrect guess
|
| 220 |
-
fake_word = "ZZZZZ"
|
| 221 |
-
print(f"\nAttempting incorrect guess: {fake_word}")
|
| 222 |
-
before_score = state.score
|
| 223 |
-
result, points = guess_word(state, fake_word)
|
| 224 |
-
|
| 225 |
-
assert not result, "Incorrect guess should fail"
|
| 226 |
-
assert state.score == before_score, "Score should not change on incorrect guess"
|
| 227 |
-
print(f"β Incorrect guess correctly rejected")
|
| 228 |
-
|
| 229 |
-
return state
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
def test_game_completion(state, puzzle, letter_map):
|
| 233 |
-
"""Test game completion detection."""
|
| 234 |
-
print("\n" + "="*60)
|
| 235 |
-
print("TEST 5: Game Completion")
|
| 236 |
-
print("="*60)
|
| 237 |
-
|
| 238 |
-
# Check current game status
|
| 239 |
-
is_over = is_game_over(state)
|
| 240 |
-
print(f"\nGame over status: {is_over}")
|
| 241 |
-
print(f"Words guessed: {len(state.guessed)}/{len(puzzle.words)}")
|
| 242 |
-
|
| 243 |
-
# Guess all remaining words
|
| 244 |
-
for word in puzzle.words:
|
| 245 |
-
if word.text not in state.guessed:
|
| 246 |
-
# Reveal at least one letter from the word
|
| 247 |
-
for cell in word.cells:
|
| 248 |
-
if cell not in state.revealed:
|
| 249 |
-
reveal_cell(state, letter_map, cell)
|
| 250 |
-
break
|
| 251 |
-
|
| 252 |
-
state.can_guess = True
|
| 253 |
-
result, points = guess_word(state, word.text)
|
| 254 |
-
if result:
|
| 255 |
-
print(f" β Guessed: {word.text}")
|
| 256 |
-
|
| 257 |
-
# Check if game is now complete
|
| 258 |
-
is_over = is_game_over(state)
|
| 259 |
-
print(f"\nFinal game over status: {is_over}")
|
| 260 |
-
print(f"Final score: {state.score}")
|
| 261 |
-
|
| 262 |
-
# Compute tier
|
| 263 |
-
tier = compute_tier(state.score)
|
| 264 |
-
print(f"Score tier: {tier}")
|
| 265 |
-
|
| 266 |
-
assert is_over or len(state.guessed) == len(puzzle.words), "Game should be complete"
|
| 267 |
-
print(f"β Game completion detection working")
|
| 268 |
-
|
| 269 |
-
return state
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def test_auto_mark_completed():
|
| 273 |
-
"""Test auto-marking of completed words."""
|
| 274 |
-
print("\n" + "="*60)
|
| 275 |
-
print("TEST 6: Auto-Mark Completed Words")
|
| 276 |
-
print("="*60)
|
| 277 |
-
|
| 278 |
-
# Create a simple test puzzle
|
| 279 |
-
words_dict = load_word_list("classic")
|
| 280 |
-
puzzle = generate_puzzle(grid_rows=6, grid_cols=8, words_by_len=words_dict)
|
| 281 |
-
letter_map = build_letter_map(puzzle)
|
| 282 |
-
|
| 283 |
-
state = GameState(
|
| 284 |
-
grid_rows=6,
|
| 285 |
-
grid_cols=8,
|
| 286 |
-
puzzle=puzzle,
|
| 287 |
-
revealed=set(),
|
| 288 |
-
guessed=set(),
|
| 289 |
-
score=0,
|
| 290 |
-
last_action="",
|
| 291 |
-
can_guess=False,
|
| 292 |
-
game_mode="classic",
|
| 293 |
-
points_by_word={},
|
| 294 |
-
)
|
| 295 |
-
|
| 296 |
-
# Reveal all cells of first word
|
| 297 |
-
first_word = puzzle.words[0]
|
| 298 |
-
print(f"\nRevealing all cells of: {first_word.text}")
|
| 299 |
-
for cell in first_word.cells:
|
| 300 |
-
state.revealed.add(cell)
|
| 301 |
-
|
| 302 |
-
before_guessed = len(state.guessed)
|
| 303 |
-
auto_mark_completed_words(state)
|
| 304 |
-
after_guessed = len(state.guessed)
|
| 305 |
-
|
| 306 |
-
assert after_guessed > before_guessed, "Should auto-mark completed word"
|
| 307 |
-
assert first_word.text in state.guessed, "First word should be auto-guessed"
|
| 308 |
-
print(f"β Auto-marked completed word: {first_word.text}")
|
| 309 |
-
|
| 310 |
-
return True
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
def test_state_consistency():
|
| 314 |
-
"""Test state management consistency."""
|
| 315 |
-
print("\n" + "="*60)
|
| 316 |
-
print("TEST 7: State Consistency")
|
| 317 |
-
print("="*60)
|
| 318 |
-
|
| 319 |
-
# Create game state
|
| 320 |
-
words_dict = load_word_list("classic")
|
| 321 |
-
puzzle = generate_puzzle(grid_rows=6, grid_cols=8, words_by_len=words_dict)
|
| 322 |
-
|
| 323 |
-
state = GameState(
|
| 324 |
-
grid_rows=6,
|
| 325 |
-
grid_cols=8,
|
| 326 |
-
puzzle=puzzle,
|
| 327 |
-
revealed=set(),
|
| 328 |
-
guessed=set(),
|
| 329 |
-
score=0,
|
| 330 |
-
last_action="Test",
|
| 331 |
-
can_guess=False,
|
| 332 |
-
game_mode="classic",
|
| 333 |
-
points_by_word={},
|
| 334 |
-
free_letters=set(),
|
| 335 |
-
free_letters_used=0,
|
| 336 |
-
)
|
| 337 |
-
|
| 338 |
-
# Verify field consistency
|
| 339 |
-
assert state.grid_rows == 6, "grid_rows should be 6"
|
| 340 |
-
assert state.grid_cols == 8, "grid_cols should be 8"
|
| 341 |
-
assert state.puzzle == puzzle, "puzzle should match"
|
| 342 |
-
assert isinstance(state.revealed, set), "revealed should be a set"
|
| 343 |
-
assert isinstance(state.guessed, set), "guessed should be a set"
|
| 344 |
-
assert isinstance(state.free_letters, set), "free_letters should be a set"
|
| 345 |
-
assert state.free_letters_used == 0, "free_letters_used should start at 0"
|
| 346 |
-
|
| 347 |
-
print("β All GameState fields have correct types")
|
| 348 |
-
print("β State consistency verified")
|
| 349 |
-
|
| 350 |
-
return True
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
def run_all_tests():
|
| 354 |
-
"""Run all integration tests."""
|
| 355 |
-
print("\n" + "="*60)
|
| 356 |
-
print("SPRINT 6: INTEGRATION & TESTING")
|
| 357 |
-
print("Wrdler v0.0.2")
|
| 358 |
-
print("="*60)
|
| 359 |
-
|
| 360 |
-
try:
|
| 361 |
-
# Test 1: Puzzle Generation
|
| 362 |
-
puzzle, words_dict = test_puzzle_generation()
|
| 363 |
-
|
| 364 |
-
# Test 2: Free Letter Selection
|
| 365 |
-
state, letter_map = test_free_letter_selection(puzzle)
|
| 366 |
-
|
| 367 |
-
# Test 3: Cell Reveal
|
| 368 |
-
state = test_cell_reveal(state, puzzle, letter_map)
|
| 369 |
-
|
| 370 |
-
# Test 4: Word Guessing
|
| 371 |
-
state = test_word_guessing(state, puzzle, letter_map)
|
| 372 |
-
|
| 373 |
-
# Test 5: Game Completion
|
| 374 |
-
state = test_game_completion(state, puzzle, letter_map)
|
| 375 |
-
|
| 376 |
-
# Test 6: Auto-Mark Completed
|
| 377 |
-
test_auto_mark_completed()
|
| 378 |
-
|
| 379 |
-
# Test 7: State Consistency
|
| 380 |
-
test_state_consistency()
|
| 381 |
-
|
| 382 |
-
# Summary
|
| 383 |
-
print("\n" + "="*60)
|
| 384 |
-
print("TEST SUMMARY")
|
| 385 |
-
print("="*60)
|
| 386 |
-
print("β
All integration tests PASSED")
|
| 387 |
-
print("\nTests completed:")
|
| 388 |
-
print(" β Puzzle generation (8Γ6 grid, horizontal-only)")
|
| 389 |
-
print(" β Free letter selection (2 letters)")
|
| 390 |
-
print(" β Cell reveal mechanics")
|
| 391 |
-
print(" β Word guessing (correct and incorrect)")
|
| 392 |
-
print(" β Game completion detection")
|
| 393 |
-
print(" β Auto-mark completed words")
|
| 394 |
-
print(" β State consistency")
|
| 395 |
-
print("\nβ
Wrdler v0.0.2 is ready for deployment!")
|
| 396 |
-
|
| 397 |
-
return True
|
| 398 |
-
|
| 399 |
-
except AssertionError as e:
|
| 400 |
-
print(f"\nβ TEST FAILED: {e}")
|
| 401 |
-
import traceback
|
| 402 |
-
traceback.print_exc()
|
| 403 |
-
return False
|
| 404 |
-
except Exception as e:
|
| 405 |
-
print(f"\nβ ERROR: {e}")
|
| 406 |
-
import traceback
|
| 407 |
-
traceback.print_exc()
|
| 408 |
-
return False
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
if __name__ == "__main__":
|
| 412 |
-
success = run_all_tests()
|
| 413 |
-
sys.exit(0 if success else 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.0.
|
| 12 |
__all__ = ["models", "generator", "logic", "ui", "word_loader"]
|
|
|
|
| 8 |
- 2 free letter guesses at game start
|
| 9 |
"""
|
| 10 |
|
| 11 |
+
__version__ = "0.0.4"
|
| 12 |
__all__ = ["models", "generator", "logic", "ui", "word_loader"]
|
wrdler/game_storage.py
CHANGED
|
@@ -4,8 +4,15 @@ Wrdler-specific storage wrapper for HuggingFace storage operations.
|
|
| 4 |
|
| 5 |
This module provides high-level functions for saving and loading Wrdler games
|
| 6 |
using the shared storage module from wrdler.modules.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
-
__version__ = "0.1.
|
| 9 |
|
| 10 |
import json
|
| 11 |
import tempfile
|
|
@@ -55,8 +62,8 @@ def serialize_game_settings(
|
|
| 55 |
score: int,
|
| 56 |
time_seconds: int,
|
| 57 |
game_mode: str,
|
| 58 |
-
grid_size: int =
|
| 59 |
-
spacer: int =
|
| 60 |
may_overlap: bool = False,
|
| 61 |
wordlist_source: Optional[str] = None,
|
| 62 |
challenge_id: Optional[str] = None
|
|
@@ -66,15 +73,21 @@ def serialize_game_settings(
|
|
| 66 |
Creates initial structure with one user's result.
|
| 67 |
Each user has their own uid and word_list.
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
Args:
|
| 70 |
-
word_list: List of words used in THIS user's game
|
| 71 |
username: Player's name
|
| 72 |
score: Final score achieved
|
| 73 |
time_seconds: Time taken to complete (in seconds)
|
| 74 |
game_mode: Game mode ("classic" or "too_easy")
|
| 75 |
-
grid_size: Grid
|
| 76 |
-
spacer: Word spacing configuration (
|
| 77 |
-
may_overlap: Whether words can overlap (
|
| 78 |
wordlist_source: Source file name (e.g., "classic.txt")
|
| 79 |
challenge_id: Optional challenge ID (generated if not provided)
|
| 80 |
|
|
@@ -140,10 +153,13 @@ def add_user_result_to_game(
|
|
| 140 |
Add a user's result to an existing shared challenge.
|
| 141 |
Each user gets their own uid and word_list.
|
| 142 |
|
|
|
|
|
|
|
|
|
|
| 143 |
Args:
|
| 144 |
sid: Short ID of the existing challenge
|
| 145 |
username: Player's name
|
| 146 |
-
word_list: List of words THIS user played
|
| 147 |
score: Score achieved
|
| 148 |
time_seconds: Time taken (seconds)
|
| 149 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
|
@@ -247,8 +263,8 @@ def save_game_to_hf(
|
|
| 247 |
score: int,
|
| 248 |
time_seconds: int,
|
| 249 |
game_mode: str,
|
| 250 |
-
grid_size: int =
|
| 251 |
-
spacer: int =
|
| 252 |
may_overlap: bool = False,
|
| 253 |
repo_id: Optional[str] = None,
|
| 254 |
wordlist_source: Optional[str] = None
|
|
@@ -264,15 +280,21 @@ def save_game_to_hf(
|
|
| 264 |
4. Creates a shortened URL (sid) for sharing
|
| 265 |
5. Returns the full URL and short ID
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
Args:
|
| 268 |
-
word_list: List of words used in the game
|
| 269 |
username: Player's name
|
| 270 |
score: Final score achieved
|
| 271 |
time_seconds: Time taken to complete (in seconds)
|
| 272 |
game_mode: Game mode ("classic" or "too_easy")
|
| 273 |
-
grid_size: Grid
|
| 274 |
-
spacer: Word spacing
|
| 275 |
-
may_overlap: Whether words can overlap (
|
| 276 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
| 277 |
wordlist_source: Source wordlist file name (e.g., "classic.txt")
|
| 278 |
|
|
@@ -366,6 +388,11 @@ def load_game_from_sid(
|
|
| 366 |
Load game settings from a short ID (sid).
|
| 367 |
If settings.json cannot be found, return None and allow normal game loading.
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
Args:
|
| 370 |
sid: Short ID (8 characters) from shareable URL
|
| 371 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
|
@@ -376,13 +403,14 @@ def load_game_from_sid(
|
|
| 376 |
dict: Challenge settings containing:
|
| 377 |
- challenge_id: Unique challenge identifier
|
| 378 |
- wordlist_source: Source wordlist file (e.g., "classic.txt")
|
| 379 |
-
- game_mode: Game mode
|
| 380 |
-
- grid_size: Grid
|
| 381 |
-
- puzzle_options: Puzzle configuration (spacer, may_overlap)
|
| 382 |
- users: Array of user results, each with:
|
| 383 |
- uid: Unique user game identifier
|
| 384 |
- username: Player name
|
| 385 |
-
- word_list: Words THIS user played
|
|
|
|
| 386 |
- score: Score achieved
|
| 387 |
- time: Time taken (seconds)
|
| 388 |
- timestamp: When result was recorded
|
|
@@ -392,12 +420,12 @@ def load_game_from_sid(
|
|
| 392 |
Returns None if sid not found or download fails
|
| 393 |
|
| 394 |
Example:
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
... print(f"Wordlist: {settings['wordlist_source']}")
|
| 399 |
-
|
| 400 |
-
|
| 401 |
"""
|
| 402 |
if repo_id is None:
|
| 403 |
repo_id = HF_REPO_ID
|
|
@@ -426,15 +454,15 @@ def load_game_from_sid(
|
|
| 426 |
if len(url_parts) != 2:
|
| 427 |
logger.error(f"β Invalid URL format: {full_url}")
|
| 428 |
return None
|
| 429 |
-
|
| 430 |
file_path = url_parts[1]
|
| 431 |
-
logger.info(f"π₯ Downloading {file_path} using authenticated API...")
|
| 432 |
-
|
| 433 |
settings = _get_json_from_repo(repo_id, file_path, repo_type="dataset")
|
| 434 |
if not settings:
|
| 435 |
logger.error(f"β settings.json not found for sid: {sid}. Loading normal game.")
|
| 436 |
return None
|
| 437 |
-
|
| 438 |
logger.info(f"β
Loaded challenge: {settings.get('challenge_id', 'unknown')}")
|
| 439 |
users = settings.get('users', [])
|
| 440 |
logger.debug(f"Users in challenge: {len(users)}")
|
|
@@ -467,7 +495,7 @@ def get_shareable_url(sid: str, base_url: str = None) -> str:
|
|
| 467 |
Example:
|
| 468 |
>>> url = get_shareable_url("abc12345")
|
| 469 |
>>> print(url)
|
| 470 |
-
https://surn-
|
| 471 |
"""
|
| 472 |
import os
|
| 473 |
from wrdler.modules.constants import SPACE_NAME
|
|
@@ -515,6 +543,11 @@ if __name__ == "__main__":
|
|
| 515 |
print(f"Version: {__version__}")
|
| 516 |
print(f"Target Repository: {HF_REPO_ID}")
|
| 517 |
print(f"Space Name: {SPACE_NAME}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
|
| 519 |
# Example: Save a game
|
| 520 |
print("\n--- Example: Save Game ---")
|
|
@@ -524,7 +557,9 @@ if __name__ == "__main__":
|
|
| 524 |
username="Alice",
|
| 525 |
score=42,
|
| 526 |
time_seconds=180,
|
| 527 |
-
game_mode="classic"
|
|
|
|
|
|
|
| 528 |
)
|
| 529 |
print(f"Challenge ID: {challenge_id}")
|
| 530 |
print(f"Full URL: {full_url}")
|
|
@@ -540,7 +575,9 @@ if __name__ == "__main__":
|
|
| 540 |
if settings:
|
| 541 |
print(f"Loaded Challenge: {settings['challenge_id']}")
|
| 542 |
print(f"Wordlist Source: {settings.get('wordlist_source', 'N/A')}")
|
|
|
|
| 543 |
users = settings.get('users', [])
|
| 544 |
print(f"Users: {len(users)}")
|
| 545 |
for user in users:
|
| 546 |
print(f" - {user['username']}: {user['score']} pts in {user['time']}s")
|
|
|
|
|
|
| 4 |
|
| 5 |
This module provides high-level functions for saving and loading Wrdler games
|
| 6 |
using the shared storage module from wrdler.modules.
|
| 7 |
+
|
| 8 |
+
Wrdler Specifications:
|
| 9 |
+
- 8x6 grid (8 columns Γ6 rows = 48 cells)
|
| 10 |
+
- 6 words total (one per row, rows 0-5)
|
| 11 |
+
- Horizontal placement only (no vertical words)
|
| 12 |
+
- No word overlaps
|
| 13 |
+
- 2 free letter guesses at game start
|
| 14 |
"""
|
| 15 |
+
__version__ = "0.1.4"
|
| 16 |
|
| 17 |
import json
|
| 18 |
import tempfile
|
|
|
|
| 62 |
score: int,
|
| 63 |
time_seconds: int,
|
| 64 |
game_mode: str,
|
| 65 |
+
grid_size: int = 8,
|
| 66 |
+
spacer: int = 0,
|
| 67 |
may_overlap: bool = False,
|
| 68 |
wordlist_source: Optional[str] = None,
|
| 69 |
challenge_id: Optional[str] = None
|
|
|
|
| 73 |
Creates initial structure with one user's result.
|
| 74 |
Each user has their own uid and word_list.
|
| 75 |
|
| 76 |
+
Wrdler Configuration:
|
| 77 |
+
- Grid: 8x6 (grid_size=8 means 8 columns, 6 rows hardcoded)
|
| 78 |
+
- Words: 6 total, one per row, horizontal only
|
| 79 |
+
- Overlaps: Not allowed (may_overlap always False)
|
| 80 |
+
- Spacing: Fixed one word per row (spacer not used)
|
| 81 |
+
|
| 82 |
Args:
|
| 83 |
+
word_list: List of words used in THIS user's game (exactly 6 words)
|
| 84 |
username: Player's name
|
| 85 |
score: Final score achieved
|
| 86 |
time_seconds: Time taken to complete (in seconds)
|
| 87 |
game_mode: Game mode ("classic" or "too_easy")
|
| 88 |
+
grid_size: Grid width in columns (default: 8 for Wrdler's 8x6 grid)
|
| 89 |
+
spacer: Word spacing configuration (not used in Wrdler, kept for compatibility)
|
| 90 |
+
may_overlap: Whether words can overlap (always False in Wrdler)
|
| 91 |
wordlist_source: Source file name (e.g., "classic.txt")
|
| 92 |
challenge_id: Optional challenge ID (generated if not provided)
|
| 93 |
|
|
|
|
| 153 |
Add a user's result to an existing shared challenge.
|
| 154 |
Each user gets their own uid and word_list.
|
| 155 |
|
| 156 |
+
In Wrdler, each player gets different random words from the same wordlist,
|
| 157 |
+
so word_list will be unique per user while wordlist_source remains consistent.
|
| 158 |
+
|
| 159 |
Args:
|
| 160 |
sid: Short ID of the existing challenge
|
| 161 |
username: Player's name
|
| 162 |
+
word_list: List of words THIS user played (6 words, horizontal only)
|
| 163 |
score: Score achieved
|
| 164 |
time_seconds: Time taken (seconds)
|
| 165 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
|
|
|
| 263 |
score: int,
|
| 264 |
time_seconds: int,
|
| 265 |
game_mode: str,
|
| 266 |
+
grid_size: int = 8,
|
| 267 |
+
spacer: int = 0,
|
| 268 |
may_overlap: bool = False,
|
| 269 |
repo_id: Optional[str] = None,
|
| 270 |
wordlist_source: Optional[str] = None
|
|
|
|
| 280 |
4. Creates a shortened URL (sid) for sharing
|
| 281 |
5. Returns the full URL and short ID
|
| 282 |
|
| 283 |
+
Wrdler Configuration:
|
| 284 |
+
- Grid: 8x6 (grid_size=8, rows fixed at 6)
|
| 285 |
+
- Words: 6 total, one per row, horizontal only
|
| 286 |
+
- Overlaps: Not allowed (may_overlap=False)
|
| 287 |
+
- Each player in a challenge gets different random words
|
| 288 |
+
|
| 289 |
Args:
|
| 290 |
+
word_list: List of words used in the game (exactly 6 words)
|
| 291 |
username: Player's name
|
| 292 |
score: Final score achieved
|
| 293 |
time_seconds: Time taken to complete (in seconds)
|
| 294 |
game_mode: Game mode ("classic" or "too_easy")
|
| 295 |
+
grid_size: Grid width (default: 8 for 8x6 Wrdler grid)
|
| 296 |
+
spacer: Word spacing (not used in Wrdler, kept for compatibility)
|
| 297 |
+
may_overlap: Whether words can overlap (always False in Wrdler)
|
| 298 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
| 299 |
wordlist_source: Source wordlist file name (e.g., "classic.txt")
|
| 300 |
|
|
|
|
| 388 |
Load game settings from a short ID (sid).
|
| 389 |
If settings.json cannot be found, return None and allow normal game loading.
|
| 390 |
|
| 391 |
+
In Wrdler challenges, each player gets different random words from the same
|
| 392 |
+
wordlist_source, so the returned settings contain:
|
| 393 |
+
- Shared: wordlist_source, game_mode, grid configuration
|
| 394 |
+
- Per-user: Each user has unique word_list in their result entry
|
| 395 |
+
|
| 396 |
Args:
|
| 397 |
sid: Short ID (8 characters) from shareable URL
|
| 398 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
|
|
|
| 403 |
dict: Challenge settings containing:
|
| 404 |
- challenge_id: Unique challenge identifier
|
| 405 |
- wordlist_source: Source wordlist file (e.g., "classic.txt")
|
| 406 |
+
- game_mode: Game mode ("classic" or "too_easy")
|
| 407 |
+
- grid_size: Grid width (8 for Wrdler's 8x6 grid)
|
| 408 |
+
- puzzle_options: Puzzle configuration (spacer=0, may_overlap=False)
|
| 409 |
- users: Array of user results, each with:
|
| 410 |
- uid: Unique user game identifier
|
| 411 |
- username: Player name
|
| 412 |
+
- word_list: Words THIS user played (6 words, horizontal only)
|
| 413 |
+
- word_list_difficulty: Optional difficulty score
|
| 414 |
- score: Score achieved
|
| 415 |
- time: Time taken (seconds)
|
| 416 |
- timestamp: When result was recorded
|
|
|
|
| 420 |
Returns None if sid not found or download fails
|
| 421 |
|
| 422 |
Example:
|
| 423 |
+
>>> settings = load_game_from_sid("abc12345")
|
| 424 |
+
>>> if settings:
|
| 425 |
+
... print(f"Challenge ID: {settings['challenge_id']}")
|
| 426 |
... print(f"Wordlist: {settings['wordlist_source']}")
|
| 427 |
+
... for user in settings['users']:
|
| 428 |
+
... print(f"{user['username']}: {user['score']} pts")
|
| 429 |
"""
|
| 430 |
if repo_id is None:
|
| 431 |
repo_id = HF_REPO_ID
|
|
|
|
| 454 |
if len(url_parts) != 2:
|
| 455 |
logger.error(f"β Invalid URL format: {full_url}")
|
| 456 |
return None
|
| 457 |
+
|
| 458 |
file_path = url_parts[1]
|
| 459 |
+
logger.info(f"π₯ Downloading {file_path} using authenticated API...");
|
| 460 |
+
|
| 461 |
settings = _get_json_from_repo(repo_id, file_path, repo_type="dataset")
|
| 462 |
if not settings:
|
| 463 |
logger.error(f"β settings.json not found for sid: {sid}. Loading normal game.")
|
| 464 |
return None
|
| 465 |
+
|
| 466 |
logger.info(f"β
Loaded challenge: {settings.get('challenge_id', 'unknown')}")
|
| 467 |
users = settings.get('users', [])
|
| 468 |
logger.debug(f"Users in challenge: {len(users)}")
|
|
|
|
| 495 |
Example:
|
| 496 |
>>> url = get_shareable_url("abc12345")
|
| 497 |
>>> print(url)
|
| 498 |
+
https://surn-wrdler.hf.space/?game_id=abc12345
|
| 499 |
"""
|
| 500 |
import os
|
| 501 |
from wrdler.modules.constants import SPACE_NAME
|
|
|
|
| 543 |
print(f"Version: {__version__}")
|
| 544 |
print(f"Target Repository: {HF_REPO_ID}")
|
| 545 |
print(f"Space Name: {SPACE_NAME}")
|
| 546 |
+
print("\nWrdler Configuration:")
|
| 547 |
+
print("- Grid: 8x6 (8 columns Γ 6 rows)")
|
| 548 |
+
print("- Words: 6 total (one per row, horizontal only)")
|
| 549 |
+
print("- Overlaps: Not allowed")
|
| 550 |
+
print("- Free Letters: 2 at game start")
|
| 551 |
|
| 552 |
# Example: Save a game
|
| 553 |
print("\n--- Example: Save Game ---")
|
|
|
|
| 557 |
username="Alice",
|
| 558 |
score=42,
|
| 559 |
time_seconds=180,
|
| 560 |
+
game_mode="classic",
|
| 561 |
+
grid_size=8, # Wrdler default
|
| 562 |
+
wordlist_source="classic.txt"
|
| 563 |
)
|
| 564 |
print(f"Challenge ID: {challenge_id}")
|
| 565 |
print(f"Full URL: {full_url}")
|
|
|
|
| 575 |
if settings:
|
| 576 |
print(f"Loaded Challenge: {settings['challenge_id']}")
|
| 577 |
print(f"Wordlist Source: {settings.get('wordlist_source', 'N/A')}")
|
| 578 |
+
print(f"Grid Size: {settings.get('grid_size', 'N/A')} (8x6 for Wrdler)")
|
| 579 |
users = settings.get('users', [])
|
| 580 |
print(f"Users: {len(users)}")
|
| 581 |
for user in users:
|
| 582 |
print(f" - {user['username']}: {user['score']} pts in {user['time']}s")
|
| 583 |
+
print(f" Words: {', '.join(user['word_list'])}")
|
wrdler/ui.py
CHANGED
|
@@ -281,57 +281,54 @@ def inject_ocean_layers() -> None:
|
|
| 281 |
st.markdown(
|
| 282 |
"""
|
| 283 |
<style>
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
pointer-events: none;
|
| 297 |
-
}
|
| 298 |
-
.bw-bg-highlight {
|
| 299 |
-
z-index: 11;
|
| 300 |
-
background: radial-gradient(150% 100% at 50% -20%, rgba(255,255,255,0.10) 0%, transparent 60%);
|
| 301 |
-
background-size: 150% 150%;
|
| 302 |
-
animation: oceanHighlight 12s ease-in-out infinite;
|
| 303 |
-
pointer-events: none;
|
| 304 |
-
}
|
| 305 |
-
.bw-bg-long {
|
| 306 |
-
z-index: 12;
|
| 307 |
-
background: repeating-linear-gradient(-6deg, rgba(255,255,255,0.08) 0px, rgba(255,255,255,0.08) 18px, rgba(0,0,0,0.04) 18px, rgba(0,0,0,0.04) 48px);
|
| 308 |
-
background-size: 150% 150%;
|
| 309 |
-
animation: oceanLong 36s linear infinite;
|
| 310 |
-
opacity: 0.2;
|
| 311 |
-
pointer-events: none;
|
| 312 |
-
}
|
| 313 |
-
.bw-bg-mid {
|
| 314 |
-
z-index: 13;
|
| 315 |
-
background: repeating-linear-gradient(-12deg, rgba(255,255,255,0.10) 0px, rgba(255,255,255,0.10) 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 26px);
|
| 316 |
-
background-size: 150% 150%;
|
| 317 |
-
animation: oceanMid 24s linear infinite;
|
| 318 |
-
opacity: 0.2;
|
| 319 |
pointer-events: none;
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
<div class="bw-bg-container">
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
</div>
|
| 336 |
""",
|
| 337 |
unsafe_allow_html=True,
|
|
@@ -496,6 +493,11 @@ border-radius: 50% !important;
|
|
| 496 |
.st-emotion-cache-1n6tfoc { background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666); gap: 0.1rem !important; color: white; border-radius:15px; padding: 10px 10px 10px 5px; }
|
| 497 |
.st-emotion-cache-1n6tfoc::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; margin: 5px;}
|
| 498 |
.st-key-guess_input, .st-key-guess_submit { flex-direction: row; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: flex-end; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
/* grid adjustments */
|
| 501 |
@media (min-width: 560px){
|
|
@@ -521,10 +523,10 @@ border-radius: 50% !important;
|
|
| 521 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] { flex-direction: column-reverse !important; width: 100% !important; max-width: 100vw !important; }
|
| 522 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { width: 100% !important; min-width: 100% !important; max-width: 100% !important; flex: 1 1 100% !important; }
|
| 523 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 524 |
-
.st-emotion-cache-
|
| 525 |
-
.bw-free-letter-grid {
|
| 526 |
-
|
| 527 |
-
}
|
| 528 |
}
|
| 529 |
|
| 530 |
.bold-text { font-weight: 700; }
|
|
@@ -678,7 +680,7 @@ def _new_game() -> None:
|
|
| 678 |
effects_volume = st.session_state.get("effects_volume",25)
|
| 679 |
enable_sound_effects = st.session_state.get("enable_sound_effects", True)
|
| 680 |
# NEW: Preserve Show Challenge Share Links
|
| 681 |
-
show_challenge_share_links = st.session_state.get("show_challenge_share_links",
|
| 682 |
|
| 683 |
st.session_state.clear()
|
| 684 |
if selected:
|
|
@@ -1095,7 +1097,7 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
|
|
| 1095 |
.st-emotion-cache-ig7yu6 {
|
| 1096 |
min-width: calc(30% - 1.5rem);
|
| 1097 |
}
|
| 1098 |
-
.st-emotion-cache-
|
| 1099 |
min-width: calc(12.5% - 1rem);
|
| 1100 |
}
|
| 1101 |
}
|
|
@@ -1168,6 +1170,9 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
|
|
| 1168 |
# Note: letter_map is static and built once in _init_session(), no need to rebuild
|
| 1169 |
_sync_back(state)
|
| 1170 |
|
|
|
|
|
|
|
|
|
|
| 1171 |
# Play sound effect based on hit or miss
|
| 1172 |
action = (state.last_action or "").strip()
|
| 1173 |
if action.startswith("Revealed '"):
|
|
@@ -1427,7 +1432,7 @@ def _render_score_panel(state: GameState):
|
|
| 1427 |
th, td {{ padding: 6px 8px; }}
|
| 1428 |
|
| 1429 |
/* Hide empty table by default (until JS updates tbody) */
|
| 1430 |
-
table tr {{ display: none; }}
|
| 1431 |
</style>
|
| 1432 |
<table class='shiny-border' style="background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666);">
|
| 1433 |
{table_inner}
|
|
@@ -1472,7 +1477,7 @@ def _render_score_panel(state: GameState):
|
|
| 1472 |
}} catch (e) {{
|
| 1473 |
// no-op
|
| 1474 |
}}
|
| 1475 |
-
}})();
|
| 1476 |
</script>
|
| 1477 |
</div>
|
| 1478 |
"""
|
|
|
|
| 281 |
st.markdown(
|
| 282 |
"""
|
| 283 |
<style>
|
| 284 |
+
.bw-bg-container {
|
| 285 |
+
position: fixed; /* fixed to viewport, not stApp */
|
| 286 |
+
inset: 0;
|
| 287 |
+
z-index: 1; /* below content (z=5) but above ::before (z=0) */
|
| 288 |
+
pointer-events: none;
|
| 289 |
+
overflow: hidden; /* clip children */
|
| 290 |
+
}
|
| 291 |
+
.bw-bg-layer {
|
| 292 |
+
position: absolute;
|
| 293 |
+
inset: 0;
|
| 294 |
+
width: 100vw;
|
| 295 |
+
height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
pointer-events: none;
|
| 297 |
+
}
|
| 298 |
+
/* Explicit stacking order with slower animations */
|
| 299 |
+
.bw-bg-highlight {
|
| 300 |
+
z-index: 11;
|
| 301 |
+
background: radial-gradient(150% 100% at 50% -20%, rgba(255,255,255,0.10) 0%, transparent 60%);
|
| 302 |
+
background-size: 150% 150%; /* reduced from 300% */
|
| 303 |
+
animation: oceanHighlight 12s ease-in-out infinite; /* doubled from 6s */
|
| 304 |
+
}
|
| 305 |
+
.bw-bg-long {
|
| 306 |
+
z-index: 12;
|
| 307 |
+
background: repeating-linear-gradient(-6deg, rgba(255,255,255,0.08) 0px, rgba(255,255,255,0.08) 18px, rgba(0,0,0,0.04) 18px, rgba(0,0,0,0.04) 48px);
|
| 308 |
+
background-size: 150% 150%; /* reduced from 320% */
|
| 309 |
+
animation: oceanLong 36s linear infinite; /* doubled from 18s */
|
| 310 |
+
opacity: 0.2;
|
| 311 |
+
}
|
| 312 |
+
.bw-bg-mid {
|
| 313 |
+
z-index: 13;
|
| 314 |
+
background: repeating-linear-gradient(-12deg, rgba(255,255,255,0.10) 0px, rgba(255,255,255,0.10) 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 26px);
|
| 315 |
+
background-size: 150% 150%; /* reduced from 260% */
|
| 316 |
+
animation: oceanMid 24s linear infinite; /* doubled from 12s */
|
| 317 |
+
opacity: 0.2;
|
| 318 |
+
}
|
| 319 |
+
.bw-bg-fine {
|
| 320 |
+
z-index: 14;
|
| 321 |
+
background: repeating-linear-gradient(-18deg, var(--foam) 0px, var(--foam) 4px, transparent 4px, transparent 12px);
|
| 322 |
+
background-size: 120% 120%; /* reduced from 200% */
|
| 323 |
+
animation: oceanFine 14s linear infinite; /* doubled from 7s */
|
| 324 |
+
opacity: 0.15;
|
| 325 |
+
}
|
| 326 |
+
</style>
|
| 327 |
<div class="bw-bg-container">
|
| 328 |
+
<div class="bw-bg-layer bw-bg-highlight"></div>
|
| 329 |
+
<div class="bw-bg-layer bw-bg-long"></div>
|
| 330 |
+
<div class="bw-bg-layer bw-bg-mid"></div>
|
| 331 |
+
<div class="bw-bg-layer bw-bg-fine"></div>
|
| 332 |
</div>
|
| 333 |
""",
|
| 334 |
unsafe_allow_html=True,
|
|
|
|
| 493 |
.st-emotion-cache-1n6tfoc { background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666); gap: 0.1rem !important; color: white; border-radius:15px; padding: 10px 10px 10px 5px; }
|
| 494 |
.st-emotion-cache-1n6tfoc::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; margin: 5px;}
|
| 495 |
.st-key-guess_input, .st-key-guess_submit { flex-direction: row; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: flex-end; }
|
| 496 |
+
.st-emotion-cache-18kf3ut [class^="st-key-free_letter_"] [data-testid="stButton"] > button, button.st-emotion-cache-1ojn1jd {
|
| 497 |
+
aspect-ratio: auto !important;
|
| 498 |
+
position:relative;
|
| 499 |
+
z-index: 1200;
|
| 500 |
+
}
|
| 501 |
|
| 502 |
/* grid adjustments */
|
| 503 |
@media (min-width: 560px){
|
|
|
|
| 523 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] { flex-direction: column-reverse !important; width: 100% !important; max-width: 100vw !important; }
|
| 524 |
#bw-main-anchor + div[data-testid="stHorizontalBlock"] > div[data-testid="column"] { width: 100% !important; min-width: 100% !important; max-width: 100% !important; flex: 1 1 100% !important; }
|
| 525 |
.bw-grid-row-anchor + div[data-testid="stHorizontalBlock"] { flex-wrap: nowrap !important; overflow-x: auto !important; margin: 2px 0 !important; }
|
| 526 |
+
.st-emotion-cache-1tj828o { min-width: calc(8.33333% - 1rem); }
|
| 527 |
+
# .bw-free-letter-grid {
|
| 528 |
+
# grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
|
| 529 |
+
# }
|
| 530 |
}
|
| 531 |
|
| 532 |
.bold-text { font-weight: 700; }
|
|
|
|
| 680 |
effects_volume = st.session_state.get("effects_volume",25)
|
| 681 |
enable_sound_effects = st.session_state.get("enable_sound_effects", True)
|
| 682 |
# NEW: Preserve Show Challenge Share Links
|
| 683 |
+
show_challenge_share_links = st.session_state.get("show_challenge_share_links", True)
|
| 684 |
|
| 685 |
st.session_state.clear()
|
| 686 |
if selected:
|
|
|
|
| 1097 |
.st-emotion-cache-ig7yu6 {
|
| 1098 |
min-width: calc(30% - 1.5rem);
|
| 1099 |
}
|
| 1100 |
+
.st-emotion-cache-116javk, .st-emotion-cache-1cmetgi {
|
| 1101 |
min-width: calc(12.5% - 1rem);
|
| 1102 |
}
|
| 1103 |
}
|
|
|
|
| 1170 |
# Note: letter_map is static and built once in _init_session(), no need to rebuild
|
| 1171 |
_sync_back(state)
|
| 1172 |
|
| 1173 |
+
# Allow guessing after any letter is revealed
|
| 1174 |
+
st.session_state.can_guess = True
|
| 1175 |
+
|
| 1176 |
# Play sound effect based on hit or miss
|
| 1177 |
action = (state.last_action or "").strip()
|
| 1178 |
if action.startswith("Revealed '"):
|
|
|
|
| 1432 |
th, td {{ padding: 6px 8px; }}
|
| 1433 |
|
| 1434 |
/* Hide empty table by default (until JS updates tbody) */
|
| 1435 |
+
/* table tr {{ display: none; }} */
|
| 1436 |
</style>
|
| 1437 |
<table class='shiny-border' style="background: linear-gradient(-45deg, #a1a1a1, #ffffff, #a1a1a1, #666666);">
|
| 1438 |
{table_inner}
|
|
|
|
| 1477 |
}} catch (e) {{
|
| 1478 |
// no-op
|
| 1479 |
}}
|
| 1480 |
+
}})();
|
| 1481 |
</script>
|
| 1482 |
</div>
|
| 1483 |
"""
|