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).
6.6 KiB
Design : Play Games Services (Android) — Livrets
Date : 2026-08-28 Statut : Validé par le produit
1. Objectif
Sur Android uniquement : authentification via Play Jeux Services v2 et classements en ligne. Un leaderboard par nombre de questions (5, 10, …, 50), avec un score pondéré par la difficulté (valeur des produits réussis et temps par question). Le jeu reste pleinement fonctionnel hors connexion et sans compte.
2. Décisions clés
| Décision | Choix |
|---|---|
| Découpage des boards | 10 leaderboards, un par valeur de questionCount (5 → 50, pas de 5). Les combinaisons complètes (3360) dépassent la limite de 70 boards Play Jeux |
| Score classé | Points pondérés : round((Σ produits réussis) × 30 ÷ secondsPerQuestion) |
| Authentification | Connexion silencieuse auto au lancement (PGS v2, non bloquante, sans UI) |
| Envoi du score | Automatique en fin de partie si connecté et ≥ 1 bonne réponse, échec silencieux |
| Consultation | Bouton « Classement » sur l'accueil → UI native Play Jeux du board correspondant au questionCount sélectionné |
| Intégration KMP | Interface GamesServices dans commonMain, injectée dans App() ; implémentation Android dans androidApp ; no-op sur desktop/iOS |
| Transparence | Ligne « Points classement : X » sur l'écran des résultats |
Rejeté : un board par combinaison (questionCount × temps × table = 3360 > 70 boards max) ; boards par table ou par temps (score plus difficile à rendre équitable) ; bons réponses bruts ou taux de réussite (égalités massives à N ou 100 %) ; expect/actual dans sharedUI (ajouterait la dépendance GMS au module partagé) ; UI classement in-app (coût disproportionné).
3. Formule du score
score = round( (Σ produits des questions réussies) × 30 ÷ secondsPerQuestion )
- Chaque bonne réponse rapporte la valeur de son produit (
a × b) : les grandes tables valent plus de points. - Le facteur
(30 ÷ secondsPerQuestion)valorise les réglages rapides (3 s/question → ×10 ; 30 s/question → ×1). - Exemple : 10 questions, table 12, 5 s, 10/10 → ~2500 points.
- 0 bonne réponse → pas de soumission (score 0 non soumis).
Fonction pure dans commonMain : fun GameResult.leaderboardScore(): Long, testable sans GMS.
4. Architecture
sharedUI/commonMain/kotlin/com/livrets/games/
GamesServices.kt interface GamesServices + object NoopGamesServices
Leaderboards.kt fun GameConfig.leaderboardId(): String // "questions_N"
LeaderboardScore.kt fun GameResult.leaderboardScore(): Long
interface GamesServices {
suspend fun signIn(): Boolean // true si connecté à la fin
suspend fun isSignedIn(): Boolean
suspend fun submitScore(leaderboardId: String, score: Long): Boolean
suspend fun showLeaderboard(leaderboardId: String)
}
App(filesDirPath, gamesServices: GamesServices = NoopGamesServices, …)— même pattern quefilesDirPath.HomeScreengagneonLeaderboard: () -> Unit;ResultsScreenaffiche les points soumis.androidApp:PlayGamesServices(activity)implémente l'interface avecplay-services-games-v2:22.0.0(dépendance ajoutée au catalogue Gradle) :signIn()/isAuthenticated()viaGamesSignInClient;submitScoreImmediate(leaderboardId, score)résultat attendu, erreur →false;getLeaderboardIntent(leaderboardId)lancé via l'Activity Result API (registerForActivityResultdansAppActivity) ;- appels GMS effectués en
Dispatchers.IOviaTasks.await.
- desktop/iOS :
NoopGamesServices(toutes méthodes no-op,signIn→false).
5. Flux utilisateur
- Lancement de l'app :
App()déclenchegamesServices.signIn()dans un effet de lancement (no-op sur desktop/iOS). Connexion silencieuse, non bloquante, sans UI. Échec → l'app fonctionne normalement hors ligne. - Fin de partie : si connecté, envoi immédiat du score au board
questions_N. Échec réseau/connexion → silencieux (l'app n'attend pas). - Accueil : bouton « Classement » sous « Historique ». Si non connecté, tente la
connexion puis ouvre l'UI native Play Jeux sur le board du
questionCountsélectionné sur l'accueil. Si la connexion échoue ou que l'ouverture de l'UI échoue (Play Jeux absent, pas de compte), rien ne s'ouvre — pas d'erreur bloquante. - Écran des résultats : ligne « Points classement : X » sous le temps moyen.
6. Faisabilité
play-services-games-v222.0.0 (juillet 2026) : sign-in v2 implicite, client leaderboards, intents d'affichage ; minSdk 24 requis (le projet passe de 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 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?,nullsi le nombre de questions n'a pas de board).
7. Prérequis Play Console (manuel, hors code)
- Créer l'app dans Play Console (package
com.livrets.androidApp). - Jouer → Play Jeux Services : créer le jeu, lier l'app Android, ajouter les empreintes SHA-1 debug et release.
- Créer les 10 leaderboards (« 5 questions » … « 50 questions ») ; la console génère
les IDs (
CgkI…), à reporter dansLeaderboards.kt(correction 2026-08-31 : pas d'ID personnalisable). - Ajouter des testeurs PGS pour valider la connexion en développement.
Sans ce setup, toutes les opérations échouent silencieusement : aucune régression pour l'utilisateur, l'app reste jouable.
8. Tests & vérifications
commonTest:LeaderboardScoreTest(0 réussite → pas de score ; 100 % ; facteur vitesse ; arrondi) etLeaderboardsTest(mappingGameConfig→questions_N).- Vérification manuelle Android (appareil avec compte Play + console configurée) : connexion, envoi visible dans l'UI Play Jeux, ouverture du bon board.
- Régression desktop : le no-op ne casse rien (lancement, partie, résultats).
- Régression :
./gradlew :sharedUI:jvmTest+./gradlew build(3 cibles).
9. Hors périmètre
- iOS Game Center (l'interface
GamesServicespermettra d'injecter un équivalent plus tard) - Achievements, sauvegarde cloud, profil joueur, UI classement in-app
- Déconnexion in-app (gérée par l'app Play Jeux, standard PGS v2)
- Historisation locale des scores soumis (l'historique existant reste inchangé)