CoddyRun
Blog

Articles & Actualités

Les publications CoddyRun : nouveautés du logiciel, astuces de pseudo-code, retours d'expérience et actualités de l'environnement pédagogique.

69 publications

Génération d’une surface 3D Peaks : fonctionnement détaillé du programme
Article

Génération d’une surface 3D Peaks

Génération d’une surface 3D Peaks : fonctionnement détaillé du programme

Lire l'articleReplier l'article

Ce programme construit et affiche une surface 3D statique de type peaks, rendue en projection isométrique, avec facettes pleines, tri de profondeur et trois grilles fixes formant un repère spatial. Voici une analyse complète de son fonctionnement.

Paramétrage de la scène

La toile est définie avec une largeur de 560 px et une hauteur de 520 px. Le centre visuel est légèrement abaissé pour compenser la hauteur du relief :

  • OX = LARG / 2
  • OY = HAUT / 2 + 40

L’angle de vue est une projection iso inclinée, définie par :

  • sinA = 0,34
  • cosA = 0,62

Deux échelles sont utilisées :

  • sXY = 62 pour les axes X et Y
  • sZ = 13 pour la hauteur

La résolution de la grille est fixée à N = 22, avec un domaine allant de xmin = -3 à xmax = 3.

Calcul du relief : la fonction « peaks »

Le programme pré-calcule les hauteurs z pour chaque point de la grille. Pour chaque couple (i, j), il calcule :

  • xx et yy : coordonnées normalisées

Trois composantes du relief :

  • La hauteur finale est : z = g1 + g2 + g3

Toutes les valeurs sont stockées dans un tableau linéaire.

Colormap « jet »

Une fonction dédiée convertit une hauteur normalisée en couleur RGB. Elle reproduit le célèbre colormap jet :

  • Bleu ? Vert ? Jaune ? Rouge selon la valeur de hn (0 à 1).

Cette palette renforce la lisibilité du relief.

Construction du repère 3D

Avant de dessiner la surface, le programme trace trois grilles fixes :

  • Plan XY (sol)
  • Plan XZ (mur arrière gauche)
  • Plan YZ (mur arrière droit)

Chaque grille est dessinée en lignes fines (#dddddd), ce qui crée un repère spatial clair et stable.

Projection 3D ? 2D avec rotation

La procédure dessinerVue(rot) applique une rotation horizontale :

  • cr = cos(rot)
  • sr = sin(rot)

Chaque point (x, y, z) est transformé en coordonnées tournées (xr, yr) puis projeté en 2D via l’angle iso :

  • pX = OX + (xr·sXY – yr·sXY)·cosA pY = OY – z·sZ – (xr·sXY + yr·sXY)·sinA

Cette projection donne l’effet 3D sans utiliser de moteur graphique.

Tri de profondeur : rendu correct des facettes

Pour éviter que les facettes s’affichent dans le mauvais ordre, le programme utilise un tri par bandes de profondeur :

Chaque facette reçoit une valeur de profondeur.
Le programme parcourt les bandes de la plus éloignée à la plus proche.
Il dessine uniquement les facettes appartenant à la bande courante.

Ce tri garantit un rendu propre, sans chevauchements incohérents.

Dessin des facettes

Chaque cellule de la grille est rendue comme un polygone à 4 sommets :

  • Les quatre hauteurs za, zb, zc, zd sont récupérées.
  • Les quatre coins sont projetés en 2D.
  • La couleur est déterminée par la hauteur moyenne.
  • Le polygone est dessiné avec un contour sombre (#0a0a0f).

Le résultat est une surface 3D colorée, continue et lisible.

Résultat final

La toile affiche :

  • Le repère 3D
  • La surface « peaks » entièrement colorée
  • Un titre discret : Surface 3D « peaks »

L’appel final : dessinerVue(0) produit une vue alignée, sans rotation.

Conclusion

Ce programme combine plusieurs techniques graphiques :

  • Calcul de relief paramétrique
  • Projection isométrique
  • Colormap dynamique
  • Tri de profondeur manuel
  • Rendu polygonal optimisé

Il démontre comment produire une surface 3D complète sans moteur 3D, uniquement via calculs géométriques et dessin 2D. Une base solide pour des visualisations scientifiques, des rendus mathématiques ou des démonstrations pédagogiques.

Programme :

// ============================================================
//   SURFACE 3D "peaks" — centrée, statique (sans rotation)
//   Facettes pleines, colormap jet, fond noir, tri de profondeur.
//   INCLUS : 3 Grilles fixes (XY, XZ, YZ) formant le repère.
// ============================================================

LARG est un nombre
LARG vaut 560
HAUT est un nombre
HAUT vaut 520

// ---- Centre de la toile (surface centrée) ----
OX est un nombre
OY est un nombre
OX vaut LARG / 2
OY vaut HAUT / 2 + 40 // légèrement bas pour compenser la hauteur du relief

// ---- Angle de vue iso (inclinaison verticale) ----
sinA est un nombre
cosA est un nombre
sinA vaut 0,34
cosA vaut 0,62

// ---- Échelles ----
sXY est un nombre
sZ est un nombre
sXY vaut 62
sZ vaut 13

// ---- Résolution ----
N est un nombre
N vaut 22

xmin est un nombre
xmax est un nombre
xmin vaut - 3
xmax vaut 3

// ---- Pré-calcul des hauteurs z : index(i,j) = j*(N+1) + i ----
hauteurs est un tableau
j est un nombre
pour j de 0 à N
    i est un nombre
    pour i de 0 à N
        xx est un nombre
        yy est un nombre
        xx vaut xmin + (xmax - xmin) * i / N
        yy vaut xmin + (xmax - xmin) * j / N
        g1 est un nombre
        g2 est un nombre
        g3 est un nombre
        g1 vaut 3 * (1 - xx) * (1 - xx) * puissance(2.71828, -(xx * xx) - (yy + 1) * (yy + 1))
        g2 vaut - 10 * (xx / 5 - puissance(xx, 3) - puissance(yy, 5)) * puissance(2.71828, -(xx * xx) - (yy * yy))
        g3 vaut - 1 / 3 * puissance(2.71828, -((xx + 1) * (xx + 1)) - (yy * yy))
        hauteurs ajoute g1 + g2 + g3
    fin pour
fin pour

// ---- Couleur "jet" selon hauteur normalisée hn (0..1) ----
fonction couleur_jet(hn)
    hnc est un nombre
    hnc vaut limiter(hn, 0, 1)
    rC est un nombre
    gC est un nombre
    bC est un nombre
    rC vaut arrondi(255 * limiter(1.5 - absolue(4 * hnc - 3), 0, 1))
    gC vaut arrondi(255 * limiter(1.5 - absolue(4 * hnc - 2), 0, 1))
    bC vaut arrondi(255 * limiter(1.5 - absolue(4 * hnc - 1), 0, 1))
    retourne rgb(rC, gC, bC)
fin fonction

// ============================================================
//  DESSINE L'IMAGE pour un angle de rotation "rot" (radians)
// ============================================================
procédure dessinerVue(rot)
    Fig est une toile
    dimension(Fig, LARG, HAUT)
    remplir(Fig, #ffffff)

    cr est un nombre
    sr est un nombre
    cr vaut cosinus(rot)
    sr vaut sinus(rot)

    // --- DESSIN DES 3 GRILLES FIXES (XY, XZ, YZ) ---
    zBase est un nombre
    zBase vaut - 6
    zMax est un nombre
    zMax vaut 8

    k est un nombre
    pour k de 0 à 10
        yL est un nombre
        yL vaut xmin + (xmax - xmin) * k / 10
        xL est un nombre
        xL vaut xmin + (xmax - xmin) * k / 10
        zL est un nombre
        zL vaut zBase + (zMax - zBase) * k / 10

        // 1. GRILLE SOL (Plan XY à Z = zBase)
        px1, py1, px2, py2 est un nombre
        px1 vaut OX + (xmin * sXY - yL * sXY) * cosA
        py1 vaut OY - zBase * sZ - (xmin * sXY + yL * sXY) * sinA
        px2 vaut OX + (xmax * sXY - yL * sXY) * cosA
        py2 vaut OY - zBase * sZ - (xmax * sXY + yL * sXY) * sinA
        ligne(Fig, px1, py1, px2, py2, #dddddd, 1)

        px3, py3, px4, py4 est un nombre
        px3 vaut OX + (xL * sXY - xmin * sXY) * cosA
        py3 vaut OY - zBase * sZ - (xL * sXY + xmin * sXY) * sinA
        px4 vaut OX + (xL * sXY - xmax * sXY) * cosA
        py4 vaut OY - zBase * sZ - (xL * sXY + xmax * sXY) * sinA
        ligne(Fig, px3, py3, px4, py4, #dddddd, 1)

        // 2. GRILLE MUR XZ (Arrière-gauche, à Y = xmax)
        px5, py5, px6, py6 est un nombre
        px5 vaut OX + (xL * sXY - xmax * sXY) * cosA
        py5 vaut OY - zBase * sZ - (xL * sXY + xmax * sXY) * sinA
        px6 vaut OX + (xL * sXY - xmax * sXY) * cosA
        py6 vaut OY - zMax * sZ - (xL * sXY + xmax * sXY) * sinA
        ligne(Fig, px5, py5, px6, py6, #dddddd, 1)

        px7, py7, px8, py8 est un nombre
        px7 vaut OX + (xmin * sXY - xmax * sXY) * cosA
        py7 vaut OY - zL * sZ - (xmin * sXY + xmax * sXY) * sinA
        px8 vaut OX + (xmax * sXY - xmax * sXY) * cosA
        py8 vaut OY - zL * sZ - (xmax * sXY + xmax * sXY) * sinA
        ligne(Fig, px7, py7, px8, py8, #dddddd, 1)

        // 3. GRILLE MUR YZ (Arrière-droit, à X = xmax)
        px9, py9, px10, py10 est un nombre
        px9 vaut OX + (xmax * sXY - yL * sXY) * cosA
        py9 vaut OY - zBase * sZ - (xmax * sXY + yL * sXY) * sinA
        px10 vaut OX + (xmax * sXY - yL * sXY) * cosA
        py10 vaut OY - zMax * sZ - (xmax * sXY + yL * sXY) * sinA
        ligne(Fig, px9, py9, px10, py10, #dddddd, 1)

        px11, py11, px12, py12 est un nombre
        px11 vaut OX + (xmax * sXY - xmin * sXY) * cosA
        py11 vaut OY - zL * sZ - (xmax * sXY + xmin * sXY) * sinA
        px12 vaut OX + (xmax * sXY - xmax * sXY) * cosA
        py12 vaut OY - zL * sZ - (xmax * sXY + xmax * sXY) * sinA
        ligne(Fig, px11, py11, px12, py12, #dddddd, 1)
    fin pour

    // --- Passe unique avec tri par profondeur (bandes) pour la surface ---
    bande est un nombre
    pour bande de 40 à 0 pas - 1
        jj est un nombre
        pour jj de 0 à N - 1
            ii est un nombre
            pour ii de 0 à N - 1
                // centre (monde) de la facette
                xc est un nombre
                yc est un nombre
                xc vaut xmin + (xmax - xmin) * (ii + 0.5) / N
                yc vaut xmin + (xmax - xmin) * (jj + 0.5) / N
                // coordonnées tournées pour la surface
                xrc est un nombre
                yrc est un nombre
                xrc vaut xc * cr - yc * sr
                yrc vaut xc * sr + yc * cr
                // profondeur -> bande entière (grand = au fond)
                prof est un nombre
                prof vaut arrondi((xrc + yrc + 6) * 40 / 12)
                si prof = bande alors
                    // Les 4 coins (monde), tournés puis projetés
                    za est un nombre
                    zb est un nombre
                    zc2 est un nombre
                    zd est un nombre
                    za vaut hauteurs[jj * (N + 1) + ii]
                    zb vaut hauteurs[jj * (N + 1) + ii + 1]
                    zc2 vaut hauteurs[(jj + 1) * (N + 1) + ii + 1]
                    zd vaut hauteurs[(jj + 1) * (N + 1) + ii]

                    xa est un nombre
                    ya est un nombre
                    xb est un nombre
                    yb est un nombre
                    xa vaut xmin + (xmax - xmin) * ii / N
                    ya vaut xmin + (xmax - xmin) * jj / N
                    xb vaut xmin + (xmax - xmin) * (ii + 1) / N
                    yb vaut xmin + (xmax - xmin) * (jj + 1) / N

                    // projette un coin (x,y,z) avec rotation
                    xr1 est un nombre
                    yr1 est un nombre
                    xr1 vaut xa * cr - ya * sr
                    yr1 vaut xa * sr + ya * cr
                    pax est un nombre
                    pay est un nombre
                    pax vaut OX + (xr1 * sXY - yr1 * sXY) * cosA
                    pay vaut OY - za * sZ - (xr1 * sXY + yr1 * sXY) * sinA

                    xr2 est un nombre
                    yr2 est un nombre
                    xr2 vaut xb * cr - ya * sr
                    yr2 vaut xb * sr + ya * cr
                    pbx est un nombre
                    pby est un nombre
                    pbx vaut OX + (xr2 * sXY - yr2 * sXY) * cosA
                    pby vaut OY - zb * sZ - (xr2 * sXY + yr2 * sXY) * sinA

                    xr3 est un nombre
                    yr3 est un nombre
                    xr3 vaut xb * cr - yb * sr
                    yr3 vaut xb * sr + yb * cr
                    pcx est un nombre
                    pcy est un nombre
                    pcx vaut OX + (xr3 * sXY - yr3 * sXY) * cosA
                    pcy vaut OY - zc2 * sZ - (xr3 * sXY + yr3 * sXY) * sinA

                    xr4 est un nombre
                    yr4 est un nombre
                    xr4 vaut xa * cr - yb * sr
                    yr4 vaut xa * sr + yb * cr
                    pdx est un nombre
                    pdy est un nombre
                    pdx vaut OX + (xr4 * sXY - yr4 * sXY) * cosA
                    pdy vaut OY - zd * sZ - (xr4 * sXY + yr4 * sXY) * sinA

                    // Couleur selon hauteur moyenne
                    zmoy est un nombre
                    zmoy vaut (za + zb + zc2 + zd) / 4
                    hn est un nombre
                    hn vaut (zmoy + 7) / 14
                    coul est un texte
                    coul vaut couleur_jet(hn)

                    contour(Fig, #0a0a0f, 1)
                    polygone(Fig, pax, pay, pbx, pby, pcx, pcy, pdx, pdy, coul)
                    contour(Fig, #0)
                fin si
            fin pour
        fin pour
    fin pour

    label(Fig, 20, 34, "Surface 3d « peaks »", #1F2937, 18)

    // Affichage direct de la toile finale
    affiche Fig
fin procédure

// ============================================================
//  APPEL : on dessine avec un angle de 0 (aligné)
// ============================================================
appelle dessinerVue(0)
Pas encore noté
Génération de Labyrinthe & Rendu 3D Isométrique : Exploration Technique
Article

Génération de Labyrinthe & Rendu 3D Isométrique

Génération de Labyrinthe & Rendu 3D Isométrique : Exploration Technique

Lire l'articleReplier l'article

Ce projet s’inscrit dans une démarche d’expérimentation autour de la génération procédurale, des algorithmes de parcours, et du rendu isométrique 3D basé sur des primitives géométriques.
L’objectif : comprendre, tester et maîtriser un pipeline complet allant de la création d’un labyrinthe à son affichage en perspective isométrique.

Génération procédurale : DFS (Depth-First Search)

La structure du labyrinthe repose sur un DFS carving, une méthode simple mais extrêmement efficace pour produire des labyrinthes cohérents et non triviaux.

Caractéristiques :

  • Grille 11×11
  • Carving par sauts de 2 cases pour créer des couloirs
  • Stack pour le backtracking
  • Marquage des cellules visitées
  • Percement des murs via midpoint (cur + next) / 2
  • Sortie placée en bas-droite

Ce type d’algorithme garantit :

  • un chemin unique entre deux points,
  • une génération rapide,
  • une structure lisible et exploitable pour un rendu 3D.

Rendu isométrique 3D : géométrie pure

Le rendu repose entièrement sur des polygones, lignes et ellipses, sans sprites ni textures.
Chaque cellule est transformée en tuile isométrique selon ses coordonnées (u, v).

Points techniques :

  • Tuiles : tw = 36, th = 18
  • Murs 3D composés de trois faces + contours
  • Dalles de sol avec shading
  • Pion joueur en pseudo-3D
  • Sortie mise en valeur par une dalle verte

Rendu par diagonales (u + v) pour garantir la bonne superposition des tuiles

Ce pipeline offre un contrôle total sur la géométrie, les ombres, la lisibilité et l’esthétique.

Minimap 2D intégrée

Une minimap 2D accompagne le rendu isométrique pour améliorer la navigation :

  • Cellules mur/sol/sortie différenciées
  • Position du joueur affichée en temps réel
  • Interface compacte sous la zone de commandes

Elle permet de conserver une vision globale du labyrinthe malgré la perspective.

Gameplay & interactions

Les déplacements se font via Z Q S D, avec :

  • gestion des collisions,
  • détection de la sortie,
  • états du jeu (génération, jeu, victoire),
  • régénération complète via ESPACE.

Une boucle simple, efficace, idéale pour tester différentes configurations de labyrinthe.

Enseignements techniques

Ce projet met en lumière plusieurs aspects intéressants :

- maîtrise des algorithmes de génération procédurale,
- construction d’un pipeline de rendu isométrique custom,
- optimisation de l’ordre de dessin pour éviter les artefacts,
- réflexion sur la lisibilité dans un environnement pseudo-3D,
- structuration d’un mini-moteur graphique basé sur primitives.
- Un excellent terrain d’expérimentation pour des projets plus ambitieux :
jeux de stratégie, donjons procéduraux, moteurs isométriques, simulations, etc.

Programme :

@ Initialisation et Déclarations
toile1 est une toile
dimension(toile1, 550, 520)

gridW est un nombre
gridH est un nombre
gridW vaut 11
gridH vaut 11

g est un tableau
visite est un tableau

posU est un nombre
posV est un nombre
posU vaut 1
posV vaut 1

etatJeu est un nombre
etatJeu vaut 3 // 3 = Génération, 0 = Jeu, 1 = Victoire

monAction est un nombre
monAction vaut 0

// Variables DFS Génération
stackU est un tableau
stackV est un tableau
voisinsU est un tableau
voisinsV est un tableau

curU est un nombre
curV est un nombre
nextU est un nombre
nextV est un nombre
midU est un nombre
midV est un nombre
nbVoisins est un nombre
choix est un nombre
idx est un nombre

i est un nombre
u est un nombre
v est un nombre
sommeDiag est un nombre
valeurCase est un nombre

// Variables Rendu Isométrique 3D
tw est un nombre
tw vaut 36
th est un nombre
th vaut 18
hMur est un nombre
hMur vaut 18

// Centrage du Labyrinthe
offX est un nombre
offX vaut 275
offY est un nombre
offY vaut 225

cx est un nombre
cy est un nombre

// Minimap sous l'interface
cellSize est un nombre
cellSize vaut 8
mapOffX est un nombre
mapOffX vaut 231
mapOffY est un nombre
mapOffY vaut 72
mcx est un nombre
mcy est un nombre

// Boucle principale
tant que vrai

    @ 1. GÉNÉRATION DU LABYRINTHE (DFS 2D)
    si etatJeu = 3 alors
        vide g
        vide visite
        vide stackU
        vide stackV

        pour i de 0 à (gridW * gridH) - 1
            g ajoute 0
            visite ajoute 0
        fin pour

        posU vaut 1
        posV vaut 1
        idx vaut (posV * gridW) + posU
        visite[idx] vaut 1
        g[idx] vaut 1

        stackU ajoute posU
        stackV ajoute posV

        tant que longueur(stackU) > 0
            curU vaut dernier(stackU)
            curV vaut dernier(stackV)

            vide voisinsU
            vide voisinsV

            // Haut
            si curV >= 3 alors
                idx vaut ((curV - 2) * gridW) + curU
                si visite[idx] = 0 alors
                    voisinsU ajoute curU
                    voisinsV ajoute (curV - 2)
                fin si
            fin si
            // Bas
            si curV <= gridH - 4 alors
                idx vaut ((curV + 2) * gridW) + curU
                si visite[idx] = 0 alors
                    voisinsU ajoute curU
                    voisinsV ajoute (curV + 2)
                fin si
            fin si
            // Gauche
            si curU >= 3 alors
                idx vaut (curV * gridW) + (curU - 2)
                si visite[idx] = 0 alors
                    voisinsU ajoute (curU - 2)
                    voisinsV ajoute curV
                fin si
            fin si
            // Droite
            si curU <= gridW - 4 alors
                idx vaut (curV * gridW) + (curU + 2)
                si visite[idx] = 0 alors
                    voisinsU ajoute (curU + 2)
                    voisinsV ajoute curV
                fin si
            fin si

            nbVoisins vaut longueur(voisinsU)
            si nbVoisins > 0 alors
                choix vaut hasard(0, nbVoisins - 1)
                nextU vaut voisinsU[choix]
                nextV vaut voisinsV[choix]

                midU vaut (curU + nextU) / 2
                midV vaut (curV + nextV) / 2

                idx vaut (midV * gridW) + midU
                g[idx] vaut 1

                idx vaut (nextV * gridW) + nextU
                g[idx] vaut 1
                visite[idx] vaut 1

                stackU ajoute nextU
                stackV ajoute nextV
            sinon
                stackU supprime (longueur(stackU) - 1)
                stackV supprime (longueur(stackV) - 1)
            fin si
        fin tant que

        // Marquer la sortie
        idx vaut ((gridH - 2) * gridW) + (gridW - 2)
        g[idx] vaut 2

        posU vaut 1
        posV vaut 1
        etatJeu vaut 0
    fin si

    @ 2. RENDU GRAPHIQUE
    effacer(toile1)
    rectangle(toile1, 0, 0, 550, 520, #0f172a)

    // --- 2.1 INTERFACE DE COMMANDES (EN HAUT) ---
    rectangle(toile1, 0, 0, 550, 44, #1e293b)
    ligne(toile1, 0, 45, 550, 45, #334155, 1)
    label(toile1, 20, 20, "LABYRINTHE 3D ISOMÉTRIQUE", #f8fafc, 12)
    label(toile1, 20, 40, "Déplacement : Z Q S D  |  Nouveau : ESPACE", #94a3b8, 10)

    // --- 2.2 MINIMAP 2D ---
    rectangle(toile1, mapOffX - 8, mapOffY - 6, (gridW * cellSize) + 16, (gridH * cellSize) + 16, #1e293b)
    ligne(toile1, mapOffX - 8, mapOffY - 6, mapOffX + (gridW * cellSize) + 8, mapOffY - 6, #334155, 1)

    pour v de 0 à gridH - 1
        pour u de 0 à gridW - 1
            idx vaut (v * gridW) + u
            valeurCase vaut g[idx]
            mcx vaut mapOffX + (u * cellSize)
            mcy vaut mapOffY + (v * cellSize)

            si valeurCase = 0 alors
                rectangle(toile1, mcx, mcy, cellSize - 1, cellSize - 1, #475569)
            sinon si valeurCase = 1 alors
                rectangle(toile1, mcx, mcy, cellSize - 1, cellSize - 1, #090d16)
            sinon si valeurCase = 2 alors
                rectangle(toile1, mcx, mcy, cellSize - 1, cellSize - 1, #10b981)
            fin si

            si posU = u et posV = v alors
                cercle(toile1, mcx + (cellSize / 2), mcy + (cellSize / 2), 3, #f43f5e)
            fin si
        fin pour
    fin pour

    // --- 2.3 LABYRINTHE 3D ISOMÉTRIQUE (CENTRÉ AU MILIEU) ---
    sommeDiag vaut 0
    tant que sommeDiag <= (gridW + gridH - 2)
        v vaut 0
        tant que v < gridH
            u vaut 0
            tant que u < gridW
                si (u + v) = sommeDiag alors
                    idx vaut (v * gridW) + u
                    valeurCase vaut g[idx]

                    cx vaut offX + (u - v) * (tw / 2)
                    cy vaut offY + (u + v) * (th / 2)

                    si valeurCase = 0 alors
                        // MURS 3D ISOMÉTRIQUES (Sombres et contrastés)
                        polygone(toile1, cx, cy - th / 2 - hMur, cx + tw / 2, cy - hMur, cx, cy + th / 2 - hMur, cx - tw / 2, cy - hMur, #475569)
                        polygone(toile1, cx - tw / 2, cy - hMur, cx, cy + th / 2 - hMur, cx, cy + th / 2, cx - tw / 2, cy, #334155)
                        polygone(toile1, cx, cy + th / 2 - hMur, cx + tw / 2, cy - hMur, cx + tw / 2, cy, cx, cy + th / 2, #1e293b)

                        // Contour des murs
                        ligne(toile1, cx, cy - th / 2 - hMur, cx + tw / 2, cy - hMur, #64748b, 1)
                        ligne(toile1, cx + tw / 2, cy - hMur, cx, cy + th / 2 - hMur, #64748b, 1)
                        ligne(toile1, cx, cy + th / 2 - hMur, cx - tw / 2, cy - hMur, #64748b, 1)
                        ligne(toile1, cx - tw / 2, cy - hMur, cx, cy - th / 2 - hMur, #64748b, 1)
                        ligne(toile1, cx, cy + th / 2 - hMur, cx, cy + th / 2, #64748b, 1)
                    sinon
                        // DALLE DE SOL (Teinte anthracite)
                        polygone(toile1, cx, cy - th / 2, cx + tw / 2, cy, cx, cy + th / 2, cx - tw / 2, cy, #1e293b)
                        ligne(toile1, cx - tw / 2, cy, cx, cy + th / 2, #334155, 1)
                        ligne(toile1, cx, cy + th / 2, cx + tw / 2, cy, #334155, 1)

                        // Sortie (Vert Émeraude)
                        si valeurCase = 2 alors
                            polygone(toile1, cx, cy - th / 2, cx + tw / 2, cy, cx, cy + th / 2, cx - tw / 2, cy, #10b981)
                        fin si

                        // Pion Joueur (Rose Néon)
                        si posU = u et posV = v alors
                            ellipse(toile1, cx, cy, 8, 4, rgba(0, 0, 0, 0.4))
                            polygone(toile1, cx, cy - 16, cx + 6, cy - 12, cx, cy - 8, cx - 6, cy - 12, #fb7185)
                            polygone(toile1, cx - 6, cy - 12, cx, cy - 8, cx, cy - 2, cx - 6, cy - 6, #f43f5e)
                            polygone(toile1, cx, cy - 8, cx + 6, cy - 12, cx + 6, cy - 6, cx, cy - 2, #e11d48)
                            cercle(toile1, cx, cy - 16, 2, #ffe4e6)
                        fin si
                    fin si
                fin si
                u vaut u + 1
            fin tant que
            v vaut v + 1
        fin tant que
        sommeDiag vaut sommeDiag + 1
    fin tant que

    // MESSAGE DE VICTOIRE
    si etatJeu = 1 alors
        rectangle(toile1, 125, 260, 300, 80, rgba(30, 41, 59, 0.95))
        ligne(toile1, 125, 260, 425, 260, #10b981, 3)
        label(toile1, 205, 290, "VICTOIRE !", #10b981, 18)
        label(toile1, 155, 320, "Appuie sur ESPACE pour rejouer", #f8fafc, 11)
    fin si

    affiche toile1

    @ 3. DÉPLACEMENTS DU JOUEUR (Z, Q, S, D)
    appuyer ["z", "s", "q", "d", " "] dans monAction

    si monAction = 5 alors
        etatJeu vaut 3
    sinon si etatJeu = 0 alors
        nextU vaut posU
        nextV vaut posV

        si monAction = 1 alors nextV vaut posV - 1 fin si
        si monAction = 2 alors nextV vaut posV + 1 fin si
        si monAction = 3 alors nextU vaut posU - 1 fin si
        si monAction = 4 alors nextU vaut posU + 1 fin si

        si nextU >= 0 et nextU < gridW et nextV >= 0 et nextV < gridH alors
            idx vaut (nextV * gridW) + nextU
            si g[idx] > 0 alors
                posU vaut nextU
                posV vaut nextV
            fin si
        fin si

        idx vaut (posV * gridW) + posU
        si g[idx] = 2 alors
            etatJeu vaut 1
        fin si
    fin si
fin tant que
Pas encore noté
Exploration 2D : une map générée avec obstacles, décor et trésor !
Article

Exploration 2D

Exploration 2D : une map générée avec obstacles, décor et trésor !

Lire l'articleReplier l'article

Un système de déplacement dans une grille 2D permet de traverser différents types de terrains, éviter les obstacles et atteindre un coffre bien caché.

Ce projet peut paraître simple au premier regard, mais la logique derrière lui ouvre la porte à beaucoup plus : génération procédurale, collisions, chemin garanti, décor varié… une vraie base pour la suite.

Décors générés aléatoirement :

La carte contient plusieurs types de cases :

  • Herbe (0) – zones accessibles
  • Mur (1) – obstacles en pierre
  • Forêt (2) – arbres infranchissables
  • Eau (3) – zones bloquantes
  • Trésor (4) – objectif final

Les bords sont des murs, et l’intérieur est rempli aléatoirement pour créer une vraie petite aventure.

Chemin garanti jusqu’au trésor :

Un creusement automatique assure qu’un passage existe toujours entre le joueur et le coffre.
Peu importe la génération, une route sûre mène à la victoire.

Déplacement du joueur :

Le personnage se déplace case par case avec :

  • Z : Haut
  • S : Bas
  • D : Droite
  • Q : Gauche

Les collisions empêchent de traverser murs, arbres ou eau.

Objectif :

  • Atteindre la case du trésor.
  • Quand le joueur arrive dessus : VICTOIRE !

Graphismes simples mais efficaces :

  • Herbe fleurie, forêt dense, eau animée, mur texturé, coffre doré… et un petit personnage rouge avec des yeux expressifs.

Un système parfait pour créer :

  • un jeu d’exploration
  • un labyrinthe dynamique

Programme :

@ Initialisation de la toile et des dimensions
maToile est une toile
dimension(maToile, 320, 320)

tailleTUILE, nbLignes, nbColonnes est un nombre
tailleTUILE vaut 32
nbLignes vaut 10
nbColonnes vaut 10

// Position du joueur et du trésor
joueurX, joueurY est un nombre
joueurX vaut 1
joueurY vaut 1

tresorX, tresorY est un nombre
tresorX vaut nbColonnes - 2
tresorY vaut nbLignes - 2

// Types de décors :
// 0 = Herbe (Passage)
// 1 = Mur (Obstacle)
// 2 = Forêt (Obstacle)
// 3 = Eau (Obstacle)
// 4 = Trésor (Objectif)

grilleJeu est un tableau
i, j, tyAlea, idxActuel est un nombre

@ 1. Génération aléatoire des décors
pour i de 0 à nbLignes - 1
    pour j de 0 à nbColonnes - 1
        // Murs extérieurs de la carte
        si i = 0 ou i = nbLignes - 1 ou j = 0 ou j = nbColonnes - 1 alors
            grilleJeu ajoute 1
        sinon
            // Obstacles aléatoires
            tyAlea vaut hasard(0, 9)
            si tyAlea = 0 ou tyAlea = 1 alors
                grilleJeu ajoute 2 // Forêt
            sinon si tyAlea = 2 alors
                grilleJeu ajoute 3 // Eau
            sinon si tyAlea = 3 alors
                grilleJeu ajoute 1 // Mur
            sinon
                grilleJeu ajoute 0 // Herbe
            fin si
        fin si
    fin pour
fin pour

@ 2. CREUSEMENT DU CHEMIN GARANTI (Path Carving)
curX, curY, idxPath, dirAlea est un nombre
curX vaut joueurX
curY vaut joueurY

// On avance pas à pas du départ vers le trésor en dégageant de l'herbe (0)
tant que curX <> tresorX ou curY <> tresorY
    idxPath vaut curY * nbColonnes + curX
    grilleJeu[idxPath] vaut 0

    // Choix aléatoire de la direction (droite ou bas) vers la cible
    si curX < tresorX et curY < tresorY alors
        dirAlea vaut hasard(0, 1)
        si dirAlea = 0 alors
            curX vaut curX + 1
        sinon
            curY vaut curY + 1
        fin si
    sinon si curX < tresorX alors
        curX vaut curX + 1
    sinon
        curY vaut curY + 1
    fin si
fin tant que

// Placement du trésor à la fin du chemin garanti
idxTresor est un nombre
idxTresor vaut tresorY * nbColonnes + tresorX
grilleJeu[idxTresor] vaut 4

@ 3. Boucle principale de jeu
jeuActif est un booléen
jeuActif vaut vrai

actionClavier, valCase, px, py, nxtX, nxtY, idxCible est un nombre

tant que jeuActif = vrai
    effacer(maToile)
    remplir(maToile, #1a202c)

    @ Affichage du décor
    pour i de 0 à nbLignes - 1
        pour j de 0 à nbColonnes - 1
            idxActuel vaut i * nbColonnes + j
            valCase vaut grilleJeu[idxActuel]
            px vaut j * tailleTUILE
            py vaut i * tailleTUILE

            // 0 : HERBE (Fleuries)
            si valCase = 0 alors
                rectangle(maToile, px, py, tailleTUILE, tailleTUILE, #38a169)
                cercle(maToile, px + 16, py + 16, 2, #f6e05e)

                // 1 : MUR DE PIERRE
            sinon si valCase = 1 alors
                rectangle(maToile, px, py, tailleTUILE, tailleTUILE, #4a5568)
                rectangle(maToile, px + 2, py + 2, tailleTUILE - 4, tailleTUILE - 4, #718096)

                // 2 : ARBRE / FORÊT
            sinon si valCase = 2 alors
                rectangle(maToile, px, py, tailleTUILE, tailleTUILE, #2f855a)
                cercle(maToile, px + 16, py + 16, 11, #276749)
                cercle(maToile, px + 16, py + 16, 7, #48bb78)

                // 3 : EAU
            sinon si valCase = 3 alors
                rectangle(maToile, px, py, tailleTUILE, tailleTUILE, #3182ce)
                rectangle(maToile, px + 4, py + 10, 12, 3, #63b3ed)
                rectangle(maToile, px + 16, py + 20, 12, 3, #63b3ed)

                // 4 : COFFRE AU TRÉSOR
            sinon si valCase = 4 alors
                rectangle(maToile, px, py, tailleTUILE, tailleTUILE, #d69e2e)
                rectangle(maToile, px + 6, py + 8, 20, 16, #744210)
                rectangle(maToile, px + 8, py + 12, 16, 8, #ecc94b)
            fin si
        fin pour
    fin pour

    @ Affichage du Joueur
    px vaut joueurX * tailleTUILE
    py vaut joueurY * tailleTUILE
    cercle(maToile, px + 16, py + 16, 12, #e53e3e)
    cercle(maToile, px + 12, py + 12, 3, #ffffff)
    cercle(maToile, px + 20, py + 12, 3, #ffffff)
    cercle(maToile, px + 12, py + 12, 1, #000000)
    cercle(maToile, px + 20, py + 12, 1, #000000)

    affiche maToile

    @ Entrées clavier
    appuyer ["z", "s", "d", "q", "x"] dans actionClavier

    nxtX vaut joueurX
    nxtY vaut joueurY

    // Mouvements : 1=Z, 2=S, 3=D, 4=Q, 5=X
    si actionClavier = 1 alors
        nxtY vaut joueurY - 1
    sinon si actionClavier = 2 alors
        nxtY vaut joueurY + 1
    sinon si actionClavier = 3 alors
        nxtX vaut joueurX + 1
    sinon si actionClavier = 4 alors
        nxtX vaut joueurX - 1
    sinon si actionClavier = 5 alors
        jeuActif vaut faux
    fin si

    @ Déplacement et Collisions
    idxCible vaut nxtY * nbColonnes + nxtX

    // Autoriser le passage uniquement sur l'herbe (0) ou le trésor (4)
    si grilleJeu[idxCible] = 0 ou grilleJeu[idxCible] = 4 alors
        joueurX vaut nxtX
        joueurY vaut nxtY
    fin si

    // Victoire
    si joueurX = tresorX et joueurY = tresorY alors
        affiche "VICTOIRE ! Vous avez trouvé le trésor !"
        jeuActif vaut faux
    fin si
fin tant que

affiche "Fin de la partie."
Pas encore noté
EduCode 3.2 - Structure du programme
Nouveauté

EduCode 3.2 : Structure du programme

EduCode 3.2 - Le panneau : Structure du programme

Lire l'articleReplier l'article

Avec la version 3.2, le panneau Structure du programme évolue en profondeur pour devenir un véritable plan de navigation de votre code, digne des grands éditeurs tout en restant simple et lisible.

Une vue hiérarchique, enfin

Fini la liste plate où toutes les variables se mélangeaient. Les variables locales apparaissent désormais indentées sous leur fonction ou procédure, tandis que les variables globales restent au premier niveau. D'un coup d'œil, vous voyez qui appartient à quoi.

Toujours à jour, automatiquement

Le panneau se rafraîchit en direct pendant que vous tapez. Plus de numéros de ligne obsolètes ni d'éléments fantômes : la structure reflète votre code en permanence, sans avoir à fermer et rouvrir le panneau.

Il suit votre curseur

Déplacez-vous dans l'éditeur : l'élément correspondant se surligne automatiquement dans la structure, et le panneau défile pour le garder visible. Vous savez toujours où vous êtes, même dans un long programme.

Recherchez, trouvez, filtrez

Un nouveau champ de filtre en haut du panneau vous permet de retrouver instantanément une fonction, une procédure ou une variable par son nom — ou même par un nom de paramètre.

Les constantes rejoignent la structure

Les constantes sont désormais détectées, quelle que soit la syntaxe utilisée

Ces améliorations seront disponibles pour tout le monde dans EduCode v3.2.
Ouvrez la structure de votre programme, tapez quelques lignes… et regardez-la vivre. Bonne programmation !

Pas encore noté
Développer un moteur de jeu façon NES : un projet complet mêlant génération procédurale, physique et animation
Article

Développer un moteur de jeu façon NES

Un projet complet mêlant génération procédurale, physique et animation

Lire l'articleReplier l'article

Ce projet consiste à recréer un runner inspiré de l’esthétique et des mécaniques de Super Mario sur NES. L’objectif est de combiner plusieurs dimensions du développement de jeux : génération procédurale, gestion de la physique, interactions, animations image par image et rendu graphique pixel-art.

Structure du monde : grille, dimensions et éléments

Le jeu repose sur une carte composée de 14 colonnes et 8 lignes, chaque case mesurant 46 pixels.
Une ligne de sol fixe sert de référence pour la physique et le placement des obstacles.

La carte est représentée sous forme de tableau de chaînes de caractères, chaque caractère correspondant à un élément :

  • sol,
  • briques,
  • blocs mystère,
  • tuyaux,
  • pièces,
  • buissons,
  • ennemis.

Cette approche permet une lecture rapide et un rendu efficace.

Génération procédurale : un monde qui défile et se renouvelle

Chaque colonne est générée dynamiquement selon des probabilités :

  • tuyaux (15 %),
  • blocs mystère ou briques (25 %),
  • Goombas (15 %),
  • buissons décoratifs (20 %).

Le défilement horizontal remplace la colonne de gauche par une nouvelle colonne générée, ce qui crée un environnement infini.
À chaque défilement, la distance augmente et le score progresse automatiquement.

Physique et interactions : un moteur simple mais robuste

Le moteur physique gère :

  • la détection du sol,
  • les collisions avec les obstacles,
  • les sauts en arc sinusoidal,
  • l’écrasement des ennemis,
  • le ramassage des pièces,
  • les bonus issus des blocs mystère.

Les actions disponibles sont : avancer, sauter, reculer.
Chaque action déclenche une mise à jour de la position, des collisions et du score.

Le système de vies et l’état « estVivant » permettent de gérer la fin de partie.

Rendu graphique : un hommage au pixel-art NES

Le jeu utilise un ensemble de procédures dédiées au dessin :

  • Mario en pixel-art,
  • Goombas,
  • pièces,
  • briques,
  • blocs mystère,
  • tuyaux,
  • nuages,
  • décor du sol.

Chaque élément est dessiné à partir de rectangles, cercles et lignes, ce qui recrée fidèlement l’esthétique 8-bit.

Le décor est redessiné à chaque frame, puis Mario est ajouté à sa position courante.

Animations : marche et saut fluides

Les déplacements ne sont pas instantanés :

  • la marche est animée sur 6 frames,
  • le saut sur 8 frames avec une trajectoire en arc.

Chaque animation est construite en interpolant la position de départ et d’arrivée, puis en affichant une séquence d’images.

Ce système donne au jeu une sensation de fluidité malgré son style rétro.

Boucle principale : un runner jouable et cohérent

La boucle de jeu :

  • attend les actions du joueur,
  • applique la physique,
  • déclenche les animations,
  • met à jour le score,
  • vérifie les collisions,
  • gère les vies,
  • s’arrête lorsque Mario n’a plus de vie.

Le message d’état et les informations (vies, pièces, score) sont affichés en haut de l’écran, comme dans les jeux NES originaux.

Conclusion

Ce programme illustre la manière dont un jeu rétro peut servir de terrain d’expérimentation complet : génération procédurale, moteur physique, gestion des collisions, animations, rendu graphique et boucle de gameplay.
Un projet qui démontre qu’il est possible de recréer une expérience NES cohérente en combinant des techniques simples mais bien structurées.

Programme :

@ Configuration et Dimensions
COLS est un nombre
COLS vaut 14
ROWS est un nombre
ROWS vaut 8

CASE est un nombre
CASE vaut 46
margeH est un nombre
margeH vaut 50
LARG est un nombre
LARG vaut COLS * CASE
HAUT est un nombre
HAUT vaut ROWS * CASE + margeH

LIGNE_SOL est un nombre
LIGNE_SOL vaut 6

@ État du Jeu
carte est un tableau

posX est un nombre
posX vaut 3
posY est un nombre
posY vaut 5

distance est un nombre
distance vaut 0
pieces est un nombre
pieces vaut 0
score est un nombre
score vaut 0
sauts est un nombre
sauts vaut 0
vies est un nombre
vies vaut 3
estVivant est un booléen
estVivant vaut vrai

@ Accès et Outils Carte
fonction caseA(li, co)
    si li < 0 ou li >= ROWS ou co < 0 ou co >= COLS alors
        retourne "."
    fin si
    retourne copier(carte[li], co + 1, 1)
fin fonction

procédure poserCase(li, co, caract)
    si li >= 0 et li < ROWS et co >= 0 et co < COLS alors
        ancienne est un texte
        ancienne vaut carte[li]
        avant est un texte
        avant vaut copier(ancienne, 1, co)
        apres est un texte
        apres vaut copier(ancienne, co + 2, COLS - co - 1)
        carte[li] vaut avant + caract + apres
    fin si
fin procédure

fonction estSolide(li, co)
    si co < 0 ou co >= COLS alors
        retourne faux
    fin si
    si li >= ROWS alors
        retourne vrai
    fin si
    c est un texte
    c vaut caseA(li, co)
    si c = "#" ou c = "B" ou c = "?" ou c = "-" ou c = "T" ou c = "t" alors
        retourne vrai
    fin si
    retourne faux
fin fonction

@ Génération Procédurale du Monde
procédure genererColonne(co)
    li est un nombre
    pour li de 0 à ROWS - 1
        si li >= LIGNE_SOL alors
            appelle poserCase(li, co, "#")
        sinon
            appelle poserCase(li, co, ".")
        fin si
    fin pour

    si co > 5 alors
        tirage est un nombre
        tirage vaut hasard(1, 100)

        // Tuyau vert (15% de chance)
        si tirage <= 15 et caseA(5, co - 1) <> "T" et caseA(5, co - 1) <> "t" alors
            hauteurT est un nombre
            hauteurT vaut hasard(1, 2)
            si hauteurT = 1 alors
                appelle poserCase(5, co, "T")
            sinon
                appelle poserCase(5, co, "T")
                appelle poserCase(4, co, "t")
            fin si

            // Blocs Brique / Mystère (25% de chance)
        sinon si tirage <= 40 alors
            typeB est un nombre
            typeB vaut hasard(1, 100)
            si typeB <= 50 alors
                appelle poserCase(3, co, "?")
            sinon
                appelle poserCase(3, co, "B")
            fin si
            si hasard(1, 100) <= 40 alors
                appelle poserCase(2, co, "o")
            fin si

            // Ennemi Goomba (15% de chance)
        sinon si tirage <= 55 et caseA(5, co - 1) <> "G" alors
            appelle poserCase(5, co, "G")

            // Buisson de décor (20% de chance)
        sinon si tirage <= 75 alors
            appelle poserCase(5, co, "W")
        fin si
    fin si
fin procédure

procédure construireCarteInitiale()
    vide carte
    li est un nombre
    pour li de 0 à ROWS - 1
        ligneVide est un texte
        ligneVide vaut ""
        co est un nombre
        pour co de 0 à COLS - 1
            ligneVide vaut ligneVide + "."
        fin pour
        carte ajoute ligneVide
    fin pour

    pour co de 0 à COLS - 1
        appelle genererColonne(co)
    fin pour
fin procédure

procédure defiler()
    li est un nombre
    pour li de 0 à ROWS - 1
        ancienne est un texte
        ancienne vaut carte[li]
        nouvelleCase est un texte
        si li >= LIGNE_SOL alors
            nouvelleCase vaut "#"
        sinon
            nouvelleCase vaut "."
        fin si
        carte[li] vaut copier(ancienne, 2, COLS - 1) + nouvelleCase
    fin pour

    appelle genererColonne(COLS - 1)
    distance vaut distance + 1
    score vaut score + 10
fin procédure

@ Physique
procédure ajusterHauteurAtterrissage()
    h est un nombre
    h vaut 0
    trouve est un booléen
    trouve vaut faux

    tant que h < ROWS et non trouve
        si estSolide(h, posX) alors
            posY vaut h - 1
            trouve vaut vrai
        sinon
            h vaut h + 1
        fin si
    fin tant que

    si non trouve alors
        posY vaut LIGNE_SOL - 1
    fin si
fin procédure

@ Graphismes NES 8 - bit
procédure dessinerMario(g, cx, cy)
    rectangle(g, cx + 12, cy + 2, 22, 6, #e52521)
    rectangle(g, cx + 12, cy + 8, 28, 4, #e52521)
    rectangle(g, cx + 8, cy + 12, 12, 8, #6e3b12)
    rectangle(g, cx + 20, cy + 12, 16, 12, #f2a65a)
    rectangle(g, cx + 24, cy + 16, 16, 5, #6e3b12)
    rectangle(g, cx + 8, cy + 24, 28, 8, #e52521)
    rectangle(g, cx + 14, cy + 24, 16, 16, #0024d6)
    rectangle(g, cx + 10, cy + 32, 24, 8, #0024d6)
    rectangle(g, cx + 14, cy + 28, 3, 3, #ffd700)
    rectangle(g, cx + 27, cy + 28, 3, 3, #ffd700)
    rectangle(g, cx + 6, cy + 40, 12, 6, #6e3b12)
    rectangle(g, cx + 26, cy + 40, 12, 6, #6e3b12)
fin procédure

procédure dessinerGoomba(g, cx, cy)
    cercle(g, cx + 23, cy + 18, 16, #9c4a00)
    rectangle(g, cx + 7, cy + 18, 32, 16, #9c4a00)
    rectangle(g, cx + 12, cy + 12, 5, 10, #ffffff)
    rectangle(g, cx + 29, cy + 12, 5, 10, #ffffff)
    rectangle(g, cx + 14, cy + 15, 3, 7, #000000)
    rectangle(g, cx + 29, cy + 15, 3, 7, #000000)
    ligne(g, cx + 10, cy + 9, cx + 18, cy + 13, #000000, 2)
    ligne(g, cx + 36, cy + 9, cx + 28, cy + 13, #000000, 2)
    rectangle(g, cx + 4, cy + 32, 14, 10, #000000)
    rectangle(g, cx + 28, cy + 32, 14, 10, #000000)
fin procédure

procédure dessinerPiece(g, cx, cy)
    cercle(g, cx + 23, cy + 23, 12, #ffd700)
    cercle(g, cx + 23, cy + 23, 8, #fc9838)
    rectangle(g, cx + 21, cy + 17, 4, 12, #ffffff)
fin procédure

procédure dessinerBuisson(g, cx, cy)
    cercle(g, cx + 12, cy + 30, 12, #00a800)
    cercle(g, cx + 23, cy + 22, 15, #00a800)
    cercle(g, cx + 34, cy + 30, 12, #00a800)
    rectangle(g, cx + 4, cy + 30, 38, 14, #00a800)
fin procédure

procédure dessinerSol(g, cx, cy)
    rectangle(g, cx, cy, CASE, CASE, #d84818)
    ligne(g, cx, cy + 15, cx + CASE, cy + 15, #000000, 2)
    ligne(g, cx, cy + 31, cx + CASE, cy + 31, #000000, 2)
    ligne(g, cx + 22, cy, cx + 22, cy + 15, #000000, 2)
    ligne(g, cx + 11, cy + 15, cx + 11, cy + 31, #000000, 2)
    ligne(g, cx + 33, cy + 15, cx + 33, cy + 31, #000000, 2)
fin procédure

procédure dessinerTuyau(g, cx, cy, estHaut)
    si estHaut alors
        rectangle(g, cx - 2, cy, CASE + 4, 18, #000000)
        rectangle(g, cx, cy + 2, CASE, 14, #00a800)
        rectangle(g, cx + 4, cy + 2, 6, 14, #a0f020)
        rectangle(g, cx + 3, cy + 18, CASE - 6, CASE - 18, #000000)
        rectangle(g, cx + 5, cy + 18, CASE - 10, CASE - 18, #00a800)
        rectangle(g, cx + 8, cy + 18, 5, CASE - 18, #a0f020)
    sinon
        rectangle(g, cx + 3, cy, CASE - 6, CASE, #000000)
        rectangle(g, cx + 5, cy, CASE - 10, CASE, #00a800)
        rectangle(g, cx + 8, cy, 5, CASE, #a0f020)
    fin si
fin procédure

procédure dessinerBlocMystere(g, cx, cy, actif)
    si actif alors
        rectangle(g, cx, cy, CASE, CASE, #000000)
        rectangle(g, cx + 2, cy + 2, CASE - 4, CASE - 4, #fc9838)
        rectangle(g, cx + 5, cy + 5, CASE - 10, CASE - 10, #e5a823)
        label(g, cx + 16, cy + 32, "?", #000000,22)
    sinon
        rectangle(g, cx, cy, CASE, CASE, #000000)
        rectangle(g, cx + 2, cy + 2, CASE - 4, CASE - 4, #8b5a2b)
        rectangle(g, cx + 6, cy + 6, CASE - 12, CASE - 12, #6e3b12)
    fin si
fin procédure

procédure dessinerBrique(g, cx, cy)
    rectangle(g, cx, cy, CASE, CASE, #000000)
    rectangle(g, cx + 2, cy + 2, CASE - 4, CASE - 4, #b84418)
    ligne(g, cx + 2, cy + 15, cx + CASE - 2, cy + 15, #000000, 2)
    ligne(g, cx + 2, cy + 30, cx + CASE - 2, cy + 30, #000000, 2)
    ligne(g, cx + 22, cy + 2, cx + 22, cy + 15, #000000, 2)
    ligne(g, cx + 12, cy + 15, cx + 12, cy + 30, #000000, 2)
    ligne(g, cx + 32, cy + 15, cx + 32, cy + 30, #000000, 2)
fin procédure

procédure dessinerNuage(g, cx, cy)
    cercle(g, cx + 12, cy + 16, 12, #4a82e4)
    cercle(g, cx + 24, cy + 10, 15, #4a82e4)
    cercle(g, cx + 36, cy + 16, 12, #4a82e4)
    rectangle(g, cx + 10, cy + 16, 28, 10, #4a82e4)

    cercle(g, cx + 12, cy + 14, 12, #ffffff)
    cercle(g, cx + 24, cy + 8, 15, #ffffff)
    cercle(g, cx + 36, cy + 14, 12, #ffffff)
    rectangle(g, cx + 10, cy + 14, 28, 10, #ffffff)
fin procédure

procédure dessinerDecor(g)
    degrade(g, [#5c94fc, #87ceeb])

    appelle dessinerNuage(g, 40, 65)
    appelle dessinerNuage(g, 280, 75)
    appelle dessinerNuage(g, 500, 60)

    li est un nombre
    pour li de 0 à ROWS - 1
        co est un nombre
        pour co de 0 à COLS - 1
            cx est un nombre
            cx vaut co * CASE
            cy est un nombre
            cy vaut li * CASE + margeH
            c est un texte
            c vaut caseA(li, co)

            si c = "#" alors
                appelle dessinerSol(g, cx, cy)
            sinon si c = "B" alors
                appelle dessinerBrique(g, cx, cy)
            sinon si c = "?" alors
                appelle dessinerBlocMystere(g, cx, cy, vrai)
            sinon si c = "-" alors
                appelle dessinerBlocMystere(g, cx, cy, faux)
            sinon si c = "T" alors
                appelle dessinerTuyau(g, cx, cy, vrai)
            sinon si c = "t" alors
                appelle dessinerTuyau(g, cx, cy, faux)
            sinon si c = "G" alors
                appelle dessinerGoomba(g, cx, cy)
            sinon si c = "W" alors
                appelle dessinerBuisson(g, cx, cy)
            sinon si c = "o" alors
                appelle dessinerPiece(g, cx, cy)
            fin si
        fin pour
    fin pour
fin procédure

procédure dessinerStatut(g, message, coulMsg)
    rectangle(g, 0, 0, LARG, 42, #000000)
    label(g, 20, 26, "MARIO  x" + vies, #ffffff, 14)
    label(g, 160, 26, "PIÈCES : " + pieces, #ffd700, 14)
    label(g, 320, 26, "SCORE : " + score, #ffffff, 14)
    label(g, 500, 26, "WORLD 1-1", #ffffff, 14)

    rectangle(g, 0, HAUT - 28, LARG, 28, #000000)
    label(g, 20, HAUT - 10, message, coulMsg, 12)
    label(g, LARG - 280, HAUT - 10, "D : Avancer  Z : Sauter  Q : Reculer", #adb5bd, 11)
fin procédure

procédure dessinerScene(message, coulMsg)
    g est une toile
    dimension(g, LARG, HAUT)
    appelle dessinerDecor(g)

    jcx est un nombre
    jcx vaut posX * CASE
    jcy est un nombre
    jcy vaut posY * CASE + margeH
    appelle dessinerMario(g, jcx, jcy)

    appelle dessinerStatut(g, message, coulMsg)
    affiche g
fin procédure

@ Animations
procédure animerMarche(colD, ligD, message, coulMsg)
    frames est un tableau
    xDep est un nombre
    xDep vaut colD * CASE
    yDep est un nombre
    yDep vaut ligD * CASE + margeH
    xArr est un nombre
    xArr vaut posX * CASE
    yArr est un nombre
    yArr vaut posY * CASE + margeH

    etape est un nombre
    pour etape de 0 à 5
        gf est une toile
        dimension(gf, LARG, HAUT)
        appelle dessinerDecor(gf)

        jx est un nombre
        jx vaut xDep + (xArr - xDep) * etape / 5
        jy est un nombre
        jy vaut yDep + (yArr - yDep) * etape / 5

        appelle dessinerMario(gf, jx, jy)
        appelle dessinerStatut(gf, message, coulMsg)
        frames ajoute gf
    fin pour
    animation(frames, 30)
fin procédure

procédure animerSaut(colD, ligD, message, coulMsg)
    frames est un tableau
    xDep est un nombre
    xDep vaut colD * CASE
    yDep est un nombre
    yDep vaut ligD * CASE + margeH
    xArr est un nombre
    xArr vaut posX * CASE
    yArr est un nombre
    yArr vaut posY * CASE + margeH

    etape est un nombre
    pour etape de 0 à 8
        gf est une toile
        dimension(gf, LARG, HAUT)
        appelle dessinerDecor(gf)

        prog est un nombre
        prog vaut etape / 8

        jx est un nombre
        jx vaut xDep + (xArr - xDep) * prog

        hauteurBase est un nombre
        hauteurBase vaut yDep + (yArr - yDep) * prog
        arcSaut est un nombre
        arcSaut vaut sinus(prog * pi()) * 65

        appelle dessinerMario(gf, jx, hauteurBase - arcSaut)
        appelle dessinerStatut(gf, message, coulMsg)
        frames ajoute gf
    fin pour
    animation(frames, 25)
fin procédure

@ Interactions
procédure verifierRamassagePiece()
    si caseA(posY, posX) = "o" alors
        appelle poserCase(posY, posX, ".")
        pieces vaut pieces + 1
        score vaut score + 200
    fin si
fin procédure

procédure avancer()
    zoneDefil est un nombre
    zoneDefil vaut 6

    si non estSolide(posY, posX + 1) alors
        hauteurAvant est un nombre
        hauteurAvant vaut posY

        si posX >= zoneDefil alors
            appelle defiler()
        sinon
            posX vaut posX + 1
        fin si

        appelle ajusterHauteurAtterrissage()
        appelle verifierRamassagePiece()

        si caseA(posY, posX) = "G" alors
            si hauteurAvant < posY alors
                appelle poserCase(posY, posX, ".")
                score vaut score + 500
            sinon
                vies vaut vies - 1
                appelle poserCase(posY, posX, ".")
                si vies <= 0 alors
                    estVivant vaut faux
                fin si
            fin si
        fin si
    fin si
fin procédure

procédure sauter()
    zoneDefil est un nombre
    zoneDefil vaut 6
    colCible est un nombre
    colCible vaut posX + 1

    // Vérification des caisses '?' dans la colonne de destination
    liBloc est un nombre
    pour liBloc de 0 à ROWS - 1
        si caseA(liBloc, colCible) = "?" alors
            si posY > liBloc alors
                // Mario est EN DESSOUS : explosion de la caisse
                appelle poserCase(liBloc, colCible, ".")

                // Si une pièce se trouve au-dessus, elle tombe jusqu'au sol / obstacle
                si liBloc > 0 et caseA(liBloc - 1, colCible) = "o" alors
                    appelle poserCase(liBloc - 1, colCible, ".")

                    hChute est un nombre
                    hChute vaut liBloc + 1
                    tant que hChute < ROWS et non estSolide(hChute, colCible)
                        hChute vaut hChute + 1
                    fin tant que

                    appelle poserCase(hChute - 1, colCible, "o")
                fin si

                // Attribution du bonus
                tirageBonus est un nombre
                tirageBonus vaut hasard(1, 100)
                si tirageBonus <= 50 alors
                    pieces vaut pieces + 1
                    score vaut score + 200
                sinon si tirageBonus <= 80 alors
                    score vaut score + 1000
                sinon
                    vies vaut vies + 1
                fin si
            fin si
        fin si
    fin pour

    // Écrasement Goomba
    si caseA(posY, colCible) = "G" ou caseA(5, colCible) = "G" alors
        appelle poserCase(posY, colCible, ".")
        appelle poserCase(5, colCible, ".")
        score vaut score + 500
    fin si

    si posX >= zoneDefil alors
        appelle defiler()
    sinon
        posX vaut posX + 1
    fin si

    sauts vaut sauts + 1
    appelle ajusterHauteurAtterrissage()
    appelle verifierRamassagePiece()
fin procédure

procédure reculer()
    si posX > 0 et non estSolide(posY, posX - 1) alors
        hauteurAvant est un nombre
        hauteurAvant vaut posY

        posX vaut posX - 1
        appelle ajusterHauteurAtterrissage()
        appelle verifierRamassagePiece()

        si caseA(posY, posX) = "G" alors
            si hauteurAvant < posY alors
                appelle poserCase(posY, posX, ".")
                score vaut score + 500
            sinon
                vies vaut vies - 1
                appelle poserCase(posY, posX, ".")
                si vies <= 0 alors
                    estVivant vaut faux
                fin si
            fin si
        fin si
    fin si
fin procédure

@ Boucle Principale
appelle construireCarteInitiale()
appelle ajusterHauteurAtterrissage()
appelle dessinerScene("Appuie sur D pour avancer et Z pour sauter !", #ffffff)

continuer est un booléen
continuer vaut vrai

tant que continuer et estVivant
    action est un nombre
    action vaut 1
    appuyer ["d", "z", "q", "a"] dans action

    colAvant est un nombre
    colAvant vaut posX
    ligAvant est un nombre
    ligAvant vaut posY

    si action = 1 alors
        appelle avancer()
        si vies > 0 alors
            appelle animerMarche(colAvant, ligAvant, "En route !", #ffffff)
        fin si
    sinon si action = 2 alors
        appelle sauter()
        si vies > 0 alors
            appelle animerSaut(colAvant, ligAvant, "Saut !", #ffd700)
        fin si
    sinon si action = 3 alors
        appelle reculer()
        si vies > 0 alors
            appelle animerMarche(colAvant, ligAvant, "Recul...", #ffffff)
        fin si
    sinon
        continuer vaut faux
    fin si
fin tant que

affiche 'GAME OVER'
Pas encore noté
Développer un système de combat dynamique : retour sur la création de Dungeon Arena
Article

Dungeon Arena : concevoir un système de combat interactif

Développer un système de combat dynamique : retour sur la création de Dungeon Arena

Lire l'articleReplier l'article

Ce projet explore la conception d’un système de combat interactif, entièrement codé, intégrant une interface graphique, une boucle de jeu, des mécaniques de progression et une génération procédurale de monstres.
Une manière concrète de transformer du code en une expérience visuelle et ludique.

Une interface pensée comme un mini-jeu complet

Le programme initialise une toile graphique :

  • « dimension(graph, 600, 500) »
  • « remplir(graph, #0b0f19) »

Puis construit une interface composée de :

  • un HUD supérieur (titre, étage, or),
  • une zone de combat avec barre de vie du monstre,
  • un portrait animé du monstre (formes, couleurs, cornes, yeux),
  • une fiche du joueur (PV, Mana, Attaque, Bouclier),
  • un journal d’action,
  • quatre boutons interactifs permettant d’agir à chaque tour.

L’ensemble donne une vraie impression de jeu : clair, lisible, cohérent.

Un système de combat au tour par tour

Le cœur du programme repose sur une boucle infinie qui :

  • dessine l’interface,
  • attend un clic,
  • exécute l’action choisie,
  • met à jour les PV, Mana, Bouclier,
  • gère la riposte du monstre,
  • teste la victoire ou la défaite.

Les actions disponibles :

Attaque à l’épée : dégâts physiques + régénération de Mana

Boule de feu : attaque magique à fort impact

Soin : récupération de PV + bouclier

Forge : amélioration permanente des stats

Chaque choix influence directement le tour suivant, créant une vraie dynamique stratégique.

Une génération procédurale des monstres

Le programme inclut une procédure dédiée :

  • nouveauMonstre()

Elle génère automatiquement :

  • le niveau du monstre,
  • ses PV,
  • son attaque,
  • son type (Gobelin, Orc, Golem),
  • sa couleur,
  • son nom,
  • et même des boss tous les 5 étages.

Les boss bénéficient d’un boost de statistiques :

  • PV × 1.5
  • Attaque × 1.3
  • Couleur spécifique (#ef4444)

Cette logique crée une progression naturelle, avec des combats de plus en plus difficiles.

Une mécanique de riposte intelligente

Après chaque action du joueur, si le monstre est encore vivant :

  • il attaque,
  • le bouclier absorbe une partie des dégâts,
  • le journal affiche les événements du tour.

Le système gère même les cas où le bouclier absorbe tout ou seulement une partie des dégâts.

Victoire, défaite et boucle de progression

Le jeu propose trois états :

  • Combat (0)
  • Victoire d’étage (1)
  • Défaite (2)

Chaque état modifie l’interface et les actions disponibles :

  • Victoire ? bouton “Monstre suivant”
  • Défaite ? bouton “Recommencer”
  • Combat ? quatre actions tactiques

La progression est fluide, intuitive et entièrement gérée par le code.

Ce que ce projet démontre

  • La capacité du code à créer une expérience interactive complète.
  • L’intérêt de combiner graphisme, logique de jeu, événements utilisateur et progression procédurale.
  • La puissance d’une boucle de jeu bien structurée.
  • La richesse pédagogique d’un projet qui mélange interface, gameplay et algorithmique.

Programme :

@ Initialisation de la canvas
graph est une toile
dimension(graph, 600, 500)

// Statistiques du Joueur
jHPMax est un nombre
jHPMax vaut 100

jHP est un nombre
jHP vaut 100

jManaMax est un nombre
jManaMax vaut 50

jMana est un nombre
jMana vaut 50

jAttaque est un nombre
jAttaque vaut 18

jBouclier est un nombre
jBouclier vaut 0

jOr est un nombre
jOr vaut 0

// Statistiques du Monstre
mNiveau est un nombre
mNiveau vaut 1

mHPMax est un nombre
mHPMax vaut 60

mHP est un nombre
mHP vaut 60

mAttaque est un nombre
mAttaque vaut 7

mNom est un texte
mNom vaut "Gobelin Vert"

mType est un nombre
mType vaut 1

mCouleur est un texte
mCouleur vaut #22c55e

// État du jeu (0 = Combat, 1 = Victoire Étage, 2 = Défaite)
etatJeu est un nombre
etatJeu vaut 0

logTxt est un texte
logTxt vaut "À toi de jouer ! Clique sur un bouton en bas."

clicX est un nombre
clicY est un nombre

@ Génération du monstre suivant
procédure nouveauMonstre()
    mNiveau vaut mNiveau + 1
    mHPMax vaut 40 + (mNiveau * 22)
    mHP vaut mHPMax
    mAttaque vaut 5 + (mNiveau * 3)

    mType vaut hasard(1, 3)

    si mType = 1 alors
        mNom vaut "Gobelin Nv." + convertir_texte(mNiveau)
        mCouleur vaut #22c55e
    sinon si mType = 2 alors
        mNom vaut "Orc Guerrier Nv." + convertir_texte(mNiveau)
        mCouleur vaut #a855f7
    sinon
        mNom vaut "Golem de Pierre Nv." + convertir_texte(mNiveau)
        mCouleur vaut #64748b
    fin si

    // Boss tous les 5 étages
    si mNiveau = 5 ou mNiveau = 10 ou mNiveau = 15 alors
        mType vaut 4
        mNom vaut "BOSS Dragon Infernal Nv." + convertir_texte(mNiveau)
        mCouleur vaut #ef4444
        mHPMax vaut mHPMax * 1.5
        mHP vaut mHPMax
        mAttaque vaut mAttaque * 1.3
    fin si
fin procédure

@ Boucle principale
tant que vrai
    effacer(graph)
    remplir(graph, #0b0f19)

    // --- 1. HUD SUPÉRIEUR ---
    rectangle(graph, 0, 0, 600, 40, #0f172a)
    label(graph, 15, 25, "DUNGEON ARENA", #ffffff, 13)
    label(graph, 260, 25, "Étage : " + convertir_texte(mNiveau), #38bdf8, 12)
    label(graph, 450, 25, "Or : " + convertir_texte(jOr) + " G", #facc15, 12)

    // --- 2. ARENE DE COMBAT (MONSTRE VISIBLE) ---
    rectangle_arrondi(graph, 20, 50, 560, 180, 8, #1e293b)
    contour(graph, #334155, 2)

    // Nom et PV Monstre
    label(graph, 35, 75, mNom, #ffffff, 14)

    mHPNet est un nombre
    mHPNet vaut maximum(0, mHP)
    ratioHPm est un nombre
    ratioHPm vaut mHPNet / mHPMax

    rectangle_arrondi(graph, 250, 60, 310, 20, 4, #0f172a)
    rectangle_arrondi(graph, 250, 60, ratioHPm * 310, 20, 4, #ef4444)
    txtHPm est un texte
    txtHPm vaut convertir_texte(arrondi(mHPNet)) + " / " + convertir_texte(arrondi(mHPMax)) + " PV"
    label(graph, 350, 75, txtHPm, #ffffff, 10)

    // DESSIN DU MONSTRE EN GRAND AU CENTRE
    ellipse(graph, 300, 200, 60, 14, rgba(0, 0, 0, 0.4))
    rectangle_arrondi(graph, 265, 130, 70, 65, 12, mCouleur)
    cercle(graph, 300, 115, 32, mCouleur)

    // Yeux
    cercle(graph, 288, 110, 9, #ffffff)
    cercle(graph, 312, 110, 9, #ffffff)
    cercle(graph, 288, 110, 4, #dc2626)
    cercle(graph, 312, 110, 4, #dc2626)

    // Bouche
    rectangle(graph, 286, 128, 28, 8, #0f172a)
    ptsDent1 est un tableau
    ptsDent1 ajoute 290
    ptsDent1 ajoute 128
    ptsDent1 ajoute 294
    ptsDent1 ajoute 134
    ptsDent1 ajoute 298
    ptsDent1 ajoute 128
    polygone(graph, ptsDent1, #ffffff)

    ptsDent2 est un tableau
    ptsDent2 ajoute 302
    ptsDent2 ajoute 128
    ptsDent2 ajoute 306
    ptsDent2 ajoute 134
    ptsDent2 ajoute 310
    ptsDent2 ajoute 128
    polygone(graph, ptsDent2, #ffffff)

    // Oreilles / Cornes selon le type
    si mType = 1 alors
        ptsOreilleG est un tableau
        ptsOreilleG ajoute 270
        ptsOreilleG ajoute 110
        ptsOreilleG ajoute 240
        ptsOreilleG ajoute 95
        ptsOreilleG ajoute 265
        ptsOreilleG ajoute 125
        polygone(graph, ptsOreilleG, mCouleur)

        ptsOreilleD est un tableau
        ptsOreilleD ajoute 330
        ptsOreilleD ajoute 110
        ptsOreilleD ajoute 360
        ptsOreilleD ajoute 95
        ptsOreilleD ajoute 335
        ptsOreilleD ajoute 125
        polygone(graph, ptsOreilleD, mCouleur)
    sinon si mType = 4 alors
        ptsCorneG est un tableau
        ptsCorneG ajoute 280
        ptsCorneG ajoute 90
        ptsCorneG ajoute 265
        ptsCorneG ajoute 65
        ptsCorneG ajoute 290
        ptsCorneG ajoute 85
        polygone(graph, ptsCorneG, #991b1b)

        ptsCorneD est un tableau
        ptsCorneD ajoute 320
        ptsCorneD ajoute 90
        ptsCorneD ajoute 335
        ptsCorneD ajoute 65
        ptsCorneD ajoute 310
        ptsCorneD ajoute 85
        polygone(graph, ptsCorneD, #991b1b)
    fin si

    rectangle_arrondi(graph, 248, 140, 18, 40, 6, mCouleur)
    rectangle_arrondi(graph, 334, 140, 18, 40, 6, mCouleur)

    // --- 3. INFOS DU JOUEUR ---
    rectangle_arrondi(graph, 20, 240, 560, 75, 8, #0f172a)
    contour(graph, #38bdf8, 1)

    label(graph, 35, 262, "VOTRE HÉROS", #ffffff, 12)

    // PV Joueur
    jHPNet est un nombre
    jHPNet vaut maximum(0, jHP)
    ratioHPj est un nombre
    ratioHPj vaut jHPNet / jHPMax

    label(graph, 35, 283, "PV", #4ade80, 10)
    rectangle_arrondi(graph, 65, 272, 180, 14, 3, #1e293b)
    rectangle_arrondi(graph, 65, 272, ratioHPj * 180, 14, 3, #22c55e)
    label(graph, 115, 283, convertir_texte(arrondi(jHPNet)) + "/" + convertir_texte(jHPMax), #ffffff, 9)

    // Mana Joueur
    ratioMPj est un nombre
    ratioMPj vaut jMana / jManaMax

    label(graph, 35, 303, "MP", #38bdf8, 10)
    rectangle_arrondi(graph, 65, 292, 180, 14, 3, #1e293b)
    rectangle_arrondi(graph, 65, 292, ratioMPj * 180, 14, 3, #38bdf8)
    label(graph, 115, 303, convertir_texte(arrondi(jMana)) + "/" + convertir_texte(jManaMax), #ffffff, 9)

    // Stats
    label(graph, 275, 283, "Attaque : " + convertir_texte(jAttaque), #fbbf24, 11)
    label(graph, 275, 303, "Bouclier : " + convertir_texte(jBouclier), #38bdf8, 11)

    // --- 4. JOURNAL ---
    rectangle_arrondi(graph, 20, 322, 560, 32, 6, #1e293b)
    contour(graph, #475569, 1)
    label(graph, 30, 343, logTxt, #f1f5f9, 10)

    // --- 5. BOUTONS D'ACTION ---
    si etatJeu = 0 alors
        rectangle_arrondi(graph, 20, 362, 270, 56, 6, #dc2626)
        label(graph, 35, 387, "1. ATTAQUER À L'ÉPÉE", #ffffff, 12)
        label(graph, 35, 405, "Gratuit | Regen +8 MP", #fca5a5, 9)

        rectangle_arrondi(graph, 310, 362, 270, 56, 6, #d97706)
        label(graph, 325, 387, "2. BOULE DE FEU", #ffffff, 12)
        label(graph, 325, 405, "Coût: 20 MP | Gros dégâts", #fde68a, 9)

        rectangle_arrondi(graph, 20, 426, 270, 56, 6, #16a34a)
        label(graph, 35, 451, "3. SE SOIGNER", #ffffff, 12)
        label(graph, 35, 469, "Coût: 15 MP | +30 PV & Shield", #86efac, 9)

        rectangle_arrondi(graph, 310, 426, 270, 56, 6, #7c3aed)
        label(graph, 325, 451, "4. FORGE (Amélioration)", #ffffff, 12)
        label(graph, 325, 469, "Coût: 35 Or | +7 Atk, +20 PV", #c4b5fd, 9)

    sinon si etatJeu = 1 alors
        rectangle_arrondi(graph, 150, 375, 300, 65, 8, #16a34a)
        label(graph, 212, 403, "MONSTRE SUIVANT ->", #ffffff, 15)
        label(graph, 198, 424, "Clique ici pour continuer le donjon", #dcfce7, 10)

    sinon si etatJeu = 2 alors
        rectangle_arrondi(graph, 150, 375, 300, 65, 8, #dc2626)
        label(graph, 205, 412, "RECOMMENCER", #ffffff, 16)
        label(graph, 190, 430, "Tu as perdu ! Clique ici pour rejouer", #fee2e2, 10)
    fin si

    affiche graph

    // --- DÉTECTION DES CLICS ET TOURS ---
    tourJoue est un booléen
    tourJoue vaut faux

    cliquer graph dans clicX, clicY

    si etatJeu = 0 alors

        // CLIC SUR 1. ATTAQUE ÉPÉE
        si clique(clicX, clicY, 20, 362, 270, 56) alors
            degatsJ est un nombre
            degatsJ vaut jAttaque + hasard(-2, 3)
            mHP vaut mHP - degatsJ

            jMana vaut minimum(jManaMax, jMana + 8)
            logTxt vaut "Tu attaques à l'épée (-" + convertir_texte(degatsJ) + " PV au monstre) !"
            tourJoue vaut vrai

            // CLIC SUR 2. BOULE DE FEU
        sinon si clique(clicX, clicY, 310, 362, 270, 56) alors
            si jMana >= 20 alors
                jMana vaut jMana - 20
                degatsJ vaut (jAttaque * 2.2) + hasard(-1, 4)
                mHP vaut mHP - degatsJ
                logTxt vaut "Boule de feu magique ! (-" + convertir_texte(arrondi(degatsJ)) + " PV au monstre)"
                tourJoue vaut vrai
            sinon
                logTxt vaut "Pas assez de Mana ! (20 MP requis)"
            fin si

            // CLIC SUR 3. SE SOIGNER
        sinon si clique(clicX, clicY, 20, 426, 270, 56) alors
            si jMana >= 15 alors
                jMana vaut jMana - 15
                jHP vaut minimum(jHPMax, jHP + 30)
                jBouclier vaut jBouclier + 15
                logTxt vaut "Sort de soin lancé ! +30 PV et +15 Bouclier."
                tourJoue vaut vrai
            sinon
                logTxt vaut "Pas assez de Mana ! (15 MP requis)"
            fin si

            // CLIC SUR 4. FORGE
        sinon si clique(clicX, clicY, 310, 426, 270, 56) alors
            si jOr >= 35 alors
                jOr vaut jOr - 35
                jAttaque vaut jAttaque + 7
                jHPMax vaut jHPMax + 20
                jHP vaut jHP + 20
                logTxt vaut "Forge : Attaque +7 et PV Max +20 !"
            sinon
                logTxt vaut "Pas assez d'or ! (35 Or requis)"
            fin si
        fin si

        // TEST MORT DU MONSTRE
        si mHP <= 0 alors
            mHP vaut 0
            gainOr est un nombre
            gainOr vaut 15 + (mNiveau * 10)
            jOr vaut jOr + gainOr
            etatJeu vaut 1
            logTxt vaut "VICTOIRE ! Monstre terrassé (+ " + convertir_texte(gainOr) + " Or) !"
            tourJoue vaut faux
        fin si

        // RIPOSTE DU MONSTRE
        si tourJoue et mHP > 0 alors
            degatsM est un nombre
            degatsM vaut mAttaque + hasard(-2, 2)

            si jBouclier > 0 alors
                si jBouclier >= degatsM alors
                    jBouclier vaut jBouclier - degatsM
                    degatsM vaut 0
                sinon
                    degatsM vaut degatsM - jBouclier
                    jBouclier vaut 0
                fin si
            fin si

            jHP vaut jHP - degatsM
            logTxt vaut logTxt + " | Riposte : - " + convertir_texte(arrondi(degatsM)) + " PV"

            si jHP <= 0 alors
                jHP vaut 0
                etatJeu vaut 2
                logTxt vaut "DÉFAITE... Tu as perdu tous tes PV."
            fin si
        fin si

    sinon si etatJeu = 1 alors
        // CLIC MONSTRE SUIVANT
        si clique(clicX, clicY, 150, 375, 300, 65) alors
            appelle nouveauMonstre()
            etatJeu vaut 0
            jMana vaut jManaMax
            logTxt vaut "Nouveau combat engagé ! À toi de jouer."
        fin si

    sinon si etatJeu = 2 alors
        // CLIC RECOMMENCER
        si clique(clicX, clicY, 150, 375, 300, 65) alors
            jHPMax vaut 100
            jHP vaut 100
            jManaMax vaut 50
            jMana vaut 50
            jAttaque vaut 18
            jBouclier vaut 0
            jOr vaut 0
            mNiveau vaut 0
            appelle nouveauMonstre()
            etatJeu vaut 0
            logTxt vaut "Nouvelle partie commencée ! Bonne chance."
        fin si
    fin si
fin tant que
Pas encore noté