c96a1627ec
Build and Publish Docker Image / build-and-push-image (push) Successful in 1m0s
Navigation : - Suppression du menu hamburger, remplacé par le logo de l'app dans l'en-tête - Barre du bas à 5 onglets : Accueil, Mes jeux, Parties, Stats, Joueurs - Menu profil sur l'avatar (haut-droite) : profil/switch, Emplacements, Jeux importés, Paramètres - Nouvelle page "Mes jeux" (/jeux) : bibliothèque de tous les jeux Format d'import enrichi + gabarits : - Champ icon (lucide) au niveau du jeu, et icon/color sur les catégories - Gabarits JSON des 5 jeux natifs dans public/game-templates/ (référence, au format d'import) + doc à jour Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
381 lines
11 KiB
Markdown
381 lines
11 KiB
Markdown
# 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. |
|
|
|
|
> 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" }
|
|
}
|
|
```
|