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.content.Intent
import android.util.Log
import com.google.android.gms.games.PlayGames
import com.livrets.games.GamesServices
import kotlinx.coroutines.Dispatchers
@@ -16,19 +17,20 @@ class PlayGamesServices(
override suspend fun signIn(): Boolean = withContext(Dispatchers.IO) {
runCatching {
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) {
runCatching {
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) {
runCatching {
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 =
@@ -38,7 +40,8 @@ class PlayGamesServices(
.submitScoreImmediate(leaderboardId, score)
.await()
true
}.getOrDefault(false)
}.onFailure { Log.w(TAG, "Échec envoi du score au board $leaderboardId", it) }
.getOrDefault(false)
}
override suspend fun showLeaderboard(leaderboardId: String) {
@@ -47,8 +50,13 @@ class PlayGamesServices(
PlayGames.getLeaderboardsClient(activity)
.getLeaderboardIntent(leaderboardId)
.await()
}.getOrNull()
}.onFailure { Log.w(TAG, "Échec ouverture du board $leaderboardId", it) }
.getOrNull()
} ?: return
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).
- La dépendance GMS reste confinée à `androidApp` ; sharedUI ne dépend que de
l'interface (aucun impact desktop/iOS).
- Les IDs de leaderboard sont définis à la création en console (IDs custom
`questions_5` … `questions_50`), donc le mapping commonMain est trivial et stable.
- Les IDs de leaderboard sont générés automatiquement par la console (format `CgkI…`,
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)
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
empreintes SHA-1 debug et release.
3. Créer les 10 leaderboards avec IDs `questions_5` … `questions_50`
(« 5 questions » … « 50 questions »).
3. Créer les 10 leaderboards (« 5 questions » … « 50 questions ») ; la console génère
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.
Sans ce setup, toutes les opérations échouent silencieusement : aucune régression
@@ -74,7 +74,7 @@ fun App(
scope.launch {
if (gamesServices.isSignedIn() || gamesServices.signIn()) {
if (playerName == null) playerName = gamesServices.playerDisplayName()
gamesServices.showLeaderboard(config.leaderboardId())
config.leaderboardId()?.let { gamesServices.showLeaderboard(it) }
}
}
},
@@ -91,7 +91,9 @@ fun App(
withContext(NonCancellable) {
val score = result.leaderboardScore()
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
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 kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.time.Instant
class LeaderboardScoreTest {
@@ -84,7 +85,26 @@ class LeaderboardScoreTest {
@Test
fun `id de board par nombre de questions`() {
assertEquals("questions_10", GameConfig(10, 5, 12).leaderboardId())
assertEquals("questions_5", GameConfig(5, 3, 1).leaderboardId())
val attendu = 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",
)
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())
}
}