All files / web/src/db/schema player-stats.ts

100% Statements 85/85
100% Branches 1/1
100% Functions 1/1
100% Lines 85/85

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 862x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x  
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { players } from './players'
 
/**
 * Player stats table - game statistics per player
 *
 * Tracks aggregate performance and per-game breakdowns for each player.
 * One-to-one with players table. Deleted when player is deleted (cascade).
 */
export const playerStats = sqliteTable('player_stats', {
  /** Primary key and foreign key to players table */
  playerId: text('player_id')
    .primaryKey()
    .references(() => players.id, { onDelete: 'cascade' }),
 
  /** Total number of games played across all game types */
  gamesPlayed: integer('games_played').notNull().default(0),
 
  /** Total number of games won */
  totalWins: integer('total_wins').notNull().default(0),
 
  /** Total number of games lost */
  totalLosses: integer('total_losses').notNull().default(0),
 
  /** Best completion time in milliseconds (across all games) */
  bestTime: integer('best_time'),
 
  /** Highest accuracy percentage (0.0 - 1.0, across all games) */
  highestAccuracy: real('highest_accuracy').notNull().default(0),
 
  /** Player's most-played game type */
  favoriteGameType: text('favorite_game_type'),
 
  /**
   * Per-game statistics breakdown (JSON)
   *
   * Structure:
   * {
   *   "matching": {
   *     gamesPlayed: 10,
   *     wins: 5,
   *     losses: 5,
   *     bestTime: 45000,
   *     highestAccuracy: 0.95,
   *     averageScore: 12.5,
   *     lastPlayed: 1704326400000
   *   },
   *   "complement-race": { ... },
   *   ...
   * }
   */
  gameStats: text('game_stats', { mode: 'json' })
    .notNull()
    .default('{}')
    .$type<Record<string, GameStatsBreakdown>>(),
 
  /** When this player last played any game */
  lastPlayedAt: integer('last_played_at', { mode: 'timestamp' }),
 
  /** When this record was created */
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
 
  /** When this record was last updated */
  updatedAt: integer('updated_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
})
 
/**
 * Per-game stats breakdown stored in JSON
 */
export interface GameStatsBreakdown {
  gamesPlayed: number
  wins: number
  losses: number
  bestTime: number | null
  highestAccuracy: number
  averageScore: number
  lastPlayed: number // timestamp
}
 
export type PlayerStats = typeof playerStats.$inferSelect
export type NewPlayerStats = typeof playerStats.$inferInsert