Skip to content

Persistent Storage

Save data across games for leaderboards, ELO, and achievements.

Overview

persistentStorage provides cross-game data storage that survives beyond individual game sessions.

Basic Operations

Increment Values

// Increment wins by 1
await this.persistentStorage.increment('players', oderId, 'wins', 1);

// Increment ELO by 25
await this.persistentStorage.increment('players', oderId, 'elo', 25);

// Decrement ELO by 15
await this.persistentStorage.increment('players', oderId, 'elo', -15);

Get Ordered Data

// Top 10 players by ELO (descending)
const topByElo = await this.persistentStorage.getOrderedDesc(
  'players',  // collection
  'elo',      // field
  10          // limit
);

// Result: [{ oderId: '...', elo: 2100 }, { oderId: '...', elo: 2050 }, ...]

Common Use Cases

Leaderboard System

async onGameEnd(game: Game, winnerId: PlayerID): Promise<void> {
  // Update winner stats
  await this.persistentStorage.increment('players', winnerId, 'wins', 1);
  await this.persistentStorage.increment('players', winnerId, 'elo', 25);

  // Update all players' games played
  for (const player of game.players()) {
    await this.persistentStorage.increment('players', player.oderId(), 'gamesPlayed', 1);
  }
}

// Scheduler job to compute leaderboard
async computeLeaderboard(): Promise<void> {
  const topPlayers = await this.persistentStorage.getOrderedDesc(
    'players',
    'elo', 
    100
  );

  // Cache or process leaderboard
  console.log('Top player:', topPlayers[0]);
}

ELO Rating System

async updateElo(winnerId: PlayerID, loserId: PlayerID): Promise<void> {
  // Simple ELO calculation
  const K = 32;

  // Get current ratings (simplified - you'd fetch these)
  const winnerRating = 1500;
  const loserRating = 1500;

  // Calculate expected scores
  const expectedWinner = 1 / (1 + Math.pow(10, (loserRating - winnerRating) / 400));
  const expectedLoser = 1 / (1 + Math.pow(10, (winnerRating - loserRating) / 400));

  // Calculate rating changes
  const winnerChange = Math.round(K * (1 - expectedWinner));
  const loserChange = Math.round(K * (0 - expectedLoser));

  // Update ratings
  await this.persistentStorage.increment('players', winnerId, 'elo', winnerChange);
  await this.persistentStorage.increment('players', loserId, 'elo', loserChange);
}

Team ELO

async distributeTeamElo(
  winningTeam: string[],
  losingTeam: string[]
): Promise<void> {
  const winBonus = 25;
  const lossPenalty = 15;

  // Award winning team
  for (const oderId of winningTeam) {
    await this.persistentStorage.increment('players', oderId, 'elo', winBonus);
    await this.persistentStorage.increment('players', oderId, 'teamWins', 1);
  }

  // Penalize losing team
  for (const oderId of losingTeam) {
    await this.persistentStorage.increment('players', oderId, 'elo', -lossPenalty);
    await this.persistentStorage.increment('players', oderId, 'teamLosses', 1);
  }
}

Clan Statistics

async updateClanStats(clanId: string, territory: number): Promise<void> {
  // Track clan performance
  await this.persistentStorage.increment('clans', clanId, 'totalTerritory', territory);
  await this.persistentStorage.increment('clans', clanId, 'gamesPlayed', 1);
}

async getClanLeaderboard(): Promise<unknown[]> {
  return this.persistentStorage.getOrderedDesc('clans', 'totalTerritory', 10);
}

Achievement Tracking

async checkAchievements(oderId: string, stats: PlayerStats): Promise<void> {
  // First Win
  if (stats.wins === 1) {
    await this.unlockAchievement(playerId, 'first_win');
  }

  // Dominator - 100 wins
  if (stats.wins >= 100) {
    await this.unlockAchievement(playerId, 'dominator');
  }

  // Elite - 2000+ ELO
  if (stats.elo >= 2000) {
    await this.unlockAchievement(playerId, 'elite');
  }
}

async unlockAchievement(oderId: string, achievementId: string): Promise<void> {
  await this.persistentStorage.increment(
    'achievements', 
    `${playerId}:${achievementId}`, 
    'unlocked', 
    1
  );
}

Best Practices

Async Handling

All storage operations are async. Use in lifecycle hooks like onGameEnd, not in execute().

Rate Limiting

Batch updates when possible to reduce database load.

Data Structure

Plan your collections and fields carefully. Storage schema changes require migrations.

Consistency

Storage updates are eventually consistent. Don't depend on immediate reads after writes.