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.

76 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
Pas encore noté
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
Pas encore noté
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
Pas encore noté
Vue 3ème Personne : un moteur de perspective dynamique pour mondes infinis
Article

Vue 3ème Personne

Vue 3ème Personne : un moteur de perspective dynamique pour mondes infinis

Lire l'articleReplier l'article

La création d’un moteur de rendu en vue à la troisième personne est un défi passionnant, surtout lorsqu’il s’agit de représenter un monde infini avec une perspective qui s’agrandit naturellement à mesure que le joueur s’approche des éléments.

Ce projet explore une approche mathématique et procédurale permettant de simuler une caméra suiveuse immersive, sans modèle 3D, uniquement via des projections et des polygones.

Un moteur basé sur la perspective et la profondeur

Horizon, ciel et ambiance

Le rendu commence par une ligne d’horizon fixe et un ciel stylisé.
Cette base visuelle sert de repère pour la perspective et renforce l’effet de profondeur.

Rendu du terrain : l’algorithme du peintre

Le moteur dessine le monde du fond vers l’avant, une technique inspirée de l’algorithme du peintre.
Chaque couche de terrain est calculée en fonction de sa distance au joueur :

  • plus la tuile est éloignée, plus elle est petite
  • plus elle est proche, plus elle est grande

La projection utilise une formule simple mais efficace :

  • sLoin = 180 / dLoin
  • sPres = 180 / dPres
  • sCentre = 180 / dCentre

Ces valeurs déterminent la taille apparente des tuiles selon leur profondeur.

Projection 3D simulée

Chaque tuile est représentée par un quadrilatère dont les sommets sont calculés pour simuler une perspective :

  • sommet haut gauche
  • sommet haut droit
  • sommet bas droit
  • sommet bas gauche

Cette projection donne l’illusion d’un sol en 3D, sans utiliser de modèles polygonaux complexes.

Génération procédurale du terrain

Le type de sol est déterminé par des règles mathématiques :

  • Herbe
  • Chemins (via mod 14)
  • Eau (via une formule organique)

Cette logique crée un monde cohérent, infini et varié, sans stockage de données.

Décors avec grandissement dynamique
Les arbres, rochers et maisons sont générés procéduralement et agrandissent naturellement lorsqu’ils se rapprochent du joueur.

L’échelle dépend de la profondeur :

  • hTaille = sCentre * 0.85

Ce facteur donne un effet d’approche très immersif, proche d’une caméra 3D.

Personnage principal en vue 3P

Le joueur est représenté par un personnage stylisé placé en bas de l’écran.
La caméra ne bouge pas : c’est le monde qui se déplace autour du joueur, renforçant l’effet de vue à la troisième personne.

Déplacements et collisions

Les déplacements (ZQSD) sont validés par une détection procédurale :

  • impossibilité d’entrer dans l’eau
  • impossibilité d’entrer dans une maison
  • Aucune carte de collision n’est stockée : tout est calculé à la volée.

Pourquoi cette approche est intéressante ?

  • Perspective dynamique sans 3D
  • Monde infini généré en temps réel
  • Effet caméra suiveuse sans transformation complexe
  • Décors qui grandissent naturellement
  • Performances optimisées grâce au rendu du fond vers l’avant
  • Base idéale pour un jeu d’aventure, un RPG ou un simulateur

Programme :

@ Vue à la troisième personne (3P) avec grandissement de perspective

toile1 est une toile
dimension(toile1, 400, 400)

// Position du joueur dans le monde infini
jX est un nombre
jY est un nombre
jX vaut 0
jY vaut 0

action est un nombre

@ Boucle principale
tant que vrai
    effacer(toile1)
    remplir(toile1, #0f172a)

    // 1. Ciel et ligne d'horizon
    rectangle(toile1, 0, 0, 400, 120, #0f172a)
    cercle(toile1, 320, 40, 20, #f1c40f)

    // 2. Rendu du terrain du fond vers l'avant (Algorithme du peintre)
    iY est un nombre
    pour iY de 0 à 15
        rY est un nombre
        rY vaut 15 - iY

        rX est un nombre
        pour rX de - 8 à 8
            gx est un nombre
            gy est un nombre
            gx vaut jX + rX
            gy vaut jY + rY

            nx est un nombre
            ny est un nombre
            nx vaut gx + 50000
            ny vaut gy + 50000

            // Projection en perspective 3D
            dLoin est un nombre
            dPres est un nombre
            dLoin vaut rY + 0.5 + 2.0
            dPres vaut rY - 0.5 + 2.0

            sLoin est un nombre
            sPres est un nombre
            sLoin vaut 180 / dLoin
            sPres vaut 180 / dPres

            xHautG est un nombre
            yHautG est un nombre
            xHautD est un nombre
            yHautD est un nombre
            xBasD est un nombre
            yBasD est un nombre
            xBasG est un nombre
            yBasG est un nombre

            xHautG vaut 200 + (rX - 0.5) * sLoin * 1.2
            yHautG vaut 120 + sLoin * 2.5
            xHautD vaut 200 + (rX + 0.5) * sLoin * 1.2
            yHautD vaut 120 + sLoin * 2.5

            xBasD vaut 200 + (rX + 0.5) * sPres * 1.2
            yBasD vaut 120 + sPres * 2.5
            xBasG vaut 200 + (rX - 0.5) * sPres * 1.2
            yBasG vaut 120 + sPres * 2.5

            dCentre est un nombre
            dCentre vaut rY + 2.0
            sCentre est un nombre
            sCentre vaut 180 / dCentre

            cx est un nombre
            cy est un nombre
            cx vaut 200 + rX * sCentre * 1.2
            cy vaut 120 + sCentre * 2.5

            typeSol est un nombre
            typeSol vaut 0

            si nx mod 14 = 0 ou ny mod 14 = 0 alors
                typeSol vaut 1
            sinon si (nx * 3 + ny * 2) mod 31 < 2 alors
                typeSol vaut 2
            fin si

            ptsSol est un tableau
            ptsSol ajoute xHautG
            ptsSol ajoute yHautG
            ptsSol ajoute xHautD
            ptsSol ajoute yHautD
            ptsSol ajoute xBasD
            ptsSol ajoute yBasD
            ptsSol ajoute xBasG
            ptsSol ajoute yBasG

            si typeSol = 0 alors
                polygone(toile1, ptsSol, #27ae60)
            sinon si typeSol = 1 alors
                polygone(toile1, ptsSol, #d35400)
            sinon
                polygone(toile1, ptsSol, #2980b9)
            fin si

            contour(toile1, #1a252f, 1)

            // Décors
            typeDecor est un nombre
            typeDecor vaut 0

            si typeSol = 0 alors
                hash est un nombre
                hash vaut (nx * 17 + ny * 37) mod 100
                si hash < 10 alors
                    typeDecor vaut 1
                sinon si hash < 14 alors
                    typeDecor vaut 2
                sinon si hash = 15 alors
                    typeDecor vaut 3
                fin si
            fin si

            // Échelle augmentée pour accentuer l'effet d'approche
            hTaille est un nombre
            hTaille vaut sCentre * 0.85

            si typeDecor = 1 alors
                // Arbre
                ellipse(toile1, cx, cy, hTaille * 0.3, hTaille * 0.1, #0e171e)
                rectangle(toile1, cx - hTaille * 0.08, cy - hTaille * 0.6, hTaille * 0.16, hTaille * 0.6, #5d4037)
                cercle(toile1, cx, cy - hTaille * 0.85, hTaille * 0.35, #2e7d32)
            sinon si typeDecor = 2 alors
                // Rocher
                ellipse(toile1, cx, cy, hTaille * 0.25, hTaille * 0.1, #0e171e)
                cercle(toile1, cx, cy - hTaille * 0.15, hTaille * 0.2, #7f8c8d)
            sinon si typeDecor = 3 alors
                // Maison
                mLargeur est un nombre
                mHauteur est un nombre
                mLargeur vaut hTaille * 0.7
                mHauteur vaut hTaille * 0.5

                rectangle(toile1, cx - mLargeur / 2, cy - mHauteur, mLargeur, mHauteur, #bdc3c7)
                toit3P est un tableau
                toit3P ajoute cx - mLargeur * 0.55
                toit3P ajoute cy - mHauteur
                toit3P ajoute cx
                toit3P ajoute cy - mHauteur - hTaille * 0.35
                toit3P ajoute cx + mLargeur * 0.55
                toit3P ajoute cy - mHauteur
                polygone(toile1, toit3P, #c0392b)
                rectangle(toile1, cx - mLargeur * 0.15, cy - mHauteur * 0.5, mLargeur * 0.3, mHauteur * 0.5, #5d4037)
            fin si
        fin pour
    fin pour

    // 3. Personnage principal
    pX est un nombre
    pY est un nombre
    pX vaut 200
    pY vaut 340

    ellipse(toile1, pX, pY + 12, 18, 6, #0a0f14)
    rectangle(toile1, pX - 10, pY - 20, 20, 28, #e74c3c)
    rectangle(toile1, pX - 6, pY - 16, 12, 18, #795548)
    cercle(toile1, pX, pY - 28, 10, #f1c40f)

    // Interface HUD
    contour(toile1, #0)
    rectangle(toile1, 0, 0, 400, 40, rgba(17, 17, 17, 0.2))
    label(toile1, 10, 16, "Vue 3ème Personne (Caméra Suiveuse)", #ffffff, 12)

    txtCoords est un texte
    txtCoords vaut "Position X : " + jX + " | Y : " + jY
    label(toile1, 10, 32, txtCoords, #f1c40f, 10)

    affiche toile1

    appuyer ["z", "s", "q", "d"] dans action

    nouvX est un nombre
    nouvY est un nombre
    nouvX vaut jX
    nouvY vaut jY

    si action = 1 alors
        nouvY vaut jY + 1
    sinon si action = 2 alors
        nouvY vaut jY - 1
    sinon si action = 3 alors
        nouvX vaut jX - 1
    sinon si action = 4 alors
        nouvX vaut jX + 1
    fin si

    // Détection de collision
    chkX est un nombre
    chkY est un nombre
    chkX vaut nouvX + 50000
    chkY vaut nouvY + 50000

    solFutur est un nombre
    solFutur vaut 0
    si chkX mod 14 = 0 ou chkY mod 14 = 0 alors
        solFutur vaut 1
    sinon si (chkX * 3 + chkY * 2) mod 31 < 2 alors
        solFutur vaut 2
    fin si

    decorFutur est un nombre
    decorFutur vaut 0
    si solFutur = 0 alors
        hFutur est un nombre
        hFutur vaut (chkX * 17 + chkY * 37) mod 100
        si hFutur = 15 alors
            decorFutur vaut 3
        fin si
    fin si

    si solFutur <> 2 et decorFutur <> 3 alors
        jX vaut nouvX
        jY vaut nouvY
    fin si
fin tant que
Pas encore noté
Sokoban en 3D isométrique : un projet mêlant algorithmie, génération procédurale et rendu graphique
Article

Sokoban en 3D isométrique

Sokoban en 3D isométrique : un projet mêlant algorithmie, génération procédurale et rendu graphique

Lire l'articleReplier l'article

Ce programme illustre comment l’algorithmie, la logique de jeu et le rendu visuel peuvent s’intégrer pour créer une expérience complète inspirée du célèbre Sokoban.
L’ensemble repose sur une architecture qui combine génération procédurale, gestion des interactions et affichage isométrique 3D.

Génération procédurale d’un niveau jouable

Le système construit automatiquement une salle fermée, y place des caisses, des cibles et un joueur, puis applique une série de mouvements aléatoires pour obtenir une configuration réaliste et jouable.

Points clés :

  • Création d’une grille 10×10 avec murs et zones libres.
  • Placement des caisses uniquement sur des cibles, jamais sur les bords.
  • Mélange contrôlé via des déplacements simulés.

Validation stricte du niveau :

  • bon nombre de caisses,
  • aucune caisse bloquée,
  • au moins une caisse hors cible pour garantir un vrai défi.

Extrait du programme :
« si caisseSurBord() alors valide vaut faux »
« si caissesHorsCible() = 0 alors valide vaut faux »

Chaque partie démarre ainsi avec un puzzle cohérent et intéressant.

Logique de déplacement et gestion des collisions
Le déplacement du joueur suit les règles classiques du Sokoban :

  • impossibilité de traverser les murs,
  • poussée des caisses uniquement si l’espace derrière est libre,
  • distinction entre caisse sur cible (*) et caisse hors cible (b).

Extrait du programme :
« si estCible(al, ac) alors poser(al, ac, "") sinon poser(al, ac, "b") »

Cette mécanique garantit une fidélité totale aux principes du jeu original.

Rendu isométrique 3D : immersion et lisibilité

La partie graphique repose sur un rendu isométrique détaillé :

  • sols en losange,
  • murs en volume,
  • caisses en 3D avec variations visuelles,
  • personnage stylisé avec ombre, tronc, tête et yeux.

Chaque élément est dessiné via des polygones, ce qui crée une ambiance visuelle cohérente et agréable.

Extrait du programme :
« polygone(g, cx, cy - th/2, cx + tw/2, cy, cx, cy + th/2, cx - tw/2, cy, #3a4a5c) »

Le résultat est une carte isométrique claire, immersive et parfaitement adaptée à un jeu de réflexion.

Une architecture complète de mini-jeu

Ce projet combine :

  • génération procédurale,
  • gestion des états,
  • interactions clavier,
  • rendu graphique avancé,
  • boucle de jeu complète,
  • détection de victoire.

Un exemple concret de la manière dont l’algorithmie peut servir la créativité, et comment un ensemble de fonctions peut devenir une expérience ludique cohérente.

Perspectives

Ce type de moteur ouvre la voie à :

  • la création automatique de puzzles,
  • l’ajout d’animations,
  • l’intégration dans un moteur plus large,
  • l’export vers le web ou le mobile,
  • l’expérimentation IA autour de la résolution de niveaux.

Programme :

// ============================================================
//  PARAMETRES
// ============================================================
lignes est un nombre
lignes vaut 10
colonnes est un nombre
colonnes vaut 10
nbCaisses est un nombre
nbCaisses vaut 5
melange est un nombre
melange vaut 60

// ============================================================
//  GRILLE
// ============================================================
niveau est un tableau
cibles est un tableau

jl est un nombre
jc est un nombre
jl vaut 0
jc vaut 0

fonction caseAt(li, co)
    ln est un texte
    ln vaut niveau[li]
    retourne ln[co]
fin fonction

fonction cibleAt(li, co)
    ln est un texte
    ln vaut cibles[li]
    retourne ln[co]
fin fonction

procedure poser(li, co, nc)
    ln est un texte
    ln vaut niveau[li]
    nouvelle est un texte
    nouvelle vaut ""
    k est un nombre
    k vaut 1
    tant que k <= longueur(ln)
        si k = co alors
            nouvelle vaut nouvelle + nc
        sinon
            nouvelle vaut nouvelle + ln[k]
        fin si
        k vaut k + 1
    fin tant que
    niveau[li] vaut nouvelle
fin procedure

procedure poserCible(li, co)
    ln est un texte
    ln vaut cibles[li]
    nouvelle est un texte
    nouvelle vaut ""
    k est un nombre
    k vaut 1
    tant que k <= longueur(ln)
        si k = co alors
            nouvelle vaut nouvelle + "c"
        sinon
            nouvelle vaut nouvelle + ln[k]
        fin si
        k vaut k + 1
    fin tant que
    cibles[li] vaut nouvelle
fin procedure

fonction estCible(li, co)
    si cibleAt(li, co) = "c" alors
        retourne vrai
    fin si
    retourne faux
fin fonction

fonction estLibre(li, co)
    ca est un texte
    ca vaut caseAt(li, co)
    si (ca = "#") ou (ca = "b") ou (ca = "*") alors
        retourne faux
    fin si
    retourne vrai
fin fonction

// ============================================================
//  GENERATION DU NIVEAU
// ============================================================
procedure construireSalle()
    vide niveau
    vide cibles
    li est un nombre
    li vaut 0
    tant que li < lignes
        ligneN est un texte
        ligneN vaut ""
        ligneC est un texte
        ligneC vaut ""
        co est un nombre
        co vaut 1
        tant que co <= colonnes
            bord est un booleen
            bord vaut faux
            si (li = 0) ou (li = lignes - 1) alors
                bord vaut vrai
            fin si
            si (co = 1) ou (co = colonnes) alors
                bord vaut vrai
            fin si
            si bord alors
                ligneN vaut ligneN + "#"
            sinon
                ligneN vaut ligneN + "."
            fin si
            ligneC vaut ligneC + "."
            co vaut co + 1
        fin tant que
        niveau ajoute ligneN
        cibles ajoute ligneC
        li vaut li + 1
    fin tant que
fin procedure

procedure placerCaisses()
    poses est un nombre
    poses vaut 0
    essais est un nombre
    essais vaut 0
    tant que (poses < nbCaisses) et (essais < 800)
        essais vaut essais + 1
        rl est un nombre
        rl vaut hasard(2, lignes - 3)
        rc est un nombre
        rc vaut hasard(3, colonnes - 2)
        si caseAt(rl, rc) = "." alors
            appelle poser(rl, rc, "*")
            appelle poserCible(rl, rc)
            poses vaut poses + 1
        fin si
    fin tant que
fin procedure

procedure placerJoueur()
    ok est un booleen
    ok vaut faux
    essais est un nombre
    essais vaut 0
    tant que (non ok) et (essais < 800)
        essais vaut essais + 1
        rl est un nombre
        rl vaut hasard(1, lignes - 2)
        rc est un nombre
        rc vaut hasard(2, colonnes - 1)
        si caseAt(rl, rc) = "." alors
            appelle poser(rl, rc, "j")
            jl vaut rl
            jc vaut rc
            ok vaut vrai
        fin si
    fin tant que
fin procedure

procedure tirerUnCoup()
    dl est un nombre
    dc est un nombre
    choix est un nombre
    choix vaut hasard(1, 4)
    dl vaut 0
    dc vaut 0
    si choix = 1 alors
        dl vaut - 1
    sinon si choix = 2 alors
        dl vaut 1
    sinon si choix = 3 alors
        dc vaut - 1
    sinon
        dc vaut 1
    fin si

    reculL est un nombre
    reculL vaut jl + dl
    reculC est un nombre
    reculC vaut jc + dc
    caisseL est un nombre
    caisseL vaut jl - dl
    caisseC est un nombre
    caisseC vaut jc - dc

    si estLibre(reculL, reculC) alors
        derriere est un texte
        derriere vaut caseAt(caisseL, caisseC)
        tire est un booleen
        tire vaut faux
        si (derriere = "b") ou (derriere = "*") alors
            surBord est un booleen
            surBord vaut faux
            si (jl = 1) ou (jl = lignes - 2) alors
                surBord vaut vrai
            fin si
            si (jc = 2) ou (jc = colonnes - 1) alors
                surBord vaut vrai
            fin si
            si (non surBord) et (hasard(0, 1) = 1) alors
                tire vaut vrai
            fin si
        fin si

        si estCible(jl, jc) alors
            appelle poser(jl, jc, "c")
        sinon
            appelle poser(jl, jc, ".")
        fin si

        si tire alors
            si estCible(caisseL, caisseC) alors
                appelle poser(caisseL, caisseC, "c")
            sinon
                appelle poser(caisseL, caisseC, ".")
            fin si
            si estCible(jl, jc) alors
                appelle poser(jl, jc, "*")
            sinon
                appelle poser(jl, jc, "b")
            fin si
        fin si

        appelle poser(reculL, reculC, "j")
        jl vaut reculL
        jc vaut reculC
    fin si
fin procedure

fonction caisseSurBord()
    li est un nombre
    li vaut 1
    tant que li < lignes - 1
        co est un nombre
        co vaut 2
        tant que co < colonnes
            car est un texte
            car vaut caseAt(li, co)
            si (car = "b") ou (car = "*") alors
                si (li = 1) ou (li = lignes - 2) ou (co = 2) ou (co = colonnes - 1) alors
                    retourne vrai
                fin si
            fin si
            co vaut co + 1
        fin tant que
        li vaut li + 1
    fin tant que
    retourne faux
fin fonction

fonction compteCaisses()
    n est un nombre
    n vaut 0
    li est un nombre
    li vaut 0
    tant que li < lignes
        co est un nombre
        co vaut 1
        tant que co <= colonnes
            car est un texte
            car vaut caseAt(li, co)
            si (car = "b") ou (car = "*") alors
                n vaut n + 1
            fin si
            co vaut co + 1
        fin tant que
        li vaut li + 1
    fin tant que
    retourne n
fin fonction

fonction caissesHorsCible()
    n est un nombre
    n vaut 0
    li est un nombre
    li vaut 0
    tant que li < lignes
        co est un nombre
        co vaut 1
        tant que co <= colonnes
            si caseAt(li, co) = "b" alors
                n vaut n + 1
            fin si
            co vaut co + 1
        fin tant que
        li vaut li + 1
    fin tant que
    retourne n
fin fonction

procedure genererNiveau()
    valide est un booleen
    valide vaut faux
    tentatives est un nombre
    tentatives vaut 0

    tant que (non valide) et (tentatives < 60)
        tentatives vaut tentatives + 1
        appelle construireSalle()
        appelle placerCaisses()
        appelle placerJoueur()
        n est un nombre
        n vaut 0
        tant que n < melange
            appelle tirerUnCoup()
            n vaut n + 1
        fin tant que

        valide vaut vrai
        si compteCaisses() <> nbCaisses alors
            valide vaut faux
        fin si
        si caisseSurBord() alors
            valide vaut faux
        fin si
        si caissesHorsCible() = 0 alors
            valide vaut faux
        fin si
    fin tant que
fin procedure

// ============================================================
//  DESSIN ISOMETRIQUE 3D
// ============================================================
tw est un nombre
tw vaut 56
th est un nombre
th vaut 28

g est une toile
dimension(g, 650, 400)

procedure dessiner()
    dégradé(g, [#1e2a38, #2c3e50])

    ox est un nombre
    ox vaut 300
    oy est un nombre
    oy vaut 45  // <-- Ajusté pour centrer parfaitement la map 10x10 en hauteur

    somm est un nombre
    somm vaut 1
    tant que somm <= lignes + colonnes
        li est un nombre
        li vaut 0
        tant que li < lignes
            co est un nombre
            co vaut 1
            tant que co <= colonnes
                si (li + co) = somm alors
                    car est un texte
                    car vaut caseAt(li, co)

                    cx est un nombre
                    cx vaut ox + (co - li) * (tw / 2)
                    cy est un nombre
                    cy vaut oy + (co + li) * (th / 2)

                    // 1. Sol
                    si car <> "#" alors
                        polygone(g, cx, cy - th/2, cx + tw/2, cy, cx, cy + th/2, cx - tw/2, cy, #3a4a5c)
                        ligne(g, cx, cy - th/2, cx + tw/2, cy, #2c3e50, 1)
                        ligne(g, cx + tw/2, cy, cx, cy + th/2, #2c3e50, 1)
                        ligne(g, cx, cy + th/2, cx - tw/2, cy, #2c3e50, 1)
                        ligne(g, cx - tw/2, cy, cx, cy - th/2, #2c3e50, 1)

                        // Cible : caisse 3D rouge transparente
                        si estCible(li, co) alors
                            si car <> "*" alors
                                hcib est un nombre
                                hcib vaut 24
                                polygone(g, cx, cy - th/2 - hcib, cx + tw/2, cy - hcib, cx, cy + th/2 - hcib, cx - tw/2, cy - hcib, rgba(231, 76, 60, 0.35))
                                polygone(g, cx - tw/2, cy - hcib, cx, cy + th/2 - hcib, cx, cy + th/2, cx - tw/2, cy, rgba(192, 57, 43, 0.45))
                                polygone(g, cx, cy + th/2 - hcib, cx + tw/2, cy - hcib, cx + tw/2, cy, cx, cy + th/2, rgba(146, 43, 33, 0.45))

                                ligne(g, cx, cy - th/2 - hcib, cx + tw/2, cy - hcib, rgba(231, 76, 60, 0.8), 1)
                                ligne(g, cx + tw/2, cy - hcib, cx, cy + th/2 - hcib, rgba(231, 76, 60, 0.8), 1)
                                ligne(g, cx, cy + th/2 - hcib, cx - tw/2, cy - hcib, rgba(231, 76, 60, 0.8), 1)
                                ligne(g, cx - tw/2, cy - hcib, cx, cy - th/2 - hcib, rgba(231, 76, 60, 0.8), 1)
                                ligne(g, cx, cy + th/2 - hcib, cx, cy + th/2, rgba(231, 76, 60, 0.8), 1)
                            fin si
                        fin si
                    fin si

                    // 2. Mur
                    si car = "#" alors
                        h est un nombre
                        h vaut 28
                        polygone(g, cx, cy - th/2 - h, cx + tw/2, cy - h, cx, cy + th/2 - h, cx - tw/2, cy - h, #243342)
                        polygone(g, cx - tw/2, cy - h, cx, cy + th/2 - h, cx, cy + th/2, cx - tw/2, cy, #141c26)
                        polygone(g, cx, cy + th/2 - h, cx + tw/2, cy - h, cx + tw/2, cy, cx, cy + th/2, #1a2531)
                        ligne(g, cx, cy + th/2 - h, cx, cy + th/2, #101820, 1)
                        ligne(g, cx, cy + th/2 - h, cx - tw/2, cy - h, #101820, 1)
                        ligne(g, cx, cy + th/2 - h, cx + tw/2, cy - h, #101820, 1)
                    fin si

                    // 3. Caisse
                    si (car = "b") ou (car = "*") alors
                        cTop est un texte
                        cTop vaut #f5b041
                        cLeft est un texte
                        cLeft vaut #d68910
                        cRight est un texte
                        cRight vaut #b9770e

                        si car = "*" alors
                            cTop vaut #2ecc71
                            cLeft vaut #27ae60
                            cRight vaut #1e8449
                        fin si

                        hc est un nombre
                        hc vaut 24
                        polygone(g, cx, cy - th/2 - hc, cx + tw/2, cy - hc, cx, cy + th/2 - hc, cx - tw/2, cy - hc, cTop)
                        polygone(g, cx - tw/2, cy - hc, cx, cy + th/2 - hc, cx, cy + th/2, cx - tw/2, cy, cLeft)
                        polygone(g, cx, cy + th/2 - hc, cx + tw/2, cy - hc, cx + tw/2, cy, cx, cy + th/2, cRight)
                        ligne(g, cx, cy + th/2 - hc, cx, cy + th/2, #875c06, 1)
                    fin si

                    // 4. Joueur (Personnage amélioré)
                    si car = "j" alors
                        // Ombre au sol
                        ellipse(g, cx, cy, 18, 9, rgba(0, 0, 0, 0.4))

                        // Dimensions du corps
                        pw est un nombre
                        pw vaut 20
                        ph est un nombre
                        ph vaut 10
                        pj est un nombre
                        pj vaut 18 // hauteur du tronc

                        // Tronc (Pull rouge)
                        polygone(g, cx, cy - ph/2 - pj, cx + pw/2, cy - pj, cx, cy + ph/2 - pj, cx - pw/2, cy - pj, #e74c3c)
                        polygone(g, cx - pw/2, cy - pj, cx, cy + ph/2 - pj, cx, cy + ph/2, cx - pw/2, cy, #c0392b)
                        polygone(g, cx, cy + ph/2 - pj, cx + pw/2, cy - pj, cx + pw/2, cy, cx, cy + ph/2, #922b21)

                        // Tête (Cercle couleur peau)
                        cTop est un nombre
                        cTop vaut cy - pj - 10
                        cercle(g, cx, cTop, 11, #f5cba7)

                        // Yeux
                        cercle(g, cx - 4, cTop - 2, 2, #2c3e50)
                        cercle(g, cx + 4, cTop - 2, 2, #2c3e50)
                    fin si

                fin si
                co vaut co + 1
            fin tant que
            li vaut li + 1
        fin tant que
        somm vaut somm + 1
    fin tant que

    affiche g
fin procedure

// ============================================================
//  DEPLACEMENT
// ============================================================
procedure deplacer(dl, dc)
    nl est un nombre
    nl vaut jl + dl
    nc est un nombre
    nc vaut jc + dc
    dest est un texte
    dest vaut caseAt(nl, nc)
    bloque est un booleen
    bloque vaut faux

    si dest = "#" alors
        bloque vaut vrai
    fin si

    si (non bloque) et ((dest = "b") ou (dest = "*")) alors
        al est un nombre
        al vaut nl + dl
        ac est un nombre
        ac vaut nc + dc
        apres est un texte
        apres vaut caseAt(al, ac)
        si (apres = "#") ou (apres = "b") ou (apres = "*") alors
            bloque vaut vrai
        sinon
            si estCible(al, ac) alors
                appelle poser(al, ac, "*")
            sinon
                appelle poser(al, ac, "b")
            fin si
            si estCible(nl, nc) alors
                appelle poser(nl, nc, "c")
            sinon
                appelle poser(nl, nc, ".")
            fin si
        fin si
    fin si

    si non bloque alors
        si estCible(jl, jc) alors
            appelle poser(jl, jc, "c")
        sinon
            appelle poser(jl, jc, ".")
        fin si
        appelle poser(nl, nc, "j")
        jl vaut nl
        jc vaut nc
    fin si
fin procedure

fonction gagne()
    li est un nombre
    li vaut 0
    tant que li < lignes
        co est un nombre
        co vaut 1
        tant que co <= colonnes
            si caseAt(li, co) = "b" alors
                retourne faux
            fin si
            co vaut co + 1
        fin tant que
        li vaut li + 1
    fin tant que
    retourne vrai
fin fonction

// ============================================================
//  PARTIE
// ============================================================
appelle genererNiveau()

fini est un booleen
fini vaut faux
action est un nombre
action vaut 0
coups est un nombre
coups vaut 0

tant que non fini
    appelle dessiner()

    // Attente de la frappe d'une touche au clavier (z, s, q, d, r, x)
    appuyer ["z", "s", "q", "d", "r", "x"] dans action

    // action = 1 (z), 2 (s), 3 (q), 4 (d), 5 (r), 6 (x)
    si action = 1 alors
        appelle deplacer(-1, 0)
        coups vaut coups + 1
    sinon si action = 2 alors
        appelle deplacer(1, 0)
        coups vaut coups + 1
    sinon si action = 3 alors
        appelle deplacer(0, -1)
        coups vaut coups + 1
    sinon si action = 4 alors
        appelle deplacer(0, 1)
        coups vaut coups + 1
    sinon si action = 5 alors
        appelle genererNiveau()
        coups vaut 0
    sinon si action = 6 alors
        fini vaut vrai
    fin si

    si non fini alors
        si gagne() alors
            fini vaut vrai
        fin si
    fin si
fin tant que

effacer(g)
appelle dessiner()
si gagne() alors
    couleur(#2ecc71, "GAGNE en " + convertir_texte(coups) + " coups ! Bravo !")
sinon
    couleur(#e74c3c, "Partie terminee. A bientot !")
fin si
Pas encore noté
EduCode v3.2 arrive avec une nouveauté très attendue
Nouveauté

EduCode v3.2

EduCode v3.2 arrive avec une nouveauté très attendue.

Lire l'articleReplier l'article

EduCode 3.2 arrive avec une nouveauté très attendue : l'interaction à la souris sur les toiles.

Deux ajouts complémentaires, dans la philosophie du langage (simple, en français, lisible) :

  • Le mot-clé « cliquer » : le programme attend un clic sur le dessin et récupère sa position exacte.
  • La fonction « clique » : elle vérifie si le clic tombe dans une zone précise, rectangulaire ou circulaire.

Ensemble, ils ouvrent la porte aux boutons, aux menus graphiques, aux cibles à attraper et aux jeux de plateau entièrement jouables à la souris, de vraies petites applications interactives, en quelques lignes.

À découvrir dans la version v3.2, prochainement disponible en téléchargement.

Exemples :

maToile est une toile
dimension(maToile, 300, 200)
remplir(maToile, #0d1117)

x, y, i est un nombre vaut 0

pour i de 1 à 5
    affiche maToile
    cliquer maToile dans x, y
    cercle(maToile, x, y, 4, #e53935)

    affiche maToile
    affiche 'Clic ' + i + ' en (X : ' + x + ', Y : ' + y + ')'
fin pour

affiche 'Fin du programme.'
maToile est une toile
dimension(maToile, 300, 200)
remplir(maToile, '#0d1117')

// bouton : mêmes valeurs pour le dessin et le test
rectangle(maToile, 40, 80, 100, 45, '#1976d2')
label(maToile, 60, 108, 'Jouer', '#ffffff', 16)
affiche maToile

x, y est un nombre
clicok est un booléen vaut faux

tant que non clicok
    cliquer maToile dans x, y
    si clique(x, y, 40, 80, 100, 45) alors
        affiche 'Tu as cliqué sur le bouton !'
        clicok vaut vrai
    sinon
        affiche 'Raté, à côté du bouton.'
        clicok vaut faux
    fin si
fin tant que

affiche 'Fin du programme.'
Pas encore noté