diff --git a/README.md b/README.md
index 3e4bad7..eb4281f 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ A modern falling block puzzle game for Android, featuring smooth animations, res
- Hard drop and soft drop controls
- Advanced move detection (e.g., T-Spins) and scoring
- Persistent high score system (Top 5)
+- Global leaderboard via Google Play Games Services
### Modern Android Features
- Optimized for Android 11+ (API 30+)
@@ -20,6 +21,7 @@ A modern falling block puzzle game for Android, featuring smooth animations, res
- Automatic Dark theme support
- Intuitive and responsive touch controls
- Full edge-to-edge display utilization
+- Google Play Games Services integration for leaderboards
### Scoring System
@@ -74,6 +76,12 @@ Pixelmint Drop features a comprehensive scoring system designed to reward skillf
- Multiple theme options with ability to change manually or enable Random Mode (unlocked when 2+ themes are available).
- In Random Mode, themes change automatically every 10 line clears (1 level).
+### Online Features
+- Global leaderboard support through Google Play Games Services
+- Sign in with your Google account to compete with players worldwide
+- Your highest scores are automatically submitted to the leaderboard
+- View the global leaderboard directly from the game
+
## Technical Details
### Requirements
diff --git a/app/build.gradle b/app/build.gradle
index 0466731..42353b4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -63,6 +63,8 @@ dependencies {
implementation 'androidx.window:window:1.2.0' // For better display support
implementation 'androidx.window:window-java:1.2.0'
implementation 'com.google.code.gson:gson:2.10.1'
+ implementation 'com.google.android.gms:play-services-games-v2:19.0.0'
+ implementation 'com.google.android.gms:play-services-auth:20.7.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f3d41de..33cbafa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.pixelmintdrop">
+
Color.WHITE
}
- // Apply theme to back button
+ // Apply theme to buttons
binding.backButton.setTextColor(textColor)
+ binding.leaderboardButton?.setTextColor(textColor)
// Update adapter theme
highScoreAdapter.applyTheme(themeId)
@@ -122,6 +129,20 @@ class HighScoresActivity : AppCompatActivity() {
Log.e("HighScoresActivity", "Error updating high scores", e)
}
}
+
+ private fun showGlobalLeaderboard() {
+ try {
+ if (!highScoreManager.isGooglePlaySignedIn()) {
+ Toast.makeText(this, "Signing in to Google Play Games...", Toast.LENGTH_SHORT).show()
+ }
+
+ // Show the leaderboard
+ highScoreManager.getGooglePlayGamesManager().showLeaderboard(this)
+ } catch (e: Exception) {
+ Log.e("HighScoresActivity", "Error showing leaderboard", e)
+ Toast.makeText(this, "Could not open leaderboard", Toast.LENGTH_SHORT).show()
+ }
+ }
override fun onResume() {
super.onResume()
@@ -143,6 +164,11 @@ class HighScoresActivity : AppCompatActivity() {
finish()
return true
}
+ KeyEvent.KEYCODE_BUTTON_Y -> {
+ // Y button shows global leaderboard
+ showGlobalLeaderboard()
+ return true
+ }
}
}
diff --git a/app/src/main/java/com/pixelmintdrop/MainActivity.kt b/app/src/main/java/com/pixelmintdrop/MainActivity.kt
index 5701d19..ea584df 100644
--- a/app/src/main/java/com/pixelmintdrop/MainActivity.kt
+++ b/app/src/main/java/com/pixelmintdrop/MainActivity.kt
@@ -55,6 +55,10 @@ import androidx.core.view.updatePadding
import kotlin.math.max
import kotlin.math.min
import kotlin.random.Random
+// Google Play Games Services imports
+import com.google.android.gms.games.PlayGames
+import com.google.android.gms.games.PlayGamesSdk
+import com.google.android.gms.games.GamesSignInClient
class MainActivity : AppCompatActivity(),
GamepadController.GamepadConnectionListener,
@@ -68,6 +72,9 @@ class MainActivity : AppCompatActivity(),
// ViewModel
private val viewModel: MainActivityViewModel by viewModels() // Added ViewModel
+ // Google Play Games Services
+ private lateinit var gamesSignInClient: GamesSignInClient
+
// UI components
private lateinit var binding: ActivityMainBinding
private lateinit var gameView: GameView
@@ -157,6 +164,13 @@ class MainActivity : AppCompatActivity(),
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+ // Initialize Google Play Games Services
+ PlayGamesSdk.initialize(this)
+ gamesSignInClient = PlayGames.getGamesSignInClient(this)
+
+ // Request sign-in silently
+ signInToPlayGames()
+
// Store initial padding values before applying insets
val initialPausePadding = Rect(binding.pauseContainer.paddingLeft, binding.pauseContainer.paddingTop,
binding.pauseContainer.paddingRight, binding.pauseContainer.paddingBottom)
@@ -529,9 +543,16 @@ class MainActivity : AppCompatActivity(),
startGame()
}
+ // High Scores button
binding.highScoresButton.setOnClickListener {
- gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
- showHighScores()
+ binding.pauseContainer.visibility = View.GONE
+ val intent = Intent(this, HighScoresActivity::class.java)
+ startActivity(intent)
+ }
+
+ // Leaderboard button
+ binding.leaderboardButton?.setOnClickListener {
+ showLeaderboard()
}
binding.pauseLevelUpButton.setOnClickListener {
@@ -815,6 +836,7 @@ class MainActivity : AppCompatActivity(),
binding.pauseRestartButton.setTextColor(themeColor)
binding.resumeButton.setTextColor(themeColor)
binding.highScoresButton.setTextColor(themeColor)
+ binding.leaderboardButton?.setTextColor(themeColor)
binding.statsButton.setTextColor(themeColor)
binding.pauseLevelUpButton?.setTextColor(themeColor) // Safe call
binding.pauseLevelDownButton?.setTextColor(themeColor) // Safe call
@@ -1640,6 +1662,7 @@ class MainActivity : AppCompatActivity(),
// Group 2: Stats and Scoring
orderedViews.add(binding.highScoresButton)
+ orderedViews.add(binding.leaderboardButton)
orderedViews.add(binding.statsButton)
// Group 3: Level selection (use safe calls)
@@ -2206,4 +2229,50 @@ class MainActivity : AppCompatActivity(),
Log.d("RandomMode", "Cannot apply random theme/skin - no unlocked options available")
}
}
+
+ /**
+ * Handles sign-in to Google Play Games Services
+ */
+ private fun signInToPlayGames() {
+ gamesSignInClient.isAuthenticated.addOnCompleteListener { task ->
+ val isAuthenticated = task.isSuccessful && task.result.isAuthenticated
+
+ if (!isAuthenticated) {
+ // Silent sign-in failed, offer to sign in explicitly
+ Log.d(TAG, "Not authenticated with Play Games, will prompt user later")
+
+ // We don't show UI for this immediately, we'll add a button to the pause menu
+ } else {
+ Log.d(TAG, "Already authenticated with Play Games")
+
+ // If we were already authenticated, make sure scores are synced
+ val lastHighScore = highScoreManager.getHighScores().maxByOrNull { it.score }
+ lastHighScore?.let {
+ highScoreManager.getGooglePlayGamesManager().submitScore(it.score.toLong(), this)
+ }
+ }
+ }
+ }
+
+ // Method to show Google Play Games leaderboard
+ private fun showLeaderboard() {
+ gamesSignInClient.isAuthenticated.addOnCompleteListener { task ->
+ val isAuthenticated = task.isSuccessful && task.result.isAuthenticated
+
+ if (isAuthenticated) {
+ // Show leaderboard
+ highScoreManager.getGooglePlayGamesManager().showLeaderboard(this)
+ } else {
+ // Sign in first
+ gamesSignInClient.signIn().addOnCompleteListener { signInTask ->
+ if (signInTask.isSuccessful) {
+ // Now try to show leaderboard again
+ highScoreManager.getGooglePlayGamesManager().showLeaderboard(this)
+ } else {
+ Toast.makeText(this, "Sign-in required to view leaderboard", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/pixelmintdrop/model/GooglePlayGamesManager.kt b/app/src/main/java/com/pixelmintdrop/model/GooglePlayGamesManager.kt
new file mode 100644
index 0000000..3dcf6ae
--- /dev/null
+++ b/app/src/main/java/com/pixelmintdrop/model/GooglePlayGamesManager.kt
@@ -0,0 +1,96 @@
+package com.pixelmintdrop.model
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.activity.result.ActivityResultLauncher
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount
+import com.google.android.gms.auth.api.signin.GoogleSignInClient
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.google.android.gms.games.PlayGames
+import com.google.android.gms.games.PlayGamesSdk
+import com.google.android.gms.tasks.Task
+
+class GooglePlayGamesManager(private val context: Context) {
+ private val TAG = "GooglePlayGamesManager"
+
+ // Leaderboard ID
+ companion object {
+ const val LEADERBOARD_ID = "CgkImJW2mKsSEAIQAQ"
+ }
+
+ // Initialize the Play Games SDK
+ init {
+ try {
+ PlayGamesSdk.initialize(context)
+ Log.d(TAG, "PlayGamesSdk initialized")
+ } catch (e: Exception) {
+ Log.e(TAG, "Error initializing PlayGamesSdk", e)
+ }
+ }
+
+ // Check if user is already signed in
+ fun isSignedIn(): Boolean {
+ val account = GoogleSignIn.getLastSignedInAccount(context)
+ return account != null && !account.isExpired
+ }
+
+ // Get the sign-in client
+ fun getSignInClient(): GoogleSignInClient {
+ val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).build()
+ return GoogleSignIn.getClient(context, signInOptions)
+ }
+
+ // Handle sign-in result
+ fun handleSignInResult(task: Task): Boolean {
+ return if (task.isSuccessful) {
+ Log.d(TAG, "Sign-in successful")
+ true
+ } else {
+ Log.e(TAG, "Sign-in failed", task.exception)
+ false
+ }
+ }
+
+ // Submit score to leaderboard (requires an activity for potential sign-in)
+ fun submitScore(score: Long, activity: Activity? = null) {
+ val account = GoogleSignIn.getLastSignedInAccount(context)
+ if (account == null) {
+ Log.w(TAG, "Not signed in, score will not be submitted to Google Play Games")
+ return
+ }
+
+ try {
+ val leaderboardsClient = PlayGames.getLeaderboardsClient(activity ?: return)
+ leaderboardsClient.submitScore(LEADERBOARD_ID, score)
+ Log.d(TAG, "Score $score submitted to leaderboard $LEADERBOARD_ID")
+ } catch (e: Exception) {
+ Log.e(TAG, "Error submitting score to leaderboard", e)
+ }
+ }
+
+ // Show the leaderboard
+ fun showLeaderboard(activity: Activity) {
+ val account = GoogleSignIn.getLastSignedInAccount(context)
+ if (account == null) {
+ Log.w(TAG, "Not signed in, cannot show leaderboard")
+ return
+ }
+
+ try {
+ val leaderboardsClient = PlayGames.getLeaderboardsClient(activity)
+ leaderboardsClient
+ .getLeaderboardIntent(LEADERBOARD_ID)
+ .addOnSuccessListener { intent ->
+ activity.startActivity(intent)
+ }
+ .addOnFailureListener { e ->
+ Log.e(TAG, "Error showing leaderboard", e)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error showing leaderboard", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt b/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt
index bf35f52..de48594 100644
--- a/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt
+++ b/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt
@@ -10,6 +10,7 @@ class HighScoreManager(private val context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val gson = Gson()
private val type: Type = object : TypeToken>() {}.type
+ private val googlePlayGamesManager = GooglePlayGamesManager(context)
companion object {
private const val PREFS_NAME = "pixelmintdrop_highscores"
@@ -27,6 +28,7 @@ class HighScoreManager(private val context: Context) {
}
fun addHighScore(highScore: HighScore) {
+ // Save to local high scores
val currentScores = getHighScores().toMutableList()
currentScores.add(highScore)
@@ -37,6 +39,9 @@ class HighScoreManager(private val context: Context) {
// Save to SharedPreferences
val json = gson.toJson(topScores)
prefs.edit().putString(KEY_HIGHSCORES, json).apply()
+
+ // Submit to Google Play Games leaderboard if signed in
+ submitScoreToGooglePlay(highScore.score)
}
fun isHighScore(score: Int): Boolean {
@@ -44,4 +49,26 @@ class HighScoreManager(private val context: Context) {
return currentScores.size < MAX_HIGHSCORES ||
score > (currentScores.lastOrNull()?.score ?: 0)
}
+
+ // Submit score to Google Play Games leaderboard
+ private fun submitScoreToGooglePlay(score: Int) {
+ // We don't have an activity here, so we can't submit the score directly
+ // The score will be submitted later when an activity is available
+ try {
+ googlePlayGamesManager.submitScore(score.toLong(), null)
+ } catch (e: Exception) {
+ // Log error but don't crash
+ android.util.Log.e("HighScoreManager", "Error submitting score to leaderboard", e)
+ }
+ }
+
+ // Method to check if user is signed in to Google Play Games
+ fun isGooglePlaySignedIn(): Boolean {
+ return googlePlayGamesManager.isSignedIn()
+ }
+
+ // Get Google Play Games manager
+ fun getGooglePlayGamesManager(): GooglePlayGamesManager {
+ return googlePlayGamesManager
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 8552880..9dbef3f 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -406,6 +406,19 @@
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
+
+
-
+ android:orientation="horizontal"
+ android:gravity="center">
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0e6d13e..6b1c97a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,7 @@
Pixel Mint Drop
+ 630069234328
game over
score
level