firebase-realtime-database-ile-haftalik-liderlik-tablosu-sistemi-

Firebase Realtime Database ile Haftalık Liderlik Tablosu Sistemi

İçindekiler

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<void> 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<void> 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<List<LeaderboardEntry>> getTopPlayers({int limit = 100}) {
    return _dbRef
        .child(_currentWeek)
        .child('rankings')
        .orderByChild('score')
        .limitToLast(limit)
        .onValue
        .map((event) {
          final data = event.snapshot.value as Map<dynamic, dynamic>;
          
          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<int> 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<dynamic, dynamic>;
    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<void> _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<List<LeaderboardEntry>> _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<LeaderboardService>(
      builder: (context, service, child) {
        return StreamBuilder<List<LeaderboardEntry>>(
          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 = <String, List<LeaderboardEntry>>{};
  final _expirations = <String, DateTime>{};
  
  static const cacheDuration = Duration(minutes: 5);
  
  List<LeaderboardEntry>? 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<LeaderboardEntry> 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<void> subscribeToUpdates() async {
    await messaging.subscribeToTopic('leaderboard_updates');
  }
  
  Future<void> notifyTopPlayers(List<LeaderboardEntry> winners) async {
    final functions = FirebaseFunctions.instance;
    
    await functions.httpsCallable('notifyLeaderboardWinners').call({
      'winners': winners.map((w) => w.toJson()).toList(),
    });
  }
  
  Future<void> 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

Kaynaklar

Twitter
LinkedIn
Diğer Yazılar
İletişim

Chipode Uygulamaları

loading...
Google Play Store
loading...
Apple Store