fix(android): brancher les classements sur les vrais IDs Play Jeux

La console Play génère les IDs de leaderboard (format CgkI…), contrairement
à l'hypothèse de la spec : l'app demandait des boards questions_N inexistants,
d'où un chargement infini et des scores silencieusement perdus. Remplacer le
mapping par la table des vrais IDs (GameConfig.leaderboardId() devient
nullable, null = pas de board) et logguer les échecs GMS jusqu'ici avalés
par runCatching. Spec corrigée en conséquence (§6, §7).
This commit is contained in:
herel committed 2026-08-31 23:11:22 +02:00
1 parent b7c958cd5b
commit 853541d41b
5 files changed
+60 -14

No files matched your search

@@ -2,6 +2,7 @@ package com.livrets.androidApp
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.util.Log
import com.google.android.gms.games.PlayGames import com.google.android.gms.games.PlayGames
import com.livrets.games.GamesServices import com.livrets.games.GamesServices
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -16,19 +17,20 @@ class PlayGamesServices(
override suspend fun signIn(): Boolean = withContext(Dispatchers.IO) { override suspend fun signIn(): Boolean = withContext(Dispatchers.IO) {
runCatching { runCatching {
PlayGames.getGamesSignInClient(activity).signIn().await().isAuthenticated PlayGames.getGamesSignInClient(activity).signIn().await().isAuthenticated
}.getOrDefault(false) }.onFailure { Log.w(TAG, "Échec connexion Play Jeux", it) }.getOrDefault(false)
} }
override suspend fun isSignedIn(): Boolean = withContext(Dispatchers.IO) { override suspend fun isSignedIn(): Boolean = withContext(Dispatchers.IO) {
runCatching { runCatching {
PlayGames.getGamesSignInClient(activity).isAuthenticated().await().isAuthenticated PlayGames.getGamesSignInClient(activity).isAuthenticated().await().isAuthenticated
}.getOrDefault(false) }.onFailure { Log.w(TAG, "Échec vérification connexion Play Jeux", it) }
.getOrDefault(false)
} }
override suspend fun playerDisplayName(): String? = withContext(Dispatchers.IO) { override suspend fun playerDisplayName(): String? = withContext(Dispatchers.IO) {
runCatching { runCatching {
PlayGames.getPlayersClient(activity).currentPlayer.await().displayName PlayGames.getPlayersClient(activity).currentPlayer.await().displayName
}.getOrNull() }.onFailure { Log.w(TAG, "Échec récupération du joueur", it) }.getOrNull()
} }
override suspend fun submitScore(leaderboardId: String, score: Long): Boolean = override suspend fun submitScore(leaderboardId: String, score: Long): Boolean =
@@ -38,7 +40,8 @@ class PlayGamesServices(
.submitScoreImmediate(leaderboardId, score) .submitScoreImmediate(leaderboardId, score)
.await() .await()
true true
}.getOrDefault(false) }.onFailure { Log.w(TAG, "Échec envoi du score au board $leaderboardId", it) }
.getOrDefault(false)
} }
override suspend fun showLeaderboard(leaderboardId: String) { override suspend fun showLeaderboard(leaderboardId: String) {
@@ -47,8 +50,13 @@ class PlayGamesServices(
PlayGames.getLeaderboardsClient(activity) PlayGames.getLeaderboardsClient(activity)
.getLeaderboardIntent(leaderboardId) .getLeaderboardIntent(leaderboardId)
.await() .await()
}.getOrNull() }.onFailure { Log.w(TAG, "Échec ouverture du board $leaderboardId", it) }
.getOrNull()
} ?: return } ?: return
launchIntent(intent) launchIntent(intent)
} }
private companion object {
const val TAG = "PlayGamesServices"
}
} }
@@ -92,16 +92,19 @@ interface GamesServices {
23 à 24 — abandon des appareils Android 6, décision produit du 2026-08-28). 23 à 24 — abandon des appareils Android 6, décision produit du 2026-08-28).
- La dépendance GMS reste confinée à `androidApp` ; sharedUI ne dépend que de - La dépendance GMS reste confinée à `androidApp` ; sharedUI ne dépend que de
l'interface (aucun impact desktop/iOS). l'interface (aucun impact desktop/iOS).
- Les IDs de leaderboard sont définis à la création en console (IDs custom - Les IDs de leaderboard sont générés automatiquement par la console (format `CgkI…`,
`questions_5` … `questions_50`), donc le mapping commonMain est trivial et stable. non personnalisables — correction 2026-08-31 : le mapping commonMain est donc une
table en dur des IDs réels, `GameConfig.leaderboardId(): String?`, `null` si le
nombre de questions n'a pas de board).
## 7. Prérequis Play Console (manuel, hors code) ## 7. Prérequis Play Console (manuel, hors code)
1. Créer l'app dans Play Console (package `com.livrets.androidApp`). 1. Créer l'app dans Play Console (package `com.livrets.androidApp`).
2. Jouer → Play Jeux Services : créer le jeu, lier l'app Android, ajouter les 2. Jouer → Play Jeux Services : créer le jeu, lier l'app Android, ajouter les
empreintes SHA-1 debug et release. empreintes SHA-1 debug et release.
3. Créer les 10 leaderboards avec IDs `questions_5` … `questions_50` 3. Créer les 10 leaderboards (« 5 questions » … « 50 questions ») ; la console génère
(« 5 questions » … « 50 questions »). les IDs (`CgkI…`), à reporter dans `Leaderboards.kt` (correction 2026-08-31 : pas
d'ID personnalisable).
4. Ajouter des testeurs PGS pour valider la connexion en développement. 4. Ajouter des testeurs PGS pour valider la connexion en développement.
Sans ce setup, toutes les opérations échouent silencieusement : aucune régression Sans ce setup, toutes les opérations échouent silencieusement : aucune régression
@@ -74,7 +74,7 @@ fun App(
scope.launch { scope.launch {
if (gamesServices.isSignedIn() || gamesServices.signIn()) { if (gamesServices.isSignedIn() || gamesServices.signIn()) {
if (playerName == null) playerName = gamesServices.playerDisplayName() if (playerName == null) playerName = gamesServices.playerDisplayName()
gamesServices.showLeaderboard(config.leaderboardId()) config.leaderboardId()?.let { gamesServices.showLeaderboard(it) }
} }
} }
}, },
@@ -91,7 +91,9 @@ fun App(
withContext(NonCancellable) { withContext(NonCancellable) {
val score = result.leaderboardScore() val score = result.leaderboardScore()
if (score > 0 && (gamesServices.isSignedIn() || gamesServices.signIn())) { if (score > 0 && (gamesServices.isSignedIn() || gamesServices.signIn())) {
gamesServices.submitScore(result.config.leaderboardId(), score) result.config.leaderboardId()?.let {
gamesServices.submitScore(it, score)
}
} }
} }
} }
@@ -2,4 +2,17 @@ package com.livrets.games
import com.livrets.game.GameConfig import com.livrets.game.GameConfig
fun GameConfig.leaderboardId(): String = "questions_$questionCount" private val leaderboardIds = mapOf(
5 to "CgkIuqy3oN8HEAIQAg",
10 to "CgkIuqy3oN8HEAIQAw",
15 to "CgkIuqy3oN8HEAIQBA",
20 to "CgkIuqy3oN8HEAIQBQ",
25 to "CgkIuqy3oN8HEAIQBg",
30 to "CgkIuqy3oN8HEAIQBw",
35 to "CgkIuqy3oN8HEAIQCA",
40 to "CgkIuqy3oN8HEAIQCQ",
45 to "CgkIuqy3oN8HEAIQCg",
50 to "CgkIuqy3oN8HEAIQCw",
)
fun GameConfig.leaderboardId(): String? = leaderboardIds[questionCount]
@@ -7,6 +7,7 @@ import com.livrets.game.Question
import com.livrets.game.product import com.livrets.game.product
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.time.Instant import kotlin.time.Instant
class LeaderboardScoreTest { class LeaderboardScoreTest {
@@ -84,7 +85,26 @@ class LeaderboardScoreTest {
@Test @Test
fun `id de board par nombre de questions`() { fun `id de board par nombre de questions`() {
assertEquals("questions_10", GameConfig(10, 5, 12).leaderboardId()) val attendu = mapOf(
assertEquals("questions_5", GameConfig(5, 3, 1).leaderboardId()) 5 to "CgkIuqy3oN8HEAIQAg",
10 to "CgkIuqy3oN8HEAIQAw",
15 to "CgkIuqy3oN8HEAIQBA",
20 to "CgkIuqy3oN8HEAIQBQ",
25 to "CgkIuqy3oN8HEAIQBg",
30 to "CgkIuqy3oN8HEAIQBw",
35 to "CgkIuqy3oN8HEAIQCA",
40 to "CgkIuqy3oN8HEAIQCQ",
45 to "CgkIuqy3oN8HEAIQCg",
50 to "CgkIuqy3oN8HEAIQCw",
)
attendu.forEach { (questions, id) ->
assertEquals(id, GameConfig(questions, 5, 12).leaderboardId())
}
}
@Test
fun `pas d'id de board hors liste des nombres de questions`() {
assertNull(GameConfig(7, 5, 12).leaderboardId())
assertNull(GameConfig(60, 5, 12).leaderboardId())
} }
} }