# 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. |
| `icon` | texte | ❌ | Nom d'icône **lucide** (ex: `TreePine`, `Ghost`, `Sword`) utilisée si aucune `iconImage`. Voir les noms sur lucide.dev/icons. |
| `iconImage` | texte | ❌ | Icône du jeu (image en base64 « data URI »). Prioritaire sur `icon` — 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", "icon": "TreePine", "color": "#16a34a" },
  { "id": "montagnes", "label": "Montagnes", "icon": "Mountain", "color": "#64748b" },
  { "id": "eau", "label": "Rivière", "icon": "Waves", "color": "#0ea5e9" }
]
```

| Champ | Description |
|---|---|
| `id` | Identifiant de la catégorie. |
| `label` | Libellé affiché. |
| `icon` | *(optionnel)* nom d'icône **lucide** affichée à côté de la catégorie. |
| `color` | *(optionnel)* teinte hex de l'icône. |
| `requiresOption` | *(optionnel)* `id` d'une option booléenne : la catégorie n'apparaît (et n'est comptée) **que si cette option est activée**. Idéal pour un mode/extension (ex : « Animaux spirituels »). |

> 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" }
}
```
