Skip to content

Save game state#155

Open
markrcote wants to merge 11 commits intomainfrom
save-game-state
Open

Save game state#155
markrcote wants to merge 11 commits intomainfrom
save-game-state

Conversation

@markrcote
Copy link
Owner

No description provided.

markrcote and others added 9 commits February 2, 2026 22:30
Add schema for persisting blackjack game state:
- games table stores full game state including deck, hands, bets, timers
- game_channels table maps games to Discord channels for bot recovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add functions for serializing/deserializing cards and players:
- card_to_str/str_to_card: Convert cards to/from "H10", "S14" format
- serialize_hand/deserialize_hand: Handle lists of cards
- serialize_player/deserialize_player: Handle player state with hands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Full game state serialization/deserialization:
- to_dict() serializes all game state including deck, hands, bets, timing
- from_dict() restores game with timing adjustment to preserve elapsed time
- Timing fields are offset relative to time_last_event

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add CRUD methods for game state persistence:
- save_game/load_game: Save and load individual game state
- load_all_active_games: Load all games on server startup
- delete_game: Remove game when finished
- save_game_channel/load_game_channels/delete_game_channel: Bot recovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test card, player, and game serialization roundtrips:
- Card and hand serialization format verification
- Player state preservation
- Full game state roundtrip through to_dict/from_dict
- Timing adjustment logic verification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Server-side persistence:
- Load games from database on startup via _load_games_from_db()
- Save games after player actions in listen()
- Save games after state changes in tick()
- Save channel info on new_game for bot recovery
- Accept guild_id/channel_id in new_game requests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Server handles list_games request:
- Bot sends list_games with request_id
- Server responds with all active games and their channel associations
- Response includes game_id, state, guild_id, channel_id

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bot recovery protocol:
- On ready, bot sends list_games request to server
- Handles list_games response and recreates game wrappers
- Subscribes to game topics and announces reconnection
- Include guild_id/channel_id in new_game requests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test server restart game persistence:
- Game state preserved across server restart
- Player bets preserved (no double-deduction)
- list_games returns active games with channel info for bot recovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements game state persistence to allow the SaloonBot server and Discord bot to survive restarts without losing active games. The changes enable games to be saved to MySQL and restored on server startup, with the bot able to reconnect to ongoing games.

Changes:

  • Added database tables (games and game_channels) and operations for saving/loading game state
  • Implemented serialization/deserialization methods for complete game state including cards, players, bets, and timing information
  • Added bot recovery mechanism to restore game tracking when bot reconnects

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
cardgames/database.py Added database schema for games/game_channels tables and CRUD operations for game state persistence
cardgames/blackjack.py Added serialization helpers and to_dict/from_dict methods for game state with timing adjustment logic
cardgames/casino.py Added game loading on startup, auto-save after actions/state changes, and list_games handler for bot recovery
bot.py Added list_games request on startup and game restoration logic to resubscribe to active games
test.py Added comprehensive unit tests for serialization/deserialization roundtrip and timing adjustments
test_e2e.py Added E2E tests for game persistence across server restarts and list_games functionality

Comment on lines +177 to +181
# Try to get the channel
channel = self.bot.get_channel(channel_id)
if not channel:
logging.warning(f"Could not find channel {channel_id} for game {game_id}")
continue
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the bot cannot find a channel during game restoration, the game remains active in the database but the bot skips tracking it. This creates orphaned games that persist in the database but cannot be interacted with.

Consider notifying the server to clean up games where the bot cannot restore the channel, or implementing a mechanism to mark these games as abandoned so they can be cleaned up later.

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +233
return {
'game_id': result['game_id'],
'state': result['state'],
'current_player_idx': result['current_player_idx'],
'time_betting_started': result['time_betting_started'],
'time_last_hand_ended': result['time_last_hand_ended'],
'time_last_event': result['time_last_event'],
'deck': json.loads(result['deck_json']),
'discards': json.loads(result['discards_json']),
'dealer_hand': json.loads(result['dealer_hand_json']),
'players': json.loads(result['players_json']),
'players_waiting': json.loads(result['players_waiting_json']),
'bets': json.loads(result['bets_json']),
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the JSON data stored in the database becomes corrupted or malformed, the json.loads() calls will raise a json.JSONDecodeError which is not caught. This will cause the entire game loading process to fail with an unhandled exception.

Consider wrapping the JSON parsing in try-except blocks and either:

  1. Logging a warning and skipping corrupted game records
  2. Implementing a fallback or recovery mechanism
  3. At minimum, catching and re-raising with more context about which game and field failed to parse

Copilot uses AI. Check for mistakes.
Comment on lines +248 to +261
games.append({
'game_id': result['game_id'],
'state': result['state'],
'current_player_idx': result['current_player_idx'],
'time_betting_started': result['time_betting_started'],
'time_last_hand_ended': result['time_last_hand_ended'],
'time_last_event': result['time_last_event'],
'deck': json.loads(result['deck_json']),
'discards': json.loads(result['discards_json']),
'dealer_hand': json.loads(result['dealer_hand_json']),
'players': json.loads(result['players_json']),
'players_waiting': json.loads(result['players_waiting_json']),
'bets': json.loads(result['bets_json']),
})
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the issue in load_game, corrupted JSON data will cause load_all_active_games to fail with an unhandled exception. This is particularly problematic because it happens during server startup in _load_games_from_db, which could prevent the entire server from starting.

Consider implementing error recovery to skip corrupted games and continue loading valid ones, with appropriate logging.

Suggested change
games.append({
'game_id': result['game_id'],
'state': result['state'],
'current_player_idx': result['current_player_idx'],
'time_betting_started': result['time_betting_started'],
'time_last_hand_ended': result['time_last_hand_ended'],
'time_last_event': result['time_last_event'],
'deck': json.loads(result['deck_json']),
'discards': json.loads(result['discards_json']),
'dealer_hand': json.loads(result['dealer_hand_json']),
'players': json.loads(result['players_json']),
'players_waiting': json.loads(result['players_waiting_json']),
'bets': json.loads(result['bets_json']),
})
try:
game = {
'game_id': result['game_id'],
'state': result['state'],
'current_player_idx': result['current_player_idx'],
'time_betting_started': result['time_betting_started'],
'time_last_hand_ended': result['time_last_hand_ended'],
'time_last_event': result['time_last_event'],
'deck': json.loads(result['deck_json']),
'discards': json.loads(result['discards_json']),
'dealer_hand': json.loads(result['dealer_hand_json']),
'players': json.loads(result['players_json']),
'players_waiting': json.loads(result['players_waiting_json']),
'bets': json.loads(result['bets_json']),
}
except json.JSONDecodeError as e:
logging.error(
"Error decoding JSON for game %s: %s",
result.get('game_id'),
e,
)
continue
games.append(game)

Copilot uses AI. Check for mistakes.
Comment on lines 132 to 142
@commands.Cog.listener()
async def on_ready(self):
if self.subscribe_task is None or self.subscribe_task.done():
self.subscribe_task = asyncio.create_task(self.try_subscribe())
if not self.listen.is_running():
self.listen.start()

# Request list of active games for recovery
await self._request_list_games()

logging.info("Blackjack cog initialized.")
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _request_list_games call happens in on_ready before the subscribed Event is set, which means the bot might attempt to publish the list_games request before it's subscribed to the response channel. While the error is caught, the bot won't receive the response if the server replies before the subscription is complete.

Consider waiting for the subscription to complete before requesting the list of games, or implementing a retry mechanism if no response is received within a timeout period.

Copilot uses AI. Check for mistakes.
markrcote and others added 2 commits February 15, 2026 22:34
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments