Surn commited on
Commit
e008df8
·
1 Parent(s): 0860b52

Enhance game UI and settings management

Browse files

Updated 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 CHANGED
@@ -3,8 +3,8 @@ title: Wrdler
3
  emoji: 🎲
4
  colorFrom: blue
5
  colorTo: indigo
6
- sdk: streamlit
7
- sdk_version: 1.51.0
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="·"]::before,
 
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.2"
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 "·" # Middle dot for empty revealed cells
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
- for text, x, y, direction in state["puzzle_words"]:
 
 
 
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
- for text, x, y, direction in state["puzzle_words"]:
 
 
 
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, share_btn, state)
789
  """
790
  grid_button_updates = get_all_button_updates(state)
791
  letter_button_updates = get_letter_button_updates(state)
792
 
793
- # Show share button only when game is over AND show_challenge_links is enabled
794
- show_share = state.get("game_over", False) and state.get("show_challenge_links", True)
795
- share_btn_visible = gr.Button(visible=show_share)
 
 
 
 
796
 
797
  # Show free letter section only if enabled
798
  show_free_letters = state.get("enable_free_letters", True)
@@ -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
- share_btn_visible,
 
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 share button only when game is over AND show_challenge_links is enabled
902
- show_share = state.get("game_over", False) and state.get("show_challenge_links", True)
903
- share_btn_visible = gr.Button(visible=show_share)
 
 
 
 
904
 
905
  # Show free letter section only if enabled
906
  show_free_letters = state.get("enable_free_letters", True)
@@ -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
- share_btn_visible,
 
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"Wrdler v{__version__}"
1125
  ) as demo:
1126
 
1127
  # Game state
1128
  game_state = gr.State(value=create_new_game_state)
1129
 
1130
  # Header
1131
- gr.Markdown(f"# Wrdler v{__version__}")
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
- # Hidden audio player for sound effects
1310
- audio_player_html = gr.HTML(value="", elem_id="audio-player", visible=False)
1311
 
1312
  # Timer for updating elapsed time (ticks every second)
1313
  game_timer = gr.Timer(value=1, active=True)
1314
 
1315
- # Share Challenge Modal Dialog (outside tabs)
1316
- with Modal(visible=False) as share_modal:
1317
- gr.Markdown("## 🎮 Share Your Challenge")
1318
- gr.Markdown("Challenge your friends to beat your score!")
1319
- username_input = gr.Textbox(
1320
- label="Your Name (optional)",
1321
- placeholder="Anonymous",
1322
- max_lines=1,
1323
- elem_id="username-input"
1324
- )
1325
- share_btn = gr.Button("Generate Share Link", variant="primary", size="lg")
1326
- share_status = gr.Markdown(value="", elem_id="share-status")
1327
- share_url_display = gr.Textbox(
1328
- label="Share URL (click to copy)",
1329
  value="",
1330
- interactive=False,
1331
- visible=False,
1332
- elem_id="share-url-display"
1333
  )
1334
- close_modal_btn = gr.Button("Close", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1335
 
1336
  # Define common outputs for handlers
1337
- # Order: 48 grid buttons, 26 letter buttons, score_panel, status_msg, audio, game_over, free_letter_status, free_letter_row, share_btn_visible, state
1338
  common_outputs = [
1339
  *grid_buttons,
1340
  *letter_buttons,
@@ -1344,7 +1397,8 @@ def create_app() -> gr.Blocks:
1344
  game_over_html,
1345
  free_letter_status,
1346
  free_letter_row,
1347
- share_challenge_btn,
 
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
- share_challenge_btn,
 
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 row and update topic display
1422
- def handle_wordlist_change(wordlist, ai_topic):
1423
- """Show AI topic input when AI Generated is selected and update topic display."""
 
1424
  is_ai = wordlist == "AI Generated"
1425
  topic_value = ai_topic.strip() if is_ai and ai_topic.strip() else wordlist.replace(".txt", "")
1426
- return gr.Row(visible=is_ai), gr.Textbox(value=topic_value)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1427
 
1428
  wordlist_dropdown.change(
1429
  fn=handle_wordlist_change,
1430
- inputs=[wordlist_dropdown, ai_topic_input],
1431
- outputs=[ai_topic_row, topic_display]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1432
  )
1433
 
1434
  # AI Topic submit button - generates new game with AI words
@@ -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 = copy.deepcopy(state)
 
 
 
 
 
1551
  new_state["show_incorrect_guesses"] = show_incorrect
1552
- return new_state
 
 
 
 
1553
 
1554
  show_incorrect_guesses.change(
1555
  fn=handle_show_incorrect_change,
1556
- inputs=[show_incorrect_guesses, game_state],
1557
- outputs=[game_state]
1558
  )
1559
 
1560
- # Enable free letters toggle handler
1561
- def handle_enable_free_letters_change(enabled, state):
1562
  state = ensure_state(state)
1563
- new_state = copy.deepcopy(state)
 
 
 
 
 
 
1564
  new_state["enable_free_letters"] = enabled
1565
- # Update message based on setting
1566
  if not enabled:
1567
  new_state["last_action"] = "Free letters disabled. Click grid cells to reveal letters!"
1568
- elif new_state.get("free_letters_used", 0) < MAX_FREE_LETTERS:
1569
- remaining = MAX_FREE_LETTERS - new_state.get("free_letters_used", 0)
1570
- new_state["last_action"] = f"Choose {remaining} free letter{'s' if remaining > 1 else ''} to start."
1571
  return build_ui_outputs(new_state)
1572
 
1573
  enable_free_letters.change(
1574
  fn=handle_enable_free_letters_change,
1575
- inputs=[enable_free_letters, game_state],
1576
  outputs=common_outputs
1577
  )
1578
 
1579
- # Show challenge links toggle handler
1580
- def handle_show_challenge_links_change(enabled, state):
1581
  state = ensure_state(state)
1582
- new_state = copy.deepcopy(state)
 
 
 
 
 
 
 
1583
  new_state["show_challenge_links"] = enabled
 
 
1584
  return build_ui_outputs(new_state)
1585
 
1586
  show_challenge_links.change(
1587
  fn=handle_show_challenge_links_change,
1588
- inputs=[show_challenge_links, game_state],
1589
  outputs=common_outputs
1590
  )
1591
 
1592
- # Share challenge button handler
1593
  def on_share_click(username, state):
1594
  """Handle share button click."""
1595
  status_text, url, new_state = handle_share_challenge(username, state)
@@ -1600,23 +1722,17 @@ def create_app() -> gr.Blocks:
1600
  new_state
1601
  )
1602
 
1603
- share_btn.click(
1604
  fn=on_share_click,
1605
  inputs=[username_input, game_state],
1606
  outputs=[share_status, share_url_display, game_state]
1607
  )
1608
 
1609
- # Modal open/close handlers
1610
- share_challenge_btn.click(
1611
- fn=lambda: Modal(visible=True),
1612
- inputs=None,
1613
- outputs=share_modal
1614
- )
1615
-
1616
  close_modal_btn.click(
1617
  fn=lambda: Modal(visible=False),
1618
  inputs=None,
1619
- outputs=share_modal
1620
  )
1621
 
1622
  # Timer tick handler - updates score panel with current elapsed time
 
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: 270
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