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:
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user