Oyunlarda rekabet unsuru, kullanıcı etkileşimini ve motivasyonunu artıran en önemli faktörlerden biridir. Chipode Sudoku’da her Çarşamba yenilenen haftalık liderlik tablosu sistemimizi Firebase Realtime Database kullanarak nasıl oluşturduğumuzu bu yazıda detaylıca anlatacağız.
Sistem Tasarımı
1- Veri Yapısı
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 Konfigürasyonu
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');
}
Liderlik Tablosu Yönetimi
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- Haftalık Reset Sistemi
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;
}
}
Kullanıcı Arayüzü
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,
);
}
}
Güvenlik ve Optimizasyon
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 ve Performans
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- Hata İzleme
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();
});
}
}
Bildirim Sistemi
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,
);
}
}
Sonuç
Firebase Realtime Database kullanarak oluşturduğumuz haftalık liderlik tablosu sistemi sayesinde:
1. Gerçek zamanlı skor güncellemeleri
2. Otomatik haftalık reset
3. Başarım sistemi entegrasyonu
4. Bildirim sistemi
5. Güvenli ve ölçeklenebilir yapı
elde etmiş olduk.
İleriye Dönük Planlar
– Arkadaş bazlı sıralama
– Aylık ve sezonluk tablolar
– Özel turnuvalar
– Cross-platform senkronizasyon
– Bölgesel sıralamalar






