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'