| name | Achievements System |
| description | Rewarding players for completing specific goals through progress tracking, unlocking logic, rarity calculation, and achievement display for gamification and player engagement. |
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
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)
);
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)
);
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
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');
}
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
}
});
}
const newProgress = playerAchievement. + increment;
isComplete = newProgress >= (achievement. || );
playerAchievement = db..({
: { : playerAchievement. },
: {
: newProgress,
: isComplete,
: isComplete ? () :
}
});
(isComplete && !playerAchievement.) {
.(playerId, achievement.);
}
{
: achievement.,
: achievement.,
: newProgress,
: achievement. || ,
: isComplete,
: (newProgress / (achievement. || )) *
};
}
(: , : , ?: ): <> {
achievements = db..({
: { : eventType }
});
( achievement achievements) {
.(playerId, achievement., );
}
}
(
: ,
:
): <> {
db..({
: { achievementId },
: {
achievementId,
:
},
: {
: { : }
}
});
.(achievementId);
.(playerId, achievementId);
.(playerId, achievementId);
}
(: ): <> {
totalPlayers = db..();
stats = db..({
: { achievementId }
});
(stats && totalPlayers > ) {
unlockRate = (stats. / totalPlayers) * ;
db..({
: { achievementId },
: { unlockRate }
});
}
}
(
: ,
:
): <> {
achievement = db..({
: { : achievementId }
});
(achievement) {
io.().(, {
: achievement.,
: achievement.,
: achievement.,
: achievement.,
: achievement.
});
}
}
(: , : ): <> {
achievement = db..({
: { : achievementId }
});
(achievement && achievement. > ) {
db..({
: { : playerId },
: {
: { : achievement. }
}
});
}
}
}
{
: ;
: ;
: ;
: ;
: ;
: ;
}
Progress Calculation
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);
}
}
const multiStepAchievement = {
key: 'master_warrior',
: ,
: [
{ : , : , : },
{ : , : , : },
{ : , : , : }
]
};
(): <> {
steps = multiStepAchievement.;
completedSteps = ;
( step steps) {
progress = (playerId, step.);
(progress >= step.) {
completedSteps++;
}
}
(completedSteps / steps.) * ;
}
Unlocking Logic
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.(playerId, achievement);
:
;
}
}
(
: ,
:
): <> {
player = db..({ : { : playerId } });
(player?. || ) >= (achievement. || );
}
(
: ,
:
): <> {
player = db..({ : { : playerId } });
(player?. || ) >= (achievement. || );
}
(
: ,
:
): <> {
stats = db..({ : { playerId } });
(stats?. || ) >= (achievement. || );
}
(
: ,
:
): <> {
stats = db..({ : { playerId } });
(stats?. || ) >= (achievement. || );
}
(
: ,
:
): <> {
stats = db..({ : { playerId } });
(stats?. || ) >= (achievement. || );
}
}
Rarity Calculation
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: ,
: ,
: ,
:
};
colors[rarity] || colors.;
}
(: ): <> {
achievements = db..({
: { playerId, : },
: { : { : { : } } }
});
: <, > = {
: ,
: ,
: ,
: ,
:
};
( pa achievements) {
rarity = .(pa..);
rarityCount[rarity]++;
}
rarityCount;
}
}
= <, >;
Point System
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
export class AchievementSharingService {
async generateShareImage(
playerId: string,
achievementId: string
): Promise<string> {
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}`;
text = ;
(platform === ) {
;
} {
;
}
}
}
Achievement Display
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 && (
{progress?.progress || 0} / {achievement.requirementValue}
)}
{isUnlocked && progress?.unlockedAt && (
Unlocked: {new Date(progress.unlockedAt).toLocaleDateString()}
)}
{achievement.points} pts
{achievement.rarity}
);
}
{
: ;
?: ;
}
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., action)) {
(playerId, achievement.)
(playerId, achievement)
}
}
}
Production Checklist
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
if (kills >= 100) {
unlockAchievement('kill_100')
}
const progress = kills / 100
showProgress('kill_100', progress)
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