The competitive element in games is one of the most important factors that increase user interaction and motivation. In this article, we will explain in detail how we created our weekly leaderboard system, which is updated every Wednesday in Chipode Sudoku, using Firebase Realtime Database.
System Design
1- Data Structure
json
{
"leaderboards": {
"2024-W48": {
"rankings": {
"user123": {
"score": 15420,
"displayName": "AliceGamer",
"avatar": "avatar_1",
"lastUpdated": 1701345678901
},
"user456": {
"score": 14380,
"displayName": "BobPlayer",
"avatar": "avatar_3",
"lastUpdated": 1701345789012
}
},
"startDate": 1701302400000,
"endDate": 1701907199999
}
},
"users": {
"user123": {
"currentWeekScore": 15420,
"totalScore": 154980,
"achievements": {
"weeklyWinner": 3,
"topPlayer": true
}
}
}
}
2- Firebase Configuration
dart
class FirebaseConfig {
static Future initialize() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
name: 'chipode-sudoku',
);
if (kDebugMode) {
// Local emülatör kullanımı
FirebaseDatabase.instance.useDatabaseEmulator('localhost', 9000);
}
}
static DatabaseReference get leaderboardRef =>
FirebaseDatabase.instance.ref('leaderboards');
static DatabaseReference get usersRef =>
FirebaseDatabase.instance.ref('users');
}
Leaderboard Management
1- Leaderboard Service
dart
class LeaderboardService {
final DatabaseReference _dbRef;
final String _currentWeek;
LeaderboardService():
_dbRef = FirebaseConfig.leaderboardRef,
_currentWeek = _calculateCurrentWeek();
static String _calculateCurrentWeek() {
final now = DateTime.now();
final weekNumber = weekOfYear(now);
return '${now.year}-W${weekNumber.toString().padLeft(2, '0')}';
}
Future submitScore({
required String userId,
required int score,
required String displayName,
required String avatar,
}) async {
final userRef = _dbRef
.child(_currentWeek)
.child('rankings')
.child(userId);
// Transaction kullanarak atomic update
await userRef.runTransaction((data) {
if (data == null) {
return Transaction.success({
'score': score,
'displayName': displayName,
'avatar': avatar,
'lastUpdated': ServerValue.timestamp,
});
}
final currentScore = data['score'] as int;
if (score > currentScore) {
data['score'] = score;
data['lastUpdated'] = ServerValue.timestamp;
}
return Transaction.success(data);
});
}
Stream> getTopPlayers({int limit = 100}) {
return _dbRef
.child(_currentWeek)
.child('rankings')
.orderByChild('score')
.limitToLast(limit)
.onValue
.map((event) {
final data = event.snapshot.value as Map;
final entries = data.entries.map((e) => LeaderboardEntry(
userId: e.key as String,
score: e.value['score'] as int,
displayName: e.value['displayName'] as String,
avatar: e.value['avatar'] as String,
lastUpdated: DateTime.fromMillisecondsSinceEpoch(
e.value['lastUpdated'] as int,
),
)).toList();
// Skorlara göre sırala
entries.sort((a, b) => b.score.compareTo(a.score));
return entries;
});
}
Future getUserRank(String userId) async {
final snapshot = await _dbRef
.child(_currentWeek)
.child('rankings')
.orderByChild('score')
.get();
if (!snapshot.exists) return 0;
final data = snapshot.value as Map;
final entries = data.entries.toList();
entries.sort((a, b) =>
(b.value['score'] as int).compareTo(a.value['score'] as int));
final index = entries.indexWhere((e) => e.key == userId);
return index + 1;
}
}
2- Weekly Reset System
dart
class WeeklyResetManager {
final LeaderboardService _leaderboard;
final AchievementService _achievements;
Timer? _resetTimer;
void startWeeklyReset() {
final now = DateTime.now();
final nextWednesday = _getNextWednesday(now);
final timeUntilReset = nextWednesday.difference(now);
_resetTimer?.cancel();
_resetTimer = Timer(timeUntilReset, _performReset);
}
Future _performReset() async {
try {
// Mevcut haftanın kazananlarını belirle
final winners = await _determineWinners();
// Başarımları dağıt
await _distributeAchievements(winners);
// Yeni hafta için tabloyu hazırla
await _prepareNewWeek();
// Bildirim gönder
await _notifyUsers();
// Bir sonraki reset için zamanlayıcıyı başlat
startWeeklyReset();
} catch (e) {
log.error('Weekly reset failed', e);
// Retry mekanizması
}
}
DateTime _getNextWednesday(DateTime from) {
var nextWed = from;
while (nextWed.weekday != DateTime.wednesday) {
nextWed = nextWed.add(Duration(days: 1));
}
return DateTime(
nextWed.year,
nextWed.month,
nextWed.day,
0, 0, 0,
);
}
Future> _determineWinners() async {
final currentWeek = await _leaderboard.getTopPlayers(limit: 3);
return currentWeek;
}
}
User Interface
1- Leaderboard Widget
dart
class LeaderboardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Haftalık Liderlik Tablosu'),
actions: [
TimeUntilResetWidget(),
],
),
body: Column(
children: [
TopPlayersWidget(),
Expanded(
child: LeaderboardList(),
),
],
),
);
}
}
class LeaderboardList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, service, child) {
return StreamBuilder>(
stream: service.getTopPlayers(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return ErrorWidget(snapshot.error!);
}
if (!snapshot.hasData) {
return LoadingIndicator();
}
final entries = snapshot.data!;
return ListView.builder(
itemCount: entries.length,
itemBuilder: (context, index) {
final entry = entries[index];
return LeaderboardEntryTile(
rank: index + 1,
entry: entry,
highlighted: entry.userId == getCurrentUserId(),
);
},
);
},
);
},
);
}
}
class LeaderboardEntryTile extends StatelessWidget {
final int rank;
final LeaderboardEntry entry;
final bool highlighted;
@override
Widget build(BuildContext context) {
return Container(
color: highlighted ? Colors.amber.withOpacity(0.1) : null,
child: ListTile(
leading: _buildRankWidget(rank),
title: Text(entry.displayName),
trailing: Text(
NumberFormat('#,###').format(entry.score),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
subtitle: Text(
'Son güncelleme: ${timeAgo.format(entry.lastUpdated)}',
),
),
);
}
Widget _buildRankWidget(int rank) {
if (rank > 3) return Text('#$rank');
final colors = {
1: Colors.amber,
2: Colors.grey[400],
3: Colors.brown[300],
};
return Icon(
Icons.emoji_events,
color: colors[rank],
size: 32,
);
}
}
Security and Optimization
1- Security Rules
javascript
{
"rules": {
"leaderboards": {
"$week": {
"rankings": {
"$userId": {
// Sadece kendi skorunu güncelleyebilir
".write": "auth != null && auth.uid == $userId",
".validate": "newData.hasChildren(['score', 'displayName', 'lastUpdated']) &&
newData.child('score').isNumber() &&
(!data.exists() || newData.child('score').val() > data.child('score').val())"
},
// Herkes okuyabilir
".read": true,
// Sıralama için index
".indexOn": ["score"]
}
}
}
}
}
2- Caching and Performance
dart
class LeaderboardCache {
final _cache = >{};
final _expirations = {};
static const cacheDuration = Duration(minutes: 5);
List? getFromCache(String week) {
final expiration = _expirations[week];
if (expiration == null || expiration.isBefore(DateTime.now())) {
_cache.remove(week);
_expirations.remove(week);
return null;
}
return _cache[week];
}
void cacheEntries(String week, List entries) {
_cache[week] = entries;
_expirations[week] = DateTime.now().add(cacheDuration);
}
void invalidateCache() {
_cache.clear();
_expirations.clear();
}
}
3- Error Monitoring
dart
class LeaderboardErrorTracker {
static void trackError(String operation, dynamic error) {
FirebaseCrashlytics.instance.recordError(
error,
StackTrace.current,
reason: 'Leaderboard operation failed: $operation',
);
}
static void trackLatency(String operation, Duration duration) {
FirebasePerformance.instance
.newTrace('leaderboard_$operation')
.then((trace) async {
await trace.start();
await Future.delayed(duration);
await trace.stop();
});
}
}
Notification System
1- Notification Service
dart
class LeaderboardNotificationService {
final messaging = FirebaseMessaging.instance;
Future subscribeToUpdates() async {
await messaging.subscribeToTopic('leaderboard_updates');
}
Future notifyTopPlayers(List winners) async {
final functions = FirebaseFunctions.instance;
await functions.httpsCallable('notifyLeaderboardWinners').call({
'winners': winners.map((w) => w.toJson()).toList(),
});
}
Future notifyUserRankChange({
required String userId,
required int oldRank,
required int newRank,
}) async {
if (oldRank == 0 || newRank >= oldRank) return;
final user = await FirebaseAuth.instance.currentUser;
if (user?.uid != userId) return;
await _showRankChangeNotification(
oldRank: oldRank,
newRank: newRank,
);
}
}
Result
Thanks to the weekly leaderboard system we created using Firebase Realtime Database:
1. Real-time score updates
2. Automatic weekly reset
3. Achievement system integration
4. Notification system
5. Secure and scalable structure
we have achieved.
Future Plans
– Friend-based ranking
– Monthly and seasonal tables
– Special tournaments
– Cross-platform synchronization
– Regional rankings





