Skip to main content

Leaderboard Best Practices

This guide covers best practices for implementing efficient and engaging leaderboard systems with Moitribe JavaScript SDK.

Performance Optimization

Caching Strategy

Implement intelligent caching to reduce API calls and improve user experience:

class LeaderboardCache {
constructor() {
this.cache = new Map();
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}

set(key, data) {
this.cache.set(key, {
data: data,
timestamp: Date.now()
});
}

get(key) {
const cached = this.cache.get(key);
if (!cached) return null;

if (Date.now() - cached.timestamp > this.cacheTimeout) {
this.cache.delete(key);
return null;
}

return cached.data;
}

clear() {
this.cache.clear();
}
}

const leaderboardCache = new LeaderboardCache();

// Cached leaderboard loading
function loadLeaderboardScores(leaderboardId, collection = 1, timespan = 0) {
const cacheKey = `${leaderboardId}_${collection}_${timespan}`;
const cached = leaderboardCache.get(cacheKey);

if (cached) {
console.log('Loading from cache...');
return Promise.resolve(cached);
}

return new Promise((resolve) => {
MoitribeSDK(GAME_ID, 'loadleaderboardtopscores', {
leaderboardid: leaderboardId,
collection: collection,
timespan: timespan,
maxresults: 25,
onlyData: true,
callback: function(result) {
if (result.success && result.scores) {
leaderboardCache.set(cacheKey, result.scores);
resolve(result.scores);
} else {
resolve([]);
}
}
});
});
}

Batch Operations

Group multiple operations to reduce network overhead:

class LeaderboardBatcher {
constructor() {
this.pending = [];
this.batchTimeout = 100; // 100ms
this.timer = null;
}

add(operation) {
this.pending.push(operation);

if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.batchTimeout);
}
}

flush() {
if (this.pending.length === 0) return;

const operations = [...this.pending];
this.pending = [];
this.timer = null;

// Process batched operations
this.processBatch(operations);
}

async processBatch(operations) {
// Group by leaderboard ID
const grouped = operations.reduce((acc, op) => {
if (!acc[op.leaderboardId]) acc[op.leaderboardId] = [];
acc[op.leaderboardId].push(op);
return acc;
}, {});

// Process each group
for (const [leaderboardId, ops] of Object.entries(grouped)) {
await this.processLeaderboardGroup(leaderboardId, ops);
}
}

async processLeaderboardGroup(leaderboardId, operations) {
// Get metadata once for the group
const metadata = await this.getMetadata(leaderboardId);

// Process all operations for this leaderboard
for (const op of operations) {
switch (op.type) {
case 'submit':
await this.submitScore(leaderboardId, op.score, op.tag);
break;
case 'load':
await this.loadScores(leaderboardId, op.collection, op.timespan);
break;
}
}
}
}

const batcher = new LeaderboardBatcher();

// Use batcher for multiple operations
function submitMultipleScores(scores) {
scores.forEach(score => {
batcher.add({
type: 'submit',
leaderboardId: score.leaderboardId,
score: score.value,
tag: score.tag
});
});
}

User Experience

Progressive Loading

Load data progressively to show content quickly:

class ProgressiveLeaderboardLoader {
constructor() {
this.currentPage = 0;
this.pageSize = 25;
this.isLoading = false;
this.hasMore = true;
}

async loadMore(leaderboardId, collection = 1, timespan = 0) {
if (this.isLoading || !this.hasMore) return;

this.isLoading = true;
this.updateLoadingUI(true);

try {
const newScores = await this.loadPage(leaderboardId, collection, timespan, this.currentPage);

if (newScores.length < this.pageSize) {
this.hasMore = false;
}

this.currentPage++;
this.appendScores(newScores);
this.updateLoadMoreButton(this.hasMore);

} catch (error) {
console.error('Failed to load more scores:', error);
this.showError('Failed to load more scores');
} finally {
this.isLoading = false;
this.updateLoadingUI(false);
}
}

loadPage(leaderboardId, collection, timespan, page) {
return new Promise((resolve) => {
MoitribeSDK(GAME_ID, 'loadleaderboardtopscores', {
leaderboardid: leaderboardId,
collection: collection,
timespan: timespan,
maxresults: this.pageSize,
offset: page * this.pageSize,
onlyData: true,
callback: function(result) {
resolve(result.success ? result.scores : []);
}
});
});
}

updateLoadingUI(isLoading) {
const loadingElement = document.getElementById('leaderboard-loading');
if (loadingElement) {
loadingElement.style.display = isLoading ? 'block' : 'none';
}
}

updateLoadMoreButton(hasMore) {
const button = document.getElementById('load-more-button');
if (button) {
button.style.display = hasMore ? 'block' : 'none';
}
}

appendScores(scores) {
const container = document.getElementById('leaderboard-scores');
if (!container) return;

scores.forEach(score => {
const scoreElement = this.createScoreElement(score);
container.appendChild(scoreElement);
});
}

createScoreElement(score) {
const div = document.createElement('div');
div.className = 'leaderboard-score';
div.innerHTML = `
<div class="rank">${score.rank || '-'}</div>
<div class="player">${score.playerName || 'Anonymous'}</div>
<div class="score">${this.formatScore(score.score)}</div>
<div class="date">${this.formatDate(score.timestamp)}</div>
`;
return div;
}

formatScore(score) {
if (score >= 1000000) {
return (score / 1000000).toFixed(1) + 'M';
} else if (score >= 1000) {
return (score / 1000).toFixed(1) + 'K';
}
return score.toString();
}

formatDate(timestamp) {
return new Date(timestamp).toLocaleDateString();
}

showError(message) {
const errorElement = document.getElementById('leaderboard-error');
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}
}
}

Real-time Updates

Implement real-time score updates for engaging experience:

class RealtimeLeaderboard {
constructor(leaderboardId, collection = 1, timespan = 0) {
this.leaderboardId = leaderboardId;
this.collection = collection;
this.timespan = timespan;
this.scores = [];
this.playerScore = null;
this.updateInterval = null;
}

startRealtimeUpdates() {
// Update every 30 seconds
this.updateInterval = setInterval(() => {
this.refreshScores();
}, 30000);

// Listen for player's own score updates
this.listenForScoreUpdates();
}

stopRealtimeUpdates() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}

async refreshScores() {
try {
const newScores = await this.loadScores();

if (this.hasScoresChanged(newScores)) {
this.scores = newScores;
this.updateUI();
this.animateChanges();
}
} catch (error) {
console.error('Failed to refresh scores:', error);
}
}

hasScoresChanged(newScores) {
if (this.scores.length !== newScores.length) return true;

return this.scores.some((oldScore, index) => {
const newScore = newScores[index];
return !newScore || oldScore.score !== newScore.score || oldScore.rank !== newScore.rank;
});
}

animateChanges() {
const scoreElements = document.querySelectorAll('.leaderboard-score');
scoreElements.forEach((element, index) => {
const oldScore = this.scores[index];
const newScore = this.scores[index];

if (!oldScore || !newScore) return;

if (oldScore.rank !== newScore.rank) {
element.classList.add('rank-changed');
setTimeout(() => element.classList.remove('rank-changed'), 1000);
}

if (oldScore.score !== newScore.score) {
element.classList.add('score-changed');
setTimeout(() => element.classList.remove('score-changed'), 1000);
}
});
}

listenForScoreUpdates() {
// Override score submission to detect player's own score changes
const originalSubmit = MoitribeSDK;
const self = this;

MoitribeSDK = function(gameId, method, params, callback) {
if (method === 'submitscore' && params.leaderboardid === self.leaderboardId) {
const wrappedCallback = function(result) {
if (result.success) {
self.updatePlayerScore(params.score);
}
if (callback) callback(result);
};
return originalSubmit.call(this, gameId, method, params, wrappedCallback);
}
return originalSubmit.call(this, gameId, method, params, callback);
};
}

updatePlayerScore(newScore) {
const oldScore = this.playerScore;
this.playerScore = newScore;

if (oldScore !== null && oldScore !== newScore) {
this.showScoreChangeNotification(oldScore, newScore);
}
}

showScoreChangeNotification(oldScore, newScore) {
const notification = document.createElement('div');
notification.className = 'score-notification';
notification.innerHTML = `
<div class="notification-content">
<h3>Score Updated!</h3>
<p>Your score changed from ${this.formatScore(oldScore)} to ${this.formatScore(newScore)}</p>
</div>
`;

document.body.appendChild(notification);

setTimeout(() => {
notification.classList.add('show');
}, 100);

setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
}

Data Management

Score Validation

Validate scores before submission to ensure data integrity:

class ScoreValidator {
constructor() {
this.minScore = 0;
this.maxScore = 999999999;
this.maxScoreTagLength = 100;
}

validateScore(score, tag = '') {
const errors = [];

// Score validation
if (typeof score !== 'number') {
errors.push('Score must be a number');
} else if (isNaN(score)) {
errors.push('Score cannot be NaN');
} else if (score < this.minScore) {
errors.push(`Score cannot be less than ${this.minScore}`);
} else if (score > this.maxScore) {
errors.push(`Score cannot exceed ${this.maxScore}`);
}

// Tag validation
if (typeof tag !== 'string') {
errors.push('Score tag must be a string');
} else if (tag.length > this.maxScoreTagLength) {
errors.push(`Score tag cannot exceed ${this.maxScoreTagLength} characters`);
}

return {
isValid: errors.length === 0,
errors: errors
};
}

sanitizeScoreTag(tag) {
if (typeof tag !== 'string') return '';

return tag
.trim()
.substring(0, this.maxScoreTagLength)
.replace(/[<>]/g, '') // Remove potential HTML
.replace(/javascript:/gi, ''); // Remove potential scripts
}
}

// Usage example
const validator = new ScoreValidator();

function submitValidatedScore(leaderboardId, score, tag = '') {
const sanitizedTag = validator.sanitizeScoreTag(tag);
const validation = validator.validateScore(score, sanitizedTag);

if (!validation.isValid) {
console.error('Score validation failed:', validation.errors);
showValidationErrors(validation.errors);
return;
}

MoitribeSDK(GAME_ID, 'submitscore', {
leaderboardid: leaderboardId,
score: score,
scoretag: sanitizedTag,
callback: function(result) {
if (result.success) {
console.log('Score submitted successfully');
hideValidationErrors();
} else {
console.error('Score submission failed:', result.message);
showSubmissionError(result.message);
}
}
});
}

function showValidationErrors(errors) {
const errorContainer = document.getElementById('validation-errors');
if (errorContainer) {
errorContainer.innerHTML = errors.map(error => `<div class="error">${error}</div>`).join('');
errorContainer.style.display = 'block';
}
}

function hideValidationErrors() {
const errorContainer = document.getElementById('validation-errors');
if (errorContainer) {
errorContainer.style.display = 'none';
}
}

Leaderboard Selection

Help users choose appropriate leaderboards:

class LeaderboardSelector {
constructor() {
this.leaderboards = [];
this.selectedLeaderboard = null;
}

async loadLeaderboards() {
return new Promise((resolve) => {
MoitribeSDK(GAME_ID, 'loadleaderboardmetadata', {
onlyData: true,
callback: function(result) {
if (result.success && result.leaderboards) {
this.leaderboards = result.leaderboards;
this.populateLeaderboardUI();
resolve(result.leaderboards);
} else {
resolve([]);
}
}
});
});
}

populateLeaderboardUI() {
const selector = document.getElementById('leaderboard-selector');
if (!selector) return;

selector.innerHTML = '<option value="">Select a leaderboard...</option>';

this.leaderboards.forEach(leaderboard => {
const option = document.createElement('option');
option.value = leaderboard.id;
option.textContent = `${leaderboard.name} (${this.getLeaderboardType(leaderboard)})`;
selector.appendChild(option);
});

selector.addEventListener('change', (e) => {
this.selectLeaderboard(e.target.value);
});
}

getLeaderboardType(leaderboard) {
if (leaderboard.isSocial) return 'Social';
if (leaderboard.isTournament) return 'Tournament';
return 'Global';
}

selectLeaderboard(leaderboardId) {
this.selectedLeaderboard = this.leaderboards.find(lb => lb.id === leaderboardId);

if (this.selectedLeaderboard) {
this.updateLeaderboardInfo();
this.loadScoresForLeaderboard(leaderboardId);
}
}

updateLeaderboardInfo() {
const infoElement = document.getElementById('leaderboard-info');
if (!infoElement || !this.selectedLeaderboard) return;

const lb = this.selectedLeaderboard;
infoElement.innerHTML = `
<h3>${lb.name}</h3>
<p><strong>Type:</strong> ${this.getLeaderboardType(lb)}</p>
<p><strong>Description:</strong> ${lb.description || 'No description available'}</p>
<p><strong>Score Order:</strong> ${lb.ascending ? 'Low to High' : 'High to Low'}</p>
${lb.resetSchedule ? `<p><strong>Resets:</strong> ${lb.resetSchedule}</p>` : ''}
`;
}

loadScoresForLeaderboard(leaderboardId) {
// Show loading state
this.showLoadingState();

// Load scores for selected leaderboard
loadLeaderboardScores(leaderboardId)
.then(scores => {
this.hideLoadingState();
this.displayScores(scores);
})
.catch(error => {
this.hideLoadingState();
this.showError('Failed to load leaderboard scores');
});
}

showLoadingState() {
const loadingElement = document.getElementById('leaderboard-loading');
if (loadingElement) {
loadingElement.style.display = 'block';
}
}

hideLoadingState() {
const loadingElement = document.getElementById('leaderboard-loading');
if (loadingElement) {
loadingElement.style.display = 'none';
}
}

showError(message) {
const errorElement = document.getElementById('leaderboard-error');
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}
}
}

Security Considerations

Client-Side Validation

Always validate data on both client and server:

class SecureScoreSubmission {
constructor() {
this.lastSubmissionTime = 0;
this.minSubmissionInterval = 1000; // 1 second between submissions
this.maxSubmissionsPerMinute = 30;
this.submissionCount = 0;
this.submissionWindowStart = Date.now();
}

canSubmitScore() {
const now = Date.now();

// Rate limiting
if (now - this.lastSubmissionTime < this.minSubmissionInterval) {
return { allowed: false, reason: 'Too many submissions too quickly' };
}

// Window-based rate limiting
if (now - this.submissionWindowStart > 60000) {
this.submissionCount = 0;
this.submissionWindowStart = now;
}

if (this.submissionCount >= this.maxSubmissionsPerMinute) {
return { allowed: false, reason: 'Rate limit exceeded' };
}

return { allowed: true };
}

recordSubmission() {
this.lastSubmissionTime = Date.now();
this.submissionCount++;
}

async submitScore(leaderboardId, score, tag) {
const canSubmit = this.canSubmitScore();
if (!canSubmit.allowed) {
console.warn('Submission blocked:', canSubmit.reason);
showRateLimitError(canSubmit.reason);
return;
}

// Add client-side timestamp for server validation
const clientTimestamp = Date.now();
const submissionData = {
leaderboardid: leaderboardId,
score: score,
scoretag: tag,
clientTimestamp: clientTimestamp
};

return new Promise((resolve) => {
MoitribeSDK(GAME_ID, 'submitscore', {
...submissionData,
callback: function(result) {
if (result.success) {
this.recordSubmission();
console.log('Score submitted successfully');
} else {
console.error('Score submission failed:', result.message);
}
resolve(result);
}
});
});
}
}

function showRateLimitError(reason) {
const errorElement = document.getElementById('rate-limit-error');
if (errorElement) {
errorElement.textContent = reason;
errorElement.style.display = 'block';

setTimeout(() => {
errorElement.style.display = 'none';
}, 5000);
}
}

Common Pitfalls to Avoid

1. No Caching

  • Problem: Loading data from server on every request
  • Solution: Implement intelligent caching with appropriate TTL

2. Synchronous Loading

  • Problem: Blocking UI while loading leaderboard data
  • Solution: Use async loading and progressive enhancement

3. Poor Error Handling

  • Problem: Not handling network failures or API errors
  • Solution: Implement comprehensive error handling with user feedback

4. Inefficient Updates

  • Problem: Refreshing entire leaderboard too frequently
  • Solution: Use real-time updates and smart refresh intervals

5. No Input Validation

  • Problem: Submitting invalid scores or malformed data
  • Solution: Validate all data before submission

6. Ignoring Performance

  • Problem: Large DOM updates and memory leaks
  • Solution: Use efficient rendering techniques and proper cleanup

Next Steps

Now that you understand leaderboard best practices, explore:

tip

Test your leaderboard implementation with various scenarios: high scores, rapid submissions, network failures, and edge cases.