Skip to content
Achievements System logo

Achievements System

Rewarding players for completing specific goals through progress tracking, unlocking logic, rarity calculation, and achievement display for gamification and player engagement.

SKILL.md

Full skill instructions

Achievements System

Current Level: Intermediate
Domain: Gaming / Backend


Overview

Achievements reward players for completing specific goals. This guide covers progress tracking, unlocking logic, and rarity calculation for building engaging achievement systems that motivate players and increase retention.

Achievement Types

Progress-based

  • Track incremental progress
  • Example: "Kill 100 enemies"

Milestone

  • One-time achievements
  • Example: "Complete first level"

Hidden

  • Secret achievements
  • Revealed upon unlock

Rare

  • Difficult achievements
  • Limited unlock rate

Database Schema

-- achievements table
CREATE TABLE achievements (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  key VARCHAR(100) UNIQUE NOT NULL,
  
  name VARCHAR(255) NOT NULL,
  description TEXT,
  icon_url VARCHAR(500),
  
  type VARCHAR(50) NOT NULL,
  category VARCHAR(100),
  
  hidden BOOLEAN DEFAULT FALSE,
  
  points INTEGER DEFAULT 0,
  
  requirement_type VARCHAR(50),
  requirement_value INTEGER,
  
  created_at TIMESTAMP DEFAULT NOW(),
  
  INDEX idx_key (key),
  INDEX idx_category (category)
);

-- player_achievements table
CREATE TABLE player_achievements (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  player_id UUID REFERENCES players(id) ON DELETE CASCADE,
  achievement_id UUID REFERENCES achievements(id) ON DELETE CASCADE,
  
  progress INTEGER DEFAULT 0,
  unlocked BOOLEAN DEFAULT FALSE,
  unlocked_at TIMESTAMP,
  
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  
  UNIQUE(player_id, achievement_id),
  INDEX idx_player (player_id),
  INDEX idx_unlocked (unlocked, unlocked_at)
);

-- achievement_stats table
CREATE TABLE achievement_stats (
  achievement_id UUID PRIMARY KEY REFERENCES achievements(id),
  
  total_unlocks INTEGER DEFAULT 0,
  unlock_rate DECIMAL(5,2) DEFAULT 0,
  
  updated_at TIMESTAMP DEFAULT NOW()
);

Achievement Tracking

// services/​achievement.service.ts
export class AchievementService {
  async trackProgress(
    playerId: string,
    achievementKey: string,
    increment: number = 1
  ): Promise<AchievementProgress> {
    const achievement = await db.achievement.findUnique({
      where: { key: achievementKey }
    });

    if (!achievement) {
      throw new Error('Achievement not found');
    }

    // Get or create player achievement
    let playerAchievement = await db.playerAchievement.findUnique({
      where: {
        playerId_achievementId: {
          playerId,
          achievementId: achievement.id
        }
      }
    });

    if (!playerAchievement) {
      playerAchievement = await db.playerAchievement.create({
        data: {
          playerId,
          achievementId: achievement.id,
          progress: 0
        }
      });
    }

    // Update progress
    const newProgress = playerAchievement.progress + increment;
    const isComplete = newProgress >= (achievement.requirementValue || 1);

    playerAchievement = await db.playerAchievement.update({
      where: { id: playerAchievement.id },
      data: {
        progress: newProgress,
        unlocked: isComplete,
        unlockedAt: isComplete ? new Date() : null
      }
    });

    // If unlocked, update stats and notify
    if (isComplete && !playerAchievement.unlocked) {
      await this.onAchievementUnlocked(playerId, achievement.id);
    }

    return {
      achievementId: achievement.id,
      achievementKey: achievement.key,
      progress: newProgress,
      required: achievement.requirementValue || 1,
      unlocked: isComplete,
      percentage: (newProgress / (achievement.requirementValue || 1)) * 100
    };
  }

  async trackEvent(playerId: string, eventType: string, data?: any): Promise<void> {
    // Get achievements triggered by this event
    const achievements = await db.achievement.findMany({
      where: { requirementType: eventType }
    });

    for (const achievement of achievements) {
      await this.trackProgress(playerId, achievement.key, 1);
    }
  }

  private async onAchievementUnlocked(
    playerId: string,
    achievementId: string
  ): Promise<void> {
    // Update stats
    await db.achievementStats.upsert({
      where: { achievementId },
      create: {
        achievementId,
        totalUnlocks: 1
      },
      update: {
        totalUnlocks: { increment: 1 }
      }
    });

    // Calculate unlock rate
    await this.updateUnlockRate(achievementId);

    // Send notification
    await this.sendUnlockNotification(playerId, achievementId);

    // Award points
    await this.awardPoints(playerId, achievementId);
  }

  private async updateUnlockRate(achievementId: string): Promise<void> {
    const totalPlayers = await db.player.count();
    const stats = await db.achievementStats.findUnique({
      where: { achievementId }
    });

    if (stats && totalPlayers > 0) {
      const unlockRate = (stats.totalUnlocks / totalPlayers) * 100;

      await db.achievementStats.update({
        where: { achievementId },
        data: { unlockRate }
      });
    }
  }

  private async sendUnlockNotification(
    playerId: string,
    achievementId: string
  ): Promise<void> {
    const achievement = await db.achievement.findUnique({
      where: { id: achievementId }
    });

    if (achievement) {
      // Emit WebSocket event
      io.to(`player:${playerId}`).emit('achievement-unlocked', {
        id: achievement.id,
        name: achievement.name,
        description: achievement.description,
        iconUrl: achievement.iconUrl,
        points: achievement.points
      });
    }
  }

  private async awardPoints(playerId: string, achievementId: string): Promise<void> {
    const achievement = await db.achievement.findUnique({
      where: { id: achievementId }
    });

    if (achievement && achievement.points > 0) {
      await db.player.update({
        where: { id: playerId },
        data: {
          achievementPoints: { increment: achievement.points }
        }
      });
    }
  }
}

interface AchievementProgress {
  achievementId: string;
  achievementKey: string;
  progress: number;
  required: number;
  unlocked: boolean;
  percentage: number;
}

Progress Calculation

// Different progress calculation strategies
export class ProgressCalculator {
  calculateLinear(current: number, required: number): number {
    return Math.min((current / required) * 100, 100);
  }

  calculateTiered(current: number, tiers: number[]): number {
    let completedTiers = 0;
    
    for (const tier of tiers) {
      if (current >= tier) {
        completedTiers++;
      }
    }

    return (completedTiers / tiers.length) * 100;
  }

  calculateCumulative(values: number[], required: number): number {
    const total = values.reduce((sum, val) => sum + val, 0);
    return Math.min((total / required) * 100, 100);
  }
}

// Example: Multi-step achievement
const multiStepAchievement = {
  key: 'master_warrior',
  name: 'Master Warrior',
  steps: [
    { key: 'kills_100', name: 'Kill 100 enemies', required: 100 },
    { key: 'wins_50', name: 'Win 50 battles', required: 50 },
    { key: 'level_50', name: 'Reach level 50', required: 50 }
  ]
};

async function trackMultiStepProgress(playerId: string): Promise<number> {
  const steps = multiStepAchievement.steps;
  let completedSteps = 0;

  for (const step of steps) {
    const progress = await getStepProgress(playerId, step.key);
    if (progress >= step.required) {
      completedSteps++;
    }
  }

  return (completedSteps / steps.length) * 100;
}

Unlocking Logic

// services/​achievement-unlock.service.ts
export class AchievementUnlockService {
  async checkUnlockConditions(
    playerId: string,
    achievementKey: string
  ): Promise<boolean> {
    const achievement = await db.achievement.findUnique({
      where: { key: achievementKey }
    });

    if (!achievement) return false;

    switch (achievement.requirementType) {
      case 'score':
        return this.checkScoreRequirement(playerId, achievement);
      
      case 'level':
        return this.checkLevelRequirement(playerId, achievement);
      
      case 'kills':
        return this.checkKillsRequirement(playerId, achievement);
      
      case 'time_played':
        return this.checkTimePlayedRequirement(playerId, achievement);
      
      case 'consecutive_wins':
        return this.checkConsecutiveWinsRequirement(playerId, achievement);
      
      default:
        return false;
    }
  }

  private async checkScoreRequirement(
    playerId: string,
    achievement: Achievement
  ): Promise<boolean> {
    const player = await db.player.findUnique({ where: { id: playerId } });
    return (player?.highScore || 0) >= (achievement.requirementValue || 0);
  }

  private async checkLevelRequirement(
    playerId: string,
    achievement: Achievement
  ): Promise<boolean> {
    const player = await db.player.findUnique({ where: { id: playerId } });
    return (player?.level || 0) >= (achievement.requirementValue || 0);
  }

  private async checkKillsRequirement(
    playerId: string,
    achievement: Achievement
  ): Promise<boolean> {
    const stats = await db.playerStats.findUnique({ where: { playerId } });
    return (stats?.totalKills || 0) >= (achievement.requirementValue || 0);
  }

  private async checkTimePlayedRequirement(
    playerId: string,
    achievement: Achievement
  ): Promise<boolean> {
    const stats = await db.playerStats.findUnique({ where: { playerId } });
    return (stats?.totalPlayTime || 0) >= (achievement.requirementValue || 0);
  }

  private async checkConsecutiveWinsRequirement(
    playerId: string,
    achievement: Achievement
  ): Promise<boolean> {
    const stats = await db.playerStats.findUnique({ where: { playerId } });
    return (stats?.currentWinStreak || 0) >= (achievement.requirementValue || 0);
  }
}

Rarity Calculation

// services/​achievement-rarity.service.ts
export class AchievementRarityService {
  async calculateRarity(achievementId: string): Promise<string> {
    const stats = await db.achievementStats.findUnique({
      where: { achievementId }
    });

    if (!stats) return 'common';

    const unlockRate = stats.unlockRate;

    if (unlockRate >= 50) return 'common';
    if (unlockRate >= 20) return 'uncommon';
    if (unlockRate >= 5) return 'rare';
    if (unlockRate >= 1) return 'epic';
    return 'legendary';
  }

  async getRarityColor(rarity: string): Promise<string> {
    const colors: Record<string, string> = {
      common: '#808080',
      uncommon: '#00ff00',
      rare: '#0070dd',
      epic: '#a335ee',
      legendary: '#ff8000'
    };

    return colors[rarity] || colors.common;
  }

  async getPlayerRarityStats(playerId: string): Promise<RarityStats> {
    const achievements = await db.playerAchievement.findMany({
      where: { playerId, unlocked: true },
      include: { achievement: { include: { stats: true } } }
    });

    const rarityCount: Record<string, number> = {
      common: 0,
      uncommon: 0,
      rare: 0,
      epic: 0,
      legendary: 0
    };

    for (const pa of achievements) {
      const rarity = await this.calculateRarity(pa.achievement.id);
      rarityCount[rarity]++;
    }

    return rarityCount;
  }
}

type RarityStats = Record<string, number>;

Point System

// Achievement points based on rarity
const pointsByRarity: Record<string, number> = {
  common: 10,
  uncommon: 25,
  rare: 50,
  epic: 100,
  legendary: 250
};

async function assignPoints(achievementId: string): Promise<void> {
  const rarity = await rarityService.calculateRarity(achievementId);
  const points = pointsByRarity[rarity];

  await db.achievement.update({
    where: { id: achievementId },
    data: { points }
  });
}

Social Sharing

// services/​achievement-sharing.service.ts
export class AchievementSharingService {
  async generateShareImage(
    playerId: string,
    achievementId: string
  ): Promise<string> {
    // Generate image with achievement details
    // Return URL to generated image
    return `https://cdn.example.com/​achievements/​${achievementId}/​share.png`;
  }

  async shareToSocial(
    playerId: string,
    achievementId: string,
    platform: 'twitter' | 'facebook'
  ): Promise<string> {
    const achievement = await db.achievement.findUnique({
      where: { id: achievementId }
    });

    const player = await db.player.findUnique({
      where: { id: playerId }
    });

    if (!achievement || !player) {
      throw new Error('Not found');
    }

    const shareUrl = `https://game.example.com/​achievements/​${achievementId}`;
    const text = `I just unlocked "${achievement.name}" in Game! ${shareUrl}`;

    if (platform === 'twitter') {
      return `https://twitter.com/​intent/​tweet?text=${encodeURIComponent(text)}`;
    } else {
      return `https://www.facebook.com/​sharer/​sharer.php?u=${encodeURIComponent(shareUrl)}`;
    }
  }
}

Achievement Display

// components/​AchievementCard.tsx
export function AchievementCard({ achievement, progress }: AchievementCardProps) {
  const isUnlocked = progress?.unlocked || false;
  const percentage = progress?.percentage || 0;

  return (
    <div className={`achievement-card ${isUnlocked ? 'unlocked' : 'locked'}`}>
      <div className="achievement-icon">
        <img
          src={achievement.iconUrl}
          alt={achievement.name}
          style={{ filter: isUnlocked ? 'none' : 'grayscale(100%)' }}
        />
      </​div>

      <div className="achievement-info">
        <h3>{achievement.name}</​h3>
        <p>{achievement.hidden && !isUnlocked ? '???' : achievement.description}</​p>

        {!isUnlocked && achievement.requirementValue && (
          <div className="progress-bar">
            <div
              className="progress-fill"
              style={{ width: `${percentage}%` }}
            />
            <span>{progress?.progress || 0} / {achievement.requirementValue}</​span>
          </​div>
        )}

        {isUnlocked && progress?.unlockedAt && (
          <p className="unlock-date">
            Unlocked: {new Date(progress.unlockedAt).toLocaleDateString()}
          </​p>
        )}

        <div className="achievement-meta">
          <span className="points">{achievement.points} pts</​span>
          <span className={`rarity ${achievement.rarity}`}>
            {achievement.rarity}
          </​span>
        </​div>
      </​div>
    </​div>
  );
}

interface AchievementCardProps {
  achievement: Achievement;
  progress?: AchievementProgress;
}

Quick Start

Achievement System

interface Achievement {
  id: string
  name: string
  description: string
  type: 'progress' | 'milestone' | 'hidden'
  condition: AchievementCondition
  rarity: 'common' | 'rare' | 'epic' | 'legendary'
  points: number
}

interface AchievementCondition {
  type: 'kill_count' | 'level_complete' | 'time_played'
  target: number
}

async function checkAchievements(playerId: string, action: PlayerAction) {
  const achievements = await getUnlockedAchievements(playerId)
  const allAchievements = await getAllAchievements()
  
  for (const achievement of allAchievements) {
    if (achievements.includes(achievement.id)) continue
    
    if (checkCondition(achievement.condition, action)) {
      await unlockAchievement(playerId, achievement.id)
      await notifyPlayer(playerId, achievement)
    }
  }
}

Production Checklist

  • Achievement Design: Design achievement system
  • Progress Tracking: Track all relevant actions
  • Unlock Logic: Implement unlock conditions
  • Notifications: Celebrate unlocks
  • Rarity System: Calculate and display rarity
  • Points System: Reward with points/​badges
  • Social Features: Enable sharing
  • Categories: Organize achievements
  • Analytics: Track unlock rates
  • Balance: Mix easy and hard achievements
  • Testing: Test unlock conditions
  • Documentation: Document achievement system

Anti-patterns

❌ Don't: Too Easy or Too Hard

# ❌ Bad - Unbalanced
Achievement 1: "Play 1 game" (too easy)
Achievement 2: "Play 1,000,000 games" (too hard)
# ✅ Good - Balanced progression
Achievement 1: "Play 10 games"
Achievement 2: "Play 100 games"
Achievement 3: "Play 1,000 games"

❌ Don't: No Progress Indication

// ❌ Bad - No progress shown
if (kills >= 100) {
  unlockAchievement('kill_100')
}
// Player doesn't know progress!
// ✅ Good - Show progress
const progress = kills / 100
showProgress('kill_100', progress)  // "Kill 100 enemies: 45/​100"

Integration Points

  • Leaderboards (38-gaming-features/​leaderboards/) - Achievement rankings
  • Game Analytics (38-gaming-features/​game-analytics/) - Achievement metrics
  • Matchmaking (38-gaming-features/​matchmaking/) - Game sessions

Further Reading

Resources

More skills from diegosouzapw

accessibility logo
diegosouzapw/awesome-omni-skill

accessibility

Accessibility guidelines for VS Code features — covers accessibility help dialogs, accessible views, verbosity settings, accessibility signals, ARIA alerts/status announcements, keyboard navigation, and ARIA labels/roles. Applies to both new interactive UI surfaces and updates to existing feature...

62 1
View
mermaid-diagrams logo
diegosouzapw/awesome-omni-skill

mermaid-diagrams

Mermaid diagram creation for flowcharts, sequences, ERDs, and more. Generate diagrams from text in markdown files. Use for documentation, architecture diagrams, and visual representations. Triggers on mermaid, flowchart, sequence diagram, ERD, entity relationship, gantt chart, pie chart, class di...

62 1
View
tauri2-react-rust logo
diegosouzapw/awesome-omni-skill

tauri2-react-rust

Guides development of cross-platform desktop apps with Tauri 2, TypeScript, React, and Rust. Use when building Tauri apps, implementing IPC, designing Rust backend or TypeScript/React frontend, when researching or cloning a website (open site, snapshot elements), when verifying local dev or built...

62 1
View
fp-ts-errors logo
diegosouzapw/awesome-omni-skill

fp-ts-errors

Handle errors as values using fp-ts Either and TaskEither for cleaner, more predictable TypeScript code. Use when implementing error handling patterns with fp-ts.

62 0
View
ring:pre-dev-research logo
diegosouzapw/awesome-omni-skill

ring:pre-dev-research

Gate 0 research phase for pre-dev workflow. Dispatches 3 parallel research agents to gather codebase patterns, external best practices, and framework documentation BEFORE creating PRD/TRD. Outputs research.md with file:line references.

62 0
View
create-assistant logo
diegosouzapw/awesome-omni-skill

create-assistant

Create and configure Vapi voice AI assistants with models, voices, transcribers, tools, hooks, and advanced settings. Use when building voice agents, phone bots, customer support assistants, or any conversational AI that handles phone or web calls.

62 0
View
codex-headless logo
diegosouzapw/awesome-omni-skill

codex-headless

Delegiere Aufgaben an OpenAI Codex CLI im Headless-Mode. Nutzt ChatGPT Subscription (KEIN API Key). Nur Workspace-Zugriff, KEIN System-Zugriff. Auto-Accept mit --full-auto. Web-Recherche mit --search.

62 0
View
social-media-scheduler logo
diegosouzapw/awesome-omni-skill

social-media-scheduler

Generate a full week of social media content for any topic. Outputs platform-optimized posts for Twitter/X, LinkedIn, and Instagram with hashtags and posting times.

62 0
View
Data Privacy Compliance logo
diegosouzapw/awesome-omni-skill

Data Privacy Compliance

Data privacy and regulatory compliance specialist for GDPR, CCPA, HIPAA, and international data protection laws. Use when implementing privacy controls, conducting data protection impact assessments, ensuring regulatory compliance, or managing data subject rights. Expert in consent management, da...

62 0
View
triple-review logo
diegosouzapw/awesome-omni-skill

triple-review

Reviews a task using the TRIPLE protocol, as the review agent.

62 0
View
Tech Stack & Architecture Decision logo
diegosouzapw/awesome-omni-skill

Tech Stack & Architecture Decision

Define and document the technology stack and architecture decisions for a project. Use when the user needs to choose a tech stack, make architecture decisions, define infrastructure choices, or document technology selections. Triggers on requests like "define the tech stack", "choose technologies...

62 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View