Files
livrets/docs/superpowers/specs/2026-08-28-play-games-services-design.md
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

128 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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é)