v0.2.6
Browse filesRefactor settings and update to version 0.2.6
Refactored the settings logic by moving sidebar controls to a dedicated `settings_page.py` file, improving modularity and user experience. Added routing for the new "Settings" page and updated the footer navigation.
Centralized audio handling with a new `_handle_audio` function for consistent behavior across pages. Simplified and cleaned up redundant code in `ui.py`.
Updated version to `0.2.6` across all relevant files, including `CLAUDE.md`, `GAMEPLAY_GUIDE.md`, `README.md`, and `pyproject.toml`. Enhanced changelogs and documentation to reflect new features.
Introduced a paywall specification (`paywall.md`) for future subscription-based access. Added detailed plans for settings refactoring in `settings.md`.
Improved maintainability and prepared the app for monetization while enhancing the user interface and overall functionality.
- CLAUDE.md +2 -2
- GAMEPLAY_GUIDE.md +1 -1
- README.md +3 -3
- pyproject.toml +1 -2
- specs/leaderboard_spec.md +1 -1
- specs/paywall.md +64 -0
- specs/requirements.md +1 -1
- specs/settings.md +65 -0
- specs/specs.md +1 -1
- wrdler/__init__.py +1 -1
- wrdler/settings_page.py +347 -0
- wrdler/ui.py +50 -325
|
@@ -8,11 +8,11 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords:
|
|
| 8 |
- **2 free letter guesses at game start** (all instances revealed)
|
| 9 |
- **Word composition:** 2 four-letter, 2 five-letter, 2 six-letter words per puzzle
|
| 10 |
|
| 11 |
-
**Current Version:** 0.2.
|
| 12 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 13 |
**Branch:** AI (working branch)
|
| 14 |
|
| 15 |
-
## Current Features (v0.2.
|
| 16 |
|
| 17 |
### Core Gameplay
|
| 18 |
- 8x6 grid with 6 hidden words (one per row, horizontal only)
|
|
|
|
| 8 |
- **2 free letter guesses at game start** (all instances revealed)
|
| 9 |
- **Word composition:** 2 four-letter, 2 five-letter, 2 six-letter words per puzzle
|
| 10 |
|
| 11 |
+
**Current Version:** 0.2.6
|
| 12 |
**Repository:** https://github.com/Oncorporation/Wrdler.git
|
| 13 |
**Branch:** AI (working branch)
|
| 14 |
|
| 15 |
+
## Current Features (v0.2.6)
|
| 16 |
|
| 17 |
### Core Gameplay
|
| 18 |
- 8x6 grid with 6 hidden words (one per row, horizontal only)
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# Wrdler Gameplay Guide
|
| 2 |
-
**Version:** 0.
|
| 3 |
**Last Updated:** 2025-01-31
|
| 4 |
|
| 5 |
## Welcome to Wrdler!
|
|
|
|
| 1 |
# Wrdler Gameplay Guide
|
| 2 |
+
**Version:** 0.2.6
|
| 3 |
**Last Updated:** 2025-01-31
|
| 4 |
|
| 5 |
## Welcome to Wrdler!
|
|
@@ -4,7 +4,7 @@ emoji: 🧩
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: streamlit
|
| 7 |
-
sdk_version: 1.
|
| 8 |
python_version: 3.12.8
|
| 9 |
app_port: 8501
|
| 10 |
app_file: app.py
|
|
@@ -25,7 +25,7 @@ thumbnail: >-
|
|
| 25 |
|
| 26 |
Wrdler is a vocabulary learning game with a simplified grid and strategic letter guessing. The objective is to discover hidden words on a grid by making smart guesses before all letters are revealed.
|
| 27 |
|
| 28 |
-
**Current Version:** v0.2.
|
| 29 |
|
| 30 |
## Key Differences from BattleWords
|
| 31 |
|
|
@@ -242,7 +242,7 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
|
|
| 242 |
|
| 243 |
## Changelog
|
| 244 |
|
| 245 |
-
### v0.2.
|
| 246 |
**Word List Filtering**
|
| 247 |
- ✅ Added "Filter Wordlist" button to sidebar
|
| 248 |
- ✅ Filters words against `assets/filter.txt` blocklist
|
|
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: streamlit
|
| 7 |
+
sdk_version: 1.52.1
|
| 8 |
python_version: 3.12.8
|
| 9 |
app_port: 8501
|
| 10 |
app_file: app.py
|
|
|
|
| 25 |
|
| 26 |
Wrdler is a vocabulary learning game with a simplified grid and strategic letter guessing. The objective is to discover hidden words on a grid by making smart guesses before all letters are revealed.
|
| 27 |
|
| 28 |
+
**Current Version:** v0.2.6
|
| 29 |
|
| 30 |
## Key Differences from BattleWords
|
| 31 |
|
|
|
|
| 242 |
|
| 243 |
## Changelog
|
| 244 |
|
| 245 |
+
### v0.2.6 (Current) ✅
|
| 246 |
**Word List Filtering**
|
| 247 |
- ✅ Added "Filter Wordlist" button to sidebar
|
| 248 |
- ✅ Filters words against `assets/filter.txt` blocklist
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "wrdler"
|
| 3 |
-
version = "0.2.
|
| 4 |
description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, 2 free letter guesses, and a settings-based daily/weekly leaderboard system. Features leaderboard UI, challenge sharing, and AI word lists."
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
|
@@ -23,7 +23,6 @@ include-package-data = true
|
|
| 23 |
|
| 24 |
[tool.setuptools.packages.find]
|
| 25 |
where = [""]
|
| 26 |
-
include = ["wrdler*"]
|
| 27 |
|
| 28 |
[tool.setuptools.package-data]
|
| 29 |
"wrdler.words" = ["*.txt"]
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "wrdler"
|
| 3 |
+
version = "0.2.6"
|
| 4 |
description = "Wrdler vocabulary puzzle game - simplified version based on BattleWords with 8x6 grid, horizontal words only, no scope, 2 free letter guesses, and a settings-based daily/weekly leaderboard system. Features leaderboard UI, challenge sharing, and AI word lists."
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
|
|
|
| 23 |
|
| 24 |
[tool.setuptools.packages.find]
|
| 25 |
where = [""]
|
|
|
|
| 26 |
|
| 27 |
[tool.setuptools.package-data]
|
| 28 |
"wrdler.words" = ["*.txt"]
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Wrdler Leaderboard System Specification
|
| 2 |
|
| 3 |
**Document Version:** 1.4.0
|
| 4 |
-
**Project Version:** 0.2.
|
| 5 |
**Author:** GitHub Copilot
|
| 6 |
**Last Updated:** 2025-12-08
|
| 7 |
**Status:** ✅ Implemented and Documented
|
|
|
|
| 1 |
# Wrdler Leaderboard System Specification
|
| 2 |
|
| 3 |
**Document Version:** 1.4.0
|
| 4 |
+
**Project Version:** 0.2.6
|
| 5 |
**Author:** GitHub Copilot
|
| 6 |
**Last Updated:** 2025-12-08
|
| 7 |
**Status:** ✅ Implemented and Documented
|
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Paywall Integration Specification
|
| 2 |
+
|
| 3 |
+
This document outlines the plan to integrate `st-paywall` into the Wrdler application to enable subscription-based access.
|
| 4 |
+
|
| 5 |
+
## 1. Dependencies
|
| 6 |
+
|
| 7 |
+
Add `st-paywall` to the project dependencies.
|
| 8 |
+
|
| 9 |
+
- **File**: `pyproject.toml`
|
| 10 |
+
- **Action**: Add `st-paywall` to the `dependencies` list.
|
| 11 |
+
|
| 12 |
+
## 2. Configuration
|
| 13 |
+
|
| 14 |
+
Configure the payment provider (Stripe or Buy Me A Coffee) in Streamlit secrets.
|
| 15 |
+
|
| 16 |
+
- **File**: `.streamlit/secrets.toml` (User needs to create this if not exists)
|
| 17 |
+
- **Content**:
|
| 18 |
+
```toml
|
| 19 |
+
[stripe]
|
| 20 |
+
api_key = "sk_live_..."
|
| 21 |
+
link = "https://buy.stripe.com/..."
|
| 22 |
+
# OR
|
| 23 |
+
[bmac]
|
| 24 |
+
api_key = "..."
|
| 25 |
+
link = "https://www.buymeacoffee.com/..."
|
| 26 |
+
```
|
| 27 |
+
*Note: `st-paywall` expects specific keys. Refer to documentation for exact structure.*
|
| 28 |
+
|
| 29 |
+
## 3. Code Integration
|
| 30 |
+
|
| 31 |
+
Integrate the authentication and subscription check into the main UI flow.
|
| 32 |
+
|
| 33 |
+
- **File**: `wrdler/ui.py`
|
| 34 |
+
- **Import**: `from st_paywall import add_auth`
|
| 35 |
+
- **Placement**:
|
| 36 |
+
- The `add_auth` function should be called early in the `run_app` function or before rendering sensitive content.
|
| 37 |
+
- It handles the login UI and subscription check.
|
| 38 |
+
|
| 39 |
+
### Integration Strategy
|
| 40 |
+
|
| 41 |
+
We will add a "Premium" toggle or check, or enforce it for the whole app depending on requirements. For now, we will document how to gate the entire app or specific premium features (like AI Wordlist generation).
|
| 42 |
+
|
| 43 |
+
**Option A: Gate Entire App**
|
| 44 |
+
Call `add_auth(required=True)` at the start of `run_app`.
|
| 45 |
+
|
| 46 |
+
**Option B: Gate Premium Features**
|
| 47 |
+
Call `add_auth(required=False)` to show the login/subscribe sidebar but allow free access. Then check `st.session_state.user_subscribed` (or similar provided by the library) before enabling features like:
|
| 48 |
+
- AI Wordlist Generation
|
| 49 |
+
- Challenge Mode creation
|
| 50 |
+
|
| 51 |
+
## 4. Implementation Plan
|
| 52 |
+
|
| 53 |
+
1. **Update `pyproject.toml`**: Add `st-paywall`.
|
| 54 |
+
2. **Update `wrdler/ui.py`**:
|
| 55 |
+
- Import `add_auth`.
|
| 56 |
+
- Insert `add_auth(...)` call in `run_app` or `_render_sidebar`.
|
| 57 |
+
- (Optional) Wrap premium features with subscription checks.
|
| 58 |
+
|
| 59 |
+
## 5. Testing
|
| 60 |
+
|
| 61 |
+
- Verify that the login button appears.
|
| 62 |
+
- Verify that non-subscribed users are prompted to subscribe.
|
| 63 |
+
- Verify that subscribed users can access the content.
|
| 64 |
+
- Test with `testing_mode = true` in secrets.
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# Wrdler: Implementation Requirements
|
| 2 |
-
**Version:** 0.2.
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
**Last Updated:** 2025-12-08
|
| 5 |
|
|
|
|
| 1 |
# Wrdler: Implementation Requirements
|
| 2 |
+
**Version:** 0.2.6
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
**Last Updated:** 2025-12-08
|
| 5 |
|
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Settings Page Refactoring Plan
|
| 2 |
+
|
| 3 |
+
## Goal
|
| 4 |
+
Move the sidebar configuration controls (`_render_sidebar`) from `wrdler/ui.py` to a dedicated Settings page (`wrdler/settings_page.py`), accessible via the footer navigation.
|
| 5 |
+
|
| 6 |
+
## Files to Create
|
| 7 |
+
1. `wrdler/settings_page.py`
|
| 8 |
+
|
| 9 |
+
## Files to Modify
|
| 10 |
+
1. `wrdler/ui.py`
|
| 11 |
+
|
| 12 |
+
## Detailed Steps
|
| 13 |
+
|
| 14 |
+
### 1. Create `wrdler/settings_page.py`
|
| 15 |
+
This file will encapsulate the rendering logic for the settings.
|
| 16 |
+
|
| 17 |
+
- **Imports**:
|
| 18 |
+
- `streamlit as st`
|
| 19 |
+
- `os`
|
| 20 |
+
- `time`
|
| 21 |
+
- From `wrdler.word_loader`: `get_wordlist_files`, `get_wordlist_info`
|
| 22 |
+
- From `wrdler.generator`: `sort_word_file`, `filter_word_file`
|
| 23 |
+
- From `wrdler.audio`: `get_audio_tracks`, `_inject_audio_control_sync`
|
| 24 |
+
- From `wrdler.version_info`: `versions_html`
|
| 25 |
+
|
| 26 |
+
- **Functions**:
|
| 27 |
+
- `render_settings_page(new_game_callback)`:
|
| 28 |
+
- Renders the title "Settings".
|
| 29 |
+
- Contains the logic previously in `_render_sidebar` (Game Mode, Wordlist Controls, Grid Options, Audio Controls).
|
| 30 |
+
- **Important**: Does *not* include `_mount_background_audio` (this will be global).
|
| 31 |
+
- Uses `st.container` or main layout instead of `st.sidebar`.
|
| 32 |
+
- Accepts `new_game_callback` to trigger a game reset when settings change.
|
| 33 |
+
- `_sort_wordlist(filename)`: Moved from `ui.py`.
|
| 34 |
+
- `_filter_wordlist(filename)`: Moved from `ui.py`.
|
| 35 |
+
- `_filter_results_dialog`: Moved from `ui.py` (if used by `_filter_wordlist`).
|
| 36 |
+
- Local callbacks `_on_wordlist_change` and `_on_ai_generate` that utilize `new_game_callback`.
|
| 37 |
+
|
| 38 |
+
### 2. Modify `wrdler/ui.py`
|
| 39 |
+
|
| 40 |
+
- **Extract Audio Logic**:
|
| 41 |
+
- Create a helper function `_handle_audio()` that contains the audio initialization and mounting logic previously in `_render_sidebar`.
|
| 42 |
+
- This ensures audio persists across pages.
|
| 43 |
+
|
| 44 |
+
- **Update `run_app()`**:
|
| 45 |
+
- Call `_handle_audio()` at the top level (before page routing).
|
| 46 |
+
- Add routing logic for `page="settings"`:
|
| 47 |
+
- Import `render_settings_page`.
|
| 48 |
+
- Render background.
|
| 49 |
+
- Call `render_settings_page(_on_game_option_change)`.
|
| 50 |
+
- Render footer with `current_page="settings"`.
|
| 51 |
+
- Return (stop execution of main game).
|
| 52 |
+
- Remove `_render_sidebar()` call.
|
| 53 |
+
|
| 54 |
+
- **Update `_render_footer()`**:
|
| 55 |
+
- Add a link to `?page=settings` with label "?? Settings".
|
| 56 |
+
- Highlight it when `current_page="settings"`.
|
| 57 |
+
|
| 58 |
+
- **Cleanup**:
|
| 59 |
+
- Remove `_render_sidebar` function.
|
| 60 |
+
- Remove `_sort_wordlist`, `_filter_wordlist` (moved to settings page).
|
| 61 |
+
|
| 62 |
+
## Implementation Notes
|
| 63 |
+
- **Audio**: The audio *controls* (volume, track) will be in the Settings page, but the audio *player* (hidden HTML/JS) must be mounted on every page load via `_handle_audio()` in `run_app` to ensure continuous playback or proper state.
|
| 64 |
+
- **Callbacks**: `_on_game_option_change` in `ui.py` calls `_new_game`. This callback will be passed to `render_settings_page` so that changing settings triggers the necessary state resets.
|
| 65 |
+
- **Navigation**: The footer will serve as the primary navigation between "Play", "Leaderboard", and "Settings".
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# Wrdler Game Specifications (specs.md)
|
| 2 |
-
**Version:** 0.2.
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
**Last Updated:** 2025-12-08
|
| 5 |
|
|
|
|
| 1 |
# Wrdler Game Specifications (specs.md)
|
| 2 |
+
**Version:** 0.2.6
|
| 3 |
**Status:** Production Ready - Leaderboards Implemented
|
| 4 |
**Last Updated:** 2025-12-08
|
| 5 |
|
|
@@ -9,5 +9,5 @@ Key differences from BattleWords:
|
|
| 9 |
- Daily and weekly leaderboards
|
| 10 |
"""
|
| 11 |
|
| 12 |
-
__version__ = "0.2.
|
| 13 |
__all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
|
|
|
|
| 9 |
- Daily and weekly leaderboards
|
| 10 |
"""
|
| 11 |
|
| 12 |
+
__version__ = "0.2.6"
|
| 13 |
__all__ = ["models", "generator", "logic", "ui", "word_loader", "leaderboard", "leaderboard_page"]
|
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
from .word_loader import get_wordlist_files, get_wordlist_info
|
| 5 |
+
from .generator import sort_word_file, filter_word_file
|
| 6 |
+
from .audio import get_audio_tracks, _inject_audio_control_sync
|
| 7 |
+
from .version_info import versions_html
|
| 8 |
+
|
| 9 |
+
def _sort_wordlist(filename, new_game_callback):
|
| 10 |
+
import os
|
| 11 |
+
import time
|
| 12 |
+
|
| 13 |
+
WORDS_DIR = os.path.join(os.path.dirname(__file__), "words")
|
| 14 |
+
filepath = os.path.join(WORDS_DIR, filename)
|
| 15 |
+
sorted_words = sort_word_file(filepath)
|
| 16 |
+
# Optionally, write sorted words back to file
|
| 17 |
+
with open(filepath, "w", encoding="utf-8") as f:
|
| 18 |
+
# Re-add header if needed
|
| 19 |
+
f.write("# Optional: place a large A-Z word list here (one word per line).\n")
|
| 20 |
+
f.write("# The app falls back to built-in pools if fewer than 500 words per length are found.\n")
|
| 21 |
+
for word in sorted_words:
|
| 22 |
+
f.write(f"{word}\n")
|
| 23 |
+
# Show a message in Streamlit
|
| 24 |
+
st.success(f"{filename} sorted by length and alphabetically. Starting new game in 5 seconds...")
|
| 25 |
+
time.sleep(5) # 5 second delay before starting new game
|
| 26 |
+
new_game_callback()
|
| 27 |
+
|
| 28 |
+
def _filter_results_content(count: int, words: list[str], filename: str):
|
| 29 |
+
st.success(f"Removed {count} words from {filename}")
|
| 30 |
+
st.markdown("### Removed Words")
|
| 31 |
+
st.text_area("List", value="\n".join(words), height=200, disabled=True)
|
| 32 |
+
if st.button("Close", key="close_filter_dialog"):
|
| 33 |
+
st.rerun()
|
| 34 |
+
|
| 35 |
+
# Prefer st.dialog/experimental_dialog; fallback to st.modal if unavailable
|
| 36 |
+
_Dialog = getattr(st, "dialog", getattr(st, "experimental_dialog", None))
|
| 37 |
+
if _Dialog:
|
| 38 |
+
@_Dialog("Filter Results")
|
| 39 |
+
def _filter_results_dialog(count, words, filename):
|
| 40 |
+
_filter_results_content(count, words, filename)
|
| 41 |
+
else:
|
| 42 |
+
def _filter_results_dialog(count, words, filename):
|
| 43 |
+
modal_ctx = getattr(st, "modal", None)
|
| 44 |
+
if callable(modal_ctx):
|
| 45 |
+
with modal_ctx("Filter Results"):
|
| 46 |
+
_filter_results_content(count, words, filename)
|
| 47 |
+
else:
|
| 48 |
+
st.info(f"Removed {count} words from {filename}")
|
| 49 |
+
st.text_area("Removed Words", value="\n".join(words), height=200)
|
| 50 |
+
|
| 51 |
+
def _filter_wordlist(filename):
|
| 52 |
+
filter_path = os.path.join(os.path.dirname(__file__), "assets", "filter.txt")
|
| 53 |
+
words_path = os.path.join(os.path.dirname(__file__), "words", filename)
|
| 54 |
+
|
| 55 |
+
if not os.path.exists(filter_path):
|
| 56 |
+
st.error("Filter file not found (wrdler/assets/filter.txt).")
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
count, removed_words = filter_word_file(filter_path, words_path)
|
| 60 |
+
|
| 61 |
+
if count > 0:
|
| 62 |
+
_filter_results_dialog(count, removed_words, filename)
|
| 63 |
+
else:
|
| 64 |
+
st.info(f"No words removed from {filename}.")
|
| 65 |
+
|
| 66 |
+
def render_settings_page(new_game_callback):
|
| 67 |
+
st.header("SETTINGS")
|
| 68 |
+
|
| 69 |
+
st.header("Game Mode")
|
| 70 |
+
game_modes = ["classic", "easy", "too easy"]
|
| 71 |
+
default_mode = "classic"
|
| 72 |
+
if "game_mode" not in st.session_state:
|
| 73 |
+
st.session_state.game_mode = default_mode
|
| 74 |
+
current_mode = st.session_state.game_mode
|
| 75 |
+
st.selectbox(
|
| 76 |
+
"Select game mode",
|
| 77 |
+
options=game_modes,
|
| 78 |
+
index=game_modes.index(current_mode) if current_mode in game_modes else 0,
|
| 79 |
+
key="game_mode",
|
| 80 |
+
on_change=new_game_callback,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
st.header("Wordlist Controls")
|
| 84 |
+
|
| 85 |
+
# Initialize AI mode settings
|
| 86 |
+
if "use_ai_wordlist" not in st.session_state:
|
| 87 |
+
st.session_state.use_ai_wordlist = False
|
| 88 |
+
if "ai_topic" not in st.session_state:
|
| 89 |
+
st.session_state.ai_topic = "English"
|
| 90 |
+
|
| 91 |
+
wordlist_files = get_wordlist_files()
|
| 92 |
+
|
| 93 |
+
# Add AI Generated option to file list
|
| 94 |
+
wordlist_options = ["AI Generated"] + wordlist_files if wordlist_files else ["AI Generated"]
|
| 95 |
+
|
| 96 |
+
def _on_wordlist_change():
|
| 97 |
+
selected = st.session_state.get("wordlist_selector")
|
| 98 |
+
if selected == "AI Generated":
|
| 99 |
+
st.session_state.use_ai_wordlist = True
|
| 100 |
+
if "ai_topic" not in st.session_state:
|
| 101 |
+
st.session_state.ai_topic = "English"
|
| 102 |
+
else:
|
| 103 |
+
st.session_state.use_ai_wordlist = False
|
| 104 |
+
st.session_state.selected_wordlist = selected
|
| 105 |
+
new_game_callback()
|
| 106 |
+
|
| 107 |
+
def _on_ai_generate():
|
| 108 |
+
new_game_callback()
|
| 109 |
+
|
| 110 |
+
if wordlist_files:
|
| 111 |
+
# Determine current selection index
|
| 112 |
+
if st.session_state.get("use_ai_wordlist", False):
|
| 113 |
+
current_index = 0 # AI Generated
|
| 114 |
+
elif st.session_state.get("selected_wordlist") in wordlist_files:
|
| 115 |
+
current_index = wordlist_options.index(st.session_state.selected_wordlist)
|
| 116 |
+
else:
|
| 117 |
+
# Default to first file
|
| 118 |
+
st.session_state.selected_wordlist = wordlist_files[0]
|
| 119 |
+
st.session_state.use_ai_wordlist = False
|
| 120 |
+
current_index = 1
|
| 121 |
+
|
| 122 |
+
# Wordlist selector
|
| 123 |
+
selected = st.selectbox(
|
| 124 |
+
"Select list",
|
| 125 |
+
options=wordlist_options,
|
| 126 |
+
index=current_index,
|
| 127 |
+
format_func=lambda f: f if f == "AI Generated" else f.rsplit(".", 1)[0],
|
| 128 |
+
key="wordlist_selector",
|
| 129 |
+
on_change=_on_wordlist_change,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Show topic input if AI mode selected
|
| 133 |
+
if selected == "AI Generated":
|
| 134 |
+
st.session_state.use_ai_wordlist = True
|
| 135 |
+
|
| 136 |
+
# Topic input and Generate button in columns
|
| 137 |
+
topic_col, gen_col = st.columns([3, 1], gap="small")
|
| 138 |
+
|
| 139 |
+
with topic_col:
|
| 140 |
+
st.text_input(
|
| 141 |
+
"Topic",
|
| 142 |
+
value=st.session_state.ai_topic,
|
| 143 |
+
key="ai_topic",
|
| 144 |
+
placeholder="e.g., Ocean Life, Space, History",
|
| 145 |
+
help="Enter a topic for AI-generated words",
|
| 146 |
+
# Remove on_change to prevent automatic generation
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
with gen_col:
|
| 150 |
+
# Add custom CSS for the generate button
|
| 151 |
+
st.markdown(
|
| 152 |
+
"""
|
| 153 |
+
<style>
|
| 154 |
+
.st-key-ai_generate_btn {
|
| 155 |
+
margin-top: 1.85rem; /* Align with text input */
|
| 156 |
+
}
|
| 157 |
+
.st-key-ai_generate_btn button {
|
| 158 |
+
aspect-ratio: auto !important;
|
| 159 |
+
height: auto !important;
|
| 160 |
+
padding: 0.5rem 0.75rem !important;
|
| 161 |
+
}
|
| 162 |
+
</style>
|
| 163 |
+
""",
|
| 164 |
+
unsafe_allow_html=True,
|
| 165 |
+
)
|
| 166 |
+
st.button(
|
| 167 |
+
"🎲",
|
| 168 |
+
key="ai_generate_btn",
|
| 169 |
+
help="Generate wordlist from topic",
|
| 170 |
+
on_click=_on_ai_generate,
|
| 171 |
+
use_container_width=True,
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
# Display wordlist info below topic input
|
| 175 |
+
info_text = get_wordlist_info(
|
| 176 |
+
use_ai=True,
|
| 177 |
+
ai_topic=st.session_state.ai_topic
|
| 178 |
+
)
|
| 179 |
+
if info_text:
|
| 180 |
+
st.caption(info_text)
|
| 181 |
+
else:
|
| 182 |
+
st.session_state.use_ai_wordlist = False
|
| 183 |
+
st.session_state.selected_wordlist = selected
|
| 184 |
+
|
| 185 |
+
# Display wordlist info below selection
|
| 186 |
+
info_text = get_wordlist_info(
|
| 187 |
+
use_ai=False,
|
| 188 |
+
selected_file=selected
|
| 189 |
+
)
|
| 190 |
+
if info_text:
|
| 191 |
+
st.caption(info_text)
|
| 192 |
+
|
| 193 |
+
# Only show Sort button for file-based wordlists
|
| 194 |
+
if not st.session_state.use_ai_wordlist:
|
| 195 |
+
if st.button("Sort Wordlist", width=125, key="sort_wordlist_btn"):
|
| 196 |
+
_sort_wordlist(st.session_state.selected_wordlist, new_game_callback)
|
| 197 |
+
if st.button("Filter Wordlist", width=125, key="filter_wordlist_btn"):
|
| 198 |
+
_filter_wordlist(st.session_state.selected_wordlist)
|
| 199 |
+
else:
|
| 200 |
+
st.info("No word lists found in words/ directory. Using AI or built-in fallback.")
|
| 201 |
+
# Force AI mode if no files available
|
| 202 |
+
st.session_state.use_ai_wordlist = True
|
| 203 |
+
|
| 204 |
+
# Topic input and Generate button in columns
|
| 205 |
+
topic_col, gen_col = st.columns([3, 1], gap="small")
|
| 206 |
+
|
| 207 |
+
with topic_col:
|
| 208 |
+
st.text_input(
|
| 209 |
+
"Topic",
|
| 210 |
+
value=st.session_state.ai_topic,
|
| 211 |
+
key="ai_topic",
|
| 212 |
+
placeholder="e.g., Ocean Life, Space, History",
|
| 213 |
+
help="Enter a topic for AI-generated words",
|
| 214 |
+
# Remove on_change to prevent automatic generation
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
with gen_col:
|
| 218 |
+
# Add custom CSS for the generate button
|
| 219 |
+
st.markdown(
|
| 220 |
+
"""
|
| 221 |
+
<style>
|
| 222 |
+
.st-key-ai_generate_btn {
|
| 223 |
+
margin-top: 1.85rem; /* Align with text input */
|
| 224 |
+
}
|
| 225 |
+
.st-key-ai_generate_btn button {
|
| 226 |
+
aspect-ratio: auto !important;
|
| 227 |
+
height: auto !important;
|
| 228 |
+
padding: 0.5rem 0.75rem !important;
|
| 229 |
+
}
|
| 230 |
+
</style>
|
| 231 |
+
""",
|
| 232 |
+
unsafe_allow_html=True,
|
| 233 |
+
)
|
| 234 |
+
st.button(
|
| 235 |
+
"🎲",
|
| 236 |
+
key="ai_generate_btn",
|
| 237 |
+
help="Generate wordlist from topic",
|
| 238 |
+
on_click=_on_ai_generate,
|
| 239 |
+
use_container_width=True,
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Display wordlist info below topic input
|
| 243 |
+
info_text = get_wordlist_info(
|
| 244 |
+
use_ai=True,
|
| 245 |
+
ai_topic=st.session_state.ai_topic
|
| 246 |
+
)
|
| 247 |
+
if info_text:
|
| 248 |
+
st.caption(info_text)
|
| 249 |
+
|
| 250 |
+
# Add Show Grid ticks option
|
| 251 |
+
if "show_grid_ticks" not in st.session_state:
|
| 252 |
+
st.session_state.show_grid_ticks = False
|
| 253 |
+
st.checkbox("Show Grid ticks", value=st.session_state.show_grid_ticks, key="show_grid_ticks")
|
| 254 |
+
|
| 255 |
+
# Add Spacer option
|
| 256 |
+
spacer_options = [0,1,2]
|
| 257 |
+
if "spacer" not in st.session_state:
|
| 258 |
+
st.session_state.spacer = 1
|
| 259 |
+
st.selectbox(
|
| 260 |
+
"Spacer (space between words)",
|
| 261 |
+
options=spacer_options,
|
| 262 |
+
index=spacer_options.index(st.session_state.spacer),
|
| 263 |
+
key="spacer",
|
| 264 |
+
on_change=new_game_callback, # add callback
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# Add Show Incorrect Guesses option - now enabled by default
|
| 268 |
+
if "show_incorrect_guesses" not in st.session_state:
|
| 269 |
+
st.session_state.show_incorrect_guesses = True
|
| 270 |
+
st.checkbox("Show incorrect guesses", value=st.session_state.show_incorrect_guesses, key="show_incorrect_guesses")
|
| 271 |
+
|
| 272 |
+
# NEW: Add Show Challenge Share Links option - default ON
|
| 273 |
+
if "show_challenge_share_links" not in st.session_state:
|
| 274 |
+
st.session_state.show_challenge_share_links = True
|
| 275 |
+
st.checkbox("Show Challenge Share Links", value=st.session_state.show_challenge_share_links, key="show_challenge_share_links")
|
| 276 |
+
|
| 277 |
+
# NEW: Initialize Enable Free Letters (default OFF)
|
| 278 |
+
if "enable_free_letters" not in st.session_state:
|
| 279 |
+
st.session_state.enable_free_letters = False
|
| 280 |
+
st.checkbox("Enable Free Letters (2 per game)", value=st.session_state.enable_free_letters, key="enable_free_letters")
|
| 281 |
+
|
| 282 |
+
# Audio settings
|
| 283 |
+
st.header("Audio")
|
| 284 |
+
tracks = get_audio_tracks()
|
| 285 |
+
st.caption(f"{len(tracks)} audio file{'s' if len(tracks) != 1 else ''} found in wrdler/assets/audio/music")
|
| 286 |
+
|
| 287 |
+
if "music_enabled" not in st.session_state:
|
| 288 |
+
st.session_state.music_enabled = False
|
| 289 |
+
if "music_volume" not in st.session_state:
|
| 290 |
+
st.session_state.music_volume = 15
|
| 291 |
+
# --- Add sound effects volume ---
|
| 292 |
+
if "effects_volume" not in st.session_state:
|
| 293 |
+
st.session_state.effects_volume = 25
|
| 294 |
+
# --- Add enable sound effects (default OFF) ---
|
| 295 |
+
if "enable_sound_effects" not in st.session_state:
|
| 296 |
+
st.session_state.enable_sound_effects = False
|
| 297 |
+
st.checkbox("Enable Sound Effects", value=st.session_state.enable_sound_effects, key="enable_sound_effects")
|
| 298 |
+
|
| 299 |
+
enabled = st.checkbox("Enable music", value=st.session_state.music_enabled, key="music_enabled")
|
| 300 |
+
|
| 301 |
+
st.slider(
|
| 302 |
+
"Volume",
|
| 303 |
+
0,
|
| 304 |
+
100,
|
| 305 |
+
value=int(st.session_state.music_volume),
|
| 306 |
+
step=1,
|
| 307 |
+
key="music_volume",
|
| 308 |
+
disabled=not (enabled and bool(tracks)),
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# --- Add sound effects volume slider ---
|
| 312 |
+
st.slider(
|
| 313 |
+
"Sound Effects Volume",
|
| 314 |
+
0,
|
| 315 |
+
100,
|
| 316 |
+
value=int(st.session_state.effects_volume),
|
| 317 |
+
step=1,
|
| 318 |
+
key="effects_volume",
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
selected_path = None
|
| 322 |
+
if tracks:
|
| 323 |
+
options = [p for _, p in tracks]
|
| 324 |
+
# Default to first track if none chosen yet
|
| 325 |
+
if "music_track_path" not in st.session_state or st.session_state.music_track_path not in options:
|
| 326 |
+
st.session_state.music_track_path = options[0]
|
| 327 |
+
|
| 328 |
+
def _fmt(p: str) -> str:
|
| 329 |
+
# Find friendly label for path
|
| 330 |
+
for name, path in tracks:
|
| 331 |
+
if path == p:
|
| 332 |
+
return name
|
| 333 |
+
return os.path.splitext(os.path.basename(p))[0]
|
| 334 |
+
|
| 335 |
+
selected_path = st.selectbox(
|
| 336 |
+
"Track",
|
| 337 |
+
options=options,
|
| 338 |
+
index=options.index(st.session_state.music_track_path),
|
| 339 |
+
format_func=_fmt,
|
| 340 |
+
key="music_track_path",
|
| 341 |
+
disabled=not enabled,
|
| 342 |
+
)
|
| 343 |
+
else:
|
| 344 |
+
st.caption("Place .mp3 files in wrdler/assets/audio/music to enable music.")
|
| 345 |
+
|
| 346 |
+
_inject_audio_control_sync()
|
| 347 |
+
st.markdown(versions_html(), unsafe_allow_html=True)
|
|
@@ -30,6 +30,7 @@ from .audio import (
|
|
| 30 |
from .game_storage import load_game_from_sid, save_game_to_hf, get_shareable_url, add_user_result_to_game
|
| 31 |
from .modules.constants import APP_SETTINGS
|
| 32 |
from .leaderboard import submit_score_to_all_leaderboards
|
|
|
|
| 33 |
|
| 34 |
st.set_page_config(initial_sidebar_state="collapsed")
|
| 35 |
|
|
@@ -614,12 +615,7 @@ border-radius: 50% !important;
|
|
| 614 |
|
| 615 |
/* Make the sidebar scrollable */
|
| 616 |
section[data-testid="stSidebar"] {
|
| 617 |
-
|
| 618 |
-
overflow-y: auto;
|
| 619 |
-
overflow-x: hidden;
|
| 620 |
-
scrollbar-width: thin;
|
| 621 |
-
scrollbar-color: transparent transparent;
|
| 622 |
-
opacity:0.75;
|
| 623 |
}
|
| 624 |
|
| 625 |
.st-emotion-cache-wp60of {
|
|
@@ -993,279 +989,30 @@ def _render_header():
|
|
| 993 |
|
| 994 |
inject_styles()
|
| 995 |
|
| 996 |
-
def
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
st.selectbox(
|
| 1007 |
-
"Select game mode",
|
| 1008 |
-
options=game_modes,
|
| 1009 |
-
index=game_modes.index(current_mode) if current_mode in game_modes else 0,
|
| 1010 |
-
key="game_mode",
|
| 1011 |
-
on_change=_on_game_option_change,
|
| 1012 |
-
)
|
| 1013 |
-
|
| 1014 |
-
st.header("Wordlist Controls")
|
| 1015 |
-
|
| 1016 |
-
# Initialize AI mode settings
|
| 1017 |
-
if "use_ai_wordlist" not in st.session_state:
|
| 1018 |
-
st.session_state.use_ai_wordlist = False
|
| 1019 |
-
if "ai_topic" not in st.session_state:
|
| 1020 |
-
st.session_state.ai_topic = "English"
|
| 1021 |
-
|
| 1022 |
-
wordlist_files = get_wordlist_files()
|
| 1023 |
-
|
| 1024 |
-
# Add AI Generated option to file list
|
| 1025 |
-
wordlist_options = ["AI Generated"] + wordlist_files if wordlist_files else ["AI Generated"]
|
| 1026 |
-
|
| 1027 |
-
if wordlist_files:
|
| 1028 |
-
# Determine current selection index
|
| 1029 |
-
if st.session_state.get("use_ai_wordlist", False):
|
| 1030 |
-
current_index = 0 # AI Generated
|
| 1031 |
-
elif st.session_state.get("selected_wordlist") in wordlist_files:
|
| 1032 |
-
current_index = wordlist_options.index(st.session_state.selected_wordlist)
|
| 1033 |
-
else:
|
| 1034 |
-
# Default to first file
|
| 1035 |
-
st.session_state.selected_wordlist = wordlist_files[0]
|
| 1036 |
-
st.session_state.use_ai_wordlist = False
|
| 1037 |
-
current_index = 1
|
| 1038 |
-
|
| 1039 |
-
# Wordlist selector
|
| 1040 |
-
selected = st.selectbox(
|
| 1041 |
-
"Select list",
|
| 1042 |
-
options=wordlist_options,
|
| 1043 |
-
index=current_index,
|
| 1044 |
-
format_func=lambda f: f if f == "AI Generated" else f.rsplit(".", 1)[0],
|
| 1045 |
-
key="wordlist_selector",
|
| 1046 |
-
on_change=_on_wordlist_change,
|
| 1047 |
-
)
|
| 1048 |
-
|
| 1049 |
-
# Show topic input if AI mode selected
|
| 1050 |
-
if selected == "AI Generated":
|
| 1051 |
-
st.session_state.use_ai_wordlist = True
|
| 1052 |
-
|
| 1053 |
-
# Topic input and Generate button in columns
|
| 1054 |
-
topic_col, gen_col = st.columns([3, 1], gap="small")
|
| 1055 |
-
|
| 1056 |
-
with topic_col:
|
| 1057 |
-
st.text_input(
|
| 1058 |
-
"Topic",
|
| 1059 |
-
value=st.session_state.ai_topic,
|
| 1060 |
-
key="ai_topic",
|
| 1061 |
-
placeholder="e.g., Ocean Life, Space, History",
|
| 1062 |
-
help="Enter a topic for AI-generated words",
|
| 1063 |
-
# Remove on_change to prevent automatic generation
|
| 1064 |
-
)
|
| 1065 |
-
|
| 1066 |
-
with gen_col:
|
| 1067 |
-
# Add custom CSS for the generate button
|
| 1068 |
-
st.markdown(
|
| 1069 |
-
"""
|
| 1070 |
-
<style>
|
| 1071 |
-
.st-key-ai_generate_btn {
|
| 1072 |
-
margin-top: 1.85rem; /* Align with text input */
|
| 1073 |
-
}
|
| 1074 |
-
.st-key-ai_generate_btn button {
|
| 1075 |
-
aspect-ratio: auto !important;
|
| 1076 |
-
height: auto !important;
|
| 1077 |
-
padding: 0.5rem 0.75rem !important;
|
| 1078 |
-
}
|
| 1079 |
-
</style>
|
| 1080 |
-
""",
|
| 1081 |
-
unsafe_allow_html=True,
|
| 1082 |
-
)
|
| 1083 |
-
st.button(
|
| 1084 |
-
"🎲",
|
| 1085 |
-
key="ai_generate_btn",
|
| 1086 |
-
help="Generate wordlist from topic",
|
| 1087 |
-
on_click=_on_ai_generate,
|
| 1088 |
-
use_container_width=True,
|
| 1089 |
-
)
|
| 1090 |
-
|
| 1091 |
-
# Display wordlist info below topic input
|
| 1092 |
-
info_text = get_wordlist_info(
|
| 1093 |
-
use_ai=True,
|
| 1094 |
-
ai_topic=st.session_state.ai_topic
|
| 1095 |
-
)
|
| 1096 |
-
if info_text:
|
| 1097 |
-
st.caption(info_text)
|
| 1098 |
-
else:
|
| 1099 |
-
st.session_state.use_ai_wordlist = False
|
| 1100 |
-
st.session_state.selected_wordlist = selected
|
| 1101 |
-
|
| 1102 |
-
# Display wordlist info below selection
|
| 1103 |
-
info_text = get_wordlist_info(
|
| 1104 |
-
use_ai=False,
|
| 1105 |
-
selected_file=selected
|
| 1106 |
-
)
|
| 1107 |
-
if info_text:
|
| 1108 |
-
st.caption(info_text)
|
| 1109 |
-
|
| 1110 |
-
# Only show Sort button for file-based wordlists
|
| 1111 |
-
if not st.session_state.use_ai_wordlist:
|
| 1112 |
-
if st.button("Sort Wordlist", width=125, key="sort_wordlist_btn"):
|
| 1113 |
-
_sort_wordlist(st.session_state.selected_wordlist)
|
| 1114 |
-
if st.button("Filter Wordlist", width=125, key="filter_wordlist_btn"):
|
| 1115 |
-
_filter_wordlist(st.session_state.selected_wordlist)
|
| 1116 |
-
else:
|
| 1117 |
-
st.info("No word lists found in words/ directory. Using AI or built-in fallback.")
|
| 1118 |
-
# Force AI mode if no files available
|
| 1119 |
-
st.session_state.use_ai_wordlist = True
|
| 1120 |
-
|
| 1121 |
-
# Topic input and Generate button in columns
|
| 1122 |
-
topic_col, gen_col = st.columns([3, 1], gap="small")
|
| 1123 |
-
|
| 1124 |
-
with topic_col:
|
| 1125 |
-
st.text_input(
|
| 1126 |
-
"Topic",
|
| 1127 |
-
value=st.session_state.ai_topic,
|
| 1128 |
-
key="ai_topic",
|
| 1129 |
-
placeholder="e.g., Ocean Life, Space, History",
|
| 1130 |
-
help="Enter a topic for AI-generated words",
|
| 1131 |
-
# Remove on_change to prevent automatic generation
|
| 1132 |
-
)
|
| 1133 |
-
|
| 1134 |
-
with gen_col:
|
| 1135 |
-
# Add custom CSS for the generate button
|
| 1136 |
-
st.markdown(
|
| 1137 |
-
"""
|
| 1138 |
-
<style>
|
| 1139 |
-
.st-key-ai_generate_btn {
|
| 1140 |
-
margin-top: 1.85rem; /* Align with text input */
|
| 1141 |
-
}
|
| 1142 |
-
.st-key-ai_generate_btn button {
|
| 1143 |
-
aspect-ratio: auto !important;
|
| 1144 |
-
height: auto !important;
|
| 1145 |
-
padding: 0.5rem 0.75rem !important;
|
| 1146 |
-
}
|
| 1147 |
-
</style>
|
| 1148 |
-
""",
|
| 1149 |
-
unsafe_allow_html=True,
|
| 1150 |
-
)
|
| 1151 |
-
st.button(
|
| 1152 |
-
"🎲",
|
| 1153 |
-
key="ai_generate_btn",
|
| 1154 |
-
help="Generate wordlist from topic",
|
| 1155 |
-
on_click=_on_ai_generate,
|
| 1156 |
-
use_container_width=True,
|
| 1157 |
-
)
|
| 1158 |
-
|
| 1159 |
-
# Display wordlist info below topic input
|
| 1160 |
-
info_text = get_wordlist_info(
|
| 1161 |
-
use_ai=True,
|
| 1162 |
-
ai_topic=st.session_state.ai_topic
|
| 1163 |
-
)
|
| 1164 |
-
if info_text:
|
| 1165 |
-
st.caption(info_text)
|
| 1166 |
-
|
| 1167 |
-
# Add Show Grid ticks option
|
| 1168 |
-
if "show_grid_ticks" not in st.session_state:
|
| 1169 |
-
st.session_state.show_grid_ticks = False
|
| 1170 |
-
st.checkbox("Show Grid ticks", value=st.session_state.show_grid_ticks, key="show_grid_ticks")
|
| 1171 |
-
|
| 1172 |
-
# Add Spacer option
|
| 1173 |
-
spacer_options = [0,1,2]
|
| 1174 |
-
if "spacer" not in st.session_state:
|
| 1175 |
-
st.session_state.spacer = 1
|
| 1176 |
-
st.selectbox(
|
| 1177 |
-
"Spacer (space between words)",
|
| 1178 |
-
options=spacer_options,
|
| 1179 |
-
index=spacer_options.index(st.session_state.spacer),
|
| 1180 |
-
key="spacer",
|
| 1181 |
-
on_change=_on_game_option_change, # add callback
|
| 1182 |
-
)
|
| 1183 |
-
|
| 1184 |
-
# Add Show Incorrect Guesses option - now enabled by default
|
| 1185 |
-
if "show_incorrect_guesses" not in st.session_state:
|
| 1186 |
-
st.session_state.show_incorrect_guesses = True
|
| 1187 |
-
st.checkbox("Show incorrect guesses", value=st.session_state.show_incorrect_guesses, key="show_incorrect_guesses")
|
| 1188 |
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
|
|
|
|
|
|
|
|
|
| 1193 |
|
| 1194 |
-
|
| 1195 |
-
if
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
# Audio settings
|
| 1200 |
-
st.header("Audio")
|
| 1201 |
-
tracks = get_audio_tracks()
|
| 1202 |
-
st.caption(f"{len(tracks)} audio file{'s' if len(tracks) != 1 else ''} found in wrdler/assets/audio/music")
|
| 1203 |
-
|
| 1204 |
-
if "music_enabled" not in st.session_state:
|
| 1205 |
-
st.session_state.music_enabled = False
|
| 1206 |
-
if "music_volume" not in st.session_state:
|
| 1207 |
-
st.session_state.music_volume = 15
|
| 1208 |
-
# --- Add sound effects volume ---
|
| 1209 |
-
if "effects_volume" not in st.session_state:
|
| 1210 |
-
st.session_state.effects_volume = 25
|
| 1211 |
-
# --- Add enable sound effects (default OFF) ---
|
| 1212 |
-
if "enable_sound_effects" not in st.session_state:
|
| 1213 |
-
st.session_state.enable_sound_effects = False
|
| 1214 |
-
st.checkbox("Enable Sound Effects", value=st.session_state.enable_sound_effects, key="enable_sound_effects")
|
| 1215 |
-
|
| 1216 |
-
enabled = st.checkbox("Enable music", value=st.session_state.music_enabled, key="music_enabled")
|
| 1217 |
-
|
| 1218 |
-
st.slider(
|
| 1219 |
-
"Volume",
|
| 1220 |
-
0,
|
| 1221 |
-
100,
|
| 1222 |
-
value=int(st.session_state.music_volume),
|
| 1223 |
-
step=1,
|
| 1224 |
-
key="music_volume",
|
| 1225 |
-
disabled=not (enabled and bool(tracks)),
|
| 1226 |
-
)
|
| 1227 |
-
|
| 1228 |
-
# --- Add sound effects volume slider ---
|
| 1229 |
-
st.slider(
|
| 1230 |
-
"Sound Effects Volume",
|
| 1231 |
-
0,
|
| 1232 |
-
100,
|
| 1233 |
-
value=int(st.session_state.effects_volume),
|
| 1234 |
-
step=1,
|
| 1235 |
-
key="effects_volume",
|
| 1236 |
-
)
|
| 1237 |
-
|
| 1238 |
-
selected_path = None
|
| 1239 |
-
if tracks:
|
| 1240 |
-
options = [p for _, p in tracks]
|
| 1241 |
-
# Default to first track if none chosen yet
|
| 1242 |
-
if "music_track_path" not in st.session_state or st.session_state.music_track_path not in options:
|
| 1243 |
-
st.session_state.music_track_path = options[0]
|
| 1244 |
-
|
| 1245 |
-
def _fmt(p: str) -> str:
|
| 1246 |
-
# Find friendly label for path
|
| 1247 |
-
for name, path in tracks:
|
| 1248 |
-
if path == p:
|
| 1249 |
-
return name
|
| 1250 |
-
return os.path.splitext(os.path.basename(p))[0]
|
| 1251 |
-
|
| 1252 |
-
selected_path = st.selectbox(
|
| 1253 |
-
"Track",
|
| 1254 |
-
options=options,
|
| 1255 |
-
index=options.index(st.session_state.music_track_path),
|
| 1256 |
-
format_func=_fmt,
|
| 1257 |
-
key="music_track_path",
|
| 1258 |
-
disabled=not enabled,
|
| 1259 |
-
)
|
| 1260 |
-
src_url = _load_audio_data_url(selected_path) if enabled else None
|
| 1261 |
-
_mount_background_audio(enabled, src_url, (st.session_state.music_volume or 0) / 100)
|
| 1262 |
-
else:
|
| 1263 |
-
st.caption("Place .mp3 files in wrdler/assets/audio/music to enable music.")
|
| 1264 |
-
_mount_background_audio(False, None, 0.0)
|
| 1265 |
-
|
| 1266 |
-
_inject_audio_control_sync()
|
| 1267 |
-
st.markdown(versions_html(), unsafe_allow_html=True)
|
| 1268 |
-
|
| 1269 |
|
| 1270 |
# NOTE: Radar/scope visualization functions removed for Wrdler (Sprint 3)
|
| 1271 |
# - get_scope_image() removed
|
|
@@ -1658,6 +1405,16 @@ def _render_guess_form(state: GameState):
|
|
| 1658 |
.st-emotion-cache-1xwdq91, .st-emotion-cache-1r70o5 {
|
| 1659 |
max-width: max-content; min-width:33%;
|
| 1660 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1661 |
}
|
| 1662 |
</style>
|
| 1663 |
""",
|
|
@@ -2319,10 +2076,6 @@ if _Dialog:
|
|
| 2319 |
@_Dialog("Game Over")
|
| 2320 |
def _game_over_dialog(state: GameState):
|
| 2321 |
_game_over_content(state)
|
| 2322 |
-
|
| 2323 |
-
@_Dialog("Filter Results")
|
| 2324 |
-
def _filter_results_dialog(count, words, filename):
|
| 2325 |
-
_filter_results_content(count, words, filename)
|
| 2326 |
else:
|
| 2327 |
def _game_over_dialog(state: GameState):
|
| 2328 |
modal_ctx = getattr(st, "modal", None)
|
|
@@ -2334,15 +2087,6 @@ else:
|
|
| 2334 |
st.subheader("Game Over")
|
| 2335 |
_game_over_content(state)
|
| 2336 |
|
| 2337 |
-
def _filter_results_dialog(count, words, filename):
|
| 2338 |
-
modal_ctx = getattr(st, "modal", None)
|
| 2339 |
-
if callable(modal_ctx):
|
| 2340 |
-
with modal_ctx("Filter Results"):
|
| 2341 |
-
_filter_results_content(count, words, filename)
|
| 2342 |
-
else:
|
| 2343 |
-
st.info(f"Removed {count} words from {filename}")
|
| 2344 |
-
st.text_area("Removed Words", value="\n".join(words), height=200)
|
| 2345 |
-
|
| 2346 |
def _render_game_over(state: GameState):
|
| 2347 |
# Use hide_gameover_overlay (same key as run_app) with inverted logic
|
| 2348 |
visible = not st.session_state.get("hide_gameover_overlay", False) and is_game_over(state)
|
|
@@ -2412,44 +2156,16 @@ def _on_game_option_change() -> None:
|
|
| 2412 |
# Start a fresh game with updated options
|
| 2413 |
_new_game()
|
| 2414 |
|
| 2415 |
-
def _on_wordlist_change() -> None:
|
| 2416 |
-
"""
|
| 2417 |
-
Callback when wordlist selection changes.
|
| 2418 |
-
Updates session state flags for AI vs file mode.
|
| 2419 |
-
"""
|
| 2420 |
-
selected = st.session_state.get("wordlist_selector")
|
| 2421 |
-
|
| 2422 |
-
if selected == "AI Generated":
|
| 2423 |
-
st.session_state.use_ai_wordlist = True
|
| 2424 |
-
# Preserve current AI topic or use default
|
| 2425 |
-
if "ai_topic" not in st.session_state:
|
| 2426 |
-
st.session_state.ai_topic = "English"
|
| 2427 |
-
# Don't trigger new game automatically for AI mode
|
| 2428 |
-
# User must click Generate button explicitly
|
| 2429 |
-
else:
|
| 2430 |
-
st.session_state.use_ai_wordlist = False
|
| 2431 |
-
st.session_state.selected_wordlist = selected
|
| 2432 |
-
# Trigger new game immediately for file-based wordlists
|
| 2433 |
-
_on_game_option_change()
|
| 2434 |
-
|
| 2435 |
-
|
| 2436 |
-
def _on_ai_generate() -> None:
|
| 2437 |
-
"""
|
| 2438 |
-
Callback when Generate button is clicked for AI wordlist.
|
| 2439 |
-
Triggers new game generation with AI words.
|
| 2440 |
-
"""
|
| 2441 |
-
# Start a fresh game with AI-generated words
|
| 2442 |
-
_on_game_option_change()
|
| 2443 |
-
|
| 2444 |
def _render_footer(current_page: str = "play"):
|
| 2445 |
"""Render footer with navigation links to leaderboards and main game.
|
| 2446 |
|
| 2447 |
Args:
|
| 2448 |
-
current_page: Which page is currently active ("play", "today", "daily", "weekly", "history")
|
| 2449 |
"""
|
| 2450 |
# Determine which link should be highlighted as active
|
| 2451 |
play_active = "active" if current_page == "play" else ""
|
| 2452 |
leaderboard_active = "active" if current_page in {"today", "daily", "weekly", "history"} else ""
|
|
|
|
| 2453 |
|
| 2454 |
# Check if we're in challenge mode and need to preserve game_id
|
| 2455 |
game_id = None
|
|
@@ -2467,16 +2183,14 @@ def _render_footer(current_page: str = "play"):
|
|
| 2467 |
# Build URLs with game_id if in challenge mode
|
| 2468 |
if game_id:
|
| 2469 |
today_url = f"?page=today&game_id={game_id}"
|
| 2470 |
-
daily_url = f"?page=daily&game_id={game_id}"
|
| 2471 |
-
weekly_url = f"?page=weekly&game_id={game_id}"
|
| 2472 |
leaderboard_url = f"?page=today&game_id={game_id}"
|
| 2473 |
play_url = f"?game_id={game_id}"
|
|
|
|
| 2474 |
else:
|
| 2475 |
today_url = "?page=today"
|
| 2476 |
-
daily_url = "?page=daily"
|
| 2477 |
-
weekly_url = "?page=weekly"
|
| 2478 |
leaderboard_url = "?page=today"
|
| 2479 |
play_url = "/"
|
|
|
|
| 2480 |
|
| 2481 |
st.markdown(
|
| 2482 |
f"""
|
|
@@ -2537,6 +2251,7 @@ def _render_footer(current_page: str = "play"):
|
|
| 2537 |
<nav class="bw-footer-nav">
|
| 2538 |
<a href="{leaderboard_url}" title="View Leaderboards" target="_self" class="{leaderboard_active}">🏆 Leaderboard</a>
|
| 2539 |
<a href="{play_url}" title="Play Wrdler" target="_self" class="{play_active}">🎮 Play</a>
|
|
|
|
| 2540 |
</nav>
|
| 2541 |
</div>
|
| 2542 |
""",
|
|
@@ -2562,8 +2277,19 @@ def run_app():
|
|
| 2562 |
pass
|
| 2563 |
st.session_state["hide_gameover_overlay"] = True
|
| 2564 |
|
|
|
|
|
|
|
|
|
|
| 2565 |
# Handle page navigation via query params
|
| 2566 |
page = params.get("page", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2567 |
if page in {"today", "daily", "weekly"}:
|
| 2568 |
from .leaderboard_page import render_leaderboard_page
|
| 2569 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
|
@@ -2609,7 +2335,6 @@ def run_app():
|
|
| 2609 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 2610 |
inject_ocean_layers() # <-- add the animated layers
|
| 2611 |
_render_header()
|
| 2612 |
-
_render_sidebar()
|
| 2613 |
|
| 2614 |
state = _to_state()
|
| 2615 |
|
|
|
|
| 30 |
from .game_storage import load_game_from_sid, save_game_to_hf, get_shareable_url, add_user_result_to_game
|
| 31 |
from .modules.constants import APP_SETTINGS
|
| 32 |
from .leaderboard import submit_score_to_all_leaderboards
|
| 33 |
+
from .settings_page import render_settings_page
|
| 34 |
|
| 35 |
st.set_page_config(initial_sidebar_state="collapsed")
|
| 36 |
|
|
|
|
| 615 |
|
| 616 |
/* Make the sidebar scrollable */
|
| 617 |
section[data-testid="stSidebar"] {
|
| 618 |
+
display: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
}
|
| 620 |
|
| 621 |
.st-emotion-cache-wp60of {
|
|
|
|
| 989 |
|
| 990 |
inject_styles()
|
| 991 |
|
| 992 |
+
def _handle_audio():
|
| 993 |
+
# Initialize state if needed
|
| 994 |
+
if "music_enabled" not in st.session_state:
|
| 995 |
+
st.session_state.music_enabled = False
|
| 996 |
+
if "music_volume" not in st.session_state:
|
| 997 |
+
st.session_state.music_volume = 15
|
| 998 |
+
if "effects_volume" not in st.session_state:
|
| 999 |
+
st.session_state.effects_volume = 25
|
| 1000 |
+
if "enable_sound_effects" not in st.session_state:
|
| 1001 |
+
st.session_state.enable_sound_effects = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1002 |
|
| 1003 |
+
tracks = get_audio_tracks()
|
| 1004 |
+
enabled = st.session_state.music_enabled
|
| 1005 |
+
|
| 1006 |
+
if tracks:
|
| 1007 |
+
options = [p for _, p in tracks]
|
| 1008 |
+
if "music_track_path" not in st.session_state or st.session_state.music_track_path not in options:
|
| 1009 |
+
st.session_state.music_track_path = options[0]
|
| 1010 |
|
| 1011 |
+
selected_path = st.session_state.music_track_path
|
| 1012 |
+
src_url = _load_audio_data_url(selected_path) if enabled else None
|
| 1013 |
+
_mount_background_audio(enabled, src_url, (st.session_state.music_volume or 0) / 100)
|
| 1014 |
+
else:
|
| 1015 |
+
_mount_background_audio(False, None, 0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
|
| 1017 |
# NOTE: Radar/scope visualization functions removed for Wrdler (Sprint 3)
|
| 1018 |
# - get_scope_image() removed
|
|
|
|
| 1405 |
.st-emotion-cache-1xwdq91, .st-emotion-cache-1r70o5 {
|
| 1406 |
max-width: max-content; min-width:33%;
|
| 1407 |
}
|
| 1408 |
+
.st-emotion-cache-emqb34 {
|
| 1409 |
+
min-width: calc(50% - 1.5rem);
|
| 1410 |
+
width: calc(50% - 1.5rem);
|
| 1411 |
+
flex:unset;
|
| 1412 |
+
}
|
| 1413 |
+
.st-emotion-cache-1c94k11 {
|
| 1414 |
+
min-width: calc(50% - 1.5rem);
|
| 1415 |
+
width: calc(50% - 1.5rem);
|
| 1416 |
+
flex:unset;
|
| 1417 |
+
}
|
| 1418 |
}
|
| 1419 |
</style>
|
| 1420 |
""",
|
|
|
|
| 2076 |
@_Dialog("Game Over")
|
| 2077 |
def _game_over_dialog(state: GameState):
|
| 2078 |
_game_over_content(state)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2079 |
else:
|
| 2080 |
def _game_over_dialog(state: GameState):
|
| 2081 |
modal_ctx = getattr(st, "modal", None)
|
|
|
|
| 2087 |
st.subheader("Game Over")
|
| 2088 |
_game_over_content(state)
|
| 2089 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2090 |
def _render_game_over(state: GameState):
|
| 2091 |
# Use hide_gameover_overlay (same key as run_app) with inverted logic
|
| 2092 |
visible = not st.session_state.get("hide_gameover_overlay", False) and is_game_over(state)
|
|
|
|
| 2156 |
# Start a fresh game with updated options
|
| 2157 |
_new_game()
|
| 2158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2159 |
def _render_footer(current_page: str = "play"):
|
| 2160 |
"""Render footer with navigation links to leaderboards and main game.
|
| 2161 |
|
| 2162 |
Args:
|
| 2163 |
+
current_page: Which page is currently active ("play", "today", "daily", "weekly", "history", "settings")
|
| 2164 |
"""
|
| 2165 |
# Determine which link should be highlighted as active
|
| 2166 |
play_active = "active" if current_page == "play" else ""
|
| 2167 |
leaderboard_active = "active" if current_page in {"today", "daily", "weekly", "history"} else ""
|
| 2168 |
+
settings_active = "active" if current_page == "settings" else ""
|
| 2169 |
|
| 2170 |
# Check if we're in challenge mode and need to preserve game_id
|
| 2171 |
game_id = None
|
|
|
|
| 2183 |
# Build URLs with game_id if in challenge mode
|
| 2184 |
if game_id:
|
| 2185 |
today_url = f"?page=today&game_id={game_id}"
|
|
|
|
|
|
|
| 2186 |
leaderboard_url = f"?page=today&game_id={game_id}"
|
| 2187 |
play_url = f"?game_id={game_id}"
|
| 2188 |
+
settings_url = f"?page=settings&game_id={game_id}"
|
| 2189 |
else:
|
| 2190 |
today_url = "?page=today"
|
|
|
|
|
|
|
| 2191 |
leaderboard_url = "?page=today"
|
| 2192 |
play_url = "/"
|
| 2193 |
+
settings_url = "?page=settings"
|
| 2194 |
|
| 2195 |
st.markdown(
|
| 2196 |
f"""
|
|
|
|
| 2251 |
<nav class="bw-footer-nav">
|
| 2252 |
<a href="{leaderboard_url}" title="View Leaderboards" target="_self" class="{leaderboard_active}">🏆 Leaderboard</a>
|
| 2253 |
<a href="{play_url}" title="Play Wrdler" target="_self" class="{play_active}">🎮 Play</a>
|
| 2254 |
+
<a href="{settings_url}" title="Settings" target="_self" class="{settings_active}">⚙️ Settings</a>
|
| 2255 |
</nav>
|
| 2256 |
</div>
|
| 2257 |
""",
|
|
|
|
| 2277 |
pass
|
| 2278 |
st.session_state["hide_gameover_overlay"] = True
|
| 2279 |
|
| 2280 |
+
# Handle audio globally
|
| 2281 |
+
_handle_audio()
|
| 2282 |
+
|
| 2283 |
# Handle page navigation via query params
|
| 2284 |
page = params.get("page", "")
|
| 2285 |
+
|
| 2286 |
+
if page == "settings":
|
| 2287 |
+
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 2288 |
+
inject_ocean_layers()
|
| 2289 |
+
render_settings_page(_on_game_option_change)
|
| 2290 |
+
_render_footer(current_page="settings")
|
| 2291 |
+
return
|
| 2292 |
+
|
| 2293 |
if page in {"today", "daily", "weekly"}:
|
| 2294 |
from .leaderboard_page import render_leaderboard_page
|
| 2295 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
|
|
|
| 2335 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 2336 |
inject_ocean_layers() # <-- add the animated layers
|
| 2337 |
_render_header()
|
|
|
|
| 2338 |
|
| 2339 |
state = _to_state()
|
| 2340 |
|