Database System
SQLite-based persistent storage for game data and statistics.
Overview
The Liar's Dice database system provides persistent storage for game history, player statistics, AI behavior patterns, and performance metrics. Built on SQLite for portability and ease of deployment, the system includes connection pooling, schema versioning, and comprehensive error handling.
Architecture
The database system consists of several key components:
Database Manager
The liarsdice::
- Connection lifecycle management
- Transaction support with RAII semantics
- Prepared statement caching
- Error handling and recovery
// Example usage DatabaseManager db("game_data.db"); db.initialize(); auto transaction = db.beginTransaction(); db.saveGameSession(session); db.updatePlayerStatistics(playerId, stats); transaction.commit();
Connection Pool
The connection pool (managed by liarsdice::
- Thread-safe connection acquisition and release
- Configurable pool size (default: 4 connections)
- Automatic connection validation and recycling
- Connection timeout handling
ConnectionPool pool("game_data.db", 4); { auto conn = pool.acquire(); conn->execute("INSERT INTO games ..."); } // Connection automatically returned to pool
Schema Manager
The liarsdice::
- Version tracking for schema upgrades
- Automatic migration execution
- Rollback support for failed migrations
- Schema validation
Database Schema
Core Tables (V001 - Initial Schema)
players - Player account information
CREATE TABLE players ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, email TEXT UNIQUE, total_games INTEGER DEFAULT 0, games_won INTEGER DEFAULT 0, games_lost INTEGER DEFAULT 0, total_points INTEGER DEFAULT 0, highest_score INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, is_active BOOLEAN DEFAULT 1 );
game_history - Game session records
CREATE TABLE game_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_uuid TEXT NOT NULL UNIQUE, started_at TIMESTAMP NOT NULL, ended_at TIMESTAMP, total_players INTEGER NOT NULL, total_rounds INTEGER DEFAULT 0, winner_id INTEGER, game_mode TEXT DEFAULT 'classic', is_completed BOOLEAN DEFAULT 0, FOREIGN KEY (winner_id) REFERENCES players(id) );
game_participants - Player participation in games
CREATE TABLE game_participants ( game_id INTEGER NOT NULL, player_id INTEGER NOT NULL, starting_position INTEGER NOT NULL, final_position INTEGER, points_scored INTEGER DEFAULT 0, rounds_survived INTEGER DEFAULT 0, total_guesses INTEGER DEFAULT 0, successful_calls INTEGER DEFAULT 0, bluffs_attempted INTEGER DEFAULT 0, bluffs_caught INTEGER DEFAULT 0, PRIMARY KEY (game_id, player_id), FOREIGN KEY (game_id) REFERENCES game_history(id), FOREIGN KEY (player_id) REFERENCES players(id) );
achievements - Achievement definitions
CREATE TABLE achievements ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, description TEXT, points INTEGER DEFAULT 0, icon_path TEXT, category TEXT DEFAULT 'general' );
player_achievements - Player earned achievements
CREATE TABLE player_achievements ( player_id INTEGER NOT NULL, achievement_id INTEGER NOT NULL, earned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, game_id INTEGER, PRIMARY KEY (player_id, achievement_id), FOREIGN KEY (player_id) REFERENCES players(id), FOREIGN KEY (achievement_id) REFERENCES achievements(id), FOREIGN KEY (game_id) REFERENCES game_history(id) );
Statistics Tables (V002 - Statistics)
game_rounds - Round-by-round game details
CREATE TABLE game_rounds ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_id INTEGER NOT NULL, round_number INTEGER NOT NULL, starting_player_id INTEGER NOT NULL, ending_player_id INTEGER, total_dice INTEGER NOT NULL, final_guess_count INTEGER, final_guess_face INTEGER, actual_count INTEGER, liar_called BOOLEAN DEFAULT 0, duration_seconds INTEGER, FOREIGN KEY (game_id) REFERENCES game_history(id), FOREIGN KEY (starting_player_id) REFERENCES players(id), FOREIGN KEY (ending_player_id) REFERENCES players(id) );
player_actions - Individual player actions
CREATE TABLE player_actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_id INTEGER NOT NULL, round_number INTEGER NOT NULL, player_id INTEGER NOT NULL, action_type TEXT NOT NULL, -- 'guess', 'call_liar' action_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, guess_count INTEGER, guess_face INTEGER, was_bluff BOOLEAN, was_correct BOOLEAN, FOREIGN KEY (game_id) REFERENCES game_history(id), FOREIGN KEY (player_id) REFERENCES players(id) );
player_statistics - Aggregated player statistics
CREATE TABLE player_statistics ( player_id INTEGER PRIMARY KEY, avg_game_duration INTEGER DEFAULT 0, avg_rounds_per_game REAL DEFAULT 0, win_rate REAL DEFAULT 0, favorite_dice_face INTEGER, bluff_success_rate REAL DEFAULT 0, call_accuracy_rate REAL DEFAULT 0, most_played_hour INTEGER, streak_current INTEGER DEFAULT 0, streak_best INTEGER DEFAULT 0, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (player_id) REFERENCES players(id) );
leaderboard_history - Historical leaderboard snapshots
CREATE TABLE leaderboard_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_date DATE NOT NULL, rank INTEGER NOT NULL, player_id INTEGER NOT NULL, elo_rating INTEGER NOT NULL, -- Player's ELO rating at snapshot time games_played INTEGER NOT NULL, win_rate REAL NOT NULL, UNIQUE(snapshot_date, rank), FOREIGN KEY (player_id) REFERENCES players(id) );
Common Operations
Saving Game Data
void saveGame(const Game& game) { DatabaseManager db("game_data.db"); // Begin transaction for consistency auto transaction = db.beginTransaction(); try { // Save game history record auto gameId = db.saveGameHistory({ .gameUuid = game.getUuid(), .startedAt = game.getStartTime(), .endedAt = game.getEndTime(), .totalPlayers = game.getPlayerCount(), .totalRounds = game.getRoundCount(), .winnerId = game.getWinner()->getId(), .gameMode = "classic", .isCompleted = true }); // Save participant details for (const auto& player : game.getPlayers()) { db.saveGameParticipant(gameId, player->getId(), { .startingPosition = player->getStartPosition(), .finalPosition = player->getFinalPosition(), .pointsScored = player->getScore(), .roundsSurvived = player->getRoundsSurvived(), .totalGuesses = player->getGuessCount(), .successfulCalls = player->getSuccessfulCalls(), .bluffsAttempted = player->getBluffCount(), .bluffsCaught = player->getBluffsCaught() }); } // Save round details (V002 schema) for (const auto& round : game.getRounds()) { db.saveGameRound(gameId, round); } transaction.commit(); } catch (const std::exception& e) { // Transaction automatically rolls back throw; } }
Querying Statistics
PlayerStatistics getPlayerStats(const std::string& username) { DatabaseManager db("game_data.db"); auto stmt = db.prepare(R"( SELECT p.total_games, p.games_won, CAST(p.games_won AS REAL) / NULLIF(p.total_games, 0) as win_rate, p.total_points, p.highest_score, ps.bluff_success_rate, ps.call_accuracy_rate, ps.avg_rounds_per_game, ps.streak_current, ps.streak_best FROM players p LEFT JOIN player_statistics ps ON p.id = ps.player_id WHERE p.username = ? )"); stmt.bind(1, username); if (stmt.step()) { return { .totalGames = stmt.column<int>(0), .gamesWon = stmt.column<int>(1), .winRate = stmt.column<double>(2), .totalPoints = stmt.column<int>(3), .highestScore = stmt.column<int>(4), .bluffSuccessRate = stmt.column<double>(5), .callAccuracyRate = stmt.column<double>(6), .avgRoundsPerGame = stmt.column<double>(7), .currentStreak = stmt.column<int>(8), .bestStreak = stmt.column<int>(9) }; } throw std::runtime_error("Player not found"); }
Achievement Tracking
void checkAndAwardAchievements(int playerId, int gameId) { DatabaseManager db("game_data.db"); // Check for perfect game achievement auto stmt = db.prepare(R"( SELECT points_scored FROM game_participants WHERE game_id = ? AND player_id = ? )"); stmt.bind(1, gameId); stmt.bind(2, playerId); if (stmt.step()) { int pointsScored = stmt.column<int>(0); // Award perfect game achievement if applicable if (pointsScored == 5) { // No points lost db.execute(R"( INSERT OR IGNORE INTO player_achievements (player_id, achievement_id, game_id) SELECT ?, id, ? FROM achievements WHERE code = 'perfect_game' )", playerId, gameId); } } }
Performance Considerations
Indexing Strategy
Key indexes for optimal query performance (created by migrations):
-- Player lookups CREATE INDEX idx_players_username ON players(username); CREATE INDEX idx_players_email ON players(email); CREATE INDEX idx_players_last_login ON players(last_login); -- Game history queries CREATE INDEX idx_game_history_uuid ON game_history(game_uuid); CREATE INDEX idx_game_history_dates ON game_history(started_at, ended_at); -- Round and action queries (V002) CREATE INDEX idx_game_rounds_game ON game_rounds(game_id); CREATE INDEX idx_player_actions_game ON player_actions(game_id, round_number); CREATE INDEX idx_player_actions_player ON player_actions(player_id); -- Leaderboard queries (V002) CREATE INDEX idx_leaderboard_date ON leaderboard_history(snapshot_date);
Query Optimization
- Use prepared statements for repeated queries
- Batch inserts within transactions
- Use connection pooling for concurrent access
- Implement caching for frequently accessed statistics
- Regular VACUUM operations for database maintenance
Error Handling
The database system uses a comprehensive exception hierarchy:
try { db.executeQuery(sql); } catch (const DatabaseConnectionError& e) { // Handle connection failures } catch (const DatabaseConstraintError& e) { // Handle constraint violations } catch (const DatabaseTimeoutError& e) { // Handle timeout conditions } catch (const DatabaseError& e) { // Handle general database errors }
Testing Strategy
Database testing includes:
- Unit tests for each database component
- Integration tests for transaction handling
- Performance benchmarks for query optimization
- Stress tests for connection pooling
- Migration tests for schema updates