Dans la plupart des langages, la fonction hasard() (ou random()) semble être la solution magique pour générer de l’imprévu. Pourtant, dans le game design, le hasard brut est rarement une bonne idée. Un jeu intéressant n’est pas un chaos : c’est un système où l’aléatoire est orienté, contraint, validé.
Le programme Le Hasard Maîtrisé illustre cette idée à travers trois démonstrations fondamentales :
- Pondérer : toutes les valeurs ne doivent pas avoir la même chance.
- Exclure : certaines zones doivent être interdites au tirage.
- Valider : on rejette les résultats qui ne conviennent pas.
Ces trois techniques sont au cœur de la conception de niveaux, de loot, de cartes, de puzzles, et de mécaniques équilibrées.
Pondérer : donner une forme au hasard
La première démonstration compare trois types de tirages :
Tirage uniforme
Chaque face d’un dé (1 à 6) a la même probabilité.
Résultat : une distribution plate, sans surprise.
Tirage pondéré
Le programme découpe 100 en tranches inégales :
- 40 % ? 1
- 25 % ? 2
- 15 % ? 3
- 10 % ? 4
- 6 % ? 5
- 4 % ? 6
Résultat : une distribution orientée, où les petites valeurs dominent.
Somme de deux dés
Combiner deux tirages uniformes produit une courbe en cloche :
le 7 apparaît six fois plus souvent que le 2 ou le 12.
Pourquoi c’est essentiel en game design ?
Parce que le hasard brut produit souvent des situations injustes ou déséquilibrées.
Exemples :
- Un jeu de fusion où les grosses pièces sortent trop souvent devient sans intérêt.
- Un RPG où les objets rares tombent trop facilement perd toute progression.
- Un jeu de stratégie où les ressources apparaissent n’importe comment devient ingérable.
Décider de la forme de la distribution, c’est du game design.
Exclure : interdire certaines zones au tirage
La deuxième démonstration concerne le démineur.
Version brute
Les mines sont placées n’importe où.
Résultat : sur 400 parties, la case du premier clic contient une mine plusieurs dizaines de fois.
Version contrainte
Le programme interdit toute mine :
- sur la case du premier clic,
- dans les 8 cases autour.
Résultat : 0 explosion au premier clic, par construction.
La contrainte tient en une seule ligne :
- si absolue(mi - 4) <= 1 et absolue(mj - 4) <= 1 alors refuser
Pourquoi c’est essentiel ?
Parce qu’un jeu doit être juste.
Le joueur ne doit jamais perdre avant d’avoir joué.
Le hasard doit servir le plaisir, pas le punir.
C’est pour cela que :
- les jeux de cartes évitent les mains impossibles,
- les roguelikes garantissent un minimum de ressources,
- les jeux de puzzle empêchent les configurations insolubles.
Le hasard doit être guidé.
Valider : recommencer tant que le résultat n’est pas bon
La troisième démonstration génère une carte 12×12 avec des murs.
Version brute
Les murs sont placés au hasard.
Résultat : la carte contient souvent des zones inatteignables, isolées du point de départ.
Le programme calcule l’accessibilité en propageant une vague depuis la case (1,1).
Il compte ensuite les cases libres mais inaccessibles.
Version validée
Le programme génère une carte…
puis la rejette si elle est trop morcelée.
Il recommence jusqu’à obtenir :
- au moins 60 cases accessibles,
- une structure cohérente,
- un niveau jouable.
Résultat : une carte toujours praticable, obtenue en quelques essais.
Pourquoi c’est essentiel ?
Parce qu’un niveau généré procéduralement doit respecter des critères :
- être jouable,
- être intéressant,
- être équilibré,
- être cohérent.
La génération procédurale moderne (Minecraft, Hades, Dead Cells, Slay the Spire…) repose sur ce principe :
- Générer ? Tester ? Rejeter ? Recommencer
Le hasard brut ne suffit jamais.
La validation est indispensable.
Ce que ce programme enseigne Ce programme est une véritable leçon de game design :
- Le hasard doit être modelé (pondération).
- Le hasard doit être limité (exclusion).
- Le hasard doit être contrôlé (validation).
Un bon jeu n’est pas un tirage aléatoire.
C’est un système où l’aléatoire est maîtrisé, orienté, équilibré.
Conclusion Le Hasard Maîtrisé montre que la qualité d’un jeu dépend rarement de la fonction hasard(), mais toujours des contraintes qu’on impose au tirage.
C’est cette maîtrise qui permet :
- d’éviter les injustices,
- de garantir la jouabilité,
- de créer des niveaux cohérents,
- d’équilibrer les récompenses,
- de rendre l’expérience agréable.
Le hasard n’est pas l’ennemi du game design.
Le hasard non maîtrisé, si.
Programme :
// ===========================================================
// LE HASARD MAÎTRISÉ
// hasard() ne suffit jamais. Ce qui fait la qualité d'un jeu,
// ce sont les contraintes qu'on impose au tirage.
// Trois démonstrations :
// [A] pondérer [Z] exclure [E] valider
// ===========================================================
@ CONFIGURATION
vue est une toile
dimension(vue, 700, 520)
ecran est un texte
ecran vaut "ponderer"
touche est un nombre
NBTIR est un nombre
NBTIR vaut 600
@ DONNÉES DES TROIS DÉMONSTRATIONS
// --- 1. comptages des tirages ---
cptUni est un tableau
cptPon est un tableau
cptDeux est un tableau
pour k de 0 à 12
cptUni ajoute 0
cptPon ajoute 0
cptDeux ajoute 0
fin pour
// --- 2. mines du démineur ---
minesA est un tableau
minesB est un tableau
pour i de 0 à 8
ligA est un tableau
ligB est un tableau
pour j de 0 à 8
ligA ajoute 0
ligB ajoute 0
fin pour
minesA ajoute ligA
minesB ajoute ligB
fin pour
perduesA est un nombre
perduesB est un nombre
perduesA vaut 0
perduesB vaut 0
// --- 3. cartes générées ---
carteA est un tableau
carteB est un tableau
accesA est un tableau
pour i de 0 à 11
lgA est un tableau
lgB est un tableau
lgAc est un tableau
pour j de 0 à 11
lgA ajoute 0
lgB ajoute 0
lgAc ajoute faux
fin pour
carteA ajoute lgA
carteB ajoute lgB
accesA ajoute lgAc
fin pour
isolesA est un nombre
essaisB est un nombre
libresB est un nombre
isolesA vaut 0
essaisB vaut 0
libresB vaut 0
refaire est un booléen
refaire vaut vrai
@ UNE BARRE D'HISTOGRAMME
procédure barre(t, x, ybas, larg, haut, coul, txt, val)
rectangle_arrondi(t, x, ybas - haut, larg, haut, 3, coul)
label(t, x + 2, ybas + 14, txt, #64748B, 10)
label(t, x - 1, ybas - haut - 6, "" + val, #334155, 9)
fin procédure
@ BOUCLE PRINCIPALE
tant que vrai
// =========================================================
// CALCULS : on refait tous les tirages à la demande
// =========================================================
si refaire = vrai alors
refaire vaut faux
// ---------- 1. UNIFORME, PONDÉRÉ, SOMME DE DEUX DÉS ----------
pour k de 0 à 12
cptUni[k] vaut 0
cptPon[k] vaut 0
cptDeux[k] vaut 0
fin pour
pour n de 1 à NBTIR
// Tirage brut : chaque face a la même chance
u est un nombre
u vaut hasard(1, 6)
cptUni[u] vaut cptUni[u] + 1
// Tirage pondéré : on découpe 100 en tranches inégales
p est un nombre
t100 est un nombre
t100 vaut hasard(1, 100)
si t100 <= 40 alors
p vaut 1
sinon si t100 <= 65 alors
p vaut 2
sinon si t100 <= 80 alors
p vaut 3
sinon si t100 <= 90 alors
p vaut 4
sinon si t100 <= 96 alors
p vaut 5
sinon
p vaut 6
fin si
cptPon[p] vaut cptPon[p] + 1
// Somme de deux dés : combiner deux hasards en change la forme
s est un nombre
s vaut hasard(1, 6) + hasard(1, 6)
cptDeux[s] vaut cptDeux[s] + 1
fin pour
// ---------- 2. LES MINES DU DÉMINEUR ----------
// À gauche : 10 mines posées n'importe où.
// À droite : jamais sur la case du premier clic ni autour.
pour i de 0 à 8
pour j de 0 à 8
minesA[i][j] vaut 0
minesB[i][j] vaut 0
fin pour
fin pour
posees est un nombre
posees vaut 0
tant que posees < 10
mi est un nombre
mj est un nombre
mi vaut hasard(0, 8)
mj vaut hasard(0, 8)
si minesA[mi][mj] = 0 alors
minesA[mi][mj] vaut 1
posees ajoute 1
fin si
fin tant que
posees vaut 0
tant que posees < 10
mi vaut hasard(0, 8)
mj vaut hasard(0, 8)
libre est un booléen
libre vaut vrai
si minesB[mi][mj] = 1 alors
libre vaut faux
fin si
// La contrainte tient en une ligne : rien près du premier clic
si absolue(mi - 4) <= 1 et absolue(mj - 4) <= 1 alors
libre vaut faux
fin si
si libre = vrai alors
minesB[mi][mj] vaut 1
posees ajoute 1
fin si
fin tant que
// Statistique : sur 400 parties, combien explosent au premier clic ?
perduesA vaut 0
perduesB vaut 0
pour n de 1 à 400
// version brute : on repose 10 mines et on regarde la case (4,4)
touchee est un booléen
touchee vaut faux
restant est un nombre
restant vaut 10
caseM est un tableau
// on tire 10 cases distinctes parmi 81 en testant la case du clic
pour k de 1 à 10
ci est un nombre
cj est un nombre
ci vaut hasard(0, 8)
cj vaut hasard(0, 8)
si ci = 4 et cj = 4 alors
touchee vaut vrai
fin si
fin pour
si touchee = vrai alors
perduesA ajoute 1
fin si
fin pour
// ---------- 3. UNE CARTE PRATICABLE ----------
// À gauche : murs au hasard, sans vérification.
// À droite : on rejette la carte tant qu'elle est trop morcelée.
pour i de 0 à 11
pour j de 0 à 11
si i = 0 ou i = 11 ou j = 0 ou j = 11 alors
carteA[i][j] vaut 1
sinon si hasard(1, 100) <= 34 alors
carteA[i][j] vaut 1
sinon
carteA[i][j] vaut 0
fin si
accesA[i][j] vaut faux
fin pour
fin pour
carteA[1][1] vaut 0
// Propagation depuis le coin : quelles cases peut-on atteindre ?
accesA[1][1] vaut vrai
vague est un booléen
vague vaut vrai
tant que vague = vrai
vague vaut faux
pour i de 1 à 10
pour j de 1 à 10
si accesA[i][j] = vrai alors
pour d de 0 à 3
di est un nombre
dj est un nombre
si d = 0 alors
di vaut 0 - 1
dj vaut 0
sinon si d = 1 alors
di vaut 1
dj vaut 0
sinon si d = 2 alors
di vaut 0
dj vaut 0 - 1
sinon
di vaut 0
dj vaut 1
fin si
si carteA[i + di][j + dj] = 0 et accesA[i + di][j + dj] = faux alors
accesA[i + di][j + dj] vaut vrai
vague vaut vrai
fin si
fin pour
fin si
fin pour
fin pour
fin tant que
isolesA vaut 0
pour i de 1 à 10
pour j de 1 à 10
si carteA[i][j] = 0 et accesA[i][j] = faux alors
isolesA ajoute 1
fin si
fin pour
fin pour
// Version validée : on recommence tant que le résultat ne convient pas
essaisB vaut 0
bonne est un booléen
bonne vaut faux
tant que bonne = faux et essaisB < 60
essaisB ajoute 1
pour i de 0 à 11
pour j de 0 à 11
si i = 0 ou i = 11 ou j = 0 ou j = 11 alors
carteB[i][j] vaut 1
sinon si hasard(1, 100) <= 34 alors
carteB[i][j] vaut 1
sinon
carteB[i][j] vaut 0
fin si
fin pour
fin pour
carteB[1][1] vaut 0
// On réutilise accesA comme brouillon de calcul
pour i de 0 à 11
pour j de 0 à 11
accesA[i][j] vaut faux
fin pour
fin pour
accesA[1][1] vaut vrai
vague vaut vrai
tant que vague = vrai
vague vaut faux
pour i de 1 à 10
pour j de 1 à 10
si accesA[i][j] = vrai alors
pour d de 0 à 3
si d = 0 alors
di vaut 0 - 1
dj vaut 0
sinon si d = 1 alors
di vaut 1
dj vaut 0
sinon si d = 2 alors
di vaut 0
dj vaut 0 - 1
sinon
di vaut 0
dj vaut 1
fin si
si carteB[i + di][j + dj] = 0 et accesA[i + di][j + dj] = faux alors
accesA[i + di][j + dj] vaut vrai
vague vaut vrai
fin si
fin pour
fin si
fin pour
fin pour
fin tant que
libresB vaut 0
pour i de 1 à 10
pour j de 1 à 10
si accesA[i][j] = vrai alors
libresB ajoute 1
fin si
fin pour
fin pour
si libresB >= 60 alors
bonne vaut vrai
fin si
fin tant que
// On recalcule l'accessibilité de la carte de gauche pour l'affichage
pour i de 0 à 11
pour j de 0 à 11
accesA[i][j] vaut faux
fin pour
fin pour
accesA[1][1] vaut vrai
vague vaut vrai
tant que vague = vrai
vague vaut faux
pour i de 1 à 10
pour j de 1 à 10
si accesA[i][j] = vrai alors
pour d de 0 à 3
si d = 0 alors
di vaut 0 - 1
dj vaut 0
sinon si d = 1 alors
di vaut 1
dj vaut 0
sinon si d = 2 alors
di vaut 0
dj vaut 0 - 1
sinon
di vaut 0
dj vaut 1
fin si
si carteA[i + di][j + dj] = 0 et accesA[i + di][j + dj] = faux alors
accesA[i + di][j + dj] vaut vrai
vague vaut vrai
fin si
fin pour
fin si
fin pour
fin pour
fin tant que
fin si
// =========================================================
// AFFICHAGE
// =========================================================
effacer(vue)
contour(vue, #0)
remplir(vue, #EEF2F7)
rectangle_arrondi(vue, 12, 10, 676, 74, 14, #FFFFFF)
label(vue, 26, 40, "LE HASARD MAÎTRISÉ", #B45309, 20)
label(vue, 26, 62, "hasard() ne suffit jamais : ce sont les contraintes qui font la qualité du tirage", #64748B, 12)
label(vue, 26, 80, "[A] pondérer [Z] exclure [E] valider [R] refaire les tirages", #2563EB, 11)
si ecran = "ponderer" alors
// -------------------------------------------------------
// 1. PONDÉRER : toutes les faces ne se valent pas
// -------------------------------------------------------
rectangle_arrondi(vue, 12, 92, 676, 416, 14, #FFFFFF)
label(vue, 26, 118, "1. PONDÉRER — " + NBTIR + " tirages", #0F172A, 15)
label(vue, 40, 150, "hasard(1, 6)", #DC2626, 12)
label(vue, 40, 168, "chaque face a la même chance", #94A3B8, 10)
pour v de 1 à 6
hb est un nombre
hb vaut arrondi(cptUni[v] * 140 / 200)
appelle barre(vue, 34 + (v - 1) * 28, 340, 20, hb, #F87171, "" + v, cptUni[v])
fin pour
label(vue, 250, 150, "tranches de 1 à 100", #2563EB, 12)
label(vue, 250, 168, "40 % de 1, puis 25, 15, 10, 6, 4", #94A3B8, 10)
pour v de 1 à 6
hb vaut arrondi(cptPon[v] * 140 / 250)
appelle barre(vue, 244 + (v - 1) * 28, 340, 20, hb, #60A5FA, "" + v, cptPon[v])
fin pour
label(vue, 448, 150, "somme de deux dés", #16A34A, 12)
label(vue, 448, 168, "combiner deux hasards change la forme", #94A3B8, 10)
pour v de 2 à 12
hb vaut arrondi(cptDeux[v] * 140 / 120)
appelle barre(vue, 442 + (v - 2) * 21, 340, 15, hb, #4ADE80, "" + v, cptDeux[v])
fin pour
label(vue, 40, 386, "Le tirage uniforme est plat. Le tirage pondéré privilégie les petites valeurs.", #334155, 12)
label(vue, 40, 408, "La somme de deux dés dessine une cloche : le 7 sort six fois plus souvent que le 2.", #334155, 12)
label(vue, 40, 438, "Dans un jeu de fusion, sans pondération le joueur reçoit trop de grosses pièces", #64748B, 11)
label(vue, 40, 456, "et n'a jamais besoin de fusionner. La règle du jeu tient dans le tirage.", #64748B, 11)
label(vue, 40, 486, "Écrire hasard(1, 6) est facile. Décider de la forme de la distribution, c'est du game design.", #B45309, 11)
sinon si ecran = "exclure" alors
// -------------------------------------------------------
// 2. EXCLURE : interdire une zone au tirage
// -------------------------------------------------------
rectangle_arrondi(vue, 12, 92, 676, 416, 14, #FFFFFF)
label(vue, 26, 118, "2. EXCLURE — les mines du démineur", #0F172A, 15)
label(vue, 40, 146, "Tirage brut", #DC2626, 13)
label(vue, 40, 164, "10 mines n'importe où", #94A3B8, 10)
label(vue, 400, 146, "Tirage contraint", #16A34A, 13)
label(vue, 400, 164, "rien sur la case cliquée ni autour", #94A3B8, 10)
pour i de 0 à 8
pour j de 0 à 8
xa est un nombre
xb est un nombre
ya est un nombre
xa vaut 40 + j * 26
xb vaut 400 + j * 26
ya vaut 180 + i * 26
rectangle_arrondi(vue, xa, ya, 23, 23, 4, #E2E8F0)
rectangle_arrondi(vue, xb, ya, 23, 23, 4, #E2E8F0)
si minesA[i][j] = 1 alors
cercle(vue, xa + 11, ya + 11, 7, #DC2626)
fin si
si minesB[i][j] = 1 alors
cercle(vue, xb + 11, ya + 11, 7, #DC2626)
fin si
// La case du premier clic et son voisinage
si absolue(i - 4) <= 1 et absolue(j - 4) <= 1 alors
contour(vue, #2563EB, 2)
rectangle_arrondi(vue, xb, ya, 23, 23, 4, rgba(37, 99, 235, 0.12))
contour(vue, #0)
fin si
si i = 4 et j = 4 alors
contour(vue, #2563EB, 2)
rectangle_arrondi(vue, xa, ya, 23, 23, 4, rgba(37, 99, 235, 0.12))
contour(vue, #0)
fin si
fin pour
fin pour
label(vue, 40, 436, "Sur 400 parties, une mine se trouve sous le premier clic : " + perduesA + " fois", #DC2626, 12)
label(vue, 40, 458, "Avec la zone d'exclusion : 0 fois, par construction", #16A34A, 12)
label(vue, 40, 486, "Une seule ligne de contrainte : si absolue(mi - 4) <= 1 et absolue(mj - 4) <= 1 alors refuser", #B45309, 11)
sinon
// -------------------------------------------------------
// 3. VALIDER : rejeter ce qui ne convient pas
// -------------------------------------------------------
rectangle_arrondi(vue, 12, 92, 676, 416, 14, #FFFFFF)
label(vue, 26, 118, "3. VALIDER — une carte praticable", #0F172A, 15)
label(vue, 40, 146, "Carte brute", #DC2626, 13)
label(vue, 40, 164, "en rouge : les cases inatteignables", #94A3B8, 10)
label(vue, 400, 146, "Carte validée", #16A34A, 13)
label(vue, 400, 164, "rejetée tant que trop morcelée", #94A3B8, 10)
pour i de 0 à 11
pour j de 0 à 11
xa vaut 40 + j * 21
xb vaut 400 + j * 21
ya vaut 180 + i * 21
coulA est un texte
si carteA[i][j] = 1 alors
coulA vaut #64748B
sinon si accesA[i][j] = vrai alors
coulA vaut #86EFAC
sinon
coulA vaut #FCA5A5
fin si
rectangle_arrondi(vue, xa, ya, 18, 18, 3, coulA)
coulB est un texte
si carteB[i][j] = 1 alors
coulB vaut #64748B
sinon
coulB vaut #86EFAC
fin si
rectangle_arrondi(vue, xb, ya, 18, 18, 3, coulB)
fin pour
fin pour
cercle(vue, 49, 189, 5, #2563EB)
cercle(vue, 409, 189, 5, #2563EB)
label(vue, 40, 452, "Carte brute : " + isolesA + " cases libres mais inatteignables depuis le point bleu", #DC2626, 12)
label(vue, 40, 474, "Carte validée : " + libresB + " cases accessibles, obtenue au bout de " + essaisB + " essai(s)", #16A34A, 12)
label(vue, 40, 498, "Générer puis vérifier, et recommencer si besoin : c'est le prix d'un niveau toujours jouable.", #B45309, 11)
fin si
affiche vue
// =========================================================
appuyer ["a", "z", "e", "r"] dans touche
si touche = 1 alors
ecran vaut "ponderer"
sinon si touche = 2 alors
ecran vaut "exclure"
sinon si touche = 3 alors
ecran vaut "valider"
sinon
refaire vaut vrai
fin si
fin tant que





