diff --git a/docs/superpowers/specs/2026-08-28-play-games-services-design.md b/docs/superpowers/specs/2026-08-28-play-games-services-design.md new file mode 100644 index 0000000..5468b68 --- /dev/null +++ b/docs/superpowers/specs/2026-08-28-play-games-services-design.md @@ -0,0 +1,123 @@ +# 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 23 compatible. +- 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. + +## 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 »). +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é)