|
5 | 5 | import datetime |
6 | 6 | import logging |
7 | 7 | import random |
8 | | -from typing import List, Optional, Set, cast |
| 8 | +import json |
| 9 | +from typing import List, Optional, Set, Dict, Any, Tuple, cast |
9 | 10 |
|
10 | | -from nicegui import ui |
| 11 | +from nicegui import ui, app |
11 | 12 |
|
12 | 13 | from src.config.constants import ( |
13 | 14 | CLOSED_HEADER_TEXT, |
@@ -103,6 +104,9 @@ def toggle_tile(row: int, col: int) -> None: |
103 | 104 | clicked_tiles.add(key) |
104 | 105 |
|
105 | 106 | check_winner() |
| 107 | + |
| 108 | + # Save state to storage after each tile toggle for persistence |
| 109 | + save_state_to_storage() |
106 | 110 |
|
107 | 111 | for view_key, (container, tile_buttons_local) in board_views.items(): |
108 | 112 | for (r, c), tile in tile_buttons_local.items(): |
@@ -143,6 +147,10 @@ def toggle_tile(row: int, col: int) -> None: |
143 | 147 | }, 50); |
144 | 148 | """ |
145 | 149 | ui.run_javascript(js_code) |
| 150 | + |
| 151 | + # In NiceGUI 2.11+, updates are automatically synchronized between clients |
| 152 | + # via the timer-based sync_board_state function running frequently |
| 153 | + logging.debug("UI updates will be synchronized by timers") |
146 | 154 | except Exception as e: |
147 | 155 | logging.debug(f"JavaScript execution failed: {e}") |
148 | 156 |
|
@@ -265,6 +273,9 @@ def reset_board() -> None: |
265 | 273 | for c, phrase in enumerate(row): |
266 | 274 | if phrase.upper() == FREE_SPACE_TEXT: |
267 | 275 | clicked_tiles.add((r, c)) |
| 276 | + |
| 277 | + # Save state after reset for persistence |
| 278 | + save_state_to_storage() |
268 | 279 |
|
269 | 280 |
|
270 | 281 | def generate_new_board(phrases: List[str]) -> None: |
@@ -322,19 +333,17 @@ def close_game() -> None: |
322 | 333 | if controls_row is not None: |
323 | 334 | controls_row.clear() |
324 | 335 | with controls_row: |
325 | | - with ui.button("", icon="autorenew", on_click=reopen_game).classes( |
326 | | - "rounded-full w-12 h-12" |
| 336 | + with ui.button("Start New Game", icon="autorenew", on_click=reopen_game).classes( |
| 337 | + "px-4 py-2" |
327 | 338 | ) as new_game_btn: |
328 | | - ui.tooltip("Start New Game") |
| 339 | + ui.tooltip("Start a new game with a fresh board") |
329 | 340 |
|
330 | | - # Update stream page as well - this will trigger sync_board_state on connected clients |
331 | | - # Note: ui.broadcast() was used in older versions of NiceGUI, but may not be available |
332 | | - try: |
333 | | - ui.broadcast() # Broadcast changes to all connected clients |
334 | | - except AttributeError: |
335 | | - # In newer versions of NiceGUI, broadcast might not be available |
336 | | - # We rely on the timer-based sync instead |
337 | | - logging.info("ui.broadcast not available, relying on timer-based sync") |
| 341 | + # Save game state with is_game_closed=True for persistence |
| 342 | + save_state_to_storage() |
| 343 | + |
| 344 | + # In NiceGUI 2.11+, updates are automatically synchronized between clients |
| 345 | + # via the timer-based sync_board_state function |
| 346 | + logging.info("Game closed - changes will be synchronized by timers") |
338 | 347 |
|
339 | 348 | # Notify that game has been closed |
340 | 349 | ui.notify("Game has been closed", color="red", duration=3) |
@@ -385,11 +394,86 @@ def reopen_game() -> None: |
385 | 394 | # Notify that a new game has started |
386 | 395 | ui.notify("New game started", color="green", duration=3) |
387 | 396 |
|
388 | | - # Update stream page and all other connected clients |
389 | | - # This will trigger sync_board_state on all clients including the stream view |
| 397 | + # In NiceGUI 2.11+, updates are automatically synchronized between clients |
| 398 | + # via the timer-based sync_board_state function |
| 399 | + logging.info("Game reopened - changes will be synchronized by timers") |
| 400 | + |
| 401 | + # Save state to storage for persistence across app restarts |
| 402 | + save_state_to_storage() |
| 403 | + |
| 404 | + |
| 405 | +def save_state_to_storage() -> bool: |
| 406 | + """ |
| 407 | + Save the current game state to app.storage.general for persistence |
| 408 | + across application restarts. |
| 409 | + |
| 410 | + Returns: |
| 411 | + bool: True if state was saved successfully, False otherwise |
| 412 | + """ |
| 413 | + try: |
| 414 | + if not hasattr(app, 'storage') or not hasattr(app.storage, 'general'): |
| 415 | + logging.warning("app.storage.general not available") |
| 416 | + return False |
| 417 | + |
| 418 | + # Convert non-JSON-serializable types to serializable equivalents |
| 419 | + clicked_tiles_list = list(tuple(coord) for coord in clicked_tiles) |
| 420 | + bingo_patterns_list = list(bingo_patterns) |
| 421 | + |
| 422 | + # Prepare state dictionary |
| 423 | + state = { |
| 424 | + 'board': board, |
| 425 | + 'clicked_tiles': clicked_tiles_list, |
| 426 | + 'bingo_patterns': bingo_patterns_list, |
| 427 | + 'board_iteration': board_iteration, |
| 428 | + 'is_game_closed': is_game_closed, |
| 429 | + 'today_seed': today_seed |
| 430 | + } |
| 431 | + |
| 432 | + # Save to storage |
| 433 | + app.storage.general['game_state'] = state |
| 434 | + logging.debug("Game state saved to persistent storage") |
| 435 | + return True |
| 436 | + except Exception as e: |
| 437 | + logging.error(f"Error saving state to storage: {e}") |
| 438 | + return False |
| 439 | + |
| 440 | + |
| 441 | +def load_state_from_storage() -> bool: |
| 442 | + """ |
| 443 | + Load game state from app.storage.general if available. |
| 444 | + |
| 445 | + Returns: |
| 446 | + bool: True if state was loaded successfully, False otherwise |
| 447 | + """ |
| 448 | + global board, clicked_tiles, bingo_patterns, board_iteration, is_game_closed, today_seed |
| 449 | + |
390 | 450 | try: |
391 | | - ui.broadcast() # Broadcast changes to all connected clients |
392 | | - except AttributeError: |
393 | | - # In newer versions of NiceGUI, broadcast might not be available |
394 | | - # We rely on the timer-based sync instead |
395 | | - logging.info("ui.broadcast not available, relying on timer-based sync") |
| 451 | + if not hasattr(app, 'storage') or not hasattr(app.storage, 'general'): |
| 452 | + logging.warning("app.storage.general not available") |
| 453 | + return False |
| 454 | + |
| 455 | + if 'game_state' not in app.storage.general: |
| 456 | + logging.debug("No saved game state found in storage") |
| 457 | + return False |
| 458 | + |
| 459 | + state = app.storage.general['game_state'] |
| 460 | + |
| 461 | + # Load board |
| 462 | + board = state['board'] |
| 463 | + |
| 464 | + # Convert clicked_tiles from list back to set |
| 465 | + clicked_tiles = set(tuple(coord) for coord in state['clicked_tiles']) |
| 466 | + |
| 467 | + # Convert bingo_patterns from list back to set |
| 468 | + bingo_patterns = set(state['bingo_patterns']) |
| 469 | + |
| 470 | + # Load other state variables |
| 471 | + board_iteration = state['board_iteration'] |
| 472 | + is_game_closed = state['is_game_closed'] |
| 473 | + today_seed = state['today_seed'] |
| 474 | + |
| 475 | + logging.debug("Game state loaded from persistent storage") |
| 476 | + return True |
| 477 | + except Exception as e: |
| 478 | + logging.error(f"Error loading state from storage: {e}") |
| 479 | + return False |
0 commit comments