Spaces:
Running
Running
Enhance game UI and settings management
Browse filesUpdated README.md with new SDK versions and Python 3.12.8. Enhanced CSS for grid cells and incorrect guesses display. Added "Game Over" modal styling. Incremented version in __init__.py to 0.1.3. Introduced new parameters in game_storage.py for game settings serialization. Implemented settings loading from settings.json in gradio_ui.py, and updated UI logic to use a modal for game over and sharing. Corrected word count in cooking.txt and removed "BEVAP".
- README.md +2 -2
- style_wrdler.css +178 -2
- wrdler/__init__.py +1 -1
- wrdler/game_storage.py +24 -4
- wrdler/gradio_ui.py +219 -103
- wrdler/words/cooking.txt +1 -2
README.md
CHANGED
|
@@ -3,8 +3,8 @@ title: Wrdler
|
|
| 3 |
emoji: 🎲
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version:
|
| 8 |
python_version: 3.12.8
|
| 9 |
app_port: 8501
|
| 10 |
app_file: app.py
|
|
|
|
| 3 |
emoji: 🎲
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.50.0
|
| 8 |
python_version: 3.12.8
|
| 9 |
app_port: 8501
|
| 10 |
app_file: app.py
|
style_wrdler.css
CHANGED
|
@@ -159,8 +159,10 @@ button[aria-disabled="true"] {
|
|
| 159 |
}
|
| 160 |
|
| 161 |
/* Empty revealed cells - solid dark grey fill */
|
|
|
|
| 162 |
.grid-cell-revealed button:has(span:empty),
|
| 163 |
-
.grid-cell-revealed button[value="
|
|
|
|
| 164 |
.grid-cell-revealed button[value=""] {
|
| 165 |
background: #2d2d44 !important;
|
| 166 |
border: 2px solid #3d3d5c !important;
|
|
@@ -173,7 +175,8 @@ button[aria-disabled="true"] {
|
|
| 173 |
|
| 174 |
/* Ensure empty cells are completely filled with dark grey */
|
| 175 |
.grid-cell-revealed button:has(span:empty)::before,
|
| 176 |
-
.grid-cell-revealed button[value="
|
|
|
|
| 177 |
.grid-cell-revealed button[value=""]::before {
|
| 178 |
content: '' !important;
|
| 179 |
position: absolute !important;
|
|
@@ -544,11 +547,88 @@ button[aria-disabled="true"] {
|
|
| 544 |
background-color: rgba(0, 191, 165, 0.75);
|
| 545 |
}
|
| 546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
/* Accordion styling */
|
| 548 |
.accordion {
|
| 549 |
border-radius: 8px;
|
| 550 |
}
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
/* Hide bridge elements - keep functional but visually hidden */
|
| 553 |
/* Using clip-rect to hide visually while maintaining DOM accessibility */
|
| 554 |
.hidden-bridge {
|
|
@@ -980,3 +1060,99 @@ div#topic-display input::placeholder,
|
|
| 980 |
letter-spacing: 0.5px !important;
|
| 981 |
}
|
| 982 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
}
|
| 160 |
|
| 161 |
/* Empty revealed cells - solid dark grey fill */
|
| 162 |
+
/* Target cells with non-breaking space (\u00A0) or empty content */
|
| 163 |
.grid-cell-revealed button:has(span:empty),
|
| 164 |
+
.grid-cell-revealed button[value=" "],
|
| 165 |
+
.grid-cell-revealed button[value="\00A0"],
|
| 166 |
.grid-cell-revealed button[value=""] {
|
| 167 |
background: #2d2d44 !important;
|
| 168 |
border: 2px solid #3d3d5c !important;
|
|
|
|
| 175 |
|
| 176 |
/* Ensure empty cells are completely filled with dark grey */
|
| 177 |
.grid-cell-revealed button:has(span:empty)::before,
|
| 178 |
+
.grid-cell-revealed button[value=" "]::before,
|
| 179 |
+
.grid-cell-revealed button[value="\00A0"]::before,
|
| 180 |
.grid-cell-revealed button[value=""]::before {
|
| 181 |
content: '' !important;
|
| 182 |
position: absolute !important;
|
|
|
|
| 547 |
background-color: rgba(0, 191, 165, 0.75);
|
| 548 |
}
|
| 549 |
|
| 550 |
+
/* Incorrect Guesses - Show on Hover or Click (dropdown) */
|
| 551 |
+
.wrdler-incorrect-guesses {
|
| 552 |
+
position: relative;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.wrdler-incorrect-guesses details {
|
| 556 |
+
position: relative;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.wrdler-incorrect-guesses summary {
|
| 560 |
+
cursor: pointer;
|
| 561 |
+
list-style: none; /* Remove default arrow */
|
| 562 |
+
user-select: none;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.wrdler-incorrect-guesses summary::-webkit-details-marker {
|
| 566 |
+
display: none; /* Remove arrow in Chrome/Safari */
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.wrdler-incorrect-guesses summary::marker {
|
| 570 |
+
display: none; /* Remove arrow in Firefox */
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
/* Add dropdown indicator */
|
| 574 |
+
.wrdler-incorrect-guesses summary::after {
|
| 575 |
+
content: ' ▼';
|
| 576 |
+
font-size: 0.7em;
|
| 577 |
+
opacity: 0.7;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
/* Show guess list on hover or click - drops DOWN */
|
| 581 |
+
.wrdler-incorrect-guesses details:hover .guess-list,
|
| 582 |
+
.wrdler-incorrect-guesses details[open] .guess-list {
|
| 583 |
+
display: block !important;
|
| 584 |
+
position: absolute;
|
| 585 |
+
top: 100%;
|
| 586 |
+
left: 50%;
|
| 587 |
+
transform: translateX(-50%);
|
| 588 |
+
background: linear-gradient(145deg, #1a1a2e, #16213e);
|
| 589 |
+
border: 1px solid rgba(255, 100, 100, 0.4);
|
| 590 |
+
border-radius: 8px;
|
| 591 |
+
padding: 12px 16px;
|
| 592 |
+
min-width: 150px;
|
| 593 |
+
max-width: 250px;
|
| 594 |
+
box-shadow: 0 4px 20px rgba(255, 100, 100, 0.2);
|
| 595 |
+
z-index: 100;
|
| 596 |
+
margin-top: 8px;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
/* Hide by default (override inline display) */
|
| 600 |
+
.wrdler-incorrect-guesses details:not(:hover):not([open]) .guess-list {
|
| 601 |
+
display: none !important;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
/* Arrow pointing UP to summary */
|
| 605 |
+
.wrdler-incorrect-guesses details:hover .guess-list::before,
|
| 606 |
+
.wrdler-incorrect-guesses details[open] .guess-list::before {
|
| 607 |
+
content: '';
|
| 608 |
+
position: absolute;
|
| 609 |
+
top: -8px;
|
| 610 |
+
left: 50%;
|
| 611 |
+
transform: translateX(-50%);
|
| 612 |
+
border-left: 8px solid transparent;
|
| 613 |
+
border-right: 8px solid transparent;
|
| 614 |
+
border-bottom: 8px solid rgba(255, 100, 100, 0.4);
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
/* Accordion styling */
|
| 618 |
.accordion {
|
| 619 |
border-radius: 8px;
|
| 620 |
}
|
| 621 |
|
| 622 |
+
/* Hidden audio player - scripts still execute */
|
| 623 |
+
.hidden-audio {
|
| 624 |
+
position: absolute !important;
|
| 625 |
+
width: 1px !important;
|
| 626 |
+
height: 1px !important;
|
| 627 |
+
overflow: hidden !important;
|
| 628 |
+
opacity: 0 !important;
|
| 629 |
+
pointer-events: none !important;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
/* Hide bridge elements - keep functional but visually hidden */
|
| 633 |
/* Using clip-rect to hide visually while maintaining DOM accessibility */
|
| 634 |
.hidden-bridge {
|
|
|
|
| 1060 |
letter-spacing: 0.5px !important;
|
| 1061 |
}
|
| 1062 |
}
|
| 1063 |
+
|
| 1064 |
+
/* ============================================
|
| 1065 |
+
Game Over Modal Styling
|
| 1066 |
+
============================================ */
|
| 1067 |
+
|
| 1068 |
+
/* Game Over Modal Container */
|
| 1069 |
+
.modal-container {
|
| 1070 |
+
background: linear-gradient(145deg, #1a1a2e, #16213e) !important;
|
| 1071 |
+
border: 2px solid #00d2ff !important;
|
| 1072 |
+
border-radius: 16px !important;
|
| 1073 |
+
box-shadow:
|
| 1074 |
+
0 0 30px rgba(0, 210, 255, 0.3),
|
| 1075 |
+
0 0 60px rgba(0, 191, 165, 0.2),
|
| 1076 |
+
inset 0 0 30px rgba(0, 0, 0, 0.3) !important;
|
| 1077 |
+
max-width: 500px !important;
|
| 1078 |
+
padding: 24px !important;
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
/* Game Over Container inside modal */
|
| 1082 |
+
#game-over-container {
|
| 1083 |
+
text-align: center;
|
| 1084 |
+
padding: 16px;
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
#game-over-container .wrdler-game-over {
|
| 1088 |
+
background: transparent !important;
|
| 1089 |
+
border: none !important;
|
| 1090 |
+
padding: 0 !important;
|
| 1091 |
+
margin: 0 !important;
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
#game-over-container h2 {
|
| 1095 |
+
color: #00d2ff !important;
|
| 1096 |
+
font-size: 2rem !important;
|
| 1097 |
+
margin-bottom: 16px !important;
|
| 1098 |
+
text-shadow: 0 0 20px rgba(0, 210, 255, 0.5);
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
#game-over-container .final-score {
|
| 1102 |
+
font-size: 2.5rem !important;
|
| 1103 |
+
color: #00bfa5 !important;
|
| 1104 |
+
text-shadow: 0 0 15px rgba(0, 191, 165, 0.5);
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
#game-over-container .tier {
|
| 1108 |
+
font-size: 1.5rem !important;
|
| 1109 |
+
color: #ffd700 !important;
|
| 1110 |
+
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
|
| 1111 |
+
animation: tier-glow 2s ease-in-out infinite;
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
@keyframes tier-glow {
|
| 1115 |
+
0%, 100% {
|
| 1116 |
+
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
|
| 1117 |
+
}
|
| 1118 |
+
50% {
|
| 1119 |
+
text-shadow: 0 0 20px rgba(255, 215, 0, 0.8), 0 0 30px rgba(255, 215, 0, 0.4);
|
| 1120 |
+
}
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
#game-over-container .breakdown table {
|
| 1124 |
+
background: rgba(0, 0, 0, 0.2) !important;
|
| 1125 |
+
border-radius: 8px;
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
/* Share Challenge Section in Modal */
|
| 1129 |
+
.share-challenge-section {
|
| 1130 |
+
margin-top: 20px !important;
|
| 1131 |
+
padding-top: 20px !important;
|
| 1132 |
+
border-top: 1px solid rgba(0, 210, 255, 0.3) !important;
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
.share-challenge-section h2 {
|
| 1136 |
+
color: #00d2ff !important;
|
| 1137 |
+
font-size: 1.3rem !important;
|
| 1138 |
+
margin-bottom: 8px !important;
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
+
.share-challenge-section p {
|
| 1142 |
+
color: #a8e6cf !important;
|
| 1143 |
+
font-size: 0.9rem !important;
|
| 1144 |
+
margin-bottom: 16px !important;
|
| 1145 |
+
}
|
| 1146 |
+
|
| 1147 |
+
/* Modal close button */
|
| 1148 |
+
.modal-container button[variant="secondary"] {
|
| 1149 |
+
margin-top: 16px !important;
|
| 1150 |
+
background: rgba(0, 0, 0, 0.3) !important;
|
| 1151 |
+
border: 1px solid rgba(0, 210, 255, 0.3) !important;
|
| 1152 |
+
color: #a8e6cf !important;
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
.modal-container button[variant="secondary"]:hover {
|
| 1156 |
+
background: rgba(0, 210, 255, 0.1) !important;
|
| 1157 |
+
border-color: #00d2ff !important;
|
| 1158 |
+
}
|
wrdler/__init__.py
CHANGED
|
@@ -8,5 +8,5 @@ Key differences from BattleWords:
|
|
| 8 |
- 2 free letter guesses at game start
|
| 9 |
"""
|
| 10 |
|
| 11 |
-
__version__ = "0.1.
|
| 12 |
__all__ = ["models", "generator", "logic", "ui", "word_loader"]
|
|
|
|
| 8 |
- 2 free letter guesses at game start
|
| 9 |
"""
|
| 10 |
|
| 11 |
+
__version__ = "0.1.3"
|
| 12 |
__all__ = ["models", "generator", "logic", "ui", "word_loader"]
|
wrdler/game_storage.py
CHANGED
|
@@ -66,7 +66,10 @@ def serialize_game_settings(
|
|
| 66 |
spacer: int = 0,
|
| 67 |
may_overlap: bool = False,
|
| 68 |
wordlist_source: Optional[str] = None,
|
| 69 |
-
challenge_id: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
| 70 |
) -> Dict[str, Any]:
|
| 71 |
"""
|
| 72 |
Serialize game settings into a JSON-compatible dictionary.
|
|
@@ -90,6 +93,9 @@ def serialize_game_settings(
|
|
| 90 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
| 91 |
wordlist_source: Source file name (e.g., "classic.txt")
|
| 92 |
challenge_id: Optional challenge ID (generated if not provided)
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
Returns:
|
| 95 |
dict: Serialized game settings with users array
|
|
@@ -132,12 +138,17 @@ def serialize_game_settings(
|
|
| 132 |
},
|
| 133 |
"users": [user_result],
|
| 134 |
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 135 |
-
"version": __version__
|
|
|
|
|
|
|
| 136 |
}
|
| 137 |
|
| 138 |
if wordlist_source:
|
| 139 |
settings["wordlist_source"] = wordlist_source
|
| 140 |
|
|
|
|
|
|
|
|
|
|
| 141 |
return settings
|
| 142 |
|
| 143 |
|
|
@@ -267,7 +278,10 @@ def save_game_to_hf(
|
|
| 267 |
spacer: int = 0,
|
| 268 |
may_overlap: bool = False,
|
| 269 |
repo_id: Optional[str] = None,
|
| 270 |
-
wordlist_source: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
| 271 |
) -> Tuple[str, Optional[str], Optional[str]]:
|
| 272 |
"""
|
| 273 |
Save game settings to HuggingFace repository and generate shareable URL.
|
|
@@ -297,6 +311,9 @@ def save_game_to_hf(
|
|
| 297 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
| 298 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
| 299 |
wordlist_source: Source wordlist file name (e.g., "classic.txt")
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
Returns:
|
| 302 |
tuple: (challenge_id, full_url, sid) where:
|
|
@@ -335,7 +352,10 @@ def save_game_to_hf(
|
|
| 335 |
spacer=spacer,
|
| 336 |
may_overlap=may_overlap,
|
| 337 |
challenge_id=challenge_id,
|
| 338 |
-
wordlist_source=wordlist_source
|
|
|
|
|
|
|
|
|
|
| 339 |
)
|
| 340 |
|
| 341 |
logger.debug(f"🆔 Generated Challenge ID: {challenge_id}")
|
|
|
|
| 66 |
spacer: int = 0,
|
| 67 |
may_overlap: bool = False,
|
| 68 |
wordlist_source: Optional[str] = None,
|
| 69 |
+
challenge_id: Optional[str] = None,
|
| 70 |
+
game_title: Optional[str] = None,
|
| 71 |
+
show_incorrect_guesses: bool = True,
|
| 72 |
+
enable_free_letters: bool = True
|
| 73 |
) -> Dict[str, Any]:
|
| 74 |
"""
|
| 75 |
Serialize game settings into a JSON-compatible dictionary.
|
|
|
|
| 93 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
| 94 |
wordlist_source: Source file name (e.g., "classic.txt")
|
| 95 |
challenge_id: Optional challenge ID (generated if not provided)
|
| 96 |
+
game_title: Game title (e.g., "Wrdler Gradio AI")
|
| 97 |
+
show_incorrect_guesses: Whether to show incorrect guesses
|
| 98 |
+
enable_free_letters: Whether free letters feature is enabled
|
| 99 |
|
| 100 |
Returns:
|
| 101 |
dict: Serialized game settings with users array
|
|
|
|
| 138 |
},
|
| 139 |
"users": [user_result],
|
| 140 |
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 141 |
+
"version": __version__,
|
| 142 |
+
"show_incorrect_guesses": show_incorrect_guesses,
|
| 143 |
+
"enable_free_letters": enable_free_letters
|
| 144 |
}
|
| 145 |
|
| 146 |
if wordlist_source:
|
| 147 |
settings["wordlist_source"] = wordlist_source
|
| 148 |
|
| 149 |
+
if game_title:
|
| 150 |
+
settings["game_title"] = game_title
|
| 151 |
+
|
| 152 |
return settings
|
| 153 |
|
| 154 |
|
|
|
|
| 278 |
spacer: int = 0,
|
| 279 |
may_overlap: bool = False,
|
| 280 |
repo_id: Optional[str] = None,
|
| 281 |
+
wordlist_source: Optional[str] = None,
|
| 282 |
+
game_title: Optional[str] = None,
|
| 283 |
+
show_incorrect_guesses: bool = True,
|
| 284 |
+
enable_free_letters: bool = True
|
| 285 |
) -> Tuple[str, Optional[str], Optional[str]]:
|
| 286 |
"""
|
| 287 |
Save game settings to HuggingFace repository and generate shareable URL.
|
|
|
|
| 311 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
| 312 |
repo_id: HF repository ID (uses HF_REPO_ID from env if None)
|
| 313 |
wordlist_source: Source wordlist file name (e.g., "classic.txt")
|
| 314 |
+
game_title: Game title (e.g., "Wrdler Gradio AI")
|
| 315 |
+
show_incorrect_guesses: Whether to show incorrect guesses
|
| 316 |
+
enable_free_letters: Whether free letters feature is enabled
|
| 317 |
|
| 318 |
Returns:
|
| 319 |
tuple: (challenge_id, full_url, sid) where:
|
|
|
|
| 352 |
spacer=spacer,
|
| 353 |
may_overlap=may_overlap,
|
| 354 |
challenge_id=challenge_id,
|
| 355 |
+
wordlist_source=wordlist_source,
|
| 356 |
+
game_title=game_title,
|
| 357 |
+
show_incorrect_guesses=show_incorrect_guesses,
|
| 358 |
+
enable_free_letters=enable_free_letters
|
| 359 |
)
|
| 360 |
|
| 361 |
logger.debug(f"🆔 Generated Challenge ID: {challenge_id}")
|
wrdler/gradio_ui.py
CHANGED
|
@@ -46,6 +46,40 @@ GRID_ROWS = 6
|
|
| 46 |
GRID_COLS = 8
|
| 47 |
MAX_FREE_LETTERS = 2
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
# ---------------------------------------------------------------------------
|
| 51 |
# Word Loading (Gradio-compatible, no Streamlit dependencies)
|
|
@@ -303,16 +337,16 @@ def create_new_game_state(
|
|
| 303 |
"wordlist": wordlist,
|
| 304 |
"incorrect_guesses": [],
|
| 305 |
"game_over": False,
|
| 306 |
-
# Audio settings
|
| 307 |
-
"sound_effects_enabled": True,
|
| 308 |
-
"sound_effects_volume": 50,
|
| 309 |
-
"music_enabled": False,
|
| 310 |
-
"music_volume": 30,
|
| 311 |
"pending_sound": None, # Sound effect to play on next render
|
| 312 |
-
# Display settings
|
| 313 |
-
"show_incorrect_guesses": True,
|
| 314 |
-
"enable_free_letters": True,
|
| 315 |
-
"show_challenge_links": True,
|
| 316 |
# Challenge mode
|
| 317 |
"challenge_mode": False,
|
| 318 |
"challenge_sid": None,
|
|
@@ -399,7 +433,7 @@ def get_cell_label(state: Dict[str, Any], row: int, col: int) -> str:
|
|
| 399 |
|
| 400 |
if coord_str in revealed_set:
|
| 401 |
letter = letter_map.get(coord_str, "")
|
| 402 |
-
return letter if letter else "
|
| 403 |
else:
|
| 404 |
return "?"
|
| 405 |
|
|
@@ -530,11 +564,14 @@ def render_score_panel_html(state: Dict[str, Any]) -> str:
|
|
| 530 |
seconds = int(elapsed % 60)
|
| 531 |
time_str = f"{minutes:02d}:{seconds:02d}"
|
| 532 |
|
| 533 |
-
# Build table rows
|
| 534 |
table_rows = []
|
| 535 |
table_rows.append('<tr><th>Word</th><th>Len</th><th>Bonus</th></tr>')
|
| 536 |
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
| 538 |
if text in guessed:
|
| 539 |
total_points = points_by_word.get(text, len(text))
|
| 540 |
base_points = len(text)
|
|
@@ -614,12 +651,12 @@ def render_score_panel_html(state: Dict[str, Any]) -> str:
|
|
| 614 |
.wrdler-incorrect-guesses .guess-list {{ color: #ff9999; font-size: 0.8rem; margin-top: 6px; padding-left: 8px; }}
|
| 615 |
.wrdler-incorrect-guesses .guess-item {{ margin: 2px 0; }}
|
| 616 |
</style>
|
|
|
|
| 617 |
<div class="score-header">Score: <span class="score-value">{score}</span></div>
|
| 618 |
{timer_html}
|
| 619 |
<table class='shiny-border' style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing: 0;\">
|
| 620 |
{table_inner}
|
| 621 |
-
</table>
|
| 622 |
-
{incorrect_html}
|
| 623 |
</div>
|
| 624 |
'''
|
| 625 |
|
|
@@ -708,7 +745,10 @@ def render_game_over_html(state: Dict[str, Any]) -> str:
|
|
| 708 |
'<tr><th>Word</th><th>Length</th><th>Bonus</th><th>Total</th></tr>'
|
| 709 |
]
|
| 710 |
|
| 711 |
-
|
|
|
|
|
|
|
|
|
|
| 712 |
points = points_by_word.get(text, len(text))
|
| 713 |
base = len(text)
|
| 714 |
bonus = points - base
|
|
@@ -785,14 +825,18 @@ def get_letter_button_updates(state: Dict[str, Any]) -> List[gr.Button]:
|
|
| 785 |
def build_ui_outputs(state: Dict[str, Any], audio_html: str = "") -> tuple:
|
| 786 |
"""Build the complete UI output tuple for all handlers.
|
| 787 |
|
| 788 |
-
Returns: (48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, free_letter_row,
|
| 789 |
"""
|
| 790 |
grid_button_updates = get_all_button_updates(state)
|
| 791 |
letter_button_updates = get_letter_button_updates(state)
|
| 792 |
|
| 793 |
-
# Show
|
| 794 |
-
|
| 795 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
|
| 797 |
# Show free letter section only if enabled
|
| 798 |
show_free_letters = state.get("enable_free_letters", True)
|
|
@@ -807,7 +851,8 @@ def build_ui_outputs(state: Dict[str, Any], audio_html: str = "") -> tuple:
|
|
| 807 |
render_game_over_html(state),
|
| 808 |
render_free_letters_status(state) if show_free_letters else "",
|
| 809 |
free_letter_row_visible,
|
| 810 |
-
|
|
|
|
| 811 |
state
|
| 812 |
)
|
| 813 |
|
|
@@ -898,9 +943,13 @@ def build_guess_outputs(state: Dict[str, Any], audio_html: str = "", clear_input
|
|
| 898 |
grid_button_updates = get_all_button_updates(state)
|
| 899 |
letter_button_updates = get_letter_button_updates(state)
|
| 900 |
|
| 901 |
-
# Show
|
| 902 |
-
|
| 903 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
|
| 905 |
# Show free letter section only if enabled
|
| 906 |
show_free_letters = state.get("enable_free_letters", True)
|
|
@@ -916,7 +965,8 @@ def build_guess_outputs(state: Dict[str, Any], audio_html: str = "", clear_input
|
|
| 916 |
render_game_over_html(state),
|
| 917 |
render_free_letters_status(state) if show_free_letters else "",
|
| 918 |
free_letter_row_visible,
|
| 919 |
-
|
|
|
|
| 920 |
state
|
| 921 |
)
|
| 922 |
|
|
@@ -1080,7 +1130,10 @@ def handle_share_challenge(
|
|
| 1080 |
grid_size=6, # Wrdler: 6 rows
|
| 1081 |
spacer=0,
|
| 1082 |
may_overlap=False,
|
| 1083 |
-
wordlist_source=wordlist_source
|
|
|
|
|
|
|
|
|
|
| 1084 |
)
|
| 1085 |
|
| 1086 |
if sid:
|
|
@@ -1118,17 +1171,20 @@ def create_app() -> gr.Blocks:
|
|
| 1118 |
except Exception:
|
| 1119 |
css_content = None
|
| 1120 |
|
|
|
|
|
|
|
|
|
|
| 1121 |
with gr.Blocks(
|
| 1122 |
css=css_content,
|
| 1123 |
theme="Surn/beeuty",
|
| 1124 |
-
title=f"
|
| 1125 |
) as demo:
|
| 1126 |
|
| 1127 |
# Game state
|
| 1128 |
game_state = gr.State(value=create_new_game_state)
|
| 1129 |
|
| 1130 |
# Header
|
| 1131 |
-
gr.Markdown(f"#
|
| 1132 |
gr.Markdown("Find all 6 hidden words in the 8×6 grid!")
|
| 1133 |
|
| 1134 |
# Tab layout
|
|
@@ -1207,18 +1263,6 @@ def create_app() -> gr.Blocks:
|
|
| 1207 |
# Status message
|
| 1208 |
status_msg = gr.Markdown(value="Welcome!")
|
| 1209 |
|
| 1210 |
-
# Game over display
|
| 1211 |
-
game_over_html = gr.HTML(value="", elem_id="game-over-container")
|
| 1212 |
-
|
| 1213 |
-
# Share Challenge Button (visible after game over)
|
| 1214 |
-
share_challenge_btn = gr.Button(
|
| 1215 |
-
"🎮 Share Your Challenge",
|
| 1216 |
-
variant="primary",
|
| 1217 |
-
visible=False,
|
| 1218 |
-
elem_id="share-challenge-btn",
|
| 1219 |
-
elem_classes=["share-challenge-trigger"]
|
| 1220 |
-
)
|
| 1221 |
-
|
| 1222 |
# Right column - Score panel and New Game
|
| 1223 |
with gr.Column(scale=3):
|
| 1224 |
guess_input = gr.Textbox(
|
|
@@ -1269,72 +1313,81 @@ def create_app() -> gr.Blocks:
|
|
| 1269 |
label="Game Mode"
|
| 1270 |
)
|
| 1271 |
show_incorrect_guesses = gr.Checkbox(
|
| 1272 |
-
value=True,
|
| 1273 |
label="Show Incorrect Guesses"
|
| 1274 |
)
|
| 1275 |
enable_free_letters = gr.Checkbox(
|
| 1276 |
-
value=True,
|
| 1277 |
label="Enable Free Letters"
|
| 1278 |
)
|
| 1279 |
show_challenge_links = gr.Checkbox(
|
| 1280 |
-
value=True,
|
| 1281 |
label="Show Challenge Share Links"
|
| 1282 |
)
|
| 1283 |
|
| 1284 |
with gr.Column():
|
| 1285 |
gr.Markdown("### Audio Settings")
|
| 1286 |
sfx_enabled = gr.Checkbox(
|
| 1287 |
-
value=True,
|
| 1288 |
label="Enable Sound Effects"
|
| 1289 |
)
|
| 1290 |
sfx_volume = gr.Slider(
|
| 1291 |
minimum=0,
|
| 1292 |
maximum=100,
|
| 1293 |
-
value=50,
|
| 1294 |
step=1,
|
| 1295 |
label="Sound Effects Volume"
|
| 1296 |
)
|
| 1297 |
music_enabled = gr.Checkbox(
|
| 1298 |
-
value=False,
|
| 1299 |
label="Enable Background Music"
|
| 1300 |
)
|
| 1301 |
music_volume = gr.Slider(
|
| 1302 |
minimum=0,
|
| 1303 |
maximum=100,
|
| 1304 |
-
value=30,
|
| 1305 |
step=1,
|
| 1306 |
label="Music Volume"
|
| 1307 |
)
|
| 1308 |
|
| 1309 |
-
#
|
| 1310 |
-
audio_player_html = gr.HTML(value="", elem_id="audio-player",
|
| 1311 |
|
| 1312 |
# Timer for updating elapsed time (ticks every second)
|
| 1313 |
game_timer = gr.Timer(value=1, active=True)
|
| 1314 |
|
| 1315 |
-
#
|
| 1316 |
-
with Modal(visible=False) as
|
| 1317 |
-
|
| 1318 |
-
gr.
|
| 1319 |
-
username_input = gr.Textbox(
|
| 1320 |
-
label="Your Name (optional)",
|
| 1321 |
-
placeholder="Anonymous",
|
| 1322 |
-
max_lines=1,
|
| 1323 |
-
elem_id="username-input"
|
| 1324 |
-
)
|
| 1325 |
-
share_btn = gr.Button("Generate Share Link", variant="primary", size="lg")
|
| 1326 |
-
share_status = gr.Markdown(value="", elem_id="share-status")
|
| 1327 |
-
share_url_display = gr.Textbox(
|
| 1328 |
-
label="Share URL (click to copy)",
|
| 1329 |
value="",
|
| 1330 |
-
|
| 1331 |
-
visible=False,
|
| 1332 |
-
elem_id="share-url-display"
|
| 1333 |
)
|
| 1334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1335 |
|
| 1336 |
# Define common outputs for handlers
|
| 1337 |
-
# Order: 48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, free_letter_row,
|
| 1338 |
common_outputs = [
|
| 1339 |
*grid_buttons,
|
| 1340 |
*letter_buttons,
|
|
@@ -1344,7 +1397,8 @@ def create_app() -> gr.Blocks:
|
|
| 1344 |
game_over_html,
|
| 1345 |
free_letter_status,
|
| 1346 |
free_letter_row,
|
| 1347 |
-
|
|
|
|
| 1348 |
game_state
|
| 1349 |
]
|
| 1350 |
|
|
@@ -1359,7 +1413,8 @@ def create_app() -> gr.Blocks:
|
|
| 1359 |
game_over_html,
|
| 1360 |
free_letter_status,
|
| 1361 |
free_letter_row,
|
| 1362 |
-
|
|
|
|
| 1363 |
game_state
|
| 1364 |
]
|
| 1365 |
|
|
@@ -1418,17 +1473,63 @@ def create_app() -> gr.Blocks:
|
|
| 1418 |
outputs=topic_display
|
| 1419 |
)
|
| 1420 |
|
| 1421 |
-
# Wordlist dropdown change handler - show/hide AI topic input
|
| 1422 |
-
def handle_wordlist_change(wordlist, ai_topic):
|
| 1423 |
-
"""
|
|
|
|
| 1424 |
is_ai = wordlist == "AI Generated"
|
| 1425 |
topic_value = ai_topic.strip() if is_ai and ai_topic.strip() else wordlist.replace(".txt", "")
|
| 1426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1427 |
|
| 1428 |
wordlist_dropdown.change(
|
| 1429 |
fn=handle_wordlist_change,
|
| 1430 |
-
inputs=[wordlist_dropdown, ai_topic_input],
|
| 1431 |
-
outputs=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1432 |
)
|
| 1433 |
|
| 1434 |
# AI Topic submit button - generates new game with AI words
|
|
@@ -1544,52 +1645,73 @@ def create_app() -> gr.Blocks:
|
|
| 1544 |
outputs=[game_state]
|
| 1545 |
)
|
| 1546 |
|
| 1547 |
-
# Show incorrect guesses toggle handler
|
| 1548 |
-
def handle_show_incorrect_change(show_incorrect, state):
|
| 1549 |
state = ensure_state(state)
|
| 1550 |
-
new_state =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1551 |
new_state["show_incorrect_guesses"] = show_incorrect
|
| 1552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1553 |
|
| 1554 |
show_incorrect_guesses.change(
|
| 1555 |
fn=handle_show_incorrect_change,
|
| 1556 |
-
inputs=[show_incorrect_guesses, game_state],
|
| 1557 |
-
outputs=
|
| 1558 |
)
|
| 1559 |
|
| 1560 |
-
# Enable free letters toggle handler
|
| 1561 |
-
def handle_enable_free_letters_change(enabled, state):
|
| 1562 |
state = ensure_state(state)
|
| 1563 |
-
new_state =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1564 |
new_state["enable_free_letters"] = enabled
|
| 1565 |
-
|
| 1566 |
if not enabled:
|
| 1567 |
new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
|
| 1568 |
-
elif new_state.get("free_letters_used", 0) < MAX_FREE_LETTERS:
|
| 1569 |
-
remaining = MAX_FREE_LETTERS - new_state.get("free_letters_used", 0)
|
| 1570 |
-
new_state["last_action"] = f"Choose {remaining} free letter{'s' if remaining > 1 else ''} to start."
|
| 1571 |
return build_ui_outputs(new_state)
|
| 1572 |
|
| 1573 |
enable_free_letters.change(
|
| 1574 |
fn=handle_enable_free_letters_change,
|
| 1575 |
-
inputs=[enable_free_letters, game_state],
|
| 1576 |
outputs=common_outputs
|
| 1577 |
)
|
| 1578 |
|
| 1579 |
-
# Show challenge links toggle handler
|
| 1580 |
-
def handle_show_challenge_links_change(enabled, state):
|
| 1581 |
state = ensure_state(state)
|
| 1582 |
-
new_state =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1583 |
new_state["show_challenge_links"] = enabled
|
|
|
|
|
|
|
| 1584 |
return build_ui_outputs(new_state)
|
| 1585 |
|
| 1586 |
show_challenge_links.change(
|
| 1587 |
fn=handle_show_challenge_links_change,
|
| 1588 |
-
inputs=[show_challenge_links, game_state],
|
| 1589 |
outputs=common_outputs
|
| 1590 |
)
|
| 1591 |
|
| 1592 |
-
# Share challenge button handler
|
| 1593 |
def on_share_click(username, state):
|
| 1594 |
"""Handle share button click."""
|
| 1595 |
status_text, url, new_state = handle_share_challenge(username, state)
|
|
@@ -1600,23 +1722,17 @@ def create_app() -> gr.Blocks:
|
|
| 1600 |
new_state
|
| 1601 |
)
|
| 1602 |
|
| 1603 |
-
|
| 1604 |
fn=on_share_click,
|
| 1605 |
inputs=[username_input, game_state],
|
| 1606 |
outputs=[share_status, share_url_display, game_state]
|
| 1607 |
)
|
| 1608 |
|
| 1609 |
-
# Modal
|
| 1610 |
-
share_challenge_btn.click(
|
| 1611 |
-
fn=lambda: Modal(visible=True),
|
| 1612 |
-
inputs=None,
|
| 1613 |
-
outputs=share_modal
|
| 1614 |
-
)
|
| 1615 |
-
|
| 1616 |
close_modal_btn.click(
|
| 1617 |
fn=lambda: Modal(visible=False),
|
| 1618 |
inputs=None,
|
| 1619 |
-
outputs=
|
| 1620 |
)
|
| 1621 |
|
| 1622 |
# Timer tick handler - updates score panel with current elapsed time
|
|
|
|
| 46 |
GRID_COLS = 8
|
| 47 |
MAX_FREE_LETTERS = 2
|
| 48 |
|
| 49 |
+
# ---------------------------------------------------------------------------
|
| 50 |
+
# Settings Loading
|
| 51 |
+
# ---------------------------------------------------------------------------
|
| 52 |
+
|
| 53 |
+
def load_settings() -> Dict[str, Any]:
|
| 54 |
+
"""Load settings from settings.json file."""
|
| 55 |
+
settings_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "settings.json")
|
| 56 |
+
default_settings = {
|
| 57 |
+
"game_title": "Wrdler Gradio AI",
|
| 58 |
+
"show_incorrect_guesses": True,
|
| 59 |
+
"enable_free_letters": True,
|
| 60 |
+
"show_challenge_links": True,
|
| 61 |
+
"sound_effects_enabled": True,
|
| 62 |
+
"sound_effects_volume": 50,
|
| 63 |
+
"music_enabled": False,
|
| 64 |
+
"music_volume": 30,
|
| 65 |
+
"default_wordlist": "classic.txt",
|
| 66 |
+
"default_game_mode": "classic"
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
if os.path.exists(settings_path):
|
| 71 |
+
with open(settings_path, "r", encoding="utf-8") as f:
|
| 72 |
+
loaded = json.load(f)
|
| 73 |
+
# Merge with defaults (loaded settings override defaults)
|
| 74 |
+
return {**default_settings, **loaded}
|
| 75 |
+
except Exception:
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
return default_settings
|
| 79 |
+
|
| 80 |
+
# Load settings at module level
|
| 81 |
+
APP_SETTINGS = load_settings()
|
| 82 |
+
|
| 83 |
|
| 84 |
# ---------------------------------------------------------------------------
|
| 85 |
# Word Loading (Gradio-compatible, no Streamlit dependencies)
|
|
|
|
| 337 |
"wordlist": wordlist,
|
| 338 |
"incorrect_guesses": [],
|
| 339 |
"game_over": False,
|
| 340 |
+
# Audio settings (from APP_SETTINGS)
|
| 341 |
+
"sound_effects_enabled": APP_SETTINGS.get("sound_effects_enabled", True),
|
| 342 |
+
"sound_effects_volume": APP_SETTINGS.get("sound_effects_volume", 50),
|
| 343 |
+
"music_enabled": APP_SETTINGS.get("music_enabled", False),
|
| 344 |
+
"music_volume": APP_SETTINGS.get("music_volume", 30),
|
| 345 |
"pending_sound": None, # Sound effect to play on next render
|
| 346 |
+
# Display settings (from APP_SETTINGS)
|
| 347 |
+
"show_incorrect_guesses": APP_SETTINGS.get("show_incorrect_guesses", True),
|
| 348 |
+
"enable_free_letters": APP_SETTINGS.get("enable_free_letters", True),
|
| 349 |
+
"show_challenge_links": APP_SETTINGS.get("show_challenge_links", True),
|
| 350 |
# Challenge mode
|
| 351 |
"challenge_mode": False,
|
| 352 |
"challenge_sid": None,
|
|
|
|
| 433 |
|
| 434 |
if coord_str in revealed_set:
|
| 435 |
letter = letter_map.get(coord_str, "")
|
| 436 |
+
return letter if letter else "\u00A0" # Non-breaking space for empty revealed cells
|
| 437 |
else:
|
| 438 |
return "?"
|
| 439 |
|
|
|
|
| 564 |
seconds = int(elapsed % 60)
|
| 565 |
time_str = f"{minutes:02d}:{seconds:02d}"
|
| 566 |
|
| 567 |
+
# Build table rows - sort words by length
|
| 568 |
table_rows = []
|
| 569 |
table_rows.append('<tr><th>Word</th><th>Len</th><th>Bonus</th></tr>')
|
| 570 |
|
| 571 |
+
# Sort puzzle words by length (shortest first)
|
| 572 |
+
sorted_words = sorted(state["puzzle_words"], key=lambda w: len(w[0]))
|
| 573 |
+
|
| 574 |
+
for text, x, y, direction in sorted_words:
|
| 575 |
if text in guessed:
|
| 576 |
total_points = points_by_word.get(text, len(text))
|
| 577 |
base_points = len(text)
|
|
|
|
| 651 |
.wrdler-incorrect-guesses .guess-list {{ color: #ff9999; font-size: 0.8rem; margin-top: 6px; padding-left: 8px; }}
|
| 652 |
.wrdler-incorrect-guesses .guess-item {{ margin: 2px 0; }}
|
| 653 |
</style>
|
| 654 |
+
{incorrect_html}
|
| 655 |
<div class="score-header">Score: <span class="score-value">{score}</span></div>
|
| 656 |
{timer_html}
|
| 657 |
<table class='shiny-border' style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing: 0;\">
|
| 658 |
{table_inner}
|
| 659 |
+
</table>
|
|
|
|
| 660 |
</div>
|
| 661 |
'''
|
| 662 |
|
|
|
|
| 745 |
'<tr><th>Word</th><th>Length</th><th>Bonus</th><th>Total</th></tr>'
|
| 746 |
]
|
| 747 |
|
| 748 |
+
# Sort puzzle words by length (shortest first)
|
| 749 |
+
sorted_words = sorted(state["puzzle_words"], key=lambda w: len(w[0]))
|
| 750 |
+
|
| 751 |
+
for text, x, y, direction in sorted_words:
|
| 752 |
points = points_by_word.get(text, len(text))
|
| 753 |
base = len(text)
|
| 754 |
bonus = points - base
|
|
|
|
| 825 |
def build_ui_outputs(state: Dict[str, Any], audio_html: str = "") -> tuple:
|
| 826 |
"""Build the complete UI output tuple for all handlers.
|
| 827 |
|
| 828 |
+
Returns: (48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, free_letter_row, game_over_modal, share_section, state)
|
| 829 |
"""
|
| 830 |
grid_button_updates = get_all_button_updates(state)
|
| 831 |
letter_button_updates = get_letter_button_updates(state)
|
| 832 |
|
| 833 |
+
# Show modal when game is over
|
| 834 |
+
is_game_over = state.get("game_over", False)
|
| 835 |
+
game_over_modal_visible = Modal(visible=is_game_over)
|
| 836 |
+
|
| 837 |
+
# Show share section only when show_challenge_links is enabled
|
| 838 |
+
show_share = state.get("show_challenge_links", True)
|
| 839 |
+
share_section_visible = gr.Column(visible=show_share)
|
| 840 |
|
| 841 |
# Show free letter section only if enabled
|
| 842 |
show_free_letters = state.get("enable_free_letters", True)
|
|
|
|
| 851 |
render_game_over_html(state),
|
| 852 |
render_free_letters_status(state) if show_free_letters else "",
|
| 853 |
free_letter_row_visible,
|
| 854 |
+
game_over_modal_visible,
|
| 855 |
+
share_section_visible,
|
| 856 |
state
|
| 857 |
)
|
| 858 |
|
|
|
|
| 943 |
grid_button_updates = get_all_button_updates(state)
|
| 944 |
letter_button_updates = get_letter_button_updates(state)
|
| 945 |
|
| 946 |
+
# Show modal when game is over
|
| 947 |
+
is_game_over = state.get("game_over", False)
|
| 948 |
+
game_over_modal_visible = Modal(visible=is_game_over)
|
| 949 |
+
|
| 950 |
+
# Show share section only when show_challenge_links is enabled
|
| 951 |
+
show_share = state.get("show_challenge_links", True)
|
| 952 |
+
share_section_visible = gr.Column(visible=show_share)
|
| 953 |
|
| 954 |
# Show free letter section only if enabled
|
| 955 |
show_free_letters = state.get("enable_free_letters", True)
|
|
|
|
| 965 |
render_game_over_html(state),
|
| 966 |
render_free_letters_status(state) if show_free_letters else "",
|
| 967 |
free_letter_row_visible,
|
| 968 |
+
game_over_modal_visible,
|
| 969 |
+
share_section_visible,
|
| 970 |
state
|
| 971 |
)
|
| 972 |
|
|
|
|
| 1130 |
grid_size=6, # Wrdler: 6 rows
|
| 1131 |
spacer=0,
|
| 1132 |
may_overlap=False,
|
| 1133 |
+
wordlist_source=wordlist_source,
|
| 1134 |
+
game_title=APP_SETTINGS.get("game_title", "Wrdler Gradio AI"),
|
| 1135 |
+
show_incorrect_guesses=state.get("show_incorrect_guesses", True),
|
| 1136 |
+
enable_free_letters=state.get("enable_free_letters", True)
|
| 1137 |
)
|
| 1138 |
|
| 1139 |
if sid:
|
|
|
|
| 1171 |
except Exception:
|
| 1172 |
css_content = None
|
| 1173 |
|
| 1174 |
+
# Get game title from settings
|
| 1175 |
+
game_title = APP_SETTINGS.get("game_title", "Wrdler Gradio AI")
|
| 1176 |
+
|
| 1177 |
with gr.Blocks(
|
| 1178 |
css=css_content,
|
| 1179 |
theme="Surn/beeuty",
|
| 1180 |
+
title=f"{game_title} v{__version__}"
|
| 1181 |
) as demo:
|
| 1182 |
|
| 1183 |
# Game state
|
| 1184 |
game_state = gr.State(value=create_new_game_state)
|
| 1185 |
|
| 1186 |
# Header
|
| 1187 |
+
gr.Markdown(f"# {game_title} v{__version__}")
|
| 1188 |
gr.Markdown("Find all 6 hidden words in the 8×6 grid!")
|
| 1189 |
|
| 1190 |
# Tab layout
|
|
|
|
| 1263 |
# Status message
|
| 1264 |
status_msg = gr.Markdown(value="Welcome!")
|
| 1265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1266 |
# Right column - Score panel and New Game
|
| 1267 |
with gr.Column(scale=3):
|
| 1268 |
guess_input = gr.Textbox(
|
|
|
|
| 1313 |
label="Game Mode"
|
| 1314 |
)
|
| 1315 |
show_incorrect_guesses = gr.Checkbox(
|
| 1316 |
+
value=APP_SETTINGS.get("show_incorrect_guesses", True),
|
| 1317 |
label="Show Incorrect Guesses"
|
| 1318 |
)
|
| 1319 |
enable_free_letters = gr.Checkbox(
|
| 1320 |
+
value=APP_SETTINGS.get("enable_free_letters", True),
|
| 1321 |
label="Enable Free Letters"
|
| 1322 |
)
|
| 1323 |
show_challenge_links = gr.Checkbox(
|
| 1324 |
+
value=APP_SETTINGS.get("show_challenge_links", True),
|
| 1325 |
label="Show Challenge Share Links"
|
| 1326 |
)
|
| 1327 |
|
| 1328 |
with gr.Column():
|
| 1329 |
gr.Markdown("### Audio Settings")
|
| 1330 |
sfx_enabled = gr.Checkbox(
|
| 1331 |
+
value=APP_SETTINGS.get("sound_effects_enabled", True),
|
| 1332 |
label="Enable Sound Effects"
|
| 1333 |
)
|
| 1334 |
sfx_volume = gr.Slider(
|
| 1335 |
minimum=0,
|
| 1336 |
maximum=100,
|
| 1337 |
+
value=APP_SETTINGS.get("sound_effects_volume", 50),
|
| 1338 |
step=1,
|
| 1339 |
label="Sound Effects Volume"
|
| 1340 |
)
|
| 1341 |
music_enabled = gr.Checkbox(
|
| 1342 |
+
value=APP_SETTINGS.get("music_enabled", False),
|
| 1343 |
label="Enable Background Music"
|
| 1344 |
)
|
| 1345 |
music_volume = gr.Slider(
|
| 1346 |
minimum=0,
|
| 1347 |
maximum=100,
|
| 1348 |
+
value=APP_SETTINGS.get("music_volume", 30),
|
| 1349 |
step=1,
|
| 1350 |
label="Music Volume"
|
| 1351 |
)
|
| 1352 |
|
| 1353 |
+
# Audio player for sound effects (hidden via CSS but still executes)
|
| 1354 |
+
audio_player_html = gr.HTML(value="", elem_id="audio-player", elem_classes=["hidden-audio"])
|
| 1355 |
|
| 1356 |
# Timer for updating elapsed time (ticks every second)
|
| 1357 |
game_timer = gr.Timer(value=1, active=True)
|
| 1358 |
|
| 1359 |
+
# Game Over Modal Dialog (outside tabs) - contains game over display and share challenge section
|
| 1360 |
+
with Modal(visible=False) as game_over_modal:
|
| 1361 |
+
# Game Over Content
|
| 1362 |
+
game_over_html = gr.HTML(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1363 |
value="",
|
| 1364 |
+
elem_id="game-over-container"
|
|
|
|
|
|
|
| 1365 |
)
|
| 1366 |
+
|
| 1367 |
+
# Share Challenge Section (conditionally visible based on show_challenge_links)
|
| 1368 |
+
with gr.Column(visible=True, elem_classes=["share-challenge-section"]) as share_section:
|
| 1369 |
+
gr.Markdown("## 🎮 Share Your Challenge")
|
| 1370 |
+
gr.Markdown("Challenge your friends to beat your score!")
|
| 1371 |
+
username_input = gr.Textbox(
|
| 1372 |
+
label="Your Name (optional)",
|
| 1373 |
+
placeholder="Anonymous",
|
| 1374 |
+
max_lines=1,
|
| 1375 |
+
elem_id="username-input"
|
| 1376 |
+
)
|
| 1377 |
+
share_challenge_btn = gr.Button("Generate Share Link", variant="primary", size="lg")
|
| 1378 |
+
share_status = gr.Markdown(value="", elem_id="share-status")
|
| 1379 |
+
share_url_display = gr.Textbox(
|
| 1380 |
+
label="Share URL (click to copy)",
|
| 1381 |
+
value="",
|
| 1382 |
+
interactive=False,
|
| 1383 |
+
visible=False,
|
| 1384 |
+
elem_id="share-url-display"
|
| 1385 |
+
)
|
| 1386 |
+
|
| 1387 |
+
close_modal_btn = gr.Button("Close", variant="secondary", size="lg")
|
| 1388 |
|
| 1389 |
# Define common outputs for handlers
|
| 1390 |
+
# Order: 48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, free_letter_row, game_over_modal, share_section, state
|
| 1391 |
common_outputs = [
|
| 1392 |
*grid_buttons,
|
| 1393 |
*letter_buttons,
|
|
|
|
| 1397 |
game_over_html,
|
| 1398 |
free_letter_status,
|
| 1399 |
free_letter_row,
|
| 1400 |
+
game_over_modal,
|
| 1401 |
+
share_section,
|
| 1402 |
game_state
|
| 1403 |
]
|
| 1404 |
|
|
|
|
| 1413 |
game_over_html,
|
| 1414 |
free_letter_status,
|
| 1415 |
free_letter_row,
|
| 1416 |
+
game_over_modal,
|
| 1417 |
+
share_section,
|
| 1418 |
game_state
|
| 1419 |
]
|
| 1420 |
|
|
|
|
| 1473 |
outputs=topic_display
|
| 1474 |
)
|
| 1475 |
|
| 1476 |
+
# Wordlist dropdown change handler - starts new game, show/hide AI topic input
|
| 1477 |
+
def handle_wordlist_change(wordlist, ai_topic, game_mode, sfx_en, sfx_vol, music_en, music_vol, show_inc, enable_fl, show_chal, state):
|
| 1478 |
+
"""Start new game when wordlist changes, show AI topic input when AI Generated is selected."""
|
| 1479 |
+
state = ensure_state(state)
|
| 1480 |
is_ai = wordlist == "AI Generated"
|
| 1481 |
topic_value = ai_topic.strip() if is_ai and ai_topic.strip() else wordlist.replace(".txt", "")
|
| 1482 |
+
|
| 1483 |
+
# Start new game with selected wordlist
|
| 1484 |
+
new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode, ai_topic=ai_topic if is_ai else "")
|
| 1485 |
+
# Apply all settings
|
| 1486 |
+
new_state["sound_effects_enabled"] = sfx_en
|
| 1487 |
+
new_state["sound_effects_volume"] = sfx_vol
|
| 1488 |
+
new_state["music_enabled"] = music_en
|
| 1489 |
+
new_state["music_volume"] = music_vol
|
| 1490 |
+
new_state["show_incorrect_guesses"] = show_inc
|
| 1491 |
+
new_state["enable_free_letters"] = enable_fl
|
| 1492 |
+
new_state["show_challenge_links"] = show_chal
|
| 1493 |
+
if not enable_fl:
|
| 1494 |
+
new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
|
| 1495 |
+
|
| 1496 |
+
base_outputs = build_ui_outputs(new_state)
|
| 1497 |
+
return (
|
| 1498 |
+
*base_outputs,
|
| 1499 |
+
gr.Row(visible=is_ai), # ai_topic_row
|
| 1500 |
+
gr.Textbox(value=topic_value) # topic_display
|
| 1501 |
+
)
|
| 1502 |
+
|
| 1503 |
+
# Extended outputs for wordlist change
|
| 1504 |
+
wordlist_change_outputs = common_outputs + [ai_topic_row, topic_display]
|
| 1505 |
|
| 1506 |
wordlist_dropdown.change(
|
| 1507 |
fn=handle_wordlist_change,
|
| 1508 |
+
inputs=[wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, show_challenge_links, game_state],
|
| 1509 |
+
outputs=wordlist_change_outputs
|
| 1510 |
+
)
|
| 1511 |
+
|
| 1512 |
+
# Game mode dropdown change handler - starts new game
|
| 1513 |
+
def handle_game_mode_change(game_mode, wordlist, ai_topic, sfx_en, sfx_vol, music_en, music_vol, show_inc, enable_fl, show_chal, state):
|
| 1514 |
+
"""Start new game when game mode changes."""
|
| 1515 |
+
state = ensure_state(state)
|
| 1516 |
+
new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode, ai_topic=ai_topic)
|
| 1517 |
+
# Apply all settings
|
| 1518 |
+
new_state["sound_effects_enabled"] = sfx_en
|
| 1519 |
+
new_state["sound_effects_volume"] = sfx_vol
|
| 1520 |
+
new_state["music_enabled"] = music_en
|
| 1521 |
+
new_state["music_volume"] = music_vol
|
| 1522 |
+
new_state["show_incorrect_guesses"] = show_inc
|
| 1523 |
+
new_state["enable_free_letters"] = enable_fl
|
| 1524 |
+
new_state["show_challenge_links"] = show_chal
|
| 1525 |
+
if not enable_fl:
|
| 1526 |
+
new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
|
| 1527 |
+
return build_ui_outputs(new_state)
|
| 1528 |
+
|
| 1529 |
+
game_mode_dropdown.change(
|
| 1530 |
+
fn=handle_game_mode_change,
|
| 1531 |
+
inputs=[game_mode_dropdown, wordlist_dropdown, ai_topic_input, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, show_challenge_links, game_state],
|
| 1532 |
+
outputs=common_outputs
|
| 1533 |
)
|
| 1534 |
|
| 1535 |
# AI Topic submit button - generates new game with AI words
|
|
|
|
| 1645 |
outputs=[game_state]
|
| 1646 |
)
|
| 1647 |
|
| 1648 |
+
# Show incorrect guesses toggle handler - starts new game
|
| 1649 |
+
def handle_show_incorrect_change(show_incorrect, wordlist, ai_topic, game_mode, sfx_en, sfx_vol, music_en, music_vol, enable_fl, show_chal, state):
|
| 1650 |
state = ensure_state(state)
|
| 1651 |
+
new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode, ai_topic=ai_topic)
|
| 1652 |
+
# Apply all settings
|
| 1653 |
+
new_state["sound_effects_enabled"] = sfx_en
|
| 1654 |
+
new_state["sound_effects_volume"] = sfx_vol
|
| 1655 |
+
new_state["music_enabled"] = music_en
|
| 1656 |
+
new_state["music_volume"] = music_vol
|
| 1657 |
new_state["show_incorrect_guesses"] = show_incorrect
|
| 1658 |
+
new_state["enable_free_letters"] = enable_fl
|
| 1659 |
+
new_state["show_challenge_links"] = show_chal
|
| 1660 |
+
if not enable_fl:
|
| 1661 |
+
new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
|
| 1662 |
+
return build_ui_outputs(new_state)
|
| 1663 |
|
| 1664 |
show_incorrect_guesses.change(
|
| 1665 |
fn=handle_show_incorrect_change,
|
| 1666 |
+
inputs=[show_incorrect_guesses, wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, enable_free_letters, show_challenge_links, game_state],
|
| 1667 |
+
outputs=common_outputs
|
| 1668 |
)
|
| 1669 |
|
| 1670 |
+
# Enable free letters toggle handler - starts new game
|
| 1671 |
+
def handle_enable_free_letters_change(enabled, wordlist, ai_topic, game_mode, sfx_en, sfx_vol, music_en, music_vol, show_inc, show_chal, state):
|
| 1672 |
state = ensure_state(state)
|
| 1673 |
+
new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode, ai_topic=ai_topic)
|
| 1674 |
+
# Apply all settings
|
| 1675 |
+
new_state["sound_effects_enabled"] = sfx_en
|
| 1676 |
+
new_state["sound_effects_volume"] = sfx_vol
|
| 1677 |
+
new_state["music_enabled"] = music_en
|
| 1678 |
+
new_state["music_volume"] = music_vol
|
| 1679 |
+
new_state["show_incorrect_guesses"] = show_inc
|
| 1680 |
new_state["enable_free_letters"] = enabled
|
| 1681 |
+
new_state["show_challenge_links"] = show_chal
|
| 1682 |
if not enabled:
|
| 1683 |
new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
|
|
|
|
|
|
|
|
|
|
| 1684 |
return build_ui_outputs(new_state)
|
| 1685 |
|
| 1686 |
enable_free_letters.change(
|
| 1687 |
fn=handle_enable_free_letters_change,
|
| 1688 |
+
inputs=[enable_free_letters, wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, show_challenge_links, game_state],
|
| 1689 |
outputs=common_outputs
|
| 1690 |
)
|
| 1691 |
|
| 1692 |
+
# Show challenge links toggle handler - starts new game
|
| 1693 |
+
def handle_show_challenge_links_change(enabled, wordlist, ai_topic, game_mode, sfx_en, sfx_vol, music_en, music_vol, show_inc, enable_fl, state):
|
| 1694 |
state = ensure_state(state)
|
| 1695 |
+
new_state = create_new_game_state(wordlist=wordlist, game_mode=game_mode, ai_topic=ai_topic)
|
| 1696 |
+
# Apply all settings
|
| 1697 |
+
new_state["sound_effects_enabled"] = sfx_en
|
| 1698 |
+
new_state["sound_effects_volume"] = sfx_vol
|
| 1699 |
+
new_state["music_enabled"] = music_en
|
| 1700 |
+
new_state["music_volume"] = music_vol
|
| 1701 |
+
new_state["show_incorrect_guesses"] = show_inc
|
| 1702 |
+
new_state["enable_free_letters"] = enable_fl
|
| 1703 |
new_state["show_challenge_links"] = enabled
|
| 1704 |
+
if not enable_fl:
|
| 1705 |
+
new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
|
| 1706 |
return build_ui_outputs(new_state)
|
| 1707 |
|
| 1708 |
show_challenge_links.change(
|
| 1709 |
fn=handle_show_challenge_links_change,
|
| 1710 |
+
inputs=[show_challenge_links, wordlist_dropdown, ai_topic_input, game_mode_dropdown, sfx_enabled, sfx_volume, music_enabled, music_volume, show_incorrect_guesses, enable_free_letters, game_state],
|
| 1711 |
outputs=common_outputs
|
| 1712 |
)
|
| 1713 |
|
| 1714 |
+
# Share challenge button handler (inside game over modal)
|
| 1715 |
def on_share_click(username, state):
|
| 1716 |
"""Handle share button click."""
|
| 1717 |
status_text, url, new_state = handle_share_challenge(username, state)
|
|
|
|
| 1722 |
new_state
|
| 1723 |
)
|
| 1724 |
|
| 1725 |
+
share_challenge_btn.click(
|
| 1726 |
fn=on_share_click,
|
| 1727 |
inputs=[username_input, game_state],
|
| 1728 |
outputs=[share_status, share_url_display, game_state]
|
| 1729 |
)
|
| 1730 |
|
| 1731 |
+
# Modal close handler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1732 |
close_modal_btn.click(
|
| 1733 |
fn=lambda: Modal(visible=False),
|
| 1734 |
inputs=None,
|
| 1735 |
+
outputs=game_over_modal
|
| 1736 |
)
|
| 1737 |
|
| 1738 |
# Timer tick handler - updates score panel with current elapsed time
|
wrdler/words/cooking.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# AI-generated word list
|
| 2 |
# Topic: Cooking
|
| 3 |
# Last updated: 2025-11-29 15:33:26
|
| 4 |
-
# Total words:
|
| 5 |
# Format: one word per line, sorted by length then alphabetically
|
| 6 |
#
|
| 7 |
BAKE
|
|
@@ -98,7 +98,6 @@ BASTE
|
|
| 98 |
BATCH
|
| 99 |
BERRY
|
| 100 |
BESTE
|
| 101 |
-
BEVAP
|
| 102 |
BITCH
|
| 103 |
BLACK
|
| 104 |
BLANK
|
|
|
|
| 1 |
# AI-generated word list
|
| 2 |
# Topic: Cooking
|
| 3 |
# Last updated: 2025-11-29 15:33:26
|
| 4 |
+
# Total words: 269
|
| 5 |
# Format: one word per line, sorted by length then alphabetically
|
| 6 |
#
|
| 7 |
BAKE
|
|
|
|
| 98 |
BATCH
|
| 99 |
BERRY
|
| 100 |
BESTE
|
|
|
|
| 101 |
BITCH
|
| 102 |
BLACK
|
| 103 |
BLANK
|