# Créer un jeu personnalisé pour Skori Ce document décrit **tous les paramètres** d'un fichier de définition de jeu (`.json`) importable dans Skori via **Paramètres → Jeux → Gérer les jeux importés → Importer**. Un jeu importé ajoute une nouvelle tuile à l'écran d'accueil, avec ses propres règles de score, options, icône et couleurs — sans recompiler l'application. > Les règles de calcul sont **déclaratives** (décrites par des données, pas du > code). C'est volontaire : un fichier importé ne peut jamais exécuter de code > arbitraire sur votre appareil. --- ## 1. Structure minimale Le plus petit fichier valide : ```json { "schemaVersion": 1, "id": "mon-jeu", "name": "Mon Jeu", "minPlayers": 2, "maxPlayers": 6, "scoreType": "positive", "scoreFormula": { "strategy": "sum_rounds" }, "colors": { "primary": "#3b82f6" } } ``` Tout le reste est optionnel. --- ## 2. Champs racine | Champ | Type | Obligatoire | Description | |---|---|---|---| | `schemaVersion` | `1` | ✅ | Toujours `1` (version du format). | | `id` | texte | ✅ | Identifiant unique. Lettres, chiffres, `_` et `-` uniquement. **Ne peut pas** être un id natif (voir §9). Réimporter avec le même `id` **met à jour** le jeu. | | `name` | texte | ✅ | Nom affiché (max 60 caractères). | | `minPlayers` | entier | ✅ | Nombre minimum de joueurs (1 à 20). | | `maxPlayers` | entier | ✅ | Nombre maximum de joueurs (1 à 20). | | `scoreType` | énum | ✅ | Nature du score — voir §3. | | `scoreFormula` | objet | ✅ | Comment additionner les manches — voir §4. | | `colors` | objet | ✅ | Couleur(s) du thème — voir §7. | | `description` | texte | ❌ | Phrase affichée sur l'écran de la partie (max 400 caractères). | | `targetScore` | objet | ❌ | Score de fin de partie — voir §5. | | `targetScoreCondition` | `"reaches"` \| `"exceeds"` | ❌ | `reaches` = fin si score **≥** cible (défaut logique) ; `exceeds` = fin si score **>** cible. | | `fixedRounds` | entier | ❌ | La partie se termine après ce nombre de manches — voir §6. | | `scoringCategories` | tableau | ❌ | Catégories de score saisies une par une — voir §8. | | `options` | tableau | ❌ | Options configurables avant la partie — voir §6/§8. | | `iconImage` | texte | ❌ | Icône du jeu (image en base64 « data URI ») — voir §7. | | `bannerImage` | texte | ❌ | Bannière de l'écran de fin (base64 data URI) — voir §7. | --- ## 3. `scoreType` — nature du score Détermine **qui gagne** et la façon dont le score est interprété. | Valeur | Le gagnant est… | Exemple | |---|---|---| | `positive` | le **plus haut** score | Azul, Harmonies | | `negative` | le **plus bas** score | Skyjo, Cabo, Odin | | `target` | le plus haut (course vers un objectif) | jeux à objectif de points | | `fixed_rounds` | le plus haut, sur un nombre fixe de manches | — | | `sudden_death` | le plus haut (mort subite) | — | > En pratique, le classement de fin de partie utilise `negative` → plus petit > gagne, **tous les autres** → plus grand gagne. Choisissez `negative` pour un > jeu « le moins de points possible », sinon `positive`. --- ## 4. `scoreFormula` — calcul du total Comment le total d'un joueur est calculé à partir de ses scores de manche. ### `sum_rounds` (par défaut) Simple addition de toutes les manches. ```json "scoreFormula": { "strategy": "sum_rounds" } ``` ### `sum_rounds_with_reset` Addition, avec **remise à un palier** quand le total atteint *exactement* un seuil (règle « Cabo » : tomber pile sur 100 redescend à 50). ```json "scoreFormula": { "strategy": "sum_rounds_with_reset", "resetThreshold": 100, "resetTo": 50, "oncePerPlayer": true } ``` | Paramètre | Type | Description | |---|---|---| | `resetThreshold` | nombre | Si le total **égale exactement** cette valeur… | | `resetTo` | nombre | …il est ramené à cette valeur. | | `oncePerPlayer` | booléen | `true` = la remise ne peut avoir lieu qu'**une seule fois** par joueur. | --- ## 5. `targetScore` — fin de partie sur un score Déclenche la fin de la partie dès qu'un joueur atteint (ou dépasse, selon `targetScoreCondition`) une valeur cible. Combinable avec `fixedRounds` (la première condition atteinte l'emporte). ### Cible fixe ```json "targetScore": { "type": "fixed", "value": 100 } ``` ### Cible dépendant d'une option Permet à l'utilisateur de choisir la cible avant la partie (comme Odin : « 15 pts / 20 pts / personnalisé »). ```json "targetScore": { "type": "fromOption", "optionId": "target_score", "customSentinel": "custom", "customOptionId": "custom_target_score" } ``` | Paramètre | Description | |---|---| | `optionId` | id de l'option (`select` ou `number`) qui contient la cible. | | `customSentinel` | *(optionnel)* valeur de l'option signifiant « personnalisé ». | | `customOptionId` | *(optionnel)* id de l'option `number` lue si la valeur = `customSentinel`. | --- ## 6. `fixedRounds` — nombre de manches fixe ```json "fixedRounds": 5 ``` - La partie se termine automatiquement après ce nombre de manches. - `fixedRounds: 1` active un mode spécial **« Décompte final »** (une seule saisie de scores, idéal pour un jeu où l'on ne compte qu'à la fin). --- ## 7. Apparence : `colors`, `iconImage`, `bannerImage` ### Couleurs ```json "colors": { "primary": "#6e1d5a", "secondary": "#c2410c" } ``` - `primary` (obligatoire) : couleur principale du thème du jeu (format hex). - `secondary` (optionnel) : couleur d'appoint. ### Images (base64) `iconImage` et `bannerImage` sont des **data URI** : l'image encodée directement dans le fichier (aucun chemin externe). Formats : PNG, SVG, WebP, JPEG. ```json "iconImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…" ``` Pour convertir une image en data URI : - en ligne : chercher « image to base64 data uri » ; - ou dans un terminal : - macOS/Linux : `echo "data:image/png;base64,$(base64 -w0 icone.png)"` - Windows PowerShell : `"data:image/png;base64," + [Convert]::ToBase64String([IO.File]::ReadAllBytes("icone.png"))` > Gardez les images légères (idéalement < 200 Ko) : elles sont stockées telles > quelles dans l'appareil. Si `iconImage` est absent, une icône générique est > utilisée. --- ## 8. Options et catégories ### `options` — réglages avant la partie Chaque option apparaît sur l'écran de préparation de la partie. ```json "options": [ { "id": "kamikaze", "label": "Règle Kamikaze", "type": "boolean", "defaultValue": true }, { "id": "target_score", "label": "Score de fin", "type": "select", "defaultValue": "15", "options": [ { "label": "Courte (15)", "value": "15" }, { "label": "Classique (20)", "value": "20" }, { "label": "Personnalisé", "value": "custom" } ] }, { "id": "custom_target_score", "label": "Score personnalisé", "type": "number", "defaultValue": 25 } ] ``` | Champ | Description | |---|---| | `id` | Identifiant de l'option (référencé par `targetScore.fromOption`). | | `label` | Libellé affiché. | | `type` | `boolean` (case à cocher), `number` (champ numérique), `select` (liste). | | `defaultValue` | Valeur par défaut. | | `options` | *(select uniquement)* liste de `{ label, value }`. | ### `scoringCategories` — saisie par catégories Active une saisie **étape par étape** (une catégorie à la fois, comme Harmonies). Le score de la manche = **somme des catégories**. ```json "scoringCategories": [ { "id": "arbres", "label": "Arbres" }, { "id": "montagnes", "label": "Montagnes" }, { "id": "eau", "label": "Champs d'eau" } ] ``` > Généralement associé à `fixedRounds: 1` (un seul décompte final réparti en > catégories). --- ## 9. Contraintes & validation À l'import, le fichier est validé. En cas d'erreur, un message précis indique le champ fautif. - `schemaVersion` doit valoir `1`. - `id` : lettres/chiffres/`_`/`-`, et **pas** un id réservé : `azul`, `cabo`, `skyjo`, `harmonie`, `odin_cards`. - `name` ≤ 60 caractères, `description` ≤ 400 caractères. - `minPlayers` / `maxPlayers` entre 1 et 20. - `scoreType` parmi les valeurs du §3. - `scoreFormula.strategy` parmi `sum_rounds`, `sum_rounds_with_reset`. - Les jeux importés sont **propres à chaque profil** (pas encore synchronisés entre appareils). > **Non pris en charge en v1** : les formules 100 % personnalisées par expression > (`custom_expression`). Les deux stratégies ci-dessus couvrent tous les jeux > fournis en standard. --- ## 10. Exemples complets ### A. Skyjo (le moins de points, fin à 100) ```json { "schemaVersion": 1, "id": "mon-skyjo", "name": "Skyjo maison", "minPlayers": 2, "maxPlayers": 8, "scoreType": "negative", "scoreFormula": { "strategy": "sum_rounds" }, "targetScore": { "type": "fixed", "value": 100 }, "targetScoreCondition": "reaches", "description": "Le moins de points possible. Fin dès qu'un joueur atteint 100.", "colors": { "primary": "#0f766e" } } ``` ### B. Cabo (remise à 50 sur 100 pile) ```json { "schemaVersion": 1, "id": "mon-cabo", "name": "Cabo maison", "minPlayers": 2, "maxPlayers": 5, "scoreType": "negative", "scoreFormula": { "strategy": "sum_rounds_with_reset", "resetThreshold": 100, "resetTo": 50, "oncePerPlayer": true }, "targetScore": { "type": "fixed", "value": 100 }, "targetScoreCondition": "reaches", "colors": { "primary": "#6e1d5a" } } ``` ### C. Odin (cible choisie par option) ```json { "schemaVersion": 1, "id": "mon-odin", "name": "Odin maison", "minPlayers": 2, "maxPlayers": 6, "scoreType": "negative", "scoreFormula": { "strategy": "sum_rounds" }, "targetScore": { "type": "fromOption", "optionId": "target_score", "customSentinel": "custom", "customOptionId": "custom_target_score" }, "targetScoreCondition": "reaches", "options": [ { "id": "target_score", "label": "Score de fin de partie", "type": "select", "defaultValue": "15", "options": [ { "label": "Partie courte (15 pts)", "value": "15" }, { "label": "Partie classique (20 pts)", "value": "20" }, { "label": "Personnalisé", "value": "custom" } ] }, { "id": "custom_target_score", "label": "Score personnalisé", "type": "number", "defaultValue": 25 } ], "colors": { "primary": "#c2410c" } } ``` ### D. Jeu par catégories (le plus de points, décompte final) ```json { "schemaVersion": 1, "id": "mon-jeu-categories", "name": "Jeu à catégories", "minPlayers": 1, "maxPlayers": 4, "scoreType": "positive", "scoreFormula": { "strategy": "sum_rounds" }, "fixedRounds": 1, "scoringCategories": [ { "id": "cat1", "label": "Objectifs" }, { "id": "cat2", "label": "Bonus" }, { "id": "cat3", "label": "Pénalités" } ], "colors": { "primary": "#16a34a" } } ```