# 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 ``` ```kotlin 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é)