GAME_CONFIGURATION_SCHEMA.md

Format de configuration d'un jeu — Slot Factory Engine. Un jeu = un dossier games/<gameId>/ contenant 4 fichiers de config + les assets. Créer un jeu « aliens » = dupliquer un dossier de thème et éditer ces fichiers. Aucune modification du moteur.


1. Vue d'ensemble des fichiers

games/space-raiders/
├── game.config.json      # LE fichier maître (ci-dessous)
├── theme/
│   ├── assets.json       # clé logique → fichier (+ variantes/états)
│   ├── audio.json        # clé logique → fichier son
│   └── tokens.json       # couleurs, fonts, style (→ CSS custom properties)
├── locales/
│   ├── en.json           # référence obligatoire
│   └── fr.json …
├── math/
│   ├── publish_files/    # index.json + books + lookup (format ACP)
│   └── math.meta.json    # GÉNÉRÉ par math-tools (jamais édité à la main)
├── assets/               # fichiers finaux (nomenclature SLOT_ASSET_MANIFEST)
└── art-src/              # sources de travail (non buildé)

2. game.config.json — schéma maître

{
  "$schema": "../../engine/schemas/game.config.schema.json",
  "gameId": "space_raiders",            // [a-z0-9_], = identifiant ACP Stake
  "displayName": "Space Raiders",
  "studio": "coolkids",
  "version": "1.0.0",

  "template": "lines-5x3",              // template d'origine (traçabilité)
  "theme": "./theme",                   // dossier thème

  "layout": {
    "preset": "standard",               // preset moteur : standard | tall | wide | custom
    "grid": { "reels": 5, "rows": 3 },
    "winSystem": "lines",               // lines | ways | scatterPays  (cluster/megaways : v2)
    "paylineSetId": "lines20-classic",  // réf. à une définition du moteur OU "./math/paylines.json"
    "symbolSize": 1.0,                  // facteur vs cellule
    "symbolGap": 0.06,
    "landscape": { "gridAnchor": [0.5, 0.44], "gridWidth": 0.52 },   // fractions du stage 1920×1080
    "portrait":  { "gridAnchor": [0.5, 0.38], "gridWidth": 0.92 },
    "spin": {
      "duration": 2.4,                  // s, borné [2.0 .. 3.5] par le lint (turbo à part)
      "stopStagger": 0.22,
      "turbo": true,                    // sera ignoré si flag RGS disabledTurbo
      "anticipation": true
    }
  },

  "mathProfile": "./math",              // le front NE porte AUCUNE constante math :
                                        // costs/modes/wincap/paytable lus depuis math.meta.json

  "features": {                         // un module absent = pas chargé (tree-shaken)
    "freeSpins":  { "introScreen": true, "summaryScreen": true, "retrigger": true },
    "bonusBuy":   { "modes": ["bonusbuy"] },        // les coûts viennent du math (index.json)
    "multiplier": { "style": "orbs", "collectTo": "header" },
    "holdAndWin": false,
    "autoplay":   { "counts": [10, 25, 50, 100], "stopConditions": true },
    "turbo":      true
  },

  "symbols": [                          // mapping id math ↔ clé de thème ↔ type
    { "id": "L1", "themeKey": "symbol.low.01",  "kind": "low"  },
    { "id": "H1", "themeKey": "symbol.high.01", "kind": "high" },
    { "id": "W",  "themeKey": "symbol.wild",    "kind": "wild" },
    { "id": "S",  "themeKey": "symbol.scatter", "kind": "scatter", "anticipation": true }
  ],

  "winTiers": {                         // multiples de mise (conventions marché)
    "big": 15, "mega": 30, "epic": 100,
    "rollupSeconds": { "base": 1.0, "big": 3.0, "mega": 5.0, "epic": 8.0 }
  },

  "localization": { "default": "en", "supported": ["en", "fr", "de", "es", "pt", "ja"] },

  "characterActor": false,              // OU config du module chroma-key (cf. FEATURE_MODULE_SPEC)
  "cinematics": {},                     // clés vidéo éventuelles

  "compliance": {
    "maxWinX": null,                    // null = lu depuis math.meta.json (source de vérité)
    "malfunctionClause": true,          // toujours true — le lint refuse false
    "celebrateWinsBelowStake": false
  }
}

3. theme/assets.json — manifest d'assets

{
  "themeId": "space",
  "fallbackTheme": "placeholder",       // TOUJOURS présent — un asset manquant → placeholder + warning
  "entries": {
    "brand.logo":            { "src": "space_brand_logo.webp" },
    "bg.base":               { "landscape": "space_bg_base_landscape.webp",
                                "portrait":  "space_bg_base_portrait.webp", "preload": "P0" },
    "bg.bonus":              { "landscape": "space_bg_bonus_landscape.webp",
                                "portrait":  "space_bg_bonus_portrait.webp", "preload": "P2" },
    "frame.reels":           { "src": "space_frame_reels.webp", "slice": [120, 120, 120, 120] },
    "frame.betbar":          { "landscape": "space_frame_betbar_landscape.webp",
                                "portrait":  "space_frame_betbar_portrait.webp" },
    "reels.bg":              { "src": "space_reels_bg.webp" },

    "symbol.low.01":         { "states": { "idle": "space_symbol_low_01_idle.webp",
                                            "win":  "space_symbol_low_01_win.webp" } },
    "symbol.wild":           { "states": { "idle": "space_symbol_wild_idle.webp",
                                            "win":  "space_symbol_wild_win.webp" } },

    "ui.spin":               { "states": { "default": "space_ui_spin_default.webp",
                                            "pressed": "space_ui_spin_pressed.webp" } },
    "ui.plaque":             { "src": "space_ui_plaque.webp", "slice": [40, 60, 40, 60] },

    "bonus.freespins.intro": { "src": "space_bonus_freespins_intro.webp", "preload": "P2" },
    "video.bonus.intro":     { "src": "space_video_bonus_intro.mp4", "preload": "P2" }
  }
}

Règles : preload = P0 (avant premier écran) / P1 (avant premier spin) / P2 (lazy). État manquant → fallback procédural du moteur. Clé manquante → placeholder + warning Factory.

4. theme/tokens.json — style

{
  "colors": { "primary": "#f4c95d", "panel": "#170f29", "text": "#ece6ff",
              "win": "#3ddc84", "accent": "#a64dff", "danger": "#ff5a6e" },
  "fonts":  { "display": "space_font_display.woff2", "ui": null },   // null = fallback moteur (Cinzel/system)
  "style":  { "buttonShape": "round", "borderRadius": 14, "glowIntensity": 0.8,
              "shadowDepth": 0.6, "gradientAngle": 180 }
}

→ injecté en CSS custom properties (--ck-primary, --ck-font-display…). Aucune couleur codée en dur dans les composants moteur.

5. math/math.meta.json — GÉNÉRÉ (source de vérité front↔math)

Produit par factory/math-tools à partir de publish_files/ — jamais édité à la main (leçon TNF : BUY_COST dupliqué).

{
  "generatedFrom": "publish_files/index.json (sha256:…)",
  "rtp": 0.96,
  "wincapX": 10000,
  "modes": [
    { "name": "base",     "cost": 1.0,   "label": "strings:mode.base" },
    { "name": "bonusbuy", "cost": 100.0, "label": "strings:mode.bonusbuy", "isBuy": true }
  ],
  "paytable": [ { "symbol": "H1", "of": [3,4,5], "pays": [5,15,50] } ],
  "hitRates": { "base": 0.281, "freeSpins": 0.00124 },
  "reviewEvents": "REVIEW-EVENTS.md",
  "verification": { "books": 57819, "lutMismatches": 0, "rtpExactPerMode": true }
}

6. Ce qu'un créateur de jeu NE PEUT PAS faire

7. Validation

factory validate <gameId> exécute : JSON Schema du config → cohérence symbols↔math (ids du paytable) → manifest complet (checklist SLOT_ASSET_MANIFEST) → dimensions/poids → budgets perf → lint layout (touch targets, safe areas) → maths (RTP par mode, book↔LUT) → build test + smoke Playwright. Rapport bloquant/warnings. C'est la même passe que l'onglet « Validation » du site Factory.