Aller au contenu
EnglishCréer une partie

Référence du format de scénario Stratilead — version 1.0

Ce document décrit intégralement le format d’un scénario Stratilead. Il fait foi pour :

  • les auteurs de scénarios (éditeur du jeu, intervenants) ;
  • les IA à qui l’on demande de générer un scénario (voir aussi le kit IA, plus condensé) ;
  • les développeurs du moteur, de l’éditeur et du validateur.

Le langage des formules est décrit à part, dans langage-formules.md. La façon dont le moteur exécute un tour est détaillée dans ../04-moteur-simulation.md.

Conventions de rédaction : « DOIT » = obligatoire (le validateur refuse sinon) ; « DEVRAIT » = recommandé (le validateur avertit) ; « PEUT » = facultatif.


1. Le modèle mental : Entrées → Règles → Sorties → État

Section intitulée « 1. Le modèle mental : Entrées → Règles → Sorties → État »

Un scénario décrit une ou plusieurs entreprises qui jouent plusieurs tours (un trimestre, un mois, une saison… le nom est libre). À chaque tour :

┌──────────────────────────────────────────────────────────┐
│ UN TOUR │
ÉTAT au │ ENTRÉES RÈGLES SORTIES │ ÉTAT au
début du ──┼─►(décisions des ──► (formules, ──► (indicateurs, ─┼─► tour suivant
tour │ joueurs) marché, résultats, │ (variables
(variables)│ événements) classement) │ mises à jour)
└──────────────────────────────────────────────────────────┘
Brique Rôle Exemple (usine de RAM)
Constantes Paramètres fixes du scénario Coût matière d’une barrette, salaire trimestriel
Variables d’état Ce qui se reporte d’un tour au suivant (les « stocks » au sens dynamique) Trésorerie, stock, effectif, qualité
Décisions Les entrées des joueurs à chaque tour Prix de vente, volume de production, embauches
Formules Les règles de calcul du tour Coût de production = volume × coût unitaire
Marché Répartit la demande entre entreprises concurrentes Parts de marché selon prix, qualité, notoriété
Événements Ce qui arrive en cours de partie Pénurie de silicium, grève, commande géante
Score Ce qui classe les entreprises Bénéfice cumulé

Tout le reste (thème, images, rapports, pédagogie, bots, difficultés) habille et paramètre ces briques.

Règle d’or : tout ce qui se calcule est une « grandeur » nommée. Constantes, variables, décisions, formules et sorties du marché partagent un même espace de noms. Une formule peut utiliser n’importe quelle grandeur par son identifiant, et le moteur calcule tout dans le bon ordre automatiquement.


  • Encodage UTF-8, JSON strict (pas de commentaires, pas de virgule finale).
  • Taille maximale : 2 Mo.
  • La première clé DEVRAIT être $schema, qui pointe vers le JSON Schema du format, pour l’autocomplétion dans les éditeurs de code : "$schema": "https://stratilead.fr/schema/scenario-1.0.json" (domaine définitif à confirmer, voir la spec technique).

Pour embarquer des images, un scénario s’exporte et s’importe sous forme d’archive ZIP portant l’extension .stratilead :

mon-scenario.stratilead
├── scenario.json
└── assets/
├── cover.webp
├── fond-usine.jpg
└── evt-penurie.png
Contrainte Valeur
Taille maximale du paquet 25 Mo
Taille maximale d’une image 5 Mo
Formats d’image acceptés PNG, JPEG, WebP, AVIF (le SVG est refusé pour des raisons de sécurité)
Dimensions maximales 4096 × 4096 px
Nombre maximal d’images 100

À l’import, la plateforme recompresse les images (WebP), supprime les métadonnées EXIF et les héberge elle-même.

Un scenario.json seul (sans images) est toujours importable : les images manquantes sont remplacées par des illustrations par défaut du secteur, et l’éditeur signale ce qui manque.


Élément Règle Exemple
Identifiant de scénario (id) minuscules, chiffres, tirets : ^[a-z0-9]+(-[a-z0-9]+)*$, 3 à 64 caractères usine-ram
Tous les autres identifiants (grandeurs, listes, éléments de liste, options, événements, courbes, groupes, rôles, niveaux…) camelCase ASCII : ^[a-z][a-zA-Z0-9_]{0,47}$ tresorerie, prixVente, ddr5
  • Les identifiants de grandeurs (constantes, variables, décisions, formules, sorties de marché) DOIVENT être uniques entre eux.
  • Les autres familles (listes, éléments d’une même liste, options d’une même décision, événements, courbes, groupes, rôles, niveaux, prix, bots) ont chacune leur propre espace de noms.
  • Les identifiants sont en français sans accents de préférence (le public est francophone), en camelCase : coutMatiere, pas coût_matière.
  • Mots réservés, interdits comme identifiants de grandeur : les noms de fonctions du langage de formules, les grandeurs intégrées (§22), et true, false, and, or, not, item, previous.

Partout où un texte est affiché, on peut écrire :

  • soit une chaîne simple, dans la langue principale du scénario (meta.language) : "label": "Trésorerie" ;
  • soit un objet par langue : "label": { "fr": "Trésorerie", "en": "Cash" }.

Un scénario multilingue déclare ses langues dans meta.languages (la langue principale en premier) ; chaque texte affiché doit alors exister dans chacune d’elles (avertissement P720). Les noms propres qui ne sont pas des textes (meta.authors[].name, meta.tags, assets.*.credit, unit) restent des chaînes simples. La langue d’une session est choisie à sa création parmi meta.languages (I18N-03) : tous les textes rendus (vues, gabarits, événements, rapports, débrief, PDF, CSV) le sont dans cette langue, et un texte absent de cette langue retombe sur la langue principale. Exemple : le scénario officiel limonade est bilingue (["fr", "en"]).

Les champs « markdown » acceptent du Markdown simple : gras, italique, listes, liens, tableaux. Le HTML est interdit (il est retiré à l’affichage).

Les textes d’événements, de conseils, d’enseignements et de débrief peuvent afficher des valeurs calculées :

"Votre stock de {{ stockTotal }} barrettes vaut {{ valeurStock | money }}."
  • Entre {{ et }} : n’importe quelle expression du langage de formules, évaluée dans le contexte de l’entreprise concernée (ou du monde).
  • Filtre de format facultatif après | : money, percent, integer, number, number(2) (2 décimales).
  • Sans filtre, une grandeur nommée s’affiche avec son propre format ; une expression calculée s’affiche comme number.
  • Un gabarit est évalué au moment où le texte s’affiche, et suit les règles de ce moment (§3.11) : annonce pour les textes d’événements, conseils, aides des décisions et briefing ; saisie pour les messages de contraintes ; résolution pour les issues de dilemmes ; fin de partie pour les enseignements.

Chaque grandeur PEUT déclarer un format d’affichage :

format Affichage Décimales par défaut
money 12 500 € (devise de settings.currency) 0
number 1 234,5 1
integer 1 235 0
percent 15,0 % 1
boolean Oui / Non —

decimals PEUT surcharger le nombre de décimales affichées. unit PEUT ajouter une unité libre ("unités", "pers.", "pts").

Les exemples ci-dessus sont en français. Dans une session en anglais (lot L26), les mêmes valeurs s’écrivent €12,500, 1,234.5, 15.0%, Yes / No ; toute autre langue garde les conventions françaises.

Important — les pourcentages sont stockés en ratio. Une valeur au format percent vaut 0.15 pour 15 %. C’est vrai partout : constantes, décisions (une décision percent bornée de 0 à 0.3 s’affiche de 0 à 30 %), formules. Ne jamais écrire 15 pour 15 %.

Certains champs acceptent soit une valeur littérale, soit une expression du langage de formules. La règle est uniforme :

  • un nombre ou un booléen JSON est une valeur littérale ;
  • une chaîne JSON est une expression ("max": "tresorerie / 2") ;
  • un objet JSON est une valeur par élément de liste (§3.7) ;
  • seule exception : les champs initial et default des décisions choice / multiChoice contiennent des identifiants d’options (et default accepte le mot-clé "previous"), voir §13.

Dans une expression, une chaîne littérale s’écrit entre apostrophes : fournisseur == 'europe'.

scope Signification Exemple
company (défaut) La grandeur existe en un exemplaire par entreprise Trésorerie, prix de vente
world La grandeur existe en un seul exemplaire pour tout le marché Prix du silicium, taux d’intérêt, taille du marché

Une formule de monde peut agréger les entreprises (market_sum(marketing)) ; une formule d’entreprise peut lire les grandeurs du monde. Les décisions sont toujours de portée company.

En marché parallèle, chaque entreprise joue dans son propre monde, identique pour tous (§15.4).

Une liste énumère des éléments (produits, gammes de chambres, lignes aériennes, services…). Une grandeur déclarée avec "per": "produit" possède une valeur par élément de la liste produit.

Valeur par élément, dans les champs value / initial / default :

"value": { "ddr4": 15, "ddr5": 26 }
"value": { "*": 10, "ddr5": 16 } // "*" = valeur pour tous les éléments non cités
"value": 10 // même valeur pour tous les éléments

Dans une formule elle-même déclarée per la même liste, une grandeur par élément s’utilise sans indice : elle désigne l’élément courant. Les détails (indice explicite prix[ddr5], agrégations sum(ventes), élément courant item) sont dans le langage de formules.

Le format 1.0 gère une seule dimension par grandeur. Pour croiser deux dimensions (ligne aérienne × classe), on crée une liste d’éléments combinés (parisNiceEco, parisNiceBusiness…).

Qui voit la valeur d’une grandeur pendant la partie :

Valeur Qui voit Défaut pour
private L’entreprise elle-même et l’intervenant grandeurs company (y compris décisions)
public Tout le monde : les joueurs voient la valeur de chaque concurrent après la résolution du tour grandeurs world
facilitator Seulement l’intervenant —
hidden Calcul intermédiaire : jamais affiché comme grandeur dans les rapports, mais détaillé dans « Comprendre ce chiffre » quand il entre dans le calcul d’une grandeur que le spectateur a le droit de voir ; visible dans l’éditeur, le banc d’essai et la vue de débogage de l’intervenant. Pour un secret, utiliser facilitator —

revealInDebrief: true PEUT rendre la grandeur visible de tous au débrief final (ex. : une qualité cachée révélée à la fin).

Les décisions des concurrents marquées public (typiquement le prix) ne sont révélées qu’après la résolution du tour, jamais pendant la saisie.

La plupart des éléments (décisions, formules, événements, contraintes, conseils, éléments de liste, bots…) acceptent "levels": ["normal", "difficile"] : l’élément n’existe que dans ces niveaux de difficulté. Sans levels, il existe dans tous les niveaux. Voir §18.

  • icon : un nom d’icône de la bibliothèque Lucide ("factory", "users", "euro", "megaphone"…).
  • Couleurs : hexadécimal #RRGGBB.

Une expression n’est pas toujours évaluée au même moment du tour, et elle ne peut utiliser que ce qui est déjà connu à ce moment-là :

Moment Expressions concernées Peut utiliser
Création (début de partie) initial des variables Constantes, décisions de setup, grandeurs intégrées
Annonce (début du tour, avant la saisie) trigger.probability, trigger.when, select.by, enabledIf, auto, tips[].when, décisions des bots, value des effets d’un événement effect, gabarits des textes d’annonce (text des événements, help, briefing, conseils) Constantes, variables (valeur de début de tour), décisions de setup, grandeurs intégrées, prev(…) de n’importe quoi, et les formules qui ne dépendent que de ces éléments
Saisie (pendant que les joueurs décident) min, max, step des décisions, constraints, preview Tout ce qui précède, plus les décisions du tour et les formules qui en dépendent, sauf le marché et le hasard
Résolution (fin du tour) expr des formules, next des variables, marché, effets des dilemmes et quiz Tout ; les variables valent leur valeur de début de tour
Clôture (après la mise à jour des variables) scoring.score, scoring.tiebreak, bankruptcy.when, valeurs des effets de faillite Tout ; les variables valent leur valeur de fin de tour (résultat de next), les formules leur valeur du tour
Fin de partie (clôture du dernier tour) insights, awards[].by, objectives[].check Comme la clôture, sur toute l’historique (cumul, prev)

Le validateur signale toute expression qui utilise une grandeur pas encore disponible à son moment (code T340). Pour utiliser un résultat du tour précédent à l’annonce, écrivez prev(resultatNet).

Dans les rapports, une variable affichée pour un tour donné montre sa valeur de fin de ce tour (la trésorerie « au 31 mars »), une formule sa valeur du tour.


{
"$schema": "https://stratilead.fr/schema/scenario-1.0.json",
"formatVersion": "1.0",
"id": "usine-ram",
"version": "1.0.0",
"meta": { },
"settings": { },
"theme": { },
"assets": { },
"groups": [ ],
"roles": [ ],
"lists": { },
"constants": [ ],
"curves": { },
"variables": [ ],
"decisions": [ ],
"constraints": [ ],
"formulas": [ ],
"market": { },
"events": [ ],
"bots": { },
"difficulties": { },
"scoring": { },
"reports": { },
"pedagogy": { }
}
Clé Requis Section
$schema non §2.1
formatVersion oui — "1.0" §24
id oui §3.1
version oui — semver "1.0.0" §24
meta oui §5
settings oui §6
theme non §7
assets non §7
groups non (recommandé dès 5 décisions) §8
roles non §8
lists non §9
constants non §10
curves non §11
variables oui (au moins une) §12
decisions oui (au moins une) §13
constraints non §13.6
formulas non §14
market non (sans marché, la demande est entièrement définie par les formules) §15
events non §16
bots non §17
difficulties non (un niveau unique normal est alors créé) §18
scoring oui §19
reports non (un tableau de bord par défaut est généré) §20
pedagogy non (fortement recommandé) §21

"meta": {
"title": "Mémoire Vive",
"subtitle": "Dirigez une usine de barrettes de RAM",
"summary": "Production, prix, stocks et RH : survivez au marché de la mémoire.",
"description": "## L'histoire\nVous reprenez une usine…",
"sector": "industrie",
"tags": ["production", "stocks", "prix", "rh"],
"language": "fr",
"languages": ["fr"],
"audience": "Post-bac, écoles de commerce et d'ingénieurs",
"estimatedDuration": { "min": 60, "max": 120 },
"authors": [{ "name": "Équipe Stratilead" }],
"license": "CC-BY-4.0",
"cover": "cover"
}
Champ Type Requis Description
title LocalizedText (≤ 60 car.) oui Nom du scénario
subtitle LocalizedText (≤ 100) non Accroche
summary LocalizedText (≤ 200) oui Résumé pour la marketplace
description LocalizedText markdown non Description longue, histoire, contexte
sector enum oui industrie, hotellerie, restauration, transport, commerce, services, tech, agriculture, sante, finance, energie, culture, autre
tags string[] (≤ 10) non Mots-clés libres de recherche
language code langue (fr, en…) oui Langue principale des textes
languages code langue[] non Langues disponibles (défaut : [language])
audience LocalizedText non Public conseillé
estimatedDuration { min, max } minutes non Durée indicative d’une session
authors { name, url? }[] non Auteurs (l’auteur du compte est ajouté automatiquement)
license string non CC-BY-4.0, CC-BY-SA-4.0, CC-BY-NC-4.0, proprietary (défaut)
cover clé d’asset non Image de couverture (marketplace, lobby)

Les objectifs pédagogiques sont dans pedagogy (§21).


"settings": {
"currency": "EUR",
"roundLabel": { "singular": "trimestre", "plural": "trimestres" },
"companyLabel": { "singular": "usine", "plural": "usines" },
"rounds": { "default": 8, "min": 4, "max": 12 },
"practiceRound": true,
"marketModes": ["shared", "parallel"],
"playModes": ["teams", "individual"],
"sharedModeLuck": "independent",
"limits": { "maxPlayers": 256, "maxCompanies": 64, "teamSize": { "min": 1, "max": 5 } },
"decisionTime": { "default": 420, "min": 120, "max": 1800 }
}
Champ Type Défaut Description
currency code ISO 4217 "EUR" Devise affichée
roundLabel { singular, plural } LocalizedText « tour / tours » Nom d’un tour (« mois », « saison », « trimestre »…)
companyLabel { singular, plural } « entreprise / entreprises » Nom d’une entreprise (« hôtel », « compagnie »…)
rounds { default, min, max } {8, 3, 20} Nombre de tours ; l’intervenant choisit dans [min, max]. Maximum absolu : 40
practiceRound booléen false Propose un tour d’essai, rejoué ensuite depuis l’état initial (l’intervenant peut le désactiver)
marketModes ("shared" | "parallel")[] ["parallel"] Modes de marché supportés ; le premier est le défaut. Voir §15
playModes ("teams" | "individual")[] ["teams", "individual"] Équipes, ou une entreprise par joueur
sharedModeLuck "independent" | "identical" "independent" En marché partagé : les tirages aléatoires propres aux entreprises (fonctions aléatoires des formules et événements company à sélection each, issues des dilemmes) sont-ils indépendants, ou identiques pour toutes (équité maximale : un événement aléatoire touche alors toutes les entreprises ou aucune) ? En marché parallèle, ils sont toujours identiques
limits.maxPlayers entier 1–256 256 Joueurs maximum par session pour ce scénario (la plateforme plafonne à 256)
limits.maxCompanies entier 1–64 64 Entreprises maximum (équipes + bots) dans un marché partagé. maxPlayers DOIT rester atteignable : au plus maxCompanies × teamSize.max joueurs (256 joueurs = 64 équipes de 4) ; sinon avertissement P740. Un marché partagé à beaucoup d’entreprises garde son équilibre si sa taille dépend de companyCount
limits.teamSize { min, max } {1, 6} Taille des équipes
decisionTime { default, min, max } secondes {300, 60, 3600} Temps de décision conseillé par tour

Dictionnaire clé → image. Les autres sections référencent les images par leur clé.

"assets": {
"cover": { "src": "assets/cover.webp", "alt": "Une salle blanche de production de RAM" },
"fondUsine": { "src": "assets/fond-usine.jpg", "alt": "Ligne de production", "credit": "Photo : X" },
"evtPenurie": { "alt": "Wafers de silicium" }
}
Champ Type Requis Description
src chemin relatif dans le paquet (assets/…) non Absent = image à fournir plus tard (l’éditeur le signale, une illustration par défaut est utilisée)
alt LocalizedText oui Texte alternatif (accessibilité), et description pour qui devra fournir l’image
credit string non Crédit / licence de l’image

Les URL externes (https://…) ne sont pas acceptées dans src : la plateforme héberge toutes les images.

"theme": {
"primaryColor": "#2563EB",
"accentColor": "#F59E0B",
"background": "fondUsine",
"logo": "logoScenario",
"companyAvatars": ["avatarUsine1", "avatarUsine2"]
}
Champ Type Description
primaryColor couleur Couleur principale de l’interface pendant la partie
accentColor couleur Couleur secondaire
background clé d’asset Image de fond (lobby, écran de projection)
logo clé d’asset Logo du scénario
companyAvatars clés d’assets Avatars proposés aux équipes pour leur entreprise (sinon : avatars génériques)

Le nom, la couleur et l’avatar de chaque entreprise sont choisis par les joueurs en début de partie : ce n’est pas défini par le scénario.


8. groups et roles — organisation des décisions

Section intitulée « 8. groups et roles — organisation des décisions »

Les groupes rangent les décisions (onglets de l’écran de décision) et peuvent servir de sections dans les rapports.

"groups": [
{ "id": "production", "label": "Production", "icon": "factory", "description": "Volumes, lignes, maintenance, achats." },
{ "id": "rh", "label": "Ressources humaines", "icon": "users" },
{ "id": "commercial", "label": "Commercial", "icon": "megaphone" },
{ "id": "finance", "label": "Finance", "icon": "landmark" }
]

L’ordre du tableau est l’ordre d’affichage. Une décision sans group va dans un groupe « Général ».

En mode équipe, les rôles répartissent les décisions entre les membres. Facultatif : sans rôles, tous les membres éditent toutes les décisions et le capitaine valide.

"roles": [
{ "id": "pdg", "label": "PDG", "description": "Arbitre et valide les décisions.", "groups": ["strategie"], "captain": true },
{ "id": "prod", "label": "Directeur de production", "groups": ["production"] },
{ "id": "drh", "label": "DRH", "groups": ["rh"] },
{ "id": "mkt", "label": "Directeur commercial", "groups": ["commercial"] },
{ "id": "daf", "label": "Directeur financier", "groups": ["finance"] }
]
Champ Description
groups Groupes de décisions que ce rôle peut modifier (tous les rôles voient tout)
captain Exactement un rôle DOIT être captain: true si roles est défini : il valide l’envoi des décisions

Si l’équipe compte moins de membres que de rôles, la plateforme cumule les rôles (le capitaine récupère les rôles non attribués). L’intervenant PEUT désactiver les rôles pour une session.


"lists": {
"produit": {
"label": "Produits",
"items": [
{ "id": "ddr4", "label": "DDR4 16 Go", "description": "Technologie mature, marché en déclin.", "image": "imgDdr4" },
{ "id": "ddr5", "label": "DDR5 32 Go", "description": "Technologie récente, marché en croissance." }
]
}
}
Champ Type Description
label LocalizedText Nom de la liste
items tableau, 1 à 50 éléments Éléments de la liste
items[].id identifiant Unique dans la liste
items[].label LocalizedText Nom affiché
items[].description LocalizedText Description
items[].image / icon clé d’asset / icône Illustration
items[].levels niveaux L’élément n’existe que dans ces niveaux de difficulté

Les caractéristiques chiffrées des éléments (coût, capacité…) se déclarent comme constantes par élément (§10), pas dans la liste.


"constants": [
{ "id": "salaireTrimestriel", "label": "Salaire chargé par employé", "value": 10000, "format": "money", "group": "rh" },
{ "id": "coutMatiere", "label": "Coût matière unitaire", "per": "produit", "value": { "ddr4": 15, "ddr5": 26 }, "format": "money" },
{ "id": "tresorerieInitiale", "label": "Trésorerie de départ", "value": 250000, "format": "money",
"adjustable": { "min": 100000, "max": 500000, "step": 10000 } }
]
Champ Type Requis Description
id identifiant oui
label LocalizedText oui
value nombre, booléen ou valeurs par élément oui Littéral uniquement (pas d’expression)
per id de liste non Une valeur par élément
scope — — Sans objet : une constante est la même pour tous
format, decimals, unit, description, concept, group, icon non Affichage (§3.4, §21)
visibility non Défaut public (les règles du jeu sont connues) ; facilitator ou hidden pour garder un paramètre secret
adjustable { min, max, step? } non L’intervenant peut modifier cette valeur en créant sa session (« paramètres avancés »)
levels niveaux non

Les constantes peuvent être modifiées temporairement par des événements (§16.6) et par niveau de difficulté (§18).


Une courbe transforme une valeur en une autre par interpolation entre des points. C’est le moyen le plus simple d’exprimer un rendement décroissant, un seuil ou un barème sans mathématiques.

"curves": {
"effetFormation": {
"label": "Effet du budget de formation sur la compétence",
"points": [[0, -2.5], [250, 0], [500, 2.5], [1000, 5], [2000, 7]],
"interpolation": "linear"
},
"baremeRemise": {
"label": "Remise selon le volume commandé",
"points": [[0, 0], [1000, 0.05], [5000, 0.1]],
"interpolation": "step"
}
}
Champ Valeurs Description
points [x, y][], 2 à 50 points, x strictement croissants Points de la courbe
interpolation linear (défaut), step, smooth step = palier : la valeur du dernier point dont x ≤ entrée. smooth = interpolation monotone (Fritsch–Carlson)
extrapolation clamp (défaut), linear, ou { "left": …, "right": … } Hors des bornes : on garde la valeur du premier / dernier point, ou on prolonge la pente ; la forme objet règle chaque côté séparément

Usage dans une formule : curve(effetFormation, formationParEmploye).


12. variables — l’état qui se reporte d’un tour à l’autre

Section intitulée « 12. variables — l’état qui se reporte d’un tour à l’autre »
"variables": [
{ "id": "tresorerie", "label": "Trésorerie", "format": "money",
"initial": "tresorerieInitiale",
"next": "tresorerie + fluxTresorerie",
"visibility": "private" },
{ "id": "stock", "label": "Stock", "per": "produit", "format": "integer", "unit": "barrettes",
"initial": { "ddr4": 1500, "ddr5": 500 },
"next": "(stock + productionReelle - ventesTotales) * (1 - obsolescence)",
"min": 0 },
{ "id": "qualite", "label": "Qualité", "format": "number", "decimals": 0, "unit": "/100",
"initial": "if(positionnement == 'premium', 66, 60)",
"next": "qualite + 0.5 * (qualiteCible - qualite)",
"min": 0, "max": 100, "visibility": "public" },
{ "id": "prixSilicium", "label": "Indice du prix du silicium", "scope": "world", "format": "number", "decimals": 2,
"initial": 1,
"next": "prixSilicium + 0.3 * (1 - prixSilicium) + normal(0, 0.04)",
"min": 0.6, "max": 2 }
]
Champ Type Requis Description
id, label oui
initial valeur, valeurs par élément, ou expression oui Valeur au début du tour 1. Une expression ne PEUT utiliser que des constantes, des décisions de setup (§13.4) et des grandeurs intégrées
next expression non Valeur au début du tour suivant, calculée en fin de résolution. Toutes les grandeurs du tour sont disponibles. Sans next, la variable ne change que par les événements
per, scope non §3.6, §3.7
min, max nombre non Bornes appliquées à la valeur après chaque mise à jour (next et événements)
format, decimals, unit, description, concept, group, icon, visibility, revealInDebrief, levels non

Sémantique essentielle : pendant le tour t, écrire tresorerie dans une formule donne la valeur au début du tour t. La valeur de fin de tour est le résultat de next, qui devient la valeur de début du tour t + 1.

Arrondis à l’enregistrement : à la fin de chaque tour, une variable integer est arrondie à l’entier le plus proche, une variable money au centime. Les formules, elles, ne sont jamais arrondies (seulement à l’affichage).


{
"id": "prix",
"label": "Prix de vente",
"per": "produit",
"type": "money",
"min": 10, "max": 150, "step": 0.5,
"initial": { "ddr4": 38, "ddr5": 65 },
"default": "previous",
"group": "commercial",
"widget": "slider",
"help": "Un prix bas attire les clients mais réduit votre marge.",
"concept": "elasticitePrix",
"visibility": "public",
"preview": ["margeUnitaire"]
}
Champ Type Requis Description
id, label oui
type voir §13.2 oui
per id de liste non Une saisie par élément (ex. un prix par produit)
phase round (défaut) | setup non setup = décision prise une seule fois avant le tour 1 (§13.4)
initial valeur / valeurs par élément non Valeur proposée au premier tour (défaut : min pour les nombres, false, première option)
default valeur, "previous", ou expression non Valeur proposée aux tours suivants et utilisée si l’entreprise n’envoie rien. Défaut : "previous" (on reprend la décision du tour précédent). Pour un flux ponctuel (embauches, investissement), mettre 0
auto expression non Valeur utilisée quand la décision n’est pas proposée aux joueurs (niveau de difficulté qui l’exclut, enabledIf faux), évaluée au moment « annonce » (donc sans les autres décisions du tour). Permet d’automatiser une décision en mode facile. Défaut : la valeur de default
enabledIf expression booléenne non Décision proposée seulement si vrai (ex. round >= 3, lignes > 0). Évaluée au début du tour
levels niveaux non Niveaux où la décision est proposée. Hors de ces niveaux, auto s’applique
group id de groupe non Onglet de l’écran de décision
widget voir §13.2 non Contrôle d’interface
help LocalizedText markdown DEVRAIT Aide affichée à côté du champ : à quoi sert la décision, quel arbitrage
concept id de concept non Lien vers le glossaire (§21)
visibility non Défaut private. public = les concurrents voient la décision après la résolution
preview ids de formules non Formules calculées en direct pendant la saisie, affichées comme estimation (ex. coût estimé). Elles ne DOIVENT pas dépendre du marché ni du hasard (§13.6)
format, decimals, unit, icon, description non
type Valeur Champs propres Widgets possibles (premier = défaut)
number nombre réel min, max, step slider, input, stepper
integer entier min, max, step stepper, slider, input
money nombre (format money) min, max, step input, slider, stepper
percent ratio 0–1 min, max, step (en ratio) slider, input
boolean booléen labels: { true, false } toggle, cards
choice id d’une option options cards, select, radio
multiChoice liste d’ids d’options options, minSelected, maxSelected cards, checkboxes
allocation une valeur par option options, total, allocationMode allocation

min, max et step acceptent un nombre, une expression (ex. "max": "employes" pour les licenciements) ou, pour une décision per, des valeurs par élément ("max": { "toulouse": 400, "*": 250 }). Une expression de borne est évaluée pour chaque élément ; elle PEUT dépendre d’autres décisions du même tour (l’interface recalcule en direct), mais ni du marché ni du hasard.

{
"id": "fournisseur",
"label": "Fournisseur de wafers",
"type": "choice",
"group": "production",
"initial": "europe",
"options": [
{ "id": "asie", "label": "Asie (low cost)", "description": "Moins cher, qualité moyenne.", "icon": "ship",
"props": { "facteurCout": 0.85, "facteurQualite": 0.6 } },
{ "id": "europe", "label": "Europe", "props": { "facteurCout": 1.0, "facteurQualite": 0.8 } },
{ "id": "premium", "label": "Premium (Japon)", "props": { "facteurCout": 1.2, "facteurQualite": 1.0 },
"enabledIf": "round >= 2" }
]
}
Champ d’option Description
id, label requis
description, image, icon présentation (le widget cards affiche des cartes illustrées)
props caractéristiques numériques ou booléennes de l’option, lues dans les formules par fournisseur.facteurCout
enabledIf option proposée seulement si vrai
levels niveaux où l’option existe

Dans les formules :

  • choice : fournisseur vaut l’id choisi (fournisseur == 'asie') ; fournisseur.facteurCout vaut la propriété de l’option choisie.
  • multiChoice : has(certifications, 'iso9001') ; count(certifications) ; certifications.cout = somme de la propriété sur les options cochées.
  • allocation : canaux.web = montant alloué à l’option web.

initial / default d’un choice : un id d’option (ou "previous" pour default). Pour un multiChoice : un tableau d’ids. Pour une allocation : un objet { idOption: valeur }.

Allocation — répartir un total entre des options (budget marketing entre canaux, temps de travail entre activités) :

{
"id": "canaux", "label": "Répartition du budget marketing", "type": "allocation", "format": "percent",
"total": 1, "allocationMode": "exact",
"options": [ { "id": "web", "label": "Web" }, { "id": "salons", "label": "Salons" }, { "id": "presse", "label": "Presse spécialisée" } ],
"initial": { "web": 0.5, "salons": 0.3, "presse": 0.2 }
}

total : nombre ou expression. allocationMode : exact (la somme DOIT égaler le total ; défaut) ou upTo (somme ≤ total). Une allocation ne peut pas être déclarée per.

Prises une fois, pendant la phase de création des entreprises (avec le nom et l’avatar), avant le tour 1. Elles servent aux choix stratégiques fondateurs : positionnement, implantation, statut juridique…

{
"id": "positionnement", "phase": "setup", "type": "choice", "label": "Positionnement de votre usine",
"options": [
{ "id": "volume", "label": "Volume", "description": "Coûts réduits, qualité de départ plus faible.", "props": { "bonusCout": -0.05, "bonusQualite": -5 } },
{ "id": "equilibre", "label": "Équilibre", "props": { "bonusCout": 0, "bonusQualite": 0 } },
{ "id": "premium", "label": "Premium", "description": "Meilleure qualité, coûts fixes plus élevés.", "props": { "bonusCout": 0.05, "bonusQualite": 6 } }
]
}

Leur valeur reste ensuite constante toute la partie ; elles sont utilisables dans les initial des variables et partout ailleurs.

Pour chaque entreprise et chaque tour, la valeur utilisée par le moteur est, dans l’ordre :

  1. si la décision n’est pas proposée (niveau, enabledIf faux) → auto si défini, sinon la valeur par défaut ci-dessous ;
  2. sinon, la valeur envoyée par l’entreprise ;
  3. si l’entreprise n’a rien envoyé : le dernier brouillon valide, sinon default (previous = valeur du tour précédent ; au tour 1, initial) ;
  4. la valeur est enfin bornée à [min, max], arrondie au step et à l’entier pour integer.
"constraints": [
{ "id": "budgetLignes", "check": "nouvellesLignes * coutLigne <= tresorerie + nouvelEmprunt",
"message": "Vous n'avez pas la trésorerie pour financer ces nouvelles lignes.", "severity": "error", "group": "production" },
{ "id": "surproduction", "check": "chargeDemandee <= capacite * 1.0001",
"message": "Votre production dépasse votre capacité : elle sera réduite de {{ (1 - tauxCharge) | percent }}.", "severity": "warning" }
]
Champ Description
check expression booléenne qui DOIT être vraie
message texte affiché quand elle est fausse (gabarits {{ }} permis)
severity error : bloque l’envoi des décisions ; warning : simple alerte
group onglet où afficher l’alerte
levels niveaux concernés

Les contraintes, les preview et les bornes min/max sont évaluées pendant la saisie, avant que le marché ne soit connu. Elles ne DOIVENT donc dépendre ni du marché (sorties de market, fonctions market_*, share, rank), ni du hasard. Le validateur le vérifie (code T340).

Robustesse. Une contrainte error empêche l’envoi manuel, mais si le temps est écoulé le moteur résout quand même le tour avec la dernière saisie valide ou les valeurs par défaut. Les formules DOIVENT donc rester cohérentes même avec des décisions absurdes : utilisez min() / max() dans les formules plutôt que de compter sur les contraintes.


"formulas": [
{ "id": "ca", "label": "Chiffre d'affaires", "per": "produit", "expr": "ventes * prix", "format": "money",
"description": "Prix de vente × quantités vendues." },
{ "id": "caTotal", "label": "Chiffre d'affaires total", "expr": "sum(ca)", "format": "money", "concept": "chiffreAffaires" },
{ "id": "tailleMarche", "label": "Demande potentielle", "scope": "world", "per": "produit",
"expr": "demandeBase * pow(tendance, round - 1) * saisonnalite", "format": "integer" }
]
Champ Type Requis Description
id, label oui
expr expression oui Voir le langage de formules
per, scope non §3.6, §3.7
description LocalizedText DEVRAIT Explication en français de la règle, affichée dans « Comprendre ce chiffre »
format, decimals, unit, concept, group, icon, visibility, revealInDebrief, levels non
  • L’ordre des formules dans le fichier n’a aucune importance : le moteur construit le graphe des dépendances et calcule dans le bon ordre.
  • Une dépendance circulaire entre formules est une erreur (D400). Pour boucler, passer par une variable d’état (sa valeur est celle du début du tour) ou par prev().
  • Une formule exclue par levels vaut 0 (ou false) ; le validateur avertit si une formule active en dépend.
  • Chaque formule s’affiche avec un bouton « Comprendre ce chiffre » qui décompose le calcul avec les valeurs réelles (ex. CA = ventes (9 120) × prix (38 €) = 346 560 €).

Le bloc market répartit une demande entre des entreprises selon leur attractivité. Il gère les ruptures de stock et le report des clients non servis. C’est lui qui rend possible la concurrence en marché partagé.

"market": {
"segments": [
{
"id": "oem",
"label": "Fabricants de PC et serveurs",
"per": "produit",
"size": "tailleMarche * if(mode == 'shared', companyCount * 1.2, 4.2)",
"attractiveness": "pow(prixReference / prix, elasticite) * pow(qualite / 60, 1.2) * (0.8 + 0.5 * notoriete / 100)",
"supply": "disponibleMarche",
"outsideOption": "0.2 * if(mode == 'shared', companyCount, 1)",
"spillover": 0.6,
"integerUnits": true,
"virtualCompetitors": [
{ "id": "importateurs", "label": "Importateurs établis", "modes": ["parallel"],
"attractiveness": "3 * pow(qualiteConcurrence / 60, 1.2)" }
],
"outputs": { "demand": "demande", "sales": "ventes", "share": "partDeMarche", "lost": "ventesPerdues" }
}
]
}
Champ Portée de l’expression Requis Description
id, label oui
per non Le segment est dupliqué pour chaque élément de la liste (un marché par produit)
size monde oui Demande potentielle totale du segment, en unités, pour le tour
attractiveness entreprise oui Attractivité d’une entreprise (≥ 0) : plus elle est grande, plus l’entreprise capte de clients
supply entreprise non Quantité que l’entreprise peut vendre ce tour (stock disponible). Absent = illimité
outsideOption monde non Attractivité de « ne rien acheter ». Défaut 0 : toute la demande achète. Plus elle est grande, plus des prix élevés font fuir les clients du marché
spillover nombre 0–1 non Part des clients non servis (rupture de stock) qui se reportent sur les concurrents ayant du stock. Défaut 0.5
integerUnits booléen non Ventes arrondies à l’unité inférieure. Défaut true
virtualCompetitors tableau non Concurrents fictifs pilotés par le scénario (§15.5)
outputs objet oui Noms des grandeurs produites (§15.3)

Chaque sortie déclarée dans outputs devient une grandeur d’entreprise utilisable partout (par élément si le segment est per) :

Clé Signification
demand Demande adressée à l’entreprise (part de la demande + clients reportés depuis les concurrents en rupture)
sales Quantité effectivement vendue (≤ supply)
share Part de marché : ventes de l’entreprise ÷ ventes totales du segment (concurrents virtuels inclus)
lost Clients perdus faute de stock : demand − sales

Seule sales est obligatoire. On PEUT ajouter les champs d’affichage communs (label, description, format, decimals, unit, visibility, concept, group, icon, revealInDebrief) à une sortie sous la forme d’un objet : "share": { "id": "partDeMarche", "label": "Part de marché", "format": "percent", "visibility": "public" }.

Plusieurs segments PEUVENT viser les mêmes produits (ex. grand public et professionnels, ou nuits de pointe et nuits ordinaires) : additionnez alors leurs ventes dans une formule. Règles :

  • Les segments sont indépendants : chacun répartit sa propre demande, et le report (spillover) ne passe jamais d’un segment à un autre ni vers l’option extérieure (ces clients sont perdus).
  • Chaque segment consomme sa propre supply : si deux segments puisent dans le même stock, c’est à l’auteur de le découper (ex. supply = capacité × part des nuits de pointe) ; le moteur ne partage pas une capacité entre segments.
  • Les identifiants de concurrents virtuels sont uniques au sein d’un segment.
  • demand inclut les clients reçus par report depuis les concurrents en rupture ; lost = demand − sales.
  • Valeur de spillover : 0,3 pour des clients peu mobiles (hôtel réservé à l’avance), 0,5 par défaut, 0,7 à 0,9 pour un produit banalisé où le client va voir ailleurs sans hésiter.

L’intervenant choisit le mode à la création de la session, parmi settings.marketModes.

Marché partagé (shared) Marché parallèle (parallel)
Principe Toutes les entreprises (équipes et bots) se disputent les mêmes clients Chaque entreprise joue seule dans sa copie du monde ; on compare les résultats à la fin
Concurrents dans le calcul du marché Toutes les entreprises + concurrents virtuels dont modes contient shared L’entreprise seule + concurrents virtuels dont modes contient parallel
Fonctions market_*, share, rank Portent sur toutes les entreprises Portent sur la seule entreprise
Hasard Tirages du monde communs ; tirages des entreprises selon sharedModeLuck Tous les tirages identiques pour toutes les entreprises : même météo, mêmes événements, même chance
companyCount Nombre d’entreprises du marché 1
Intérêt pédagogique Concurrence, guerre des prix, réaction aux autres Comparer des stratégies à conditions identiques ; grands groupes

Pour qu’un scénario fonctionne dans les deux modes, la taille du marché et l’option extérieure doivent tenir compte du mode (cf. l’exemple : companyCount, mode), et des concurrents virtuels doivent créer la concurrence en mode parallèle.

Entreprises fictives sans décisions ni comptabilité : elles ont seulement une attractivité (et éventuellement une capacité). Elles représentent « le reste du marché ».

Champ Description
id, label
attractiveness expression monde, évaluée par élément si le segment est per (elle peut donc différer selon le produit ou la ligne). Une attractivité nulle rend le concurrent absent ce tour-là : c’est ainsi qu’on fait entrer un concurrent en cours de partie (formule monde valant 0, puis modifiée par un événement)
supply expression monde ; absent = illimité
modes modes où ce concurrent existe (défaut : les deux)
logo clé d’asset

À ne pas confondre avec les bots (§17), qui sont de vraies entreprises simulées, avec décisions et comptes.

Pour chaque segment (et chaque élément si per) :

  1. D = size ; pour chaque participant k : A_k = attractivité (≥ 0), S_k = offre ; A_0 = outsideOption.
  2. Demande initiale : d_k = D × A_k / (A_0 + Σ A).
  3. Ventes : s_k = min(d_k, S_k). Clients non servis : U = Σ (d_k − s_k).
  4. Report : spillover × U clients sont redistribués aux participants qui ont encore du stock, au prorata de leur attractivité, l’option extérieure prenant sa part (ces clients quittent le marché), en au plus 5 passes. Le reste est perdu. Ainsi, quand tous les concurrents sont complets, un vendeur au prix absurde ne récupère pas tout l’excédent.
  5. Sorties : demand = d_k + clients reçus par report ; sales = ventes finales ; share = sales_k / Σ sales ; lost = demand − sales.

Détails, cas limites et arrondis : moteur de simulation §6.

Sans bloc market, la demande est calculée par les formules seules (ex. ventes = min(stock, demandeClient)). C’est suffisant pour un scénario purement parallèle ou pédagogique simple.


type Ce que voient les joueurs Effet
info Une actualité (flash info, rumeur, indice sur le marché) Aucun ; peut être une rumeur fausse, révélée au débrief
effect Une annonce : « Pénurie de silicium : coût matière +30 % pendant 2 trimestres » Modifie des grandeurs, immédiatement ou pendant plusieurs tours
dilemma Une situation à trancher, avec des options Les effets dépendent de l’option choisie, avec parfois une part de hasard
quiz Une question à choix multiple, minutée, façon Kahoot Récompense (ou pénalité) selon les bonnes réponses
{
"id": "penurieSilicium",
"type": "effect",
"title": "Pénurie de silicium",
"text": "Un séisme a touché les fonderies de Taïwan. Le prix du silicium s'envole.",
"image": "evtPenurie",
"tone": "negative",
"scope": "world",
"trigger": { "type": "random", "probability": 0.15, "from": 2, "to": 7 },
"maxOccurrences": 1,
"effects": [ { "target": "prixSilicium", "op": "multiply", "value": 1.35 } ],
"teaching": "Dépendre d'un seul approvisionnement expose aux chocs. Un stock de sécurité ou un fournisseur alternatif amortit le choc.",
"concept": "chaineApprovisionnement"
}
Champ Type Requis Description
id, type, title oui
text LocalizedText markdown, gabarits permis oui (facultatif pour quiz, qui a question) Texte de l’annonce, rendu après l’application des effets de l’annonce (on peut donc afficher la nouvelle valeur d’une variable modifiée)
image clé d’asset non Illustration
tone positive | negative | neutral non Couleur et son de l’annonce. Défaut neutral
display popup (défaut) | news non Fenêtre bloquante à lire, ou simple bandeau d’actualité
scope world | company non world (défaut) : un seul tirage pour tout le marché. company : chaque entreprise est concernée séparément (§16.4)
select objet non Ciblage des entreprises pour un événement company (§16.4)
trigger objet oui Déclenchement (§16.3)
maxOccurrences entier non Nombre maximal de déclenchements par partie (par entreprise si company). Défaut : illimité
cooldown entier non Tours minimum entre deux déclenchements. Défaut 0
manual booléen non L’intervenant peut le déclencher à la main. Défaut true
effects effets non Effets à l’annonce (§16.6)
teaching LocalizedText markdown DEVRAIT Leçon à retenir, affichée dans les résultats du tour et au débrief
concept, levels non
onScreen booléen non Afficher l’annonce sur l’écran de projection. Défaut true pour world, false pour company
trigger.type Champs Signification
scheduled round (entier) ou rounds (entiers) À ce(s) tour(s) précis
random probability (nombre ou expression), from, to, when À chaque tour entre from et to (inclus), avec cette probabilité, si when est vrai
condition when (expression), probability (défaut 1), from, to À chaque tour où la condition est vraie (sous réserve de maxOccurrences et cooldown) ; pour un déclenchement unique, mettre maxOccurrences: 1
manual — Uniquement à la main par l’intervenant (bibliothèque d’événements)
  • probability et when sont évalués au début du tour, sur l’état de début de tour et les résultats du tour précédent (prev(...)). Dans un événement company, ils sont évalués pour chaque entreprise : "probability": "clamp(0.3 - prev(maintenanceParLigne, 1, 3000) / 12000, 0.03, 0.3)".
  • Les événements sont tirés au début du tour, pour que les joueurs puissent réagir pendant leur saisie.
  • Pendant le tour d’essai, seuls les événements scheduled au tour 1 se déclenchent.
  • scope: "world" : un seul tirage ; l’événement concerne le monde, et ses effets sur des grandeurs d’entreprise s’appliquent à toutes les entreprises.
  • scope: "company" : l’événement touche des entreprises précises, selon select :
select.type Effet
each (défaut) Chaque entreprise fait son propre tirage (probability, when)
random Un tirage unique ; si l’événement a lieu, count entreprises au hasard (défaut 1) parmi celles qui vérifient when
top / bottom Un tirage unique ; si l’événement a lieu, les count entreprises qui ont la plus grande / la plus petite valeur de l’expression by
"scope": "company",
"select": { "type": "top", "by": "prev(resultatNet)", "count": 1 }
{
"id": "commandeGeante", "type": "dilemma", "scope": "company",
"title": "Une commande géante",
"text": "Un fabricant de serveurs vous propose 3 000 barrettes DDR5 à 52 € pièce, livrables **ce trimestre**. Chaque barrette non livrée coûtera 20 € de pénalité.",
"trigger": { "type": "random", "probability": 0.12, "from": 3, "when": "lignes >= 3" },
"maxOccurrences": 1,
"options": [
{ "id": "accepter", "label": "Accepter la commande",
"effects": [ { "target": "commandeB2B", "op": "set", "value": 3000, "items": ["ddr5"] } ] },
{ "id": "refuser", "label": "Refuser poliment" }
],
"default": "refuser",
"teaching": "Une grosse commande sécurise du chiffre d'affaires, mais engage votre capacité : vérifiez-la avant de signer."
}
Champ Description
options 2 à 5 options : id, label, description, image, enabledIf, effects, outcomes
options[].outcomes Issues aléatoires possibles, tirées à la résolution : [{ "probability": 0.7, "text": "…", "effects": [ … ] }, …] ; les probabilités DOIVENT sommer à 1
default Option retenue si l’entreprise ne répond pas (requis)
deadline decision (défaut : jusqu’à la fin de la saisie du tour)
  • Le dilemme apparaît pendant la saisie des décisions ; en équipe, c’est le capitaine qui tranche (les membres peuvent voter).
  • Les effets de l’option choisie et de son issue s’appliquent à la résolution du tour, avant le calcul des formules (§16.6).
  • Le texte de l’issue est affiché dans les résultats du tour.
  • Dans les formules, dilemma(commandeGeante) donne l’option choisie ce tour-ci ('' si le dilemme n’a pas eu lieu).
{ "target": "coutMatiere", "op": "multiply", "value": 1.3, "items": ["ddr5"], "duration": 2, "delay": 0 }
Champ Description
target Identifiant d’une variable, constante ou formule (pas d’une décision, ni d’une sortie de marché)
op add, multiply ou set
value nombre ou expression, évaluée une fois, au moment où l’effet s’applique (annonce pour un événement effect, résolution pour un dilemme ou un quiz, clôture pour une faillite), dans le contexte de la cible (entreprise ou monde). Ex. "-0.1 * tresorerie"
items pour une cible per : éléments concernés (défaut : tous)
duration pour une constante ou une formule : nombre de tours d’effet (défaut 1), ou "permanent"
delay tours avant que l’effet commence (défaut 0 = tour courant)

Deux mécanismes selon la cible :

  1. Cible = variable d’état → modification ponctuelle et définitive de sa valeur courante (tresorerie add -20000). duration est interdit. Pour un événement effect, elle s’applique à l’annonce (les joueurs la voient avant de décider) ; pour un dilemme ou un quiz, à la résolution.
  2. Cible = constante ou formule → modificateur temporaire : pendant les tours t + delay à t + delay + duration − 1, la valeur calculée de la grandeur est transformée. Plusieurs modificateurs se cumulent : on applique d’abord tous les multiply, puis tous les add ; un set remplace le résultat (le dernier déclenché l’emporte).

Le modificateur porte le contexte de l’événement : un événement de portée company (ou l’option d’un dilemme, toujours choisie par une entreprise, même pour un dilemme world) qui modifie une constante ou une formule ne la modifie que pour l’entreprise concernée — la constante prend alors une valeur effective différente selon l’entreprise. Un événement effect de portée world la modifie pour tout le monde ; s’il cible une grandeur d’entreprise, sa value est évaluée une fois par entreprise (elle peut donc dépendre de l’entreprise : -0.1 * tresorerie). Les effets d’un dilemme, d’un quiz ou d’une faillite ne peuvent pas cibler une grandeur world (E602).

Pour un effet qui dure et se voit dans les comptes, préférez une formule dédiée (commandeB2B, valant 0 par défaut) que l’événement modifie.

Dans les formules, active('penurieSilicium') vaut vrai tant qu’un modificateur de cet événement est actif, et occurred('penurieSilicium') donne le nombre de déclenchements depuis le début.

{ "id": "rumeurRachat", "type": "info", "display": "news",
"title": "Rumeur", "text": "Selon un blog spécialisé, un géant américain s'apprêterait à racheter un fabricant européen.",
"trigger": { "type": "random", "probability": 0.2, "from": 2 }, "maxOccurrences": 1,
"rumor": true, "revealText": "C'était une fausse information : il fallait recouper les sources avant de réagir." }

rumor: true signale une information non vérifiée ; revealText est affiché au débrief.

{
"id": "quizMargeBrute", "type": "quiz",
"title": "Quiz : la marge brute",
"trigger": { "type": "scheduled", "round": 2 },
"question": "Votre chiffre d'affaires est de 600 000 € et votre coût des ventes de 240 000 €. Quelle est votre marge brute ?",
"answers": [
{ "id": "a", "label": "360 000 €", "correct": true },
{ "id": "b", "label": "840 000 €" },
{ "id": "c", "label": "240 000 €" },
{ "id": "d", "label": "40 %" }
],
"timeLimit": 30,
"explanation": "Marge brute = chiffre d'affaires − coût des ventes = 600 000 − 240 000 = 360 000 €, soit 60 % du CA.",
"reward": { "threshold": 0.5, "effects": [ { "target": "tresorerie", "op": "add", "value": 5000 } ] },
"concept": "margeBrute"
}
Champ Description
question LocalizedText
answers 2 à 6 réponses, au moins une correct: true (plusieurs = plusieurs bonnes réponses acceptées)
timeLimit secondes (10 à 120, défaut 30)
explanation affichée après la réponse
reward.threshold part minimale de bonnes réponses dans l’équipe pour obtenir la récompense (défaut 0.5)
reward.effects effets appliqués à l’entreprise récompensée (à la résolution du tour)
penalty.effects effets si le seuil n’est pas atteint (facultatif)

Chaque joueur répond individuellement sur son appareil ; l’écran de projection affiche le décompte et les points « quiz » (rapidité + justesse), classement ludique distinct du score de l’entreprise. Un quiz est toujours de portée world (tous les joueurs).

  • Avant de lancer la session, l’intervenant voit la liste des événements du scénario et peut en désactiver, modifier la probabilité, ou créer des événements propres à sa session (mêmes champs, via un formulaire).
  • Pendant la partie, il peut déclencher à la main tout événement manual: true, sur tout le marché ou sur une entreprise choisie, et choisir le tour d’effet (immédiat ou prochain tour).

17. bots — entreprises pilotées par l’ordinateur

Section intitulée « 17. bots — entreprises pilotées par l’ordinateur »
"bots": {
"default": 2,
"max": 6,
"profiles": [
{
"id": "prudent", "name": "Prudentia Memory", "logo": "logoBot1",
"description": "Prix légèrement au-dessus du marché, production calée sur les ventes passées.",
"decisions": {
"prix": "prixReference * 1.05",
"production": "max(0, prev(ventes, 1, demandeBase) * 1.05 - stock)",
"embauches": "if(prev(tauxCharge, 1, 0) > 0.95, 2, 0)",
"fournisseur": "'europe'",
"marketing": "20000"
},
"noise": 0.05,
"levels": ["facile", "normal", "difficile"]
}
]
}
Champ Description
default Nombre de bots proposé par défaut à la création de session (0 = désactivés par défaut)
max Nombre maximum
profiles[] Profils de stratégie ; les bots d’une session reçoivent les profils dans l’ordre, puis en boucle
profiles[].decisions Pour chaque décision : une expression évaluée dans le contexte du bot, au début du tour (état de début de tour, résultats précédents via prev). Décision absente = valeur par défaut. Pour un choice, l’expression renvoie un id d’option ('europe') ; pour une décision per, elle est évaluée pour chaque élément
profiles[].noise Bruit aléatoire multiplicatif sur les décisions numériques (0.05 = ±5 %), pour qu’ils ne soient pas prévisibles
profiles[].dilemmas { idDilemme: "expression renvoyant un id d'option" } ; sinon l’option default

Les bots respectent les bornes des décisions, ne répondent pas aux quiz et sont signalés comme bots dans les classements. En marché parallèle, chaque bot joue dans sa propre copie du monde, comme une équipe.

  • decisions PEUT contenir des décisions de setup : l’expression est alors évaluée au moment « création » (constantes et grandeurs intégrées seulement) ; sinon le bot prend la valeur initial.
  • Les décisions d’un profil sont évaluées dans l’ordre où elles sont écrites : une décision PEUT lire celles déjà calculées du même tour (ex. planifier les vols en connaissant les heures affrétées).
  • helpers PEUT définir des expressions intermédiaires propres au bot, évaluées avant ses décisions et dans l’ordre d’écriture : "helpers": { "volsCibles": "prev(passagers, 1, demandeBase) / sieges" }, utilisables ensuite dans decisions et dilemmas par leur nom. Leurs noms ne doivent pas entrer en conflit avec une grandeur.
  • Une décision non proposée dans le niveau joué (levels) suit auto pour tout le monde, bots compris : l’expression du bot est ignorée.
  • Pour une décision per, l’expression est évaluée par élément ; item désigne l’élément courant.
  • Limite du format 1.0 : une décision allocation ne peut être pilotée ni par un bot ni par auto (une expression ne renvoie pas d’objet) ; les bots gardent sa valeur initial/default. Une allocation exclue par le niveau prend aussi initial/default.

Évolution prévue (non incluse en 1.0) : "strategy": { "type": "llm" } pour des bots pilotés par un modèle d’IA. Les champs inconnus sont refusés en 1.0.


"difficulties": {
"default": "normal",
"levels": [
{ "id": "facile", "label": "Facile", "description": "Moins de décisions, plus de trésorerie, événements cléments.",
"set": {
"constants.tresorerieInitiale.value": 350000,
"events.penurieSilicium.trigger.probability": 0.05,
"settings.rounds.default": 6,
"bots.default": 1
} },
{ "id": "normal", "label": "Normal" },
{ "id": "difficile", "label": "Difficile", "description": "Toutes les décisions, marché plus sensible aux prix.",
"set": { "constants.elasticite.value": { "ddr4": 3.4, "ddr5": 2.6 }, "constants.tresorerieInitiale.value": 180000 } }
]
}

Deux mécanismes complémentaires :

  1. Filtrer : tout élément portant levels n’existe que dans ces niveaux. C’est ainsi qu’on masque des décisions en mode facile (avec un auto pour les automatiser), qu’on ajoute des événements en mode difficile, etc.
  2. Surcharger : set modifie des champs. Chaque clé est un chemin section.id.champ[.sous-champ…], où id désigne l’élément de la section par son identifiant ; pour settings, bots, scoring le chemin part directement du champ.

Chemins autorisés :

Section Exemples de chemins
constants constants.X.value
variables variables.X.initial, variables.X.min, variables.X.max
decisions decisions.X.min, .max, .step, .initial, .default, .auto
formulas formulas.X.expr, formulas.X.visibility
events events.X.trigger.probability, events.X.trigger.from, events.X.maxOccurrences, events.X.manual
market market.segments.X.spillover, market.segments.X.outsideOption
settings settings.rounds.default, settings.rounds.min, settings.rounds.max, settings.decisionTime.default
bots bots.default, bots.profiles.X.decisions.Y, bots.profiles.X.noise
scoring scoring.bankruptcy.when

L’intervenant choisit le niveau à la création de la session. Un scénario sans difficulties a un unique niveau normal.

Paramètres ajustables par l’intervenant : les constantes déclarées adjustable (§10) sont proposées dans les paramètres avancés de la session, après application du niveau de difficulté.


19. scoring — score, classement, faillite, trophées

Section intitulée « 19. scoring — score, classement, faillite, trophées »
"scoring": {
"score": "cumul(resultatNet) - penalites",
"label": "Bénéfice cumulé",
"format": "money",
"order": "desc",
"tiebreak": ["tresorerie"],
"showDuringGame": true,
"bankruptcy": {
"when": "tresorerie < -150000",
"action": "rescue",
"message": "Votre banque a dû intervenir en urgence : apport de 200 000 € et pénalité de 300 000 € sur votre score.",
"effects": [
{ "target": "tresorerie", "op": "add", "value": 200000 },
{ "target": "penalites", "op": "add", "value": 300000 }
],
"maxRescues": 2
},
"awards": [
{ "id": "qualite", "label": "Trophée de la qualité", "icon": "gem", "by": "qualite", "order": "desc" },
{ "id": "social", "label": "Employeur de l'année", "icon": "heart-handshake", "by": "climatSocial", "order": "desc" }
],
"objectives": [
{ "id": "zeroRupture", "label": "Jamais en rupture", "description": "Aucune vente perdue sur toute la partie.", "check": "cumul(sum(ventesPerdues)) == 0", "icon": "package-check" }
]
}
Champ Description
score Expression d’entreprise, recalculée à chaque tour. Par défaut, un scénario DEVRAIT classer au profit cumulé. Tout autre critère est permis (score composite, part de marché, satisfaction…)
label, format Affichage du score
order desc (défaut : le plus grand gagne) ou asc
tiebreak Expressions départageant les ex æquo, dans l’ordre
showDuringGame Classement visible pendant la partie (défaut true) ; l’intervenant peut le masquer
bankruptcy.when Condition de faillite, évaluée en fin de tour
bankruptcy.action continue : l’entreprise est signalée en difficulté mais continue ; rescue : les effects s’appliquent (renflouement, pénalité) et l’entreprise continue, au plus maxRescues fois, puis eliminate ; eliminate : l’entreprise est éliminée, ses joueurs deviennent spectateurs. Défaut continue
awards Trophées remis au débrief : l’entreprise qui a la meilleure valeur de by en fin de partie (by peut être une expression, ex. cumul(caTotal)). Champs : id, label, icon, by, order, levels
objectives Badges obtenus si check est vrai en fin de partie. Champs : id, label, description, icon, check, levels

En classe, rescue ou continue sont préférables à eliminate : personne ne reste sans jouer.


Sans reports, la plateforme génère un tableau de bord par défaut : score, trésorerie, et toutes les grandeurs private/public regroupées par group.

"reports": {
"dashboard": { "kpis": ["tresorerie", "resultatNet", "caTotal", "tauxMargeBrute", "qualite"] },
"pages": [
{
"id": "resultat", "label": "Compte de résultat", "icon": "file-spreadsheet",
"widgets": [
{ "type": "statement", "title": "Compte de résultat", "period": "rounds",
"rows": [
{ "ref": "caTotal", "style": "total" },
{ "ref": "coutDesVentes", "sign": -1 },
{ "ref": "margeBrute", "style": "subtotal" },
{ "ref": "masseSalariale", "sign": -1 },
{ "label": "Autres charges", "expr": "fraisFixes + marketing + maintenance + formation", "sign": -1 },
{ "ref": "resultatNet", "style": "total" }
] }
]
},
{
"id": "marche", "label": "Marché", "icon": "chart-pie",
"widgets": [
{ "type": "chart", "chart": "line", "title": "Prix pratiqués", "compare": "companies", "series": ["prix"], "items": ["ddr5"] },
{ "type": "chart", "chart": "pie", "title": "Parts de marché DDR5", "compare": "companies", "series": ["partDeMarche"], "items": ["ddr5"] },
{ "type": "table", "title": "Concurrents", "compare": "companies", "columns": ["prix", "qualite", "notoriete"] },
{ "type": "text", "text": "Les prix des concurrents sont publiés après chaque trimestre." }
]
}
],
"facilitator": { "columns": ["tresorerie", "resultatNet", "caTotal", "qualite", "effectif", "lignes"] },
"screen": { "kpis": ["prixSilicium"], "charts": [ { "chart": "bar", "compare": "companies", "series": ["qualite"] } ] }
}
type Champs Affichage
kpi refs Cartes de chiffres clés avec évolution par rapport au tour précédent
statement rows, period (rounds = un tour par colonne, défaut ; last = dernier tour seul) État financier : lignes avec ref (grandeur ; label facultatif pour renommer la ligne) ou label + expr (évaluée au moment « clôture » de chaque tour, dans le contexte de l’entreprise, puis enregistrée avec le tour ; la ligne n’est montrée qu’à un spectateur qui peut voir toutes les grandeurs lues par l’expression, et une ligne calculée sans aucune valeur visible est masquée), sign (−1 pour afficher en charge), style (normal, subtotal, total, header)
chart chart (line, bar, area, pie, stackedBar), series (grandeurs), compare (rounds défaut : évolution dans le temps ; companies : comparaison des entreprises), items (pour une grandeur per) Graphique
table columns (grandeurs), compare (rounds | companies | items), items (pour les grandeurs per) Tableau
text text (markdown, gabarits) Texte libre
image image, caption Image

Règles :

  • un widget compare: "companies" vu par un joueur ne montre que des grandeurs public (le validateur le vérifie, P710). L’intervenant voit tout ;
  • toute grandeur (constante, variable, décision, formule, sortie de marché) peut être affichée dans un widget ;
  • une grandeur per affichée sans items montre tous ses éléments (une série, une colonne ou une ligne par élément, libellés par le label de l’élément) ; avec items, seulement ceux listés.
  • dashboard.kpis : chiffres clés en tête de l’écran joueur.
  • facilitator.columns : colonnes du tableau de suivi de l’intervenant (une ligne par entreprise).
  • screen : ce que l’écran de projection affiche entre les tours, en plus du classement et des actualités : kpis (grandeurs de monde ou publiques) et charts (widgets chart sans le champ type). Seules des grandeurs public ou de portée world y sont permises.

"pedagogy": {
"learningObjectives": [
"Arbitrer entre prix, volume et marge",
"Piloter un stock : coût de stockage contre ventes perdues",
"Comprendre la différence entre résultat et trésorerie"
],
"briefing": "## Bienvenue chez votre usine\nVous prenez la direction d'une usine de 15 salariés…",
"concepts": [
{ "id": "margeBrute", "term": "Marge brute", "definition": "Chiffre d'affaires moins le coût des produits vendus.", "formula": "CA − coût des ventes" },
{ "id": "pointMort", "term": "Point mort", "definition": "Niveau de chiffre d'affaires à partir duquel l'entreprise couvre toutes ses charges." }
],
"tips": [
{ "round": 1, "text": "Commencez par vérifier votre capacité de production avant de fixer vos volumes." },
{ "when": "prev(sum(ventesPerdues)) > 0.1 * prev(sum(demande))", "text": "Vous avez perdu {{ prev(sum(ventesPerdues)) }} ventes faute de stock au dernier trimestre." }
],
"insights": [
{ "id": "rupture", "when": "cumul(sum(ventesPerdues)) > 0.1 * cumul(sum(demande))", "tone": "warning",
"text": "Vous avez perdu {{ cumul(sum(ventesPerdues)) }} ventes faute de stock, soit environ {{ cumul(sum(ventesPerdues) * avg(prix)) | money }} de chiffre d'affaires." },
{ "id": "tresoNegative", "when": "occurred('decouvert') > 0", "tone": "warning",
"text": "Votre trésorerie est passée dans le rouge : le résultat n'est pas la trésorerie !" }
],
"debriefQuestions": [
"Quelle équipe a vendu le plus cher ? A-t-elle gagné ? Pourquoi ?",
"Qu'auriez-vous fait différemment au trimestre 3 ?"
],
"facilitatorNotes": "## Déroulé conseillé\n- Tour d'essai : 10 minutes, expliquez l'écran…"
}
Champ Description
learningObjectives Objectifs pédagogiques (marketplace, débrief)
briefing Histoire et consignes affichées au lancement. Ses gabarits sont évalués au moment « annonce » du tour 1 ; dans la salle d’attente, avant la mise en place, ils sont rendus avec une mise en place par défaut (valeurs initial des décisions de setup)
concepts Glossaire : chaque grandeur, décision ou événement peut s’y relier par concept ; le terme s’affiche alors avec une info-bulle
tips Conseils affichés pendant la saisie : à un tour précis (round) ou quand une condition est vraie (when, évaluée au début du tour pour chaque entreprise)
insights Enseignements personnalisés du débrief : pour chaque entreprise, ceux dont when est vrai en fin de partie sont affichés. tone : positive, warning, neutral
debriefQuestions Questions pour animer la discussion finale
facilitatorNotes Guide de l’animateur (markdown) : déroulé, pièges, corrigé ; visible seulement des intervenants

Fournies par le moteur, utilisables dans toutes les expressions, non modifiables :

Nom Type Valeur
round entier Numéro du tour en cours (1 au premier tour ; 0 pendant la mise en place)
rounds entier Nombre total de tours de la session
isLastRound booléen round == rounds
isPractice booléen Vrai pendant le tour d’essai
mode texte 'shared' ou 'parallel'
difficulty texte Id du niveau de difficulté
companyCount entier Nombre d’entreprises dans le marché (1 en mode parallèle)
isBot booléen L’entreprise est un bot (portée entreprise uniquement)
leaderboardRank entier Rang de l’entreprise au classement général après le dernier tour clôturé (toutes instances confondues, donc aussi en marché parallèle) ; 0 avant la fin du premier tour. Portée entreprise uniquement
item texte Id de l’élément courant, dans une grandeur per

L’import, l’éditeur et l’outil en ligne de commande (stratilead validate) vérifient un scénario en trois niveaux :

  1. Structure : conformité au JSON Schema.
  2. Sémantique : références, types, graphe de dépendances, portées, cohérence.
  3. Simulation (banc d’essai, stratilead bench) : des centaines de parties jouées par des stratégies automatiques pour détecter les déséquilibres.

Un scénario avec au moins une erreur ne peut être ni joué ni publié. Les avertissements n’empêchent pas de jouer ; la publication sur la marketplace exige qu’ils soient corrigés ou explicitement justifiés.

Chaque problème a un code stable, un chemin vers l’élément concerné et un message. Liste de référence :

Code Gravité Problème
S100 erreur JSON invalide
S101 erreur Champ requis manquant
S102 erreur Champ inconnu (souvent une faute de frappe)
S103 erreur Type de valeur incorrect
S104 erreur Valeur hors des bornes autorisées (ex. probability > 1)
S105 erreur formatVersion non supporté
S110 erreur Limite dépassée (taille, nombre d’éléments, longueur d’expression)
Code Gravité Problème
R200 erreur Identifiant mal formé
R201 erreur Identifiant en double
R202 erreur Identifiant réservé
R210 erreur Référence inconnue (grandeur, liste, élément, option, événement, courbe, asset, groupe, rôle, niveau, concept)
R211 erreur set de difficulté : chemin invalide ou non autorisé
R220 avertissement Grandeur jamais utilisée ni affichée
R221 avertissement Asset sans fichier (src absent)
R222 avertissement Concept défini mais jamais référencé
Code Gravité Problème
T300 erreur Erreur de syntaxe dans une expression
T301 erreur Fonction inconnue ou mauvais nombre d’arguments
T310 erreur Types incompatibles (ex. nombre comparé à un id d’option)
T311 erreur Expression qui doit être booléenne et ne l’est pas (when, check, enabledIf)
T312 erreur Id d’option comparé à une option qui n’existe pas dans la décision
T320 erreur Grandeur par élément utilisée sans agrégation hors d’un contexte de même liste
T321 erreur Indice [x] qui n’est pas un élément de la liste
T330 erreur Portée invalide (ex. grandeur d’entreprise lue dans une expression de monde sans agrégation market_*)
T340 erreur Contrainte, preview, borne, enabledIf ou initial qui dépend du marché, du hasard ou d’une grandeur non encore disponible
T350 erreur initial de variable qui utilise autre chose que des constantes, des décisions de setup et des grandeurs intégrées
T360 avertissement Division dont le dénominateur peut valoir zéro (le résultat sera 0)
Code Gravité Problème
D400 erreur Dépendance circulaire (le cycle est indiqué)
D410 avertissement Formule active qui dépend d’une formule exclue par levels
Code Gravité Problème
M500 erreur size ou outsideOption de portée entreprise
M501 erreur attractiveness ou supply de portée monde seule (doit pouvoir différer selon l’entreprise)
M510 erreur Sortie de marché en conflit avec une grandeur existante
M520 avertissement Mode parallel supporté sans concurrent virtuel ni outsideOption : les prix n’ont alors aucun effet sur les ventes
Code Gravité Problème
E600 erreur Effet qui cible une décision ou une sortie de marché
E601 erreur duration sur un effet qui cible une variable
E602 erreur Événement company qui cible une grandeur de portée world
E610 erreur Probabilités des outcomes qui ne somment pas à 1
E611 erreur Dilemme sans default, ou default inconnu
E612 erreur Quiz sans bonne réponse
E620 avertissement Événement qui ne peut jamais se déclencher (bornes from/to, manual: false sans déclencheur)
Code Gravité Problème
P700 avertissement Décision sans help
P701 avertissement Formule affichée sans description
P702 avertissement Événement sans teaching (exemptés : les quiz qui ont une explanation, et les rumeurs, c’est-à-dire les événements info qui portent un revealText : la leçon est donnée par la révélation au débrief)
P710 erreur Widget compare: "companies" d’un rapport joueur qui affiche une grandeur non public
P720 avertissement Texte manquant dans une langue déclarée dans meta.languages
P730 avertissement Aucun objectif pédagogique
P740 avertissement limits.maxPlayers inaccessible : supérieur à maxCompanies × teamSize.max
Code Gravité Problème
B800 erreur Erreur d’exécution (valeur non numérique, NaN, infini) apparue pendant les simulations
B810 avertissement Taux de faillite supérieur à 40 % des parties avec des stratégies raisonnables
B811 avertissement Taux de faillite nul même avec des stratégies aléatoires (le jeu ne punit rien)
B820 avertissement Stratégie dominante : une valeur extrême d’une décision gagne dans plus de 80 % des cas
B821 avertissement Décision sans influence mesurable sur le score
B830 avertissement Écart de score trop faible entre stratégies (le jeu ne récompense pas les bonnes décisions)
B840 avertissement Événement dont l’impact moyen dépasse 50 % du résultat moyen d’un tour
B850 avertissement Division par zéro survenue pendant les simulations

  • "1.0" pour ce document. Le moteur accepte toutes les versions 1.x antérieures ou égales à la sienne.
  • Une version mineure (1.1) n’ajoute que des champs facultatifs : un scénario 1.0 reste valide.
  • Une version majeure (2.0) peut casser la compatibilité : la plateforme fournit alors une migration automatique des scénarios existants.
  • Versionnage sémantique choisi par l’auteur : 1.0.0, 1.1.0, 2.0.0.
  • Une version publiée est immuable : toute modification crée une nouvelle version.
  • Une session reste liée à la version avec laquelle elle a été créée.
  • Changez la version majeure si l’équilibre du jeu change nettement (les statistiques et notes de la marketplace sont affichées par version majeure).