Spaces:
Running
0.2.28
Browse files- PWA INSTALL_GUIDE.md added
- PWA implementation with service worker and manifest.json added
Add PWA support and performance optimizations
- Updated `README.md` with changelog for version `0.2.28`.
- Bumped version to `0.2.28` in `__init__.py`.
- Enhanced `logic.py` with `skip_validation` in `build_letter_map`
and optimized `is_game_over` using pre-computed `_all_word_cells`.
- Added `_all_word_cells` caching to `Puzzle` class in `models.py`.
- Introduced PWA meta tags in `ui.py` for offline and installable app.
- Added new words to `classic.txt` word list.
- Created `generate_pwa_icons.py` for generating app icons.
- Added `PWA_INSTALL_GUIDE.md` with detailed installation instructions.
- Included PWA assets: `icon-192.png`, `icon-512.png`, `manifest.json`.
- Implemented `service-worker.js` for offline caching and updates.
- LOCALHOST_PWA_README.md +267 -0
- PWA_INSTALL_GUIDE.md +208 -0
- README.md +12 -0
- battlewords/__init__.py +1 -1
- battlewords/logic.py +11 -7
- battlewords/models.py +9 -1
- battlewords/static/icon-192.png +0 -0
- battlewords/static/icon-512.png +0 -0
- battlewords/static/manifest.json +27 -0
- battlewords/static/service-worker.js +99 -0
- battlewords/ui.py +55 -1
- battlewords/words/classic.txt +1 -1
- claude.md +11 -3
- generate_pwa_icons.py +98 -0
- specs/requirements.md +9 -5
- specs/specs.md +9 -1
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PWA on Localhost - Important Information
|
| 2 |
+
|
| 3 |
+
## Summary
|
| 4 |
+
|
| 5 |
+
**The PWA files were created successfully**, but they **won't work fully on `localhost:8501`** due to Streamlit's static file serving limitations.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## What You're Seeing (or Not Seeing)
|
| 10 |
+
|
| 11 |
+
### ✅ What DOES Work on Localhost:
|
| 12 |
+
|
| 13 |
+
1. **Game functionality**: Everything works normally
|
| 14 |
+
2. **Challenge Mode**: Loading `?game_id=...` works (if HF credentials configured)
|
| 15 |
+
3. **PWA meta tags**: Injected into HTML (check page source)
|
| 16 |
+
4. **Service worker registration attempt**: Runs in browser console
|
| 17 |
+
|
| 18 |
+
### ❌ What DOESN'T Work on Localhost:
|
| 19 |
+
|
| 20 |
+
1. **`manifest.json` not accessible**:
|
| 21 |
+
```
|
| 22 |
+
http://localhost:8501/app/static/manifest.json
|
| 23 |
+
→ Returns HTML instead of JSON (Streamlit doesn't serve /app/static/)
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
2. **Icons not accessible**:
|
| 27 |
+
```
|
| 28 |
+
http://localhost:8501/app/static/icon-192.png
|
| 29 |
+
→ Returns 404 or HTML
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
3. **Service worker fails to register**:
|
| 33 |
+
```javascript
|
| 34 |
+
// Browser console shows:
|
| 35 |
+
Failed to register service worker: 404 Not Found
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
4. **No PWA install prompt**:
|
| 39 |
+
- No banner at bottom of screen
|
| 40 |
+
- No install icon in address bar
|
| 41 |
+
- PWA features disabled
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
## Why This Happens
|
| 46 |
+
|
| 47 |
+
**Streamlit's Static File Serving:**
|
| 48 |
+
|
| 49 |
+
- Streamlit only serves files from:
|
| 50 |
+
- `/.streamlit/static/` (internal Streamlit assets)
|
| 51 |
+
- Component assets via `declare_component()`
|
| 52 |
+
- NOT from arbitrary `battlewords/static/` directories
|
| 53 |
+
|
| 54 |
+
- On HuggingFace Spaces:
|
| 55 |
+
- `/app/static/` is mapped by HF infrastructure
|
| 56 |
+
- Files in `battlewords/static/` are accessible at `/app/static/`
|
| 57 |
+
- ✅ PWA works perfectly
|
| 58 |
+
|
| 59 |
+
- On localhost:
|
| 60 |
+
- No `/app/static/` mapping exists
|
| 61 |
+
- Streamlit returns HTML for all unrecognized paths
|
| 62 |
+
- ❌ PWA files return 404
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## How to Test PWA Locally
|
| 67 |
+
|
| 68 |
+
### Option 1: Use ngrok (HTTPS Tunnel) ⭐ **RECOMMENDED**
|
| 69 |
+
|
| 70 |
+
This is the **best way** to test PWA locally with full functionality:
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Terminal 1: Run Streamlit
|
| 74 |
+
streamlit run app.py
|
| 75 |
+
|
| 76 |
+
# Terminal 2: Expose with HTTPS
|
| 77 |
+
ngrok http 8501
|
| 78 |
+
|
| 79 |
+
# Output shows:
|
| 80 |
+
# Forwarding https://abc123.ngrok-free.app -> http://localhost:8501
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
**Then visit the HTTPS URL on your phone or desktop:**
|
| 84 |
+
- ✅ Full PWA functionality
|
| 85 |
+
- ✅ Install prompt appears
|
| 86 |
+
- ✅ manifest.json loads
|
| 87 |
+
- ✅ Service worker registers
|
| 88 |
+
- ✅ Icons display correctly
|
| 89 |
+
|
| 90 |
+
**ngrok Setup:**
|
| 91 |
+
1. Download: https://ngrok.com/download
|
| 92 |
+
2. Sign up for free account
|
| 93 |
+
3. Install: `unzip /path/to/ngrok.zip` (or chocolatey on Windows: `choco install ngrok`)
|
| 94 |
+
4. Authenticate: `ngrok config add-authtoken <your-token>`
|
| 95 |
+
5. Run: `ngrok http 8501`
|
| 96 |
+
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
### Option 2: Deploy to HuggingFace Spaces ⭐ **PRODUCTION**
|
| 100 |
+
|
| 101 |
+
PWA works out-of-the-box on HF Spaces:
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
git add battlewords/static/ battlewords/ui.py
|
| 105 |
+
git commit -m "Add PWA support"
|
| 106 |
+
git push
|
| 107 |
+
|
| 108 |
+
# HF Spaces auto-deploys
|
| 109 |
+
# Visit: https://surn-battlewords.hf.space
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
**Then test PWA:**
|
| 113 |
+
- Android Chrome: "Add to Home Screen" prompt appears
|
| 114 |
+
- iOS Safari: Share → "Add to Home Screen"
|
| 115 |
+
- Desktop Chrome: Install icon in address bar
|
| 116 |
+
|
| 117 |
+
✅ **This is where PWA is meant to work!**
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
###Option 3: Manual Static File Server (Advanced)
|
| 122 |
+
|
| 123 |
+
You can serve the static files separately:
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
# Terminal 1: Run Streamlit
|
| 127 |
+
streamlit run app.py
|
| 128 |
+
|
| 129 |
+
# Terminal 2: Serve static files
|
| 130 |
+
cd battlewords/static
|
| 131 |
+
python3 -m http.server 8502
|
| 132 |
+
|
| 133 |
+
# Then access:
|
| 134 |
+
# Streamlit: http://localhost:8501
|
| 135 |
+
# Static files: http://localhost:8502/manifest.json
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
**Then modify the PWA paths in `ui.py`:**
|
| 139 |
+
```python
|
| 140 |
+
pwa_meta_tags = """
|
| 141 |
+
<link rel="manifest" href="http://localhost:8502/manifest.json">
|
| 142 |
+
<link rel="apple-touch-icon" href="http://localhost:8502/icon-192.png">
|
| 143 |
+
<!-- etc -->
|
| 144 |
+
"""
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
❌ **Not recommended**: Too complex, defeats the purpose
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
## What About Challenge Mode?
|
| 152 |
+
|
| 153 |
+
**Question:** "I loaded `localhost:8501/?game_id=hDjsB_dl` but don't see anything"
|
| 154 |
+
|
| 155 |
+
**Answer:** Challenge Mode is **separate from PWA**. You should see a blue banner at the top if:
|
| 156 |
+
|
| 157 |
+
### ✅ Requirements for Challenge Mode to Work:
|
| 158 |
+
|
| 159 |
+
1. **Environment variables configured** (`.env` file):
|
| 160 |
+
```bash
|
| 161 |
+
HF_API_TOKEN=hf_xxxxxxxxxxxxx
|
| 162 |
+
HF_REPO_ID=Surn/Storage
|
| 163 |
+
SPACE_NAME=Surn/BattleWords
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
2. **Valid game_id exists** in the HF repo:
|
| 167 |
+
- `hDjsB_dl` must be a real challenge created previously
|
| 168 |
+
- Check HuggingFace dataset repo: https://huggingface.co/datasets/Surn/Storage
|
| 169 |
+
- Look for: `games/<uid>/settings.json`
|
| 170 |
+
- Verify `shortener.json` has entry for `hDjsB_dl`
|
| 171 |
+
|
| 172 |
+
3. **Internet connection** (to fetch challenge data)
|
| 173 |
+
|
| 174 |
+
### If Challenge Mode ISN'T Working:
|
| 175 |
+
|
| 176 |
+
**Check browser console (F12 → Console):**
|
| 177 |
+
```javascript
|
| 178 |
+
// Look for errors:
|
| 179 |
+
"[game_storage] Could not resolve sid: hDjsB_dl" ← Challenge not found
|
| 180 |
+
"Failed to load game from sid" ← HF API error
|
| 181 |
+
"HF_API_TOKEN not configured" ← Missing credentials
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
**If you see errors:**
|
| 185 |
+
1. Verify `.env` file exists with correct variables
|
| 186 |
+
2. Restart Streamlit (`Ctrl+C` and `streamlit run app.py` again)
|
| 187 |
+
3. Try a different `game_id` from a known challenge
|
| 188 |
+
4. Check HF repo has the challenge data
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## Summary Table
|
| 193 |
+
|
| 194 |
+
| Feature | Localhost | Localhost + ngrok | HF Spaces (Production) |
|
| 195 |
+
|---------|-----------|-------------------|------------------------|
|
| 196 |
+
| **Game works** | ✅ | ✅ | ✅ |
|
| 197 |
+
| **Challenge Mode** | ✅ (if .env configured) | ✅ | ✅ |
|
| 198 |
+
| **PWA manifest loads** | ❌ | ✅ | ✅ |
|
| 199 |
+
| **Service worker registers** | ❌ | ✅ | ✅ |
|
| 200 |
+
| **Install prompt** | ❌ | ✅ | ✅ |
|
| 201 |
+
| **Icons display** | ❌ | ✅ | ✅ |
|
| 202 |
+
| **Full-screen mode** | ❌ | ✅ | ✅ |
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## What You Should Do
|
| 207 |
+
|
| 208 |
+
### For Development:
|
| 209 |
+
✅ **Just develop normally on localhost**
|
| 210 |
+
- Game features work fine
|
| 211 |
+
- Challenge Mode works (if .env configured)
|
| 212 |
+
- PWA features won't work, but that's okay
|
| 213 |
+
- Test PWA when you deploy
|
| 214 |
+
|
| 215 |
+
### For PWA Testing:
|
| 216 |
+
✅ **Use ngrok for quick local PWA testing**
|
| 217 |
+
- 5 minutes to setup
|
| 218 |
+
- Full PWA functionality
|
| 219 |
+
- Test on real phone
|
| 220 |
+
|
| 221 |
+
### For Production:
|
| 222 |
+
✅ **Deploy to HuggingFace Spaces**
|
| 223 |
+
- PWA works automatically
|
| 224 |
+
- No configuration needed
|
| 225 |
+
- `/app/static/` path works out-of-the-box
|
| 226 |
+
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
## Bottom Line
|
| 230 |
+
|
| 231 |
+
**Your question:** "Should I see something at the bottom of the screen?"
|
| 232 |
+
|
| 233 |
+
**Answer:**
|
| 234 |
+
|
| 235 |
+
1. **PWA install prompt**: ❌ Not on `localhost:8501` (Streamlit limitation)
|
| 236 |
+
- **Will work** on HF Spaces production deployment ✅
|
| 237 |
+
- **Will work** with ngrok HTTPS tunnel ✅
|
| 238 |
+
|
| 239 |
+
2. **Challenge Mode banner**: ✅ Should appear at TOP (not bottom)
|
| 240 |
+
- Check if `?game_id=hDjsB_dl` exists in your HF repo
|
| 241 |
+
- Check browser console for errors
|
| 242 |
+
- Verify `.env` has `HF_API_TOKEN` configured
|
| 243 |
+
|
| 244 |
+
The PWA implementation is **correct** and **ready for production**. It just won't work on bare localhost due to Streamlit's static file serving limitations. Once you deploy to HuggingFace Spaces, everything will work perfectly!
|
| 245 |
+
|
| 246 |
+
---
|
| 247 |
+
|
| 248 |
+
## Quick Test Command
|
| 249 |
+
|
| 250 |
+
```bash
|
| 251 |
+
# Check if .env is configured:
|
| 252 |
+
cat .env | grep HF_
|
| 253 |
+
|
| 254 |
+
# Should show:
|
| 255 |
+
# HF_API_TOKEN=hf_xxxxx
|
| 256 |
+
# HF_REPO_ID=Surn/Storage
|
| 257 |
+
# SPACE_NAME=Surn/BattleWords
|
| 258 |
+
|
| 259 |
+
# If missing, Challenge Mode won't work locally
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
---
|
| 263 |
+
|
| 264 |
+
**Next Steps:**
|
| 265 |
+
1. Test game functionality on localhost ✅
|
| 266 |
+
2. Deploy to HF Spaces for PWA testing ✅
|
| 267 |
+
3. Or install ngrok for local PWA testing ✅
|
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BattleWords PWA Installation Guide
|
| 2 |
+
|
| 3 |
+
BattleWords can now be installed as a Progressive Web App (PWA) on your mobile device or desktop, giving you a native app experience directly from your browser!
|
| 4 |
+
|
| 5 |
+
## What is a PWA?
|
| 6 |
+
|
| 7 |
+
A Progressive Web App allows you to:
|
| 8 |
+
- ✅ Install BattleWords on your home screen (Android/iOS)
|
| 9 |
+
- ✅ Run in full-screen mode without browser UI
|
| 10 |
+
- ✅ Access the app quickly from your app drawer
|
| 11 |
+
- ✅ Get automatic updates (always the latest version)
|
| 12 |
+
- ✅ Basic offline functionality (cached assets)
|
| 13 |
+
|
| 14 |
+
## Installation Instructions
|
| 15 |
+
|
| 16 |
+
### Android (Chrome, Edge, Samsung Internet)
|
| 17 |
+
|
| 18 |
+
1. **Visit the app**: Open https://surn-battlewords.hf.space in Chrome
|
| 19 |
+
2. **Look for the install prompt**: A banner will appear at the bottom saying "Add BattleWords to Home screen"
|
| 20 |
+
3. **Tap "Add"** or **"Install"**
|
| 21 |
+
4. **Alternative method** (if no prompt):
|
| 22 |
+
- Tap the **three-dot menu** (⋮) in the top-right
|
| 23 |
+
- Select **"Install app"** or **"Add to Home screen"**
|
| 24 |
+
- Tap **"Install"**
|
| 25 |
+
5. **Launch**: Find the BattleWords icon on your home screen and tap to open!
|
| 26 |
+
|
| 27 |
+
**Result**: The app opens full-screen without the browser address bar, just like a native app.
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
### iOS (Safari)
|
| 32 |
+
|
| 33 |
+
**Note**: iOS requires using Safari browser (Chrome/Firefox won't work for PWA installation)
|
| 34 |
+
|
| 35 |
+
1. **Visit the app**: Open https://surn-battlewords.hf.space in Safari
|
| 36 |
+
2. **Tap the Share button**: The square with an arrow pointing up (at the bottom of the screen)
|
| 37 |
+
3. **Scroll down** and tap **"Add to Home Screen"**
|
| 38 |
+
4. **Edit the name** (optional): You can rename it from "BattleWords" if desired
|
| 39 |
+
5. **Tap "Add"** in the top-right corner
|
| 40 |
+
6. **Launch**: Find the BattleWords icon on your home screen and tap to open!
|
| 41 |
+
|
| 42 |
+
**Result**: The app opens in standalone mode, similar to a native iOS app.
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
### Desktop (Chrome, Edge, Brave)
|
| 47 |
+
|
| 48 |
+
1. **Visit the app**: Open https://surn-battlewords.hf.space
|
| 49 |
+
2. **Look for the install icon**:
|
| 50 |
+
- Chrome/Edge: Click the **install icon** (⊕) in the address bar
|
| 51 |
+
- Or click the **three-dot menu** → **"Install BattleWords"**
|
| 52 |
+
3. **Click "Install"** in the confirmation dialog
|
| 53 |
+
4. **Launch**:
|
| 54 |
+
- Windows: Find BattleWords in Start Menu or Desktop
|
| 55 |
+
- Mac: Find BattleWords in Applications folder
|
| 56 |
+
- Linux: Find in application launcher
|
| 57 |
+
|
| 58 |
+
**Result**: BattleWords opens in its own window, separate from your browser.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## Features of the PWA
|
| 63 |
+
|
| 64 |
+
### Works Immediately ✅
|
| 65 |
+
- Full game functionality (reveal cells, guess words, scoring)
|
| 66 |
+
- Challenge Mode (create and play shared challenges)
|
| 67 |
+
- Sound effects and background music
|
| 68 |
+
- Ocean-themed animated background
|
| 69 |
+
- All current features preserved
|
| 70 |
+
|
| 71 |
+
### Offline Support 🌐
|
| 72 |
+
- App shell cached for faster loading
|
| 73 |
+
- Icons and static assets available offline
|
| 74 |
+
- **Note**: Challenge Mode requires internet connection (needs to fetch/save from HuggingFace)
|
| 75 |
+
|
| 76 |
+
### Updates 🔄
|
| 77 |
+
- Automatic updates when you open the app
|
| 78 |
+
- Always get the latest features and bug fixes
|
| 79 |
+
- No manual update process needed
|
| 80 |
+
|
| 81 |
+
### Privacy & Security 🔒
|
| 82 |
+
- No new data collection (same as web version)
|
| 83 |
+
- Environment variables stay on server (never exposed to PWA)
|
| 84 |
+
- Service worker only caches public assets
|
| 85 |
+
- All game data in Challenge Mode handled server-side
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## Uninstalling the PWA
|
| 90 |
+
|
| 91 |
+
### Android
|
| 92 |
+
1. Long-press the BattleWords icon
|
| 93 |
+
2. Tap "Uninstall" or drag to "Remove"
|
| 94 |
+
|
| 95 |
+
### iOS
|
| 96 |
+
1. Long-press the BattleWords icon
|
| 97 |
+
2. Tap "Remove App"
|
| 98 |
+
3. Confirm "Delete App"
|
| 99 |
+
|
| 100 |
+
### Desktop
|
| 101 |
+
- **Chrome/Edge**: Go to `chrome://apps` or `edge://apps`, right-click BattleWords, select "Uninstall"
|
| 102 |
+
- **Windows**: Settings → Apps → BattleWords → Uninstall
|
| 103 |
+
- **Mac**: Delete from Applications folder
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## Troubleshooting
|
| 108 |
+
|
| 109 |
+
### "Install" option doesn't appear
|
| 110 |
+
- **Android**: Make sure you're using Chrome, Edge, or Samsung Internet (not Firefox)
|
| 111 |
+
- **iOS**: Must use Safari browser
|
| 112 |
+
- **Desktop**: Check if you're using a supported browser (Chrome, Edge, Brave)
|
| 113 |
+
- Try refreshing the page (the install prompt may take a moment to appear)
|
| 114 |
+
|
| 115 |
+
### App won't open after installation
|
| 116 |
+
- Try uninstalling and reinstalling
|
| 117 |
+
- Clear browser cache and try again
|
| 118 |
+
- Make sure you have internet connection for first launch
|
| 119 |
+
|
| 120 |
+
### Service worker errors in console
|
| 121 |
+
- This is normal during development
|
| 122 |
+
- The app will still function without the service worker
|
| 123 |
+
- Full offline support requires the service worker to register successfully
|
| 124 |
+
|
| 125 |
+
### Icons don't show up correctly
|
| 126 |
+
- Wait a moment after installation (icons may take time to download)
|
| 127 |
+
- Try force-refreshing the PWA (close and reopen)
|
| 128 |
+
|
| 129 |
+
---
|
| 130 |
+
|
| 131 |
+
## Technical Details
|
| 132 |
+
|
| 133 |
+
### Files Added for PWA Support
|
| 134 |
+
|
| 135 |
+
```
|
| 136 |
+
battlewords/
|
| 137 |
+
├── static/
|
| 138 |
+
│ ├── manifest.json # PWA configuration
|
| 139 |
+
│ ├── service-worker.js # Offline caching logic
|
| 140 |
+
│ ├── icon-192.png # App icon (small)
|
| 141 |
+
│ └── icon-512.png # App icon (large)
|
| 142 |
+
└── ui.py # Added PWA meta tags
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
### What's Cached Offline
|
| 146 |
+
|
| 147 |
+
- App shell (HTML structure)
|
| 148 |
+
- Icons (192x192, 512x512)
|
| 149 |
+
- Manifest file
|
| 150 |
+
- Previous game states (if you were playing before going offline)
|
| 151 |
+
|
| 152 |
+
### What Requires Internet
|
| 153 |
+
|
| 154 |
+
- Creating new challenges
|
| 155 |
+
- Submitting results to leaderboards
|
| 156 |
+
- Loading shared challenges
|
| 157 |
+
- Downloading word lists (first time)
|
| 158 |
+
- Fetching game updates
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Comparison: PWA vs Native App
|
| 163 |
+
|
| 164 |
+
| Feature | PWA | Native App |
|
| 165 |
+
|---------|-----|------------|
|
| 166 |
+
| Installation | Quick (1 tap) | Slow (app store) |
|
| 167 |
+
| Size | ~5-10 MB | ~15-30 MB |
|
| 168 |
+
| Updates | Automatic | Manual |
|
| 169 |
+
| Platform support | Android, iOS, Desktop | Separate builds |
|
| 170 |
+
| Offline mode | Partial | Full |
|
| 171 |
+
| Performance | 90% of native | 100% |
|
| 172 |
+
| App store presence | No | Yes |
|
| 173 |
+
| Development time | 2-4 hours ✅ | 40-60 hours per platform |
|
| 174 |
+
|
| 175 |
+
---
|
| 176 |
+
|
| 177 |
+
## Feedback
|
| 178 |
+
|
| 179 |
+
If you encounter issues installing or using the PWA, please:
|
| 180 |
+
1. Check the browser console for errors (F12 → Console tab)
|
| 181 |
+
2. Report issues at: https://github.com/Oncorporation/BattleWords/issues
|
| 182 |
+
3. Include: Device type, OS version, browser version, and error messages
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
## For Developers
|
| 187 |
+
|
| 188 |
+
To regenerate the PWA icons:
|
| 189 |
+
```bash
|
| 190 |
+
python3 generate_pwa_icons.py
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
To modify PWA behavior:
|
| 194 |
+
- Edit `battlewords/static/manifest.json` (app metadata)
|
| 195 |
+
- Edit `battlewords/static/service-worker.js` (caching logic)
|
| 196 |
+
- Edit `battlewords/ui.py` (PWA meta tags, lines 34-86)
|
| 197 |
+
|
| 198 |
+
To test PWA locally:
|
| 199 |
+
```bash
|
| 200 |
+
streamlit run app.py
|
| 201 |
+
# Open http://localhost:8501 in Chrome
|
| 202 |
+
# Chrome DevTools → Application → Manifest (verify manifest.json loads)
|
| 203 |
+
# Chrome DevTools → Application → Service Workers (verify registration)
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
**Enjoy BattleWords as a native-like app experience! 🎮🌊**
|
|
@@ -61,6 +61,14 @@ BattleWords is a vocabulary learning game inspired by classic Battleship mechani
|
|
| 61 |
- **Environment variables** for Challenge Mode (HF_API_TOKEN, HF_REPO_ID, SPACE_NAME)
|
| 62 |
- Works offline without HF credentials (Challenge Mode features disabled gracefully)
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
### Planned (v0.3.0)
|
| 65 |
- Local persistent storage for personal game history
|
| 66 |
- Personal high scores sidebar (offline-capable)
|
|
@@ -179,6 +187,10 @@ CRYPTO_PK= # Reserved for future signing
|
|
| 179 |
- Personal high scores sidebar with filtering
|
| 180 |
- Player statistics tracking (games played, averages, bests)
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
-0.2.27
|
| 183 |
- Add "Show Challenge Share Links" setting (default: off)
|
| 184 |
- When disabled:
|
|
|
|
| 61 |
- **Environment variables** for Challenge Mode (HF_API_TOKEN, HF_REPO_ID, SPACE_NAME)
|
| 62 |
- Works offline without HF credentials (Challenge Mode features disabled gracefully)
|
| 63 |
|
| 64 |
+
### Progressive Web App (PWA) - v0.2.28
|
| 65 |
+
- Installable on desktop and mobile from your browser:
|
| 66 |
+
- Chrome/Edge: Menu → “Install app”
|
| 67 |
+
- Android Chrome: “Add to Home screen”
|
| 68 |
+
- iOS Safari: Share → “Add to Home Screen”
|
| 69 |
+
- Includes `service worker` and `manifest.json` with basic offline caching of static assets
|
| 70 |
+
- See `INSTALL_GUIDE.md` for platform-specific steps
|
| 71 |
+
|
| 72 |
### Planned (v0.3.0)
|
| 73 |
- Local persistent storage for personal game history
|
| 74 |
- Personal high scores sidebar (offline-capable)
|
|
|
|
| 187 |
- Personal high scores sidebar with filtering
|
| 188 |
- Player statistics tracking (games played, averages, bests)
|
| 189 |
|
| 190 |
+
-0.2.28
|
| 191 |
+
- PWA INSTALL_GUIDE.md added
|
| 192 |
+
- PWA implementation with service worker and manifest.json added
|
| 193 |
+
|
| 194 |
-0.2.27
|
| 195 |
- Add "Show Challenge Share Links" setting (default: off)
|
| 196 |
- When disabled:
|
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
|
|
|
| 1 |
+
__version__ = "0.2.28"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
|
@@ -10,7 +10,7 @@ def _chebyshev_distance(a: Coord, b: Coord) -> int:
|
|
| 10 |
return max(abs(a.x - b.x), abs(a.y - b.y))
|
| 11 |
|
| 12 |
|
| 13 |
-
def build_letter_map(puzzle: Puzzle) -> Dict[Coord, str]:
|
| 14 |
"""Build a coordinate->letter map for the given puzzle.
|
| 15 |
|
| 16 |
Spacer support (0–2):
|
|
@@ -22,6 +22,12 @@ def build_letter_map(puzzle: Puzzle) -> Dict[Coord, str]:
|
|
| 22 |
|
| 23 |
Overlaps are not handled here (negative spacer not supported in this function).
|
| 24 |
This function raises ValueError if the configured spacing is violated.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
"""
|
| 26 |
mapping: Dict[Coord, str] = {}
|
| 27 |
|
|
@@ -38,8 +44,8 @@ def build_letter_map(puzzle: Puzzle) -> Dict[Coord, str]:
|
|
| 38 |
# Keep the first-seen letter and continue.
|
| 39 |
pass
|
| 40 |
|
| 41 |
-
# Enforce spacer in the range 0–2
|
| 42 |
-
if spacer in (1, 2):
|
| 43 |
# Prepare sets of cells per word
|
| 44 |
word_cells = [set(w.cells) for w in puzzle.words]
|
| 45 |
for i in range(len(word_cells)):
|
|
@@ -126,10 +132,8 @@ def is_game_over(state: GameState) -> bool:
|
|
| 126 |
if len(state.guessed) == 6:
|
| 127 |
return True
|
| 128 |
# Game ends if all word cells are revealed
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
all_word_cells.update(w.cells)
|
| 132 |
-
if all_word_cells.issubset(state.revealed):
|
| 133 |
return True
|
| 134 |
return False
|
| 135 |
|
|
|
|
| 10 |
return max(abs(a.x - b.x), abs(a.y - b.y))
|
| 11 |
|
| 12 |
|
| 13 |
+
def build_letter_map(puzzle: Puzzle, skip_validation: bool = True) -> Dict[Coord, str]:
|
| 14 |
"""Build a coordinate->letter map for the given puzzle.
|
| 15 |
|
| 16 |
Spacer support (0–2):
|
|
|
|
| 22 |
|
| 23 |
Overlaps are not handled here (negative spacer not supported in this function).
|
| 24 |
This function raises ValueError if the configured spacing is violated.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
puzzle: The puzzle to build the letter map for
|
| 28 |
+
skip_validation: If True, skip spacer validation (default: True).
|
| 29 |
+
Validation is expensive and only needed during puzzle
|
| 30 |
+
generation, not during gameplay.
|
| 31 |
"""
|
| 32 |
mapping: Dict[Coord, str] = {}
|
| 33 |
|
|
|
|
| 44 |
# Keep the first-seen letter and continue.
|
| 45 |
pass
|
| 46 |
|
| 47 |
+
# Enforce spacer in the range 0–2 (only when validation is requested)
|
| 48 |
+
if not skip_validation and spacer in (1, 2):
|
| 49 |
# Prepare sets of cells per word
|
| 50 |
word_cells = [set(w.cells) for w in puzzle.words]
|
| 51 |
for i in range(len(word_cells)):
|
|
|
|
| 132 |
if len(state.guessed) == 6:
|
| 133 |
return True
|
| 134 |
# Game ends if all word cells are revealed
|
| 135 |
+
# Use pre-computed _all_word_cells set from puzzle for performance
|
| 136 |
+
if state.puzzle._all_word_cells.issubset(state.revealed):
|
|
|
|
|
|
|
| 137 |
return True
|
| 138 |
return False
|
| 139 |
|
|
@@ -72,12 +72,20 @@ class Puzzle:
|
|
| 72 |
may_overlap: bool = False
|
| 73 |
spacer: int = 1
|
| 74 |
# Unique identifier for this puzzle instance (used for deterministic regen and per-session assets)
|
| 75 |
-
uid: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
|
|
|
|
|
| 76 |
|
| 77 |
def __post_init__(self):
|
| 78 |
pulses = [w.last_cell for w in self.words]
|
| 79 |
self.radar = pulses
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
@dataclass
|
| 83 |
class GameState:
|
|
|
|
| 72 |
may_overlap: bool = False
|
| 73 |
spacer: int = 1
|
| 74 |
# Unique identifier for this puzzle instance (used for deterministic regen and per-session assets)
|
| 75 |
+
uid: str = field(default_factory=lambda: uuid.uuid4().hex)
|
| 76 |
+
# Cached set of all word cells (computed once, used by is_game_over check)
|
| 77 |
+
_all_word_cells: Set[Coord] = field(default_factory=set, repr=False, init=False)
|
| 78 |
|
| 79 |
def __post_init__(self):
|
| 80 |
pulses = [w.last_cell for w in self.words]
|
| 81 |
self.radar = pulses
|
| 82 |
|
| 83 |
+
# Pre-compute all word cells once for faster is_game_over() checks
|
| 84 |
+
all_cells: Set[Coord] = set()
|
| 85 |
+
for w in self.words:
|
| 86 |
+
all_cells.update(w.cells)
|
| 87 |
+
object.__setattr__(self, '_all_word_cells', all_cells)
|
| 88 |
+
|
| 89 |
|
| 90 |
@dataclass
|
| 91 |
class GameState:
|
|
|
|
|
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "BattleWords",
|
| 3 |
+
"short_name": "BattleWords",
|
| 4 |
+
"description": "Vocabulary learning game inspired by Battleship mechanics. Discover hidden words on a 12x12 grid and earn points for strategic guessing.",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"scope": "/",
|
| 7 |
+
"display": "standalone",
|
| 8 |
+
"orientation": "portrait",
|
| 9 |
+
"background_color": "#0b2a4a",
|
| 10 |
+
"theme_color": "#165ba8",
|
| 11 |
+
"icons": [
|
| 12 |
+
{
|
| 13 |
+
"src": "/app/static/icon-192.png",
|
| 14 |
+
"sizes": "192x192",
|
| 15 |
+
"type": "image/png",
|
| 16 |
+
"purpose": "any maskable"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"src": "/app/static/icon-512.png",
|
| 20 |
+
"sizes": "512x512",
|
| 21 |
+
"type": "image/png",
|
| 22 |
+
"purpose": "any maskable"
|
| 23 |
+
}
|
| 24 |
+
],
|
| 25 |
+
"categories": ["games", "education"],
|
| 26 |
+
"screenshots": []
|
| 27 |
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* BattleWords Service Worker
|
| 3 |
+
* Enables PWA functionality: offline caching, install prompt, etc.
|
| 4 |
+
*
|
| 5 |
+
* Security Note: This file contains no secrets or sensitive data.
|
| 6 |
+
* It only caches public assets for offline access.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
const CACHE_NAME = 'battlewords-v0.2.27';
|
| 10 |
+
const RUNTIME_CACHE = 'battlewords-runtime';
|
| 11 |
+
|
| 12 |
+
// Assets to cache on install (minimal for faster install)
|
| 13 |
+
const PRECACHE_URLS = [
|
| 14 |
+
'/',
|
| 15 |
+
'/app/static/manifest.json',
|
| 16 |
+
'/app/static/icon-192.png',
|
| 17 |
+
'/app/static/icon-512.png'
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
// Install event - cache essential files
|
| 21 |
+
self.addEventListener('install', event => {
|
| 22 |
+
console.log('[ServiceWorker] Installing...');
|
| 23 |
+
event.waitUntil(
|
| 24 |
+
caches.open(CACHE_NAME)
|
| 25 |
+
.then(cache => {
|
| 26 |
+
console.log('[ServiceWorker] Precaching app shell');
|
| 27 |
+
return cache.addAll(PRECACHE_URLS);
|
| 28 |
+
})
|
| 29 |
+
.then(() => self.skipWaiting()) // Activate immediately
|
| 30 |
+
);
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
// Activate event - clean up old caches
|
| 34 |
+
self.addEventListener('activate', event => {
|
| 35 |
+
console.log('[ServiceWorker] Activating...');
|
| 36 |
+
event.waitUntil(
|
| 37 |
+
caches.keys().then(cacheNames => {
|
| 38 |
+
return Promise.all(
|
| 39 |
+
cacheNames.map(cacheName => {
|
| 40 |
+
if (cacheName !== CACHE_NAME && cacheName !== RUNTIME_CACHE) {
|
| 41 |
+
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
| 42 |
+
return caches.delete(cacheName);
|
| 43 |
+
}
|
| 44 |
+
})
|
| 45 |
+
);
|
| 46 |
+
}).then(() => self.clients.claim()) // Take control immediately
|
| 47 |
+
);
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
// Fetch event - network first, fall back to cache
|
| 51 |
+
self.addEventListener('fetch', event => {
|
| 52 |
+
// Skip non-GET requests
|
| 53 |
+
if (event.request.method !== 'GET') {
|
| 54 |
+
return;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Skip chrome-extension and other non-http requests
|
| 58 |
+
if (!event.request.url.startsWith('http')) {
|
| 59 |
+
return;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
event.respondWith(
|
| 63 |
+
caches.open(RUNTIME_CACHE).then(cache => {
|
| 64 |
+
return fetch(event.request)
|
| 65 |
+
.then(response => {
|
| 66 |
+
// Cache successful responses for future offline access
|
| 67 |
+
if (response.status === 200) {
|
| 68 |
+
cache.put(event.request, response.clone());
|
| 69 |
+
}
|
| 70 |
+
return response;
|
| 71 |
+
})
|
| 72 |
+
.catch(() => {
|
| 73 |
+
// Network failed, try cache
|
| 74 |
+
return caches.match(event.request).then(cachedResponse => {
|
| 75 |
+
if (cachedResponse) {
|
| 76 |
+
console.log('[ServiceWorker] Serving from cache:', event.request.url);
|
| 77 |
+
return cachedResponse;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// No cache available, return offline page or error
|
| 81 |
+
return new Response('Offline - Please check your connection', {
|
| 82 |
+
status: 503,
|
| 83 |
+
statusText: 'Service Unavailable',
|
| 84 |
+
headers: new Headers({
|
| 85 |
+
'Content-Type': 'text/plain'
|
| 86 |
+
})
|
| 87 |
+
});
|
| 88 |
+
});
|
| 89 |
+
});
|
| 90 |
+
})
|
| 91 |
+
);
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
// Message event - handle commands from the app
|
| 95 |
+
self.addEventListener('message', event => {
|
| 96 |
+
if (event.data.action === 'skipWaiting') {
|
| 97 |
+
self.skipWaiting();
|
| 98 |
+
}
|
| 99 |
+
});
|
|
@@ -31,6 +31,60 @@ from .game_storage import load_game_from_sid, save_game_to_hf, get_shareable_url
|
|
| 31 |
|
| 32 |
st.set_page_config(initial_sidebar_state="collapsed")
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
CoordLike = Tuple[int, int]
|
| 35 |
|
| 36 |
def fig_to_pil_rgba(fig):
|
|
@@ -1088,7 +1142,7 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
|
|
| 1088 |
# Invalidate radar GIF cache to hide completed rings
|
| 1089 |
st.session_state.radar_gif_path = None
|
| 1090 |
st.session_state.radar_gif_signature = None
|
| 1091 |
-
|
| 1092 |
_sync_back(state)
|
| 1093 |
|
| 1094 |
# Play sound effect based on hit or miss
|
|
|
|
| 31 |
|
| 32 |
st.set_page_config(initial_sidebar_state="collapsed")
|
| 33 |
|
| 34 |
+
# PWA (Progressive Web App) Support
|
| 35 |
+
# Enables installing BattleWords as a native-feeling mobile app
|
| 36 |
+
pwa_meta_tags = """
|
| 37 |
+
<link rel="manifest" href="/app/static/manifest.json">
|
| 38 |
+
<meta name="theme-color" content="#165ba8">
|
| 39 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 40 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 41 |
+
<meta name="apple-mobile-web-app-title" content="BattleWords">
|
| 42 |
+
<link rel="apple-touch-icon" href="/app/static/icon-192.png">
|
| 43 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 44 |
+
<meta name="mobile-web-app-capable" content="yes">
|
| 45 |
+
|
| 46 |
+
<script>
|
| 47 |
+
// Register service worker for offline functionality
|
| 48 |
+
if ('serviceWorker' in navigator) {
|
| 49 |
+
window.addEventListener('load', () => {
|
| 50 |
+
navigator.serviceWorker.register('/app/static/service-worker.js')
|
| 51 |
+
.then(registration => {
|
| 52 |
+
console.log('[PWA] Service Worker registered successfully:', registration.scope);
|
| 53 |
+
|
| 54 |
+
// Check for updates periodically
|
| 55 |
+
registration.addEventListener('updatefound', () => {
|
| 56 |
+
const newWorker = registration.installing;
|
| 57 |
+
newWorker.addEventListener('statechange', () => {
|
| 58 |
+
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
| 59 |
+
console.log('[PWA] New version available! Refresh to update.');
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
});
|
| 63 |
+
})
|
| 64 |
+
.catch(error => {
|
| 65 |
+
console.log('[PWA] Service Worker registration failed:', error);
|
| 66 |
+
});
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Prompt user to install PWA (for browsers that support it)
|
| 71 |
+
let deferredPrompt;
|
| 72 |
+
window.addEventListener('beforeinstallprompt', (e) => {
|
| 73 |
+
console.log('[PWA] Install prompt available');
|
| 74 |
+
e.preventDefault();
|
| 75 |
+
deferredPrompt = e;
|
| 76 |
+
// Could show custom install button here if desired
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
// Track when user installs the app
|
| 80 |
+
window.addEventListener('appinstalled', () => {
|
| 81 |
+
console.log('[PWA] BattleWords installed successfully!');
|
| 82 |
+
deferredPrompt = null;
|
| 83 |
+
});
|
| 84 |
+
</script>
|
| 85 |
+
"""
|
| 86 |
+
st.markdown(pwa_meta_tags, unsafe_allow_html=True)
|
| 87 |
+
|
| 88 |
CoordLike = Tuple[int, int]
|
| 89 |
|
| 90 |
def fig_to_pil_rgba(fig):
|
|
|
|
| 1142 |
# Invalidate radar GIF cache to hide completed rings
|
| 1143 |
st.session_state.radar_gif_path = None
|
| 1144 |
st.session_state.radar_gif_signature = None
|
| 1145 |
+
# Note: letter_map is static and built once in _init_session(), no need to rebuild
|
| 1146 |
_sync_back(state)
|
| 1147 |
|
| 1148 |
# Play sound effect based on hit or miss
|
|
@@ -415,7 +415,7 @@ NINTH
|
|
| 415 |
NOISE
|
| 416 |
NURSE
|
| 417 |
OCEAN
|
| 418 |
-
|
| 419 |
OFFER
|
| 420 |
ORDER
|
| 421 |
ORGAN
|
|
|
|
| 415 |
NOISE
|
| 416 |
NURSE
|
| 417 |
OCEAN
|
| 418 |
+
OFTEN
|
| 419 |
OFFER
|
| 420 |
ORDER
|
| 421 |
ORGAN
|
|
@@ -3,14 +3,21 @@
|
|
| 3 |
## Project Overview
|
| 4 |
BattleWords is a vocabulary learning game inspired by Battleship mechanics, built with Streamlit and Python 3.12. Players reveal cells on a 12x12 grid to discover hidden words and earn points for strategic guessing.
|
| 5 |
|
| 6 |
-
**Current Version:** 0.2.
|
| 7 |
**Next Version:** 0.3.0 (In Development - Local Player History & Statistics)
|
| 8 |
**Repository:** https://github.com/Oncorporation/BattleWords.git
|
| 9 |
**Live Demo:** https://huggingface.co/spaces/Surn/BattleWords
|
| 10 |
|
| 11 |
## Recent Changes & Branch Status
|
| 12 |
|
| 13 |
-
**Latest (v0.2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
- New setting: "Show Challenge Share Links" (default OFF)
|
| 15 |
- Header Challenge Mode banner hides the "Share this challenge" link when disabled
|
| 16 |
- Game Over dialog still lets you submit or create a challenge, but hides the generated share URL when disabled (shows a success message instead)
|
|
@@ -23,11 +30,12 @@ BattleWords is a vocabulary learning game inspired by Battleship mechanics, buil
|
|
| 23 |
- Players click cells to reveal letters or empty spaces
|
| 24 |
- After revealing a letter, players can guess words
|
| 25 |
- Scoring: word length + bonus for unrevealed letters
|
| 26 |
-
- Game ends when all words are guessed or all word letters revealed
|
| 27 |
- Incorrect guess history with optional display (enabled by default)
|
| 28 |
- 10 incorrect guess limit per game
|
| 29 |
- **✅ IMPLEMENTED (v0.2.20+):** Challenge Mode with game sharing via short URLs
|
| 30 |
- **✅ IMPLEMENTED (v0.2.20+):** Remote storage via Hugging Face datasets for challenges and leaderboards
|
|
|
|
| 31 |
- **PLANNED (v0.3.0):** Local persistent storage for individual player results and high scores
|
| 32 |
|
| 33 |
### Scoring Tiers
|
|
|
|
| 3 |
## Project Overview
|
| 4 |
BattleWords is a vocabulary learning game inspired by Battleship mechanics, built with Streamlit and Python 3.12. Players reveal cells on a 12x12 grid to discover hidden words and earn points for strategic guessing.
|
| 5 |
|
| 6 |
+
**Current Version:** 0.2.28 (Stable - PWA, Challenge Mode & Remote Storage)
|
| 7 |
**Next Version:** 0.3.0 (In Development - Local Player History & Statistics)
|
| 8 |
**Repository:** https://github.com/Oncorporation/BattleWords.git
|
| 9 |
**Live Demo:** https://huggingface.co/spaces/Surn/BattleWords
|
| 10 |
|
| 11 |
## Recent Changes & Branch Status
|
| 12 |
|
| 13 |
+
**Latest (v0.2.28):**
|
| 14 |
+
- Progressive Web App (PWA) support
|
| 15 |
+
- Added `service worker` and `manifest.json`
|
| 16 |
+
- Basic offline caching of static assets
|
| 17 |
+
- INSTALL_GUIDE.md added with install steps
|
| 18 |
+
- No gameplay logic changes
|
| 19 |
+
|
| 20 |
+
**Previous (v0.2.27):**
|
| 21 |
- New setting: "Show Challenge Share Links" (default OFF)
|
| 22 |
- Header Challenge Mode banner hides the "Share this challenge" link when disabled
|
| 23 |
- Game Over dialog still lets you submit or create a challenge, but hides the generated share URL when disabled (shows a success message instead)
|
|
|
|
| 30 |
- Players click cells to reveal letters or empty spaces
|
| 31 |
- After revealing a letter, players can guess words
|
| 32 |
- Scoring: word length + bonus for unrevealed letters
|
| 33 |
+
- Game ends when all words are guessed or all word letters are revealed
|
| 34 |
- Incorrect guess history with optional display (enabled by default)
|
| 35 |
- 10 incorrect guess limit per game
|
| 36 |
- **✅ IMPLEMENTED (v0.2.20+):** Challenge Mode with game sharing via short URLs
|
| 37 |
- **✅ IMPLEMENTED (v0.2.20+):** Remote storage via Hugging Face datasets for challenges and leaderboards
|
| 38 |
+
- **✅ IMPLEMENTED (v0.2.28):** PWA install support
|
| 39 |
- **PLANNED (v0.3.0):** Local persistent storage for individual player results and high scores
|
| 40 |
|
| 41 |
### Scoring Tiers
|
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Generate PWA icons for BattleWords.
|
| 4 |
+
Creates 192x192 and 512x512 icons with ocean theme and 'BW' text.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
def create_icon(size, filename):
|
| 11 |
+
"""Create a square icon with ocean gradient background and 'BW' text."""
|
| 12 |
+
|
| 13 |
+
# Create image with ocean blue gradient
|
| 14 |
+
img = Image.new('RGB', (size, size))
|
| 15 |
+
draw = ImageDraw.Draw(img)
|
| 16 |
+
|
| 17 |
+
# Draw vertical gradient (ocean theme)
|
| 18 |
+
water_sky = (29, 100, 200) # #1d64c8
|
| 19 |
+
water_deep = (11, 42, 74) # #0b2a4a
|
| 20 |
+
|
| 21 |
+
for y in range(size):
|
| 22 |
+
# Interpolate between sky and deep
|
| 23 |
+
ratio = y / size
|
| 24 |
+
r = int(water_sky[0] * (1 - ratio) + water_deep[0] * ratio)
|
| 25 |
+
g = int(water_sky[1] * (1 - ratio) + water_deep[1] * ratio)
|
| 26 |
+
b = int(water_sky[2] * (1 - ratio) + water_deep[2] * ratio)
|
| 27 |
+
draw.rectangle([(0, y), (size, y + 1)], fill=(r, g, b))
|
| 28 |
+
|
| 29 |
+
# Draw circular background for better icon appearance
|
| 30 |
+
circle_margin = size // 10
|
| 31 |
+
circle_bbox = [circle_margin, circle_margin, size - circle_margin, size - circle_margin]
|
| 32 |
+
|
| 33 |
+
# Draw white circle with transparency
|
| 34 |
+
overlay = Image.new('RGBA', (size, size), (255, 255, 255, 0))
|
| 35 |
+
overlay_draw = ImageDraw.Draw(overlay)
|
| 36 |
+
overlay_draw.ellipse(circle_bbox, fill=(255, 255, 255, 40))
|
| 37 |
+
|
| 38 |
+
# Composite the overlay
|
| 39 |
+
img = img.convert('RGBA')
|
| 40 |
+
img = Image.alpha_composite(img, overlay)
|
| 41 |
+
|
| 42 |
+
# Draw 'BW' text
|
| 43 |
+
font_size = size // 3
|
| 44 |
+
try:
|
| 45 |
+
# Try to load a nice bold font
|
| 46 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
|
| 47 |
+
except Exception:
|
| 48 |
+
try:
|
| 49 |
+
# Fallback for Windows
|
| 50 |
+
font = ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf", font_size)
|
| 51 |
+
except Exception:
|
| 52 |
+
# Ultimate fallback
|
| 53 |
+
font = ImageFont.load_default()
|
| 54 |
+
|
| 55 |
+
draw = ImageDraw.Draw(img)
|
| 56 |
+
text = "BW"
|
| 57 |
+
|
| 58 |
+
# Get text bounding box for centering
|
| 59 |
+
bbox = draw.textbbox((0, 0), text, font=font)
|
| 60 |
+
text_width = bbox[2] - bbox[0]
|
| 61 |
+
text_height = bbox[3] - bbox[1]
|
| 62 |
+
|
| 63 |
+
# Center the text
|
| 64 |
+
x = (size - text_width) // 2
|
| 65 |
+
y = (size - text_height) // 2 - (bbox[1] // 2)
|
| 66 |
+
|
| 67 |
+
# Draw text with shadow for depth
|
| 68 |
+
shadow_offset = size // 50
|
| 69 |
+
draw.text((x + shadow_offset, y + shadow_offset), text, fill=(0, 0, 0, 100), font=font)
|
| 70 |
+
draw.text((x, y), text, fill='white', font=font)
|
| 71 |
+
|
| 72 |
+
# Convert back to RGB for saving as PNG
|
| 73 |
+
if img.mode == 'RGBA':
|
| 74 |
+
background = Image.new('RGB', img.size, (11, 42, 74))
|
| 75 |
+
background.paste(img, mask=img.split()[3]) # Use alpha channel as mask
|
| 76 |
+
img = background
|
| 77 |
+
|
| 78 |
+
# Save
|
| 79 |
+
img.save(filename, 'PNG', optimize=True)
|
| 80 |
+
print(f"[OK] Created {filename} ({size}x{size})")
|
| 81 |
+
|
| 82 |
+
def main():
|
| 83 |
+
"""Generate both icon sizes."""
|
| 84 |
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 85 |
+
static_dir = os.path.join(script_dir, 'battlewords', 'static')
|
| 86 |
+
|
| 87 |
+
# Ensure directory exists
|
| 88 |
+
os.makedirs(static_dir, exist_ok=True)
|
| 89 |
+
|
| 90 |
+
# Generate icons
|
| 91 |
+
print("Generating PWA icons for BattleWords...")
|
| 92 |
+
create_icon(192, os.path.join(static_dir, 'icon-192.png'))
|
| 93 |
+
create_icon(512, os.path.join(static_dir, 'icon-512.png'))
|
| 94 |
+
print("\n[SUCCESS] PWA icons generated successfully!")
|
| 95 |
+
print(f" Location: {static_dir}")
|
| 96 |
+
|
| 97 |
+
if __name__ == '__main__':
|
| 98 |
+
main()
|
|
@@ -132,11 +132,15 @@ Current Deltas (0.1.3 → 0.1.10)
|
|
| 132 |
- Version footer shows commit/Python/Streamlit; ocean background effect.
|
| 133 |
- Word list default/persistence fixes and sort action persists after delay.
|
| 134 |
- 0.2.24
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
Known Issues / TODO
|
| 142 |
- Word list selection bug: improper list fetched/propagated in some runs.
|
|
|
|
| 132 |
- Version footer shows commit/Python/Streamlit; ocean background effect.
|
| 133 |
- Word list default/persistence fixes and sort action persists after delay.
|
| 134 |
- 0.2.24
|
| 135 |
+
- compress height
|
| 136 |
+
- change incorrect guess tooltip location
|
| 137 |
+
- update final screen layout
|
| 138 |
+
- add word difficulty formula
|
| 139 |
+
- update documentation
|
| 140 |
+
- 0.2.28
|
| 141 |
+
- Add Progressive Web App (PWA) support with `service worker` and `manifest.json`
|
| 142 |
+
- Add INSTALL_GUIDE.md for PWA install instructions
|
| 143 |
+
- No gameplay logic changes
|
| 144 |
|
| 145 |
Known Issues / TODO
|
| 146 |
- Word list selection bug: improper list fetched/propagated in some runs.
|
|
@@ -59,6 +59,13 @@ Battlewords is inspired by the classic Battleship game, but uses words instead o
|
|
| 59 |
- **High Scores:** Top scores are tracked and displayed in the sidebar, filterable by wordlist and game mode.
|
| 60 |
- **Player Name:** Optional player name is saved with results.
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
## New Features (v0.2.24)
|
| 63 |
- **UI Improvements:** More compact layout, improved tooltip for incorrect guesses, and updated final score screen.
|
| 64 |
- **Word Difficulty:** Added a word difficulty formula and display for each game/challenge, visible in the final score and leaderboard.
|
|
@@ -104,7 +111,8 @@ Battlewords is inspired by the classic Battleship game, but uses words instead o
|
|
| 104 |
## Deployment Requirements
|
| 105 |
|
| 106 |
### Basic Deployment (Offline Mode)
|
| 107 |
-
No special configuration needed. The app will run with all core gameplay features.
|
|
|
|
| 108 |
|
| 109 |
### Challenge Mode Deployment (Remote Storage)
|
| 110 |
Requires HuggingFace Hub integration for challenge sharing and leaderboards.
|
|
|
|
| 59 |
- **High Scores:** Top scores are tracked and displayed in the sidebar, filterable by wordlist and game mode.
|
| 60 |
- **Player Name:** Optional player name is saved with results.
|
| 61 |
|
| 62 |
+
## New Features (v0.2.28)
|
| 63 |
+
- **PWA Support:** App is installable as a Progressive Web App on desktop and mobile.
|
| 64 |
+
- Added `service worker` and `manifest.json`.
|
| 65 |
+
- Basic offline caching of static assets.
|
| 66 |
+
- INSTALL_GUIDE.md added with platform-specific install steps.
|
| 67 |
+
- No gameplay logic changes.
|
| 68 |
+
|
| 69 |
## New Features (v0.2.24)
|
| 70 |
- **UI Improvements:** More compact layout, improved tooltip for incorrect guesses, and updated final score screen.
|
| 71 |
- **Word Difficulty:** Added a word difficulty formula and display for each game/challenge, visible in the final score and leaderboard.
|
|
|
|
| 111 |
## Deployment Requirements
|
| 112 |
|
| 113 |
### Basic Deployment (Offline Mode)
|
| 114 |
+
No special configuration needed. The app will run with all core gameplay features.
|
| 115 |
+
Optional: Install as PWA from the browser menu (Add to Home Screen/Install app).
|
| 116 |
|
| 117 |
### Challenge Mode Deployment (Remote Storage)
|
| 118 |
Requires HuggingFace Hub integration for challenge sharing and leaderboards.
|