EduCode
EduCode
← Retour au site
Blog

Articles & Actualités

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

19 publications

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
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
Génération procédurale : création d’un labyrinthe circulaire avec un algorithme DFS
Article

Création d’un labyrinthe circulaire avec un algorithme DFS

Génération procédurale : création d’un labyrinthe circulaire avec un algorithme DFS

Lire l'articleReplier l'article

Ce projet explore la création d’un labyrinthe circulaire en combinant algorithmique, géométrie polaire et rendu graphique.
L’objectif : transformer une structure mathématique en une représentation visuelle cohérente et esthétique.

Structure du labyrinthe

Le labyrinthe repose sur une grille polaire composée :

  • d’anneaux (r),
  • de secteurs angulaires (s),
  • de murs circulaires (arcs),
  • de murs radiaux (lignes).

Chaque cellule (r, s) possède deux types de murs :

  • murArc pour les arcs internes/externes,
  • murRadial pour les séparations angulaires.

Génération : un DFS avec backtracking

L’algorithme utilise une exploration en profondeur (DFS) :

  • Départ au centre.
  • Sélection aléatoire d’un voisin non visité.
  • Suppression du mur correspondant.
  • Retour arrière lorsqu’aucune option n’est disponible.

Ce processus produit un labyrinthe parfait, sans cycles et entièrement connecté.
Le chemin solution est capturé automatiquement au moment où l’algorithme atteint la cellule de sortie.

Rendu graphique : arcs et radiales

Le tracé repose sur une procédure dédiée :

  • dessinerArc(), qui subdivise un arc en segments pour obtenir une courbe fluide.

Les murs sont ensuite dessinés selon leur type, et la solution est mise en valeur par une couleur spécifique (#F59E0B).
Le résultat : un labyrinthe circulaire lisible, harmonieux et entièrement généré par le code.

Ce que ce projet met en lumière

La richesse des représentations polaires dans la génération procédurale.

La capacité d’un DFS à produire des structures visuelles complexes.

L’intérêt de combiner mathématiques, algorithmique et graphisme pour créer des outils pédagogiques ou interactifs.

Programme :

@ procédure de dessin d'un arc filaire
procédure dessinerArc(laToile, x, y, r, angleDeb, angleFin, coul, ep)
    pasAngle est un nombre
    pasAngle vaut (angleFin - angleDeb) / 8

    k est un nombre
    pour k de 0 à 7
        aA est un nombre
        aB est un nombre
        aA vaut radians(angleDeb + k * pasAngle)
        aB vaut radians(angleDeb + (k + 1) * pasAngle)

        x1 est un nombre
        y1 est un nombre
        x2 est un nombre
        y2 est un nombre

        x1 vaut x + r * cosinus(aA)
        y1 vaut y + r * sinus(aA)
        x2 vaut x + r * cosinus(aB)
        y2 vaut y + r * sinus(aB)

        ligne(laToile, x1, y1, x2, y2, coul, ep)
    fin pour
fin procédure

@ Configuration et toile
maToile est une toile
dimension(maToile, 400, 400)
remplir(maToile, #0F172A)

cx est un nombre
cy est un nombre
rMin est un nombre
largeurAnneau est un nombre
nbAnneaux est un nombre
nbSecteurs est un nombre

cx vaut 200
cy vaut 200
rMin vaut 25
largeurAnneau vaut 28
nbAnneaux vaut 5
nbSecteurs vaut 12

nbTotal est un nombre
nbTotal vaut nbAnneaux * nbSecteurs

// Définition de la sortie
sExit est un nombre
sExit vaut hasard(0, nbSecteurs - 1)
targetIdx est un nombre
targetIdx vaut (nbAnneaux - 1) * nbSecteurs + sExit

@ Initialisation des Structures
estVisite est un tableau
murArc est un tableau
murRadial est un tableau

i est un nombre
pour i de 0 à nbTotal - 1
    estVisite ajoute 0
    murArc ajoute 1
    murRadial ajoute 1
fin pour

pileR est un tableau
pileS est un tableau
solR est un tableau
solS est un tableau
solTrouvee est un nombre
solTrouvee vaut 0

// Départ au centre (0, 0)
pileR ajoute 0
pileS ajoute 0
estVisite[0] vaut 1

nbVisites est un nombre
nbVisites vaut 1

@ Algorithme de Génération (DFS) et Capture de la Solution
tant que nbVisites < nbTotal
    idxPile est un nombre
    idxPile vaut longueur(pileR) - 1

    currR est un nombre
    currS est un nombre
    currR vaut pileR[idxPile]
    currS vaut pileS[idxPile]

    voisinsR est un tableau
    voisinsS est un tableau
    voisinsType est un tableau

    // Voisin extérieur (r + 1)
    si currR < nbAnneaux - 1 alors
        idxExt est un nombre
        idxExt vaut (currR + 1) * nbSecteurs + currS
        si estVisite[idxExt] = 0 alors
            voisinsR ajoute (currR + 1)
            voisinsS ajoute currS
            voisinsType ajoute 1
        fin si
    fin si

    // Voisin intérieur (r - 1)
    si currR > 0 alors
        idxInt est un nombre
        idxInt vaut (currR - 1) * nbSecteurs + currS
        si estVisite[idxInt] = 0 alors
            voisinsR ajoute (currR - 1)
            voisinsS ajoute currS
            voisinsType ajoute 2
        fin si
    fin si

    // Voisin horaire (s + 1)
    sHor est un nombre
    sHor vaut (currS + 1) mod nbSecteurs
    idxHor est un nombre
    idxHor vaut currR * nbSecteurs + sHor
    si estVisite[idxHor] = 0 alors
        voisinsR ajoute currR
        voisinsS ajoute sHor
        voisinsType ajoute 3
    fin si

    // Voisin anti-horaire (s - 1)
    sAnti est un nombre
    sAnti vaut (currS - 1 + nbSecteurs) mod nbSecteurs
    idxAnti est un nombre
    idxAnti vaut currR * nbSecteurs + sAnti
    si estVisite[idxAnti] = 0 alors
        voisinsR ajoute currR
        voisinsS ajoute sAnti
        voisinsType ajoute 4
    fin si

    nbVoisins est un nombre
    nbVoisins vaut longueur(voisinsR)

    si nbVoisins > 0 alors
        choix est un nombre
        choix vaut hasard(0, nbVoisins - 1)

        prochainR est un nombre
        prochainS est un nombre
        typeMouv est un nombre

        prochainR vaut voisinsR[choix]
        prochainS vaut voisinsS[choix]
        typeMouv vaut voisinsType[choix]

        idxCourant est un nombre
        idxCourant vaut currR * nbSecteurs + currS

        si typeMouv = 1 alors
            murArc[idxCourant] vaut 0
        sinon si typeMouv = 2 alors
            idxVoisest est un nombre
            idxVoisest vaut prochainR * nbSecteurs + prochainS
            murArc[idxVoisest] vaut 0
        sinon si typeMouv = 3 alors
            murRadial[idxCourant] vaut 0
        sinon si typeMouv = 4 alors
            idxVoisest est un nombre
            idxVoisest vaut currR * nbSecteurs + prochainS
            murRadial[idxVoisest] vaut 0
        fin si

        idxSuivant est un nombre
        idxSuivant vaut prochainR * nbSecteurs + prochainS
        estVisite[idxSuivant] vaut 1
        nbVisites ajoute 1

        pileR ajoute prochainR
        pileS ajoute prochainS

        // Capture du chemin de la solution lors de la découverte
        si idxSuivant = targetIdx et solTrouvee = 0 alors
            kp est un nombre
            pour kp de 0 à longueur(pileR) - 1
                solR ajoute pileR[kp]
                solS ajoute pileS[kp]
            fin pour
            solTrouvee vaut 1
        fin si
    sinon
        idxDernier est un nombre
        idxDernier vaut longueur(pileR) - 1
        pileR supprime idxDernier
        pileS supprime idxDernier
    fin si
fin tant que

// Ouverture du mur de sortie
murArc[targetIdx] vaut 0

@ Dessin des Murs du Labyrinthe
appelle dessinerArc(maToile, cx, cy, rMin, 0, 360, #38BDF8, 2)

an est un nombre
sec est un nombre
pour an de 0 à nbAnneaux - 1
    pour sec de 0 à nbSecteurs - 1
        idxCell est un nombre
        idxCell vaut an * nbSecteurs + sec

        a1deg est un nombre
        a2deg est un nombre
        a1deg vaut (sec * 360) / nbSecteurs
        a2deg vaut ((sec + 1) * 360) / nbSecteurs

        rayonInt est un nombre
        rayonExt est un nombre
        rayonInt vaut rMin + an * largeurAnneau
        rayonExt vaut rayonInt + largeurAnneau

        // Mur arc extérieur
        si murArc[idxCell] = 1 alors
            appelle dessinerArc(maToile, cx, cy, rayonExt, a1deg, a2deg, #38BDF8, 2)
        fin si

        // Mur radial horaire
        si murRadial[idxCell] = 1 alors
            radAngle est un nombre
            radAngle vaut radians(a2deg)

            xStart est un nombre
            yStart est un nombre
            xEnd est un nombre
            yEnd est un nombre

            xStart vaut cx + rayonInt * cosinus(radAngle)
            yStart vaut cy + rayonInt * sinus(radAngle)
            xEnd vaut cx + rayonExt * cosinus(radAngle)
            yEnd vaut cy + rayonExt * sinus(radAngle)

            ligne(maToile, xStart, yStart, xEnd, yEnd, #38BDF8, 2)
        fin si
    fin pour
fin pour

@ Tracé de la Solution (Lignes droites en radial, arcs en circulaires)
nbSol est un nombre
nbSol vaut longueur(solR)

si nbSol > 0 alors
    ks est un nombre
    pour ks de 0 à nbSol - 2
        r1 est un nombre
        s1 est un nombre
        r2 est un nombre
        s2 est un nombre

        r1 vaut solR[ks]
        s1 vaut solS[ks]
        r2 vaut solR[ks + 1]
        s2 vaut solS[ks + 1]

        si r1 = r2 alors
            // Déplacement le long d'un même anneau -> Tracé d'un arc curviligne
            radCell est un nombre
            radCell vaut rMin + (r1 + 0.5) * largeurAnneau

            a1deg est un nombre
            a1deg vaut (s1 + 0.5) * 360 / nbSecteurs

            pasSec est un nombre
            pasSec vaut 360 / nbSecteurs

            a2deg est un nombre
            si (s1 + 1) mod nbSecteurs = s2 alors
                a2deg vaut a1deg + pasSec
            sinon
                a2deg vaut a1deg - pasSec
            fin si

            appelle dessinerArc(maToile, cx, cy, radCell, a1deg, a2deg, #F59E0B, 3)
        sinon
            // Déplacement entre deux anneaux -> Tracé d'une ligne radiale
            angRad est un nombre
            angRad vaut radians((s1 + 0.5) * 360 / nbSecteurs)

            rad1 est un nombre
            rad2 est un nombre
            rad1 vaut rMin + (r1 + 0.5) * largeurAnneau
            rad2 vaut rMin + (r2 + 0.5) * largeurAnneau

            x1 est un nombre
            y1 est un nombre
            x2 est un nombre
            y2 est un nombre

            x1 vaut cx + rad1 * cosinus(angRad)
            y1 vaut cy + rad1 * sinus(angRad)
            x2 vaut cx + rad2 * cosinus(angRad)
            y2 vaut cy + rad2 * sinus(angRad)

            ligne(maToile, x1, y1, x2, y2, #F59E0B, 3)
        fin si
    fin pour

    // Extension finale vers l'extérieur du labyrinthe
    rDernier est un nombre
    sDernier est un nombre
    rDernier vaut solR[nbSol - 1]
    sDernier vaut solS[nbSol - 1]

    angFin est un nombre
    angFin vaut radians((sDernier + 0.5) * 360 / nbSecteurs)

    radCenterCell est un nombre
    radOut est un nombre
    radCenterCell vaut rMin + (rDernier + 0.5) * largeurAnneau
    radOut vaut rMin + nbAnneaux * largeurAnneau + 12

    xStartSol est un nombre
    yStartSol est un nombre
    xEndSol est un nombre
    yEndSol est un nombre

    xStartSol vaut cx + radCenterCell * cosinus(angFin)
    yStartSol vaut cy + radCenterCell * sinus(angFin)
    xEndSol vaut cx + radOut * cosinus(angFin)
    yEndSol vaut cy + radOut * sinus(angFin)

    ligne(maToile, xStartSol, yStartSol, xEndSol, yEndSol, #F59E0B, 3)
fin si

affiche maToile
Générer un Sudoku complet : un projet mêlant algorithmique, logique et rendu graphique
Article

Générer un Sudoku complet

Un projet mêlant algorithmique, logique et rendu graphique

Lire l'articleReplier l'article

Ce programme propose une approche complète pour créer un Sudoku jouable : génération d’une grille valide, création d’un puzzle avec cases manquantes, et rendu visuel sur une toile.
Il combine algorithmique, backtracking, manipulation de tableaux et dessin d’interface.

Construction de la grille : une structure 9×9 prête à être remplie

La première étape consiste à initialiser une grille vide de 9 lignes et 9 colonnes, chaque case contenant la valeur 0.
Cette structure sert de base à l’algorithme de résolution.

Vérification des contraintes : respecter les règles du Sudoku

La fonction de validation contrôle trois éléments essentiels :

  • la ligne,
  • la colonne,
  • le bloc 3×3.

Elle garantit qu’un chiffre proposé ne viole aucune règle du Sudoku.
Cette étape est indispensable pour permettre un remplissage correct de la grille.

Génération de la solution : un backtracking complet

L’algorithme de backtracking remplit la grille en cherchant la prochaine case vide, puis en essayant les chiffres de 1 à 9 dans un ordre aléatoire.

Pour chaque tentative :

  • si le chiffre est valide, il est placé,
  • l’algorithme continue,
  • en cas d’impasse, il revient en arrière.

Ce processus se poursuit jusqu’à ce que la grille soit entièrement remplie.
Le résultat est une solution complète et cohérente.

Création du puzzle : retirer des valeurs pour former l’énigme

Une fois la grille solutionnée, le programme retire un nombre défini de cases (ici 45).
Chaque retrait est aléatoire, ce qui permet de générer des puzzles variés.
Le niveau de difficulté dépend du nombre de cases supprimées.

Rendu graphique : dessiner un Sudoku lisible et structuré Le programme utilise une toile de 450×450 pixels pour afficher la grille.

Le rendu inclut :

  • les chiffres présents,
  • les lignes de séparation,
  • des traits plus épais pour les blocs 3×3,
  • une mise en page claire et minimaliste.

Chaque case est représentée par un espace de 50×50 pixels, ce qui assure une lecture confortable.

Exécution finale : un Sudoku généré et affiché automatiquement

Le programme :

  • génère une solution complète,
  • crée un puzzle en retirant des valeurs,
  • dessine la grille sur une toile,
  • affiche le résultat.

Il constitue une base solide pour aller plus loin : ajout d’interactions, vérification des réponses, interface utilisateur, ou export du puzzle.

Conclusion

Ce projet démontre comment un Sudoku peut être généré de manière algorithmique grâce au backtracking, puis rendu visuellement via une interface graphique.
Il combine logique, structure de données, aléatoire contrôlé et dessin, offrant un excellent exercice pour progresser en algorithmique et en développement d’outils interactifs.

Programme :

?@ Initialisation

// Déclaration de la grille de Sudoku 9x9
grille_sudoku est un tableau
pour lig de 0 à 8
    ligne_g est un tableau
    pour col de 0 à 8
        ligne_g ajoute 0
    fin pour
    grille_sudoku ajoute ligne_g
fin pour

@ Fonctions de résolution (Backtracking)

// Vérifie si placer 'val' aux coordonnées (l, c) est autorisé par les règles
fonction estValide(g, l, c, val)
    // 1. Vérification de la ligne
    pour i de 0 à 8
        si g[l][i] = val alors
            retourne faux
        fin si
    fin pour

    // 2. Vérification de la colonne
    pour i de 0 à 8
        si g[i][c] = val alors
            retourne faux
        fin si
    fin pour

    // 3. Vérification du bloc 3x3
    debutL est un nombre
    debutL vaut arrondi_inferieur(l / 3) * 3
    debutC est un nombre
    debutC vaut arrondi_inferieur(c / 3) * 3

    pour i de 0 à 2
        pour j de 0 à 2
            si g[debutL + i][debutC + j] = val alors
                retourne faux
            fin si
        fin pour
    fin pour

    retourne vrai
fin fonction

// Algorithme de Backtracking pour remplir la grille
fonction generer(g)
    l est un nombre
    c est un nombre
    trouve est un booléen
    trouve vaut faux

    // Trouver la prochaine case vide (valeur 0)
    pour i de 0 à 8
        pour j de 0 à 8
            si g[i][j] = 0 alors
                l vaut i
                c vaut j
                trouve vaut vrai
                arrête
            fin si
        fin pour
        si trouve = vrai alors
            arrête
        fin si
    fin pour

    // Condition d'arrêt : plus aucune case vide, le Sudoku est terminé
    si trouve = faux alors
        retourne vrai
    fin si

    // Préparer les chiffres de 1 à 9 et les mélanger pour la génération aléatoire
    possibles est un tableau
    pour n de 1 à 9
        possibles ajoute n
    fin pour
    possibles vaut melanger(possibles)

    num est un nombre
    valide est un booléen
    suite est un booléen

    pour k de 0 à 8
        num vaut possibles[k]
        valide vaut estValide(g, l, c, num)

        si valide = vrai alors
            // Essayer ce chiffre
            g[l][c] vaut num

            // Continuer avec la grille modifiée
            suite vaut generer(g)
            si suite = vrai alors
                retourne vrai
            fin si

            // Échec (impasse) : annuler (Backtrack) et essayer le suivant
            g[l][c] vaut 0
        fin si
    fin pour

    // Aucun chiffre n'a fonctionné, on doit reculer
    retourne faux
fin fonction

@ Création du puzzle

// Retire un nombre défini de chiffres pour le joueur
procédure faireTrous(g, nb)
    trous_faits est un nombre
    trous_faits vaut 0
    l est un nombre
    c est un nombre

    tant que trous_faits < nb
        l vaut hasard(0, 8)
        c vaut hasard(0, 8)
        si g[l][c] <> 0 alors
            g[l][c] vaut 0
            trous_faits ajoute 1
        fin si
    fin tant que
fin procédure

@ Affichage sur la toile

// Rendu visuel de la grille de Sudoku
procédure dessinerSudoku(t, g)
    remplir(t, #FFFFFF)

    x est un nombre
    y est un nombre
    val est un nombre
    txt est un texte

    // 1. Dessiner les cases et les chiffres
    pour l de 0 à 8
        pour c de 0 à 8
            x vaut c * 50
            y vaut l * 50
            val vaut g[l][c]

            si val <> 0 alors
                txt vaut convertir_texte(val)
                label(t, x + 16, y + 35, txt, #1E2430, 24)
            fin si
        fin pour
    fin pour

    // 2. Dessiner les lignes de la grille
    epaisseur est un nombre
    pour i de 0 à 9
        // Les lignes séparant les blocs 3x3 sont plus épaisses
        si i mod 3 = 0 alors
            epaisseur vaut 3
        sinon
            epaisseur vaut 1
        fin si

        // Lignes verticales
        ligne(t, i * 50, 0, i * 50, 450, #000000, epaisseur)
        // Lignes horizontales
        ligne(t, 0, i * 50, 450, i * 50, #000000, epaisseur)
    fin pour
fin procédure

@ Exécution principale

// 1. Remplir complètement la grille avec une solution valide
succes est un booléen
succes vaut generer(grille_sudoku)

// 2. Retirer 45 numéros pour créer le défi
appelle faireTrous(grille_sudoku, 45)

// 3. Préparer et afficher la toile (canevas)
ma_toile est une toile
dimension(ma_toile, 450, 450)
appelle dessinerSudoku(ma_toile, grille_sudoku)

affiche ma_toile
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'
Construire un jeu complet : un excellent exercice de logique et d’expérience utilisateur.
Article

Construire un jeu complet

Construire un jeu complet : un excellent exercice de logique et d’expérience utilisateur.

Lire l'articleReplier l'article

Ce projet de développement d’un jeu de Tic-Tac-Toe (morpion) montre comment un exercice simple peut devenir un terrain d’apprentissage riche. Le programme intègre une interface graphique, une détection de victoire, une IA basique et une boucle de rejouabilité, ce qui permet de revisiter plusieurs fondamentaux du développement.

Les points clés abordés sont les suivants :

  • structuration du code,
  • gestion des états,
  • interaction utilisateur,
  • conception d’une IA simple,
  • mise en place d’un retour visuel clair.

Détection du gagnant : une logique essentielle La fonction de détection de victoire repose sur l’analyse des huit combinaisons gagnantes possibles.

La méthode consiste à :

  • définir les alignements potentiels,
  • extraire les indices concernés,
  • comparer les valeurs du plateau.

Dès que trois symboles identiques sont alignés, la victoire est identifiée.
Cette logique est concise, lisible et facilement réutilisable.

Dessin du plateau : transformer la logique en interface

La fonction de dessin génère une grille lisible et moderne :

  • fond uniforme,
  • quadrillage clair,
  • représentation graphique des X et O,
  • rafraîchissement visuel après chaque action.

Ce rendu améliore la compréhension du joueur et renforce l’expérience globale.

Le bot “facile” : une IA simple mais pertinente

Le bot applique une stratégie en trois étapes :

  • tenter de gagner si une opportunité existe,
  • bloquer le joueur si nécessaire,
  • jouer aléatoirement dans les autres cas.

Cette approche crée un adversaire accessible tout en introduisant une logique décisionnelle intéressante.

La boucle de jeu : une expérience fluide et continue

Le programme principal :

  • initialise une nouvelle partie,
  • attend les interactions du joueur,
  • met à jour le plateau,
  • vérifie les conditions de victoire,
  • déclenche le coup du bot,
  • affiche un écran de fin,
  • permet de relancer une partie immédiatement.

Une structure simple, robuste et agréable à utiliser.

Conclusion

Ce projet démontre qu’un jeu classique peut devenir un excellent support pour travailler la logique algorithmique, la structuration du code, la gestion d’une interface graphique et l’intégration d’une IA basique.
Un exercice complet, formateur et facilement extensible.

Si tu veux une version plus courte, plus technique ou orientée recrutement, je peux t’en préparer une autre.

Programme :

// -----------------------------------------------------------
//   FONCTION : y a-t-il un gagnant ?
// -----------------------------------------------------------
fonction gagnant(plateau)
    lignes est un tableau
    lignes ajoute "0,1,2"
    lignes ajoute "3,4,5"
    lignes ajoute "6,7,8"
    lignes ajoute "0,3,6"
    lignes ajoute "1,4,7"
    lignes ajoute "2,5,8"
    lignes ajoute "0,4,8"
    lignes ajoute "2,4,6"

    k est un nombre
    pour k de 0 a 7
        cases est un tableau
        cases vaut decouper(lignes[k], ",")
        x est un nombre
        b est un nombre
        d est un nombre
        x vaut convertir_nombre(cases[0])
        b vaut convertir_nombre(cases[1])
        d vaut convertir_nombre(cases[2])

        si plateau[x] <> "" et plateau[x] = plateau[b] et plateau[b] = plateau[d] alors
            retourne plateau[x]
        fin si
    fin pour

    retourne ""
fin fonction

// -----------------------------------------------------------
//   FONCTION : dessiner le plateau (sans chiffres)
// -----------------------------------------------------------
fonction dessiner(plateau)
    t est une toile
    t vaut dimension(t, 300, 300)
    t vaut remplir(t, "#1abc9c")

    // --- Le quadrillage ---
    ligne(t, 100, 0, 100, 300, "#159a80", 3)
    ligne(t, 200, 0, 200, 300, "#159a80", 3)
    ligne(t, 0, 100, 300, 100, "#159a80", 3)
    ligne(t, 0, 200, 300, 200, "#159a80", 3)

    c est un nombre
    pour c de 0 a 8
        marque est un texte
        marque vaut plateau[c]

        col est un nombre
        lig est un nombre
        col vaut c mod 3
        lig vaut arrondi_inferieur(c / 3)

        cx est un nombre
        cy est un nombre
        cx vaut col * 100
        cy vaut lig * 100

        si marque = "X" alors
            ligne(t, cx + 25, cy + 25, cx + 75, cy + 75, "#3a3a3a", 10)
            ligne(t, cx + 75, cy + 25, cx + 25, cy + 75, "#3a3a3a", 10)
        sinon si marque = "O" alors
            cercle(t, cx + 50, cy + 50, 28, "#faf3e0")
            cercle(t, cx + 50, cy + 50, 18, "#1abc9c")
        fin si
    fin pour

    affiche t
    retourne t
fin fonction

// -----------------------------------------------------------
//   FONCTION : créer l'écran de fin
// -----------------------------------------------------------
fonction afficher_fin(resultat)
    t_fin est une toile
    t_fin vaut dimension(t_fin, 300, 300)
    t_fin vaut remplir(t_fin, "#1abc9c")

    si resultat = "X" alors
        label(t_fin, 25, 130, "?? TU AS GAGNÉ !", "#ffffff", 24)
    sinon si resultat = "O" alors
        label(t_fin, 20, 130, "?? LE BOT A GAGNÉ", "#ffffff", 22)
    sinon
        label(t_fin, 55, 130, "?? MATCH NUL", "#ffffff", 24)
    fin si

    label(t_fin, 50, 180, "Clique pour rejouer", "#0e6e5c", 16)

    affiche t_fin
    retourne t_fin
fin fonction

// -----------------------------------------------------------
//   FONCTION : le bot facile (O)
// -----------------------------------------------------------
fonction coup_du_bot(plateau)
    c est un nombre
    libres est un tableau

    pour c de 0 a 8
        si plateau[c] = "" alors
            libres ajoute c
        fin si
    fin pour

    si longueur(libres) = 0 alors
        retourne - 1
    fin si

    // Une chance sur deux (hasard 0 ou 1) de jouer intelligent, sinon il joue au hasard
    chance est un nombre
    chance vaut hasard(0, 1)

    si chance = 1 alors
        // 1. Essayer de gagner
        pour c de 0 a 8
            si plateau[c] = "" alors
                plateau[c] vaut "O"
                si gagnant(plateau) = "O" alors
                    plateau[c] vaut ""
                    retourne c
                fin si
                plateau[c] vaut ""
            fin si
        fin pour

        // 2. Essayer de bloquer le joueur
        pour c de 0 a 8
            si plateau[c] = "" alors
                plateau[c] vaut "X"
                si gagnant(plateau) = "X" alors
                    plateau[c] vaut ""
                    retourne c
                fin si
                plateau[c] vaut ""
            fin si
        fin pour
    fin si

    // 3. Sinon, choix totalement aléatoire parmi les cases libres
    index est un nombre
    index vaut hasard(0, longueur(libres) - 1)
    retourne libres[index]
fin fonction

// -----------------------------------------------------------
//   PROGRAMME PRINCIPAL — la boucle du jeu infinie
// -----------------------------------------------------------

g est un tableau
maToile est une toile
toile_fin est une toile
fini est un booléen
coups est un nombre
x, y est un nombre
choix est un nombre
c, i, col, lig, pos est un nombre
vainqueur est un texte

// Boucle principale infinie pour rejouer
tant que vrai

    // --- INITIALISATION D'UNE NOUVELLE PARTIE ---
    vide g
    pour i de 1 a 9
        g ajoute ""
    fin pour

    fini vaut faux
    coups vaut 0
    vainqueur vaut ""
    maToile vaut dessiner(g)

    // --- BOUCLE D'UNE PARTIE ---
    tant que non fini
        cliquer maToile dans x, y

        choix vaut -1

        pour c de 0 a 8
            col vaut c mod 3
            lig vaut arrondi_inferieur(c / 3)

            si clique(x, y, col * 100, lig * 100, 100, 100) alors
                choix vaut c
            fin si
        fin pour

        si choix = -1 alors
            continue
        fin si

        si g[choix] <> "" alors
            maToile vaut dessiner(g)
        sinon
            // 1. Le joueur joue
            g[choix] vaut "X"
            coups ajoute 1

            maToile vaut dessiner(g)

            si gagnant(g) = "X" alors
                vainqueur vaut "X"
                fini vaut vrai
            sinon si coups = 9 alors
                vainqueur vaut "NUL"
                fini vaut vrai
            sinon
                // 2. Le bot joue
                pos vaut coup_du_bot(g)
                g[pos] vaut "O"
                coups ajoute 1

                maToile vaut dessiner(g)

                si gagnant(g) = "O" alors
                    vainqueur vaut "O"
                    fini vaut vrai
                sinon si coups = 9 alors
                    vainqueur vaut "NUL"
                    fini vaut vrai
                fin si
            fin si
        fin si
    fin tant que

    // --- FIN DE PARTIE ---
    toile_fin vaut afficher_fin(vainqueur)
    cliquer toile_fin dans x, y
fin tant que