Files
livrets/docs/superpowers/specs/2026-08-28-play-games-services-design.md
T
herel 853541d41b 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).
2026-08-31 23:11:22 +02:00

6.6 KiB
Raw Blame History

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 que filesDirPath. HomeScreen gagne onLeaderboard: () -> Unit ; ResultsScreen affiche les points soumis.
  • androidApp : PlayGamesServices(activity) implémente l'interface avec play-services-games-v2:22.0.0 (dépendance ajoutée au catalogue Gradle) :
    • signIn() / isAuthenticated() via GamesSignInClient ;
    • submitScoreImmediate(leaderboardId, score) résultat attendu, erreur → false ;
    • getLeaderboardIntent(leaderboardId) lancé via l'Activity Result API (registerForActivityResult dans AppActivity) ;
    • appels GMS effectués en Dispatchers.IO via Tasks.await.
  • desktop/iOS : NoopGamesServices (toutes méthodes no-op, signIn → false).

5. Flux utilisateur

  1. Lancement de l'app : App() déclenche gamesServices.signIn() dans un effet de lancement (no-op sur desktop/iOS). Connexion silencieuse, non bloquante, sans UI. Échec → l'app fonctionne normalement hors ligne.
  2. Fin de partie : si connecté, envoi immédiat du score au board questions_N. Échec réseau/connexion → silencieux (l'app n'attend pas).
  3. Accueil : bouton « Classement » sous « Historique ». Si non connecté, tente la connexion puis ouvre l'UI native Play Jeux sur le board du questionCount sé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.
  4. Écran des résultats : ligne « Points classement : X » sous le temps moyen.

6. Faisabilité

  • play-services-games-v2 22.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?, 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 (« 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 pour l'utilisateur, l'app reste jouable.

8. Tests & vérifications

  • commonTest : LeaderboardScoreTest (0 réussite → pas de score ; 100 % ; facteur vitesse ; arrondi) et LeaderboardsTest (mapping GameConfig → 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 GamesServices permettra 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é)