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.