samedi 20 septembre 2025

Le Phénomène Pac-Man : L'icône qui a dévoré le monde de l'arcade




À l'époque où les bornes d'arcade étaient dominées par les vaisseaux spatiaux et les tirs de laser, un petit personnage rond et jaune est arrivé pour tout changer. Oubliez les combats intergalactiques : Pac-Man a introduit une formule simple mais addictive qui a conquis le cœur de millions de joueurs. Bienvenue sur ArcadeForge, et préparez-vous à une plongée dans les coulisses de l'un des jeux les plus célèbres de tous les temps.

Le Jeu : Un Labyrinthe de Dangers et de Délices

Le principe de Pac-Man, sorti en 1980, est d'une simplicité désarmante. Le joueur contrôle Pac-Man, un cercle jaune à la bouche grande ouverte, dans un labyrinthe rempli de petites pac-gommes. Le but est de toutes les dévorer pour passer au niveau suivant.

Mais la simplicité s'arrête là. Quatre fantômes, chacun avec sa propre personnalité, sont à la poursuite de notre héros. Blinky le rouge, Pinky le rose, Inky le cyan et Clyde l'orange. Ces spectres sont redoutables, mais Pac-Man a une botte secrète : les "super pac-gommes" (ou "power pellets"). En les mangeant, il peut temporairement renverser la situation et dévorer ses poursuivants, marquant ainsi des points supplémentaires. Ce mélange de stratégie, de réflexes et de poursuite a rendu le jeu irrésistiblement amusant.

 


 

Le Cerveau Derrière le Labyrinthe

L'homme à l'origine de ce chef-d'œuvre est Toru Iwatani, un jeune designer de jeux vidéo travaillant pour l'entreprise japonaise Namco. Fatigué de l'omniprésence des jeux de guerre et de science-fiction, Iwatani voulait créer un jeu qui plairait également aux femmes et aux couples, en proposant quelque chose de non-violent et de mignon.

L'idée du personnage principal lui serait venue en observant une pizza à laquelle il manquait une part, donnant l'apparence d'une bouche. L'idée de manger les fantômes après avoir mangé les super pac-gommes est un clin d'œil à l'idée d'un "changement de rôle" entre le chasseur et le chassé.

Namco et le Phénomène Mondial

Sorti initialement au Japon sous le nom de "Puck-Man", le jeu a connu un succès modeste avant d'être exporté aux États-Unis par l'entreprise Midway Games. C'est là que "Puck-Man" a été rebaptisé "Pac-Man" pour éviter que le "P" ne soit transformé en "F" par des petits malins.

Le reste appartient à l'histoire. Pac-Man est devenu une véritable icône culturelle, générant un engouement sans précédent : produits dérivés, séries télévisées, chansons, et une popularité qui n'a jamais faibli. Le jeu a non seulement sauvé Namco de la faillite, mais il a également marqué la fin d'une époque et le début d'une autre dans le monde du jeu vidéo, prouvant qu'il n'était pas nécessaire de tirer pour s'amuser.

Pac-Man n'est pas seulement un jeu rétro, c'est un monument. Il incarne l'esprit d'innovation et la créativité de l'âge d'or de l'arcade.

 

Voici un projet complet en Love de Pacman : https://github.com/r-sede/lovePacMan 

ici la mécanique du jeu : 

 

 -- ============================
-- VARIABLES GLOBALES
-- ============================
-- Ces variables définissent les dimensions du jeu et d'autres paramètres essentiels.
 

PPM = 1 -- Pixels Per Meter, pour une gestion des dimensions
VW = 448 -- View Width, la largeur de la vue du jeu
VH = 576 -- View Height, la hauteur de la vue du jeu
BLOCKSIZE = 16 -- La taille en pixels d'un bloc de la grille, utilisée pour la map
MAP = nil -- La table qui contiendra la map du labyrinthe
MAPSHEET = {} -- Une table pour stocker les images des blocs de la map
FRUITSHEET = {} -- Une table pour stocker les images des fruits
MAPATLAS = nil -- L'image globale (atlas) des sprites de la map
FRUITATLAS = nil -- L'image globale (atlas) des sprites des fruits
DEBUG = false -- Active ou désactive le mode debug
DOTS = 244 -- Le nombre total de pac-gommes sur la map
PAUSE = false -- Indique si le jeu est en pause
TITLESCREEN = nil -- L'image de l'écran-titre
CURRENTSTATE = 'title' -- L'état actuel du jeu (titre, jeu, game over, etc.)
LEVEL = 1 -- Le niveau actuel
READYTIMER = 4.5 -- Le temps d'attente au début d'un niveau
FONT = nil -- La police de caractères pour l'affichage
HIGHSCORE = {} -- Une table pour stocker les meilleurs scores
CATCHPOINT = {200, 400, 800, 1600, 12000} -- Points gagnés en mangeant des fantômes (la valeur augmente pour chaque fantôme)
DEBUG_PLACEHOLDER = 0 -- Une variable pour ajuster la fenêtre en mode debug
SOUNDVOL = 1 -- Volume du son
S_INTRO, S_DOT, S_DEATH, S_EATGHOST, S_READY = nil -- Variables pour stocker les sons du jeu

-- ============================
-- FONCTION LOVE.LOAD
-- ============================
-- love.load est la fonction principale de LÖVE. Elle est exécutée une seule fois au démarrage du jeu.
function love.load(arg)
 

   -- Initialise la graine du générateur de nombres aléatoires
    love.math.setRandomSeed(love.timer.getTime())
 

   -- Définit le filtre des graphismes sur 'nearest' pour un effet pixel art net
    love.graphics.setDefaultFilter('nearest')
    -- Active la répétition des touches du clavier (pratique pour le mouvement)
    love.keyboard.setKeyRepeat(true)

    -- Importe les modules Lua contenant la logique du jeu
    require"pacMan" -- Logique du joueur (mouvement, collisions)
    require"ghosts" -- Logique et intelligence artificielle des fantômes
    require"pacManStates" -- Gestion des états du jeu
    require"levelSpec" -- Spécifications des niveaux (fruits, etc.)
    getMaps = require('map') -- Fonction pour charger les cartes du labyrinthe

    -- Vérifie si le jeu a été lancé en mode debug
    if arg[#arg] == "-debug" then
        DEBUG_PLACEHOLDER = 300
        DEBUG = true
        print('\n')
    end

    -- Configure la taille de la fenêtre du jeu
    love.window.setMode((PPM * VW) + DEBUG_PLACEHOLDER, PPM * VH)
    love.window.setTitle('LovePacMan') -- Définit le titre de la fenêtre

    -- Charge la police de caractères
    FONT = love.graphics.newFont('assets/fonts/emulogic.ttf', 8)
    love.graphics.setFont(FONT)

    -- Charge les images et les divise en feuilles de sprites
    TITLESCREEN = love.graphics.newImage('assets/img/title.png')
    FRUITATLAS = love.graphics.newImage('assets/img/fruits.png')
    FRUITSHEET['cherries'] = love.graphics.newQuad(0 * 16, 0, 16, 16, FRUITATLAS:getDimensions())
    FRUITSHEET['strawberry'] = love.graphics.newQuad(1 * 16, 0, 16, 16, FRUITATLAS:getDimensions())
    FRUITSHEET['peach'] = love.graphics.newQuad(2 * 16, 0, 16, 16, FRUITATLAS:getDimensions())
    FRUITSHEET['apple'] = love.graphics.newQuad(3 * 16, 0, 16, 16, FRUITATLAS:getDimensions())
    FRUITSHEET['grapes'] = love.graphics.newQuad(4 * 16, 0, 16, 16, FRUITATLAS:getDimensions())
    FRUITSHEET['bell'] = love.graphics.newQuad(5 * 16, 0, 16, 16, FRUITATLAS:getDimensions())
    FRUITSHEET['galaxian'] = love.graphics.newQuad(6 * 16, 0, 16, 16, FRUITATLAS:getDimensions())
    FRUITSHEET['key'] = love.graphics.newQuad(6 * 16, 0, 16, 16, FRUITATLAS:getDimensions())

    MAPATLAS = love.graphics.newImage('assets/img/pacmanSpriteSheet.png')
    MAPSHEET[1] = love.graphics.newQuad(0 * 16, 0, 16, 16, MAPATLAS:getDimensions())
    MAPSHEET[2] = love.graphics.newQuad(1 * 16, 0, 16, 16, MAPATLAS:getDimensions())
    MAPSHEET[3] = love.graphics.newQuad(2 * 16, 0, 16, 16, MAPATLAS:getDimensions())
    MAPSHEET[4] = love.graphics.newQuad(3 * 16, 0, 16, 16, MAPATLAS:getDimensions())
    MAPSHEET[5] = love.graphics.newQuad(4 * 16, 0, 16, 16, MAPATLAS:getDimensions())
    MAPSHEET[6] = love.graphics.newQuad(5 * 16, 0, 16, 16, MAPATLAS:getDimensions())
    MAPSHEET[9] = love.graphics.newQuad(6 * 16, 0, 16, 16, MAPATLAS:getDimensions())
    MAPSHEET[8] = love.graphics.newQuad(7 * 16, 0, 16, 16, MAPATLAS:getDimensions())

    -- Charge les sons du jeu
    S_INTRO = love.audio.newSource('assets/sfx/pacman_beginning.wav', 'static')
    S_DOT = love.audio.newSource('assets/sfx/pacman_chomp.wav', 'static')
    S_DEATH = love.audio.newSource('assets/sfx/pacman_death.wav', 'static')
    S_EATGHOST = love.audio.newSource('assets/sfx/pacman_eatghost.wav', 'static')
    S_EATFRUIT = love.audio.newSource('assets/sfx/pacman_eatfruit.wav', 'static')
    S_READY = love.audio.newSource('assets/sfx/pacman_intermission.wav', 'static')
    S_EXTRA = love.audio.newSource('assets/sfx/pacman_extrapac.wav', 'static')

    getHighScore() -- Récupère les meilleurs scores sauvegardés
    S_INTRO:play() -- Démarre la musique d'introduction
end

-- ============================
-- BOUCLE DE JEU
-- ============================
 

-- Ces fonctions sont appelées en continu pendant l'exécution du jeu.
function love.update(dt) -- Met à jour la logique du jeu
    if PAUSE then return end -- Si le jeu est en pause, rien ne se passe
    pacMan_states[CURRENTSTATE].update(dt) -- Appelle la fonction de mise à jour pour l'état actuel
end

function love.draw() -- Dessine tous les éléments à l'écran
    pacMan_states[CURRENTSTATE].draw() -- Appelle la fonction de dessin pour l'état actuel
end

function love.keypressed(key, scancode, isRepeat) -- Gère les entrées clavier
    if key == 'm' then
        if SOUNDVOL == 1 then SOUNDVOL = 0 else SOUNDVOL = 1 end
        love.audio.setVolume(SOUNDVOL) -- Active/désactive le son
    end
    pacMan_states[CURRENTSTATE].keypressed(key) -- Gère l'entrée clavier pour l'état actuel
end

-- ============================
-- FONCTIONS D'AFFICHAGE ET UTILITAIRES
-- ============================
 

function drawMap() -- Dessine le labyrinthe et les éléments de jeu
    -- Boucles pour parcourir la map
    for j = 1, #MAP do
        for i = 1, #MAP[j] do
            ii = i - 1
            jj = j - 1
            local curChar = MAP[j][i]
            if curChar > 0 then
                -- Dessine les éléments du labyrinthe
                love.graphics.draw(MAPATLAS, MAPSHEET[curChar], ii * BLOCKSIZE * PPM, jj * BLOCKSIZE * PPM, 0, PPM, PPM)
            end
            local collectChar = COLLECTABLE[j][i]
            if collectChar > 0 then
                -- Dessine les pac-gommes et super pac-gommes
                love.graphics.draw(MAPATLAS, MAPSHEET[collectChar], ii * BLOCKSIZE * PPM, jj * BLOCKSIZE * PPM, 0, PPM, PPM)
            end
            local fruitChar = FRUIT[j][i]
            if fruitChar > 0 then
                -- Dessine les fruits bonus
                love.graphics.draw(
                    FRUITATLAS, FRUITSHEET[levelSpec[LEVEL].bonus],
                    ii * BLOCKSIZE * PPM + BLOCKSIZE * PPM * 0.5,
                    jj * BLOCKSIZE * PPM + BLOCKSIZE * PPM * 0.5,
                    0, PPM * 1.6, PPM * 1.6,
                    16 * 0.5, 16 * 0.5
                )
            end
        end
    end

    if (DEBUG) then -- Affichage de debug pour les développeurs
        for j = 1, #MAP do
            for i = 1, #MAP[j] do
                ii = i - 1
                jj = j - 1
                local curChar = MAP[j][i]
                if curChar > 0 then
                    love.graphics.print(curChar, ii * BLOCKSIZE * PPM, jj * BLOCKSIZE * PPM)
                    love.graphics.rectangle("line", ii * BLOCKSIZE * PPM, jj * BLOCKSIZE * PPM, PPM * BLOCKSIZE, PPM * BLOCKSIZE)
                end
            end
        end
    end
end

function animate(this, dt) -- Gère l'animation des sprites (Pac-Man et fantômes)
    this.animTimer = this.animTimer - dt
    if this.animTimer <= 0 then
        this.animTimer = 1 / this.fps
        this.keyframe = this.keyframe + 1
        if this.keyframe > this.nbrFrame then this.keyframe = 1 end
    end
end

function handleDirection(this) -- Gère l'orientation du personnage
    if this.direction == 'left' then
        this.scaleSignX = -1
        this.scaleSignY = 1
        this.angle = 0
    elseif this.direction == 'right' then
        this.scaleSignX = -1
        this.scaleSignY = -1
        this.angle = math.pi
    elseif this.direction == 'up' and this == pacMan then
        this.scaleSignX = -1
        this.scaleSignY = 1
        this.angle = math.pi * 0.5
    elseif this.direction == 'down' and this == pacMan then
        this.scaleSignX = -1
        this.scaleSignY = 1
        this.angle = math.pi * 3 * 0.5
    end
end

function round(val) -- Fonction pour arrondir un nombre
    local floor = math.floor(val)
    if (val % 1 >= 0.5) then return floor + 1 end
    return floor
end

function clamp(val, min, max) -- Limite une valeur dans un intervalle
    if val < min then return min
    elseif val > max then return max
    else return val
    end
end

function writeScore() -- Écrit le score dans un fichier
    local tmp = {}
    tmp[1] = pacMan.score
    for i = 1, #HIGHSCORE do
        table.insert(tmp, HIGHSCORE[i])
    end
    local res = ''
    for i = 1, #tmp do
        res = res .. tmp[i] .. '\n'
    end
    local f = io.open('highscore.score', 'w+')
    f:write(res)
    f:close()
end

function fileExists(name) -- Vérifie l'existence d'un fichier
    local f = io.open(name, "r")
    if f ~= nil then io.close(f) return true else return false end
end

function linesFrom(file) -- Lit un fichier ligne par ligne
    if not fileExists(file) then return {0} end
    lines = {}
    for line in io.lines(file) do
        lines[#lines + 1] = tonumber(line)
    end
    return lines
end

function getHighScore() -- Récupère les scores enregistrés
    if fileExists('highscore.score') then
        HIGHSCORE = linesFrom('highscore.score')
    else
        local f = io.open('highscore.score', 'w')
        f:write('0')
        f:close()
        HIGHSCORE = {0}
    end
end 

vendredi 25 juillet 2025

Le Simon : Plus Qu'un Jeu, Un Entraînement Cérébral Lumineux !




Le Simon : Plus Qu'un Jeu, Un Entraînement Cérébral Lumineux !

Le jeu Simon, avec ses quatre couleurs vives et ses mélodies entraînantes, a marqué des générations entières depuis son apparition. Mais au-delà de son apparence ludique, ce classique du jeu électronique est un formidable outil pour stimuler la mémoire et la concentration. Plongeons dans l'histoire de ce phénomène et découvrons comment il prend vie grâce à la programmation.


Une Brève Histoire du Simon

L'histoire du Simon débute en 1978, quand Ralph H. Baer (souvent surnommé "le père des jeux vidéo") et Howard J. Morrison, sous la houpe de la société Milton Bradley (aujourd'hui partie de Hasbro), lui donnent naissance. L'inspiration pour ce jeu serait venue d'une partie de "Simon Says" (Jacques a dit) et des tonalités musicales. L'objectif était simple : créer un jeu électronique portable qui défie la mémoire auditive et visuelle des joueurs.

Le succès fut immédiat et phénoménal. Le Simon est rapidement devenu un jouet emblématique des années 80, reconnaissable à son design circulaire distinctif, ses quatre grands boutons colorés (rouge, vert, bleu, jaune) et ses sons synthétiques caractéristiques. Il a connu de multiples incarnations et a inspiré une multitude de jeux et d'applications, prouvant que même les concepts les plus simples peuvent offrir un défi illimité.


L'Objectif du Jeu : Mémoriser et Reproduire

Le principe du Simon est d'une simplicité désarmante, ce qui contribue à son succès intemporel :

  1. Le jeu montre une séquence de lumières et de sons.

  2. Le joueur doit la reproduire en appuyant sur les boutons dans le bon ordre.

  3. À chaque séquence réussie, le jeu en ajoute une nouvelle, allongeant la séquence précédente d'un élément aléatoire, augmentant ainsi le défi.

  4. Une seule erreur et la partie est terminée !

Ce mécanisme simple mais addictif fait appel à la mémoire à court terme et à la concentration, poussant le joueur à s'améliorer constamment.


Le Simon en LÖVE2D : Décryptage Pédagogique du Code

Vous avez récemment développé votre propre version du Simon avec LÖVE2D, un framework génial pour créer des jeux en Lua. Voyons comment les différentes composantes du jeu sont orchestrées dans votre code.

1. Les États du Jeu : Le Cœur de la Logique

Notre programme est structuré autour de différents états de jeu. C'est une approche fondamentale en développement de jeux pour gérer les différentes phases d'une partie :

  • START: L'écran d'accueil où le joueur attend de commencer.

  • PRESENTING_SOUNDS: Une phase d'introduction où le jeu présente chaque bouton avec son son unique, permettant au joueur de se familiariser avec les associations.

  • COUNTDOWN: Un compte à rebours visuel qui offre une pause et prépare le joueur avant le début d'une nouvelle séquence à mémoriser.

  • SHOWING_SEQUENCE: Le moment où le jeu montre la séquence de lumières et de sons que le joueur doit retenir.

  • WAITING_INPUT: Le joueur a les commandes et doit reproduire la séquence en cliquant sur les boutons.

  • GAME_OVER: La partie est terminée suite à une erreur du joueur.

Le code change de comportement en fonction de l'état actuel, géré par la variable game.state. C'est une méthode très propre pour organiser un jeu.

2. Les Boutons : Des Objets Intelligents

Au lieu d'utiliser de simples listes de nombres, nous avons défini les boutons comme des tables Lua avec des propriétés nommées (par exemple, {x = 50, y = 50, width = 100, soundName = "sound1.mp3"}). Cette approche offre une plus grande clarté :

  • Lisibilité: btn.x est beaucoup plus parlant que btn[1].

  • Maintenance: Si nous ajoutons de nouvelles propriétés aux boutons, cela ne perturbera pas l'ordre des autres.

  • Organisation: Chaque bouton est une entité complète avec toutes ses caractéristiques (position, taille, couleur, son, état d'illumination et de visibilité).

Lors du dessin, le programme parcourt cette liste de boutons, vérifie leur état (isHighlighted pour l'effet orange et isVisible pour leur apparition progressive) et les dessine en conséquence, incluant l'effet de grossissement visuel.

3. La Séquence : Le Défi de la Mémoire

La game.sequence est une table Lua qui stocke les index des boutons (1, 2, 3, etc.) dans l'ordre que le joueur doit reproduire.

  • startNewRound(): C'est la fonction clé qui gère l'allongement de la séquence. À chaque nouveau tour, elle utilise math.random(1, #game.buttons) pour choisir un nouveau bouton aléatoire et l'ajoute à la game.sequence existante.

  • math.randomseed(os.time()): C'est une ligne cruciale située dans love.load(). Sans elle, le générateur de nombres aléatoires de Lua produirait toujours la même séquence au démarrage du jeu. En utilisant os.time() (l'heure actuelle en secondes) comme "graine", vous assurez que chaque nouvelle partie commencée aura une séquence de départ différente et imprévisible.

4. La Temporalité : Gérer le Rythme du Jeu

Le jeu Simon repose entièrement sur le timing. Nous utilisons des timers (game.sequenceTimer, game.presentationTimer, game.countdownTimer) et la variable dt (delta time, le temps écoulé depuis la dernière frame) dans la fonction love.update() pour gérer précisément quand les lumières s'allument, les sons se jouent, et quand le jeu passe à l'étape suivante.

  • CONSTANTS: Regrouper toutes les durées importantes dans une table CONSTANTS au début du code est une excellente pratique. Cela rend le code plus propre et extrêmement facile à ajuster (vous pouvez changer la vitesse du jeu en modifiant une seule valeur à un seul endroit).

5. Interaction et Sons

  • love.mousepressed(): Cette fonction détecte les clics de souris. Quand le jeu est dans l'état WAITING_INPUT, elle vérifie si le clic correspond à l'un des boutons.

  • game.sounds[index]:play(): Chaque bouton a un son préchargé qui est joué quand il est activé par le jeu ou cliqué par le joueur. Le son de succès (success.mp3) et d'échec (lost.mp3) ajoutent une dimension auditive cruciale au feedback du joueur.

    Vous pouvez telecharger le code source et les sons sur https://arcadeforge.itch.io/simon 



 
 
 
Voici le code du jeu :
 
-- main.lua

-- Déclaration des variables globales du jeu
local game = {}

-- États du jeu
game.state = "START" -- START, PRESENTING_SOUNDS, SHOWING_SEQUENCE, WAITING_INPUT, COUNTDOWN, GAME_OVER

-- Constantes du jeu (pour faciliter l'ajustement des timings)
local CONSTANTS = {
    TIME_PER_LIGHT = 0.5,           -- Durée qu'un bouton reste allumé dans la séquence de jeu
    TIME_BETWEEN_LIGHTS = 0.1,      -- Temps entre l'extinction d'un bouton et l'allumage du suivant
    PRESENTATION_LIGHT_DURATION = 0.8, -- Durée d'allumage pendant la présentation initiale
    PRESENTATION_PAUSE_DURATION = 0.2, -- Pause entre les boutons pendant la présentation
    COUNTDOWN_DURATION = 3,         -- Durée totale du compte à rebours
    BUTTON_GROW_AMOUNT = 5,         -- Nombre de pixels pour l'effet de grossissement des boutons
    FRAME_THICKNESS = 5             -- Épaisseur du cadre des boutons
}

-- Propriétés des boutons (maintenant avec des noms explicites)
game.buttons = {
    {x = 50, y = 50, width = 100, height = 100, r = 255, g = 0, b = 0, soundName = "sound1.mp3", isHighlighted = false, isVisible = false},   -- Bouton 1 (Rouge)
    {x = 170, y = 50, width = 100, height = 100, r = 0, g = 255, b = 0, soundName = "sound2.mp3", isHighlighted = false, isVisible = false},  -- Bouton 2 (Vert)
    {x = 50, y = 170, width = 100, height = 100, r = 0, g = 0, b = 255, soundName = "sound3.mp3", isHighlighted = false, isVisible = false},  -- Bouton 3 (Bleu)
    {x = 170, y = 170, width = 100, height = 100, r = 255, g = 255, b = 0, soundName = "sound4.mp3", isHighlighted = false, isVisible = false}, -- Bouton 4 (Jaune)
    {x = 110, y = 290, width = 100, height = 100, r = 128, g = 0, b = 128, soundName = "sound5.mp3", isHighlighted = false, isVisible = false} -- Bouton 5 (Violet)
}

-- Ordre dans lequel les boutons doivent être présentés au démarrage
game.presentationOrder = {1, 2, 3, 4, 5}

-- Séquence du jeu et progression du joueur
game.sequence = {}
game.playerSequenceIndex = 1

-- Timers et index pour les différentes phases
game.sequenceTimer = 0
game.sequenceIndex = 0
game.presentationTimer = 0
game.presentationIndex = 0
game.countdownTimer = 0
game.currentCountdownValue = 0

-- Objets AudioSource
game.sounds = {}
game.sounds.success = nil
game.sounds.lost = nil

-- Score du joueur
game.score = 0

-- Polices et messages
game.font = nil
game.countdownFont = nil
game.message = ""

function love.load()
    -- Initialise la graine aléatoire pour que les séquences changent à chaque démarrage du jeu
    math.randomseed(os.time())

    -- Charge les sons des boutons
    for i, btn in ipairs(game.buttons) do
        game.sounds[i] = love.audio.newSource("sound/" .. btn.soundName, "static")
    end
    -- Charge les sons de succès et d'échec
    game.sounds.success = love.audio.newSource("sound/success.mp3", "static")
    game.sounds.lost = love.audio.newSource("sound/lost.mp3", "static")

    love.window.setMode(320, 480, {resizable = false, fullscreen = false})
    love.window.setTitle("Simon Says LÖVE")

    game.message = "Cliquez pour commencer !"
    game.font = love.graphics.newFont(20)
    game.countdownFont = love.graphics.newFont(80)
end

function love.update(dt)
    if game.state == "PRESENTING_SOUNDS" then
        game.presentationTimer = game.presentationTimer + dt

        local currentButtonToPresentIndex = game.presentationOrder[game.presentationIndex]

        -- Éteint le bouton s'il a été allumé assez longtemps
        if currentButtonToPresentIndex and game.presentationTimer >= CONSTANTS.PRESENTATION_LIGHT_DURATION then
            game.buttons[currentButtonToPresentIndex].isHighlighted = false
        end

        -- Passe au bouton suivant si le temps total (lumière + pause) est écoulé
        if game.presentationTimer >= (CONSTANTS.PRESENTATION_LIGHT_DURATION + CONSTANTS.PRESENTATION_PAUSE_DURATION) then
            -- Assure que le bouton précédent est éteint
            if currentButtonToPresentIndex then
                 game.buttons[currentButtonToPresentIndex].isHighlighted = false
            end

            if game.presentationIndex < #game.presentationOrder then
                game.presentationIndex = game.presentationIndex + 1
                local nextButtonToPresentIndex = game.presentationOrder[game.presentationIndex]

                game.buttons[nextButtonToPresentIndex].isHighlighted = true
                game.buttons[nextButtonToPresentIndex].isVisible = true
                game.sounds[nextButtonToPresentIndex]:play()
                game.presentationTimer = 0
            else
                -- Tous les boutons ont été présentés, rend-les tous visibles et passe au compte à rebours
                for _, btn in ipairs(game.buttons) do
                    btn.isVisible = true
                end
                game.state = "COUNTDOWN"
                game.countdownTimer = 0
                game.message = "Préparez-vous !"
            end
        end

    elseif game.state == "SHOWING_SEQUENCE" then
        game.sequenceTimer = game.sequenceTimer + dt

        -- Éteint le bouton de la séquence s'il a été allumé assez longtemps
        if game.sequenceIndex > 0 and game.sequenceIndex <= #game.sequence then
            local currentSeqButtonIndex = game.sequence[game.sequenceIndex]
            if game.sequenceTimer >= CONSTANTS.TIME_PER_LIGHT then
                game.buttons[currentSeqButtonIndex].isHighlighted = false
            end
        end

        -- Passe au prochain élément de la séquence
        if game.sequenceTimer >= (CONSTANTS.TIME_PER_LIGHT + CONSTANTS.TIME_BETWEEN_LIGHTS) then
            -- Assure que le bouton précédent est éteint
            if game.sequenceIndex > 0 and game.sequenceIndex <= #game.sequence then
                local prevSeqButtonIndex = game.sequence[game.sequenceIndex]
                game.buttons[prevSeqButtonIndex].isHighlighted = false
            end

            if game.sequenceIndex < #game.sequence then
                game.sequenceIndex = game.sequenceIndex + 1
                local nextSeqButtonIndex = game.sequence[game.sequenceIndex]

                game.sounds[nextSeqButtonIndex]:stop()
                game.sounds[nextSeqButtonIndex]:play()
                game.buttons[nextSeqButtonIndex].isHighlighted = true
                game.sequenceTimer = 0
            else
                -- La séquence est entièrement jouée, attend l'entrée du joueur
                game.state = "WAITING_INPUT"
                game.message = "Votre tour !"
            end
        end

    elseif game.state == "COUNTDOWN" then
        game.countdownTimer = game.countdownTimer + dt
        game.currentCountdownValue = math.ceil(CONSTANTS.COUNTDOWN_DURATION - game.countdownTimer)
        if game.currentCountdownValue < 0 then game.currentCountdownValue = 0 end

        if game.countdownTimer >= CONSTANTS.COUNTDOWN_DURATION then
            game.state = "SHOWING_SEQUENCE"
            game.message = "Mémorisez la séquence..."
            startNewRound()
        end
    end
end

function love.draw()
    love.graphics.setBackgroundColor(0.1, 0.1, 0.1)

    for i, btn in ipairs(game.buttons) do
        -- Accès par nom de propriété
        local x, y, width, height, r, g, b = btn.x, btn.y, btn.width, btn.height, btn.r, btn.g, btn.b
        local isHighlighted = btn.isHighlighted
        local isVisible = btn.isVisible

        if not isVisible then
            -- Ne dessine pas si non visible
        else
            local draw_x, draw_y, draw_w, draw_h = x, y, width, height
            local current_frame_thickness = CONSTANTS.FRAME_THICKNESS

            if isHighlighted then
                local grow_amount = CONSTANTS.BUTTON_GROW_AMOUNT
                draw_x = x - grow_amount
                draw_y = y - grow_amount
                draw_w = width + (2 * grow_amount)
                draw_h = height + (2 * grow_amount)
                current_frame_thickness = CONSTANTS.FRAME_THICKNESS + grow_amount
            end

            -- Dessine le CADRE
            if isHighlighted then
                love.graphics.setColor(1, 0.5, 0, 1) -- Orange vif
            else
                love.graphics.setColor(0.3, 0.3, 0.3, 1) -- Gris foncé
            end
            love.graphics.rectangle("fill", draw_x - current_frame_thickness, draw_y - current_frame_thickness, draw_w + 2*current_frame_thickness, draw_h + 2*current_frame_thickness)

            -- Dessine la COULEUR INTERNE
            love.graphics.setColor(r / 255, g / 255, b / 255, 1)
            love.graphics.rectangle("fill", draw_x, draw_y, draw_w, draw_h)
        end
    end

    love.graphics.setColor(1, 1, 1, 1)
    love.graphics.setFont(game.font)
    love.graphics.printf(game.message, 0, 10, love.graphics.getWidth(), "center")
    love.graphics.printf("Score: " .. game.score, 0, 420, love.graphics.getWidth(), "center")

    if game.state == "GAME_OVER" then
        love.graphics.setColor(1, 0, 0, 1)
        love.graphics.printf("GAME OVER!", 0, love.graphics.getHeight() / 2 - 20, love.graphics.getWidth(), "center")
    end

    -- Affichage du compte à rebours
    if game.state == "COUNTDOWN" then
        love.graphics.setColor(1, 1, 1, 1)
        love.graphics.setFont(game.countdownFont)
        love.graphics.printf(tostring(game.currentCountdownValue), 0, love.graphics.getHeight() / 2 - 40, love.graphics.getWidth(), "center")
    end
end

function love.mousepressed(x, y, button, istouch)
    if game.state == "START" then
        game.state = "PRESENTING_SOUNDS"
        game.message = "Écoutez les sons..."

        game.presentationIndex = 1
        local firstButtonToPresentIndex = game.presentationOrder[game.presentationIndex]

        game.buttons[firstButtonToPresentIndex].isHighlighted = true
        game.buttons[firstButtonToPresentIndex].isVisible = true
        game.sounds[firstButtonToPresentIndex]:play()
        game.presentationTimer = 0

        return
    end

    if game.state == "WAITING_INPUT" then
        for i, btn in ipairs(game.buttons) do
            -- Vérifie si le clic est sur ce bouton
            if x >= btn.x and x <= btn.x + btn.width and y >= btn.y and y <= btn.y + btn.height then
                game.sounds[i]:stop()
                game.sounds[i]:play()
                
                game.buttons[i].isHighlighted = true

                if i == game.sequence[game.playerSequenceIndex] then
                    game.playerSequenceIndex = game.playerSequenceIndex + 1
                    if game.playerSequenceIndex > #game.sequence then
                        game.score = game.score + 1
                        game.message = "Bien joué ! Nouveau tour..."
                        game.sounds.success:play()
                        
                        game.state = "COUNTDOWN"
                        game.countdownTimer = 0
                    end
                else
                    game.state = "GAME_OVER"
                    game.message = "Perdu ! Score final: " .. game.score
                    game.sounds.lost:play()
                end
                break -- Sort de la boucle une fois le bouton trouvé
            end
        end
    end
end

function love.mousereleased(x, y, button, istouch)
    -- Éteint les boutons seulement si le jeu est en attente d'entrée du joueur
    if game.state == "WAITING_INPUT" then
        for i, btn in ipairs(game.buttons) do
            btn.isHighlighted = false
        end
    end
end

function startNewRound()
    local newButtonIndex = math.random(1, #game.buttons)
    table.insert(game.sequence, newButtonIndex)
    game.playerSequenceIndex = 1

    game.sequenceIndex = 1
    local firstSeqButtonIndex = game.sequence[game.sequenceIndex]
    game.buttons[firstSeqButtonIndex].isHighlighted = true
    game.sounds[firstSeqButtonIndex]:stop()
    game.sounds[firstSeqButtonIndex]:play()

    game.sequenceTimer = 0

    -- S'assure que tous les autres boutons sont éteints, mais restent visibles
    for i, btn in ipairs(game.buttons) do
        if i ~= firstSeqButtonIndex then
            btn.isHighlighted = false
        end
    end
end

function love.keypressed(key)
    if game.state == "GAME_OVER" and key == "r" then
        resetGame()
    end
end

function resetGame()
    game.sequence = {}
    game.score = 0
    game.playerSequenceIndex = 1
    game.state = "START"
    game.message = "Cliquez pour commencer !"
    for i, btn in ipairs(game.buttons) do
        btn.isHighlighted = false
        btn.isVisible = false
    end
    game.presentationTimer = 0
    game.presentationIndex = 0
    game.countdownTimer = 0
    game.currentCountdownValue = 0
end 

jeudi 3 juillet 2025

Plongez dans l'Univers des Machines d'Arcade

 


Plongez dans l'Univers des Machines d'Arcade : Bien Plus que de Simples Jeux ! 🕹️

Chez ArcadeForge, notre passion, c'est l'arcade, et nous savons que vous la partagez ! Mais qu'est-ce qui rend une machine d'arcade si spéciale ? Au-delà du simple jeu, c'est une véritable icône culturelle, un objet de design, et une prouesse technologique qui a traversé les décennies.


Une Brève Histoire des Géants du Gaming 👾

Les machines d'arcade ne sont pas nées d'hier. Leurs origines remontent aux flippers et aux jeux électromécaniques des années 1930. Mais c'est dans les années 70 que la révolution numérique a vraiment commencé, avec des titres emblématiques comme Pong, qui a ouvert la voie à l'âge d'or de l'arcade dans les années 80.

Qui n'a pas de souvenirs de sessions intenses devant Pac-Man, Donkey Kong, Space Invaders, ou Street Fighter II ? Ces machines n'étaient pas seulement des jeux, elles étaient des centres sociaux, des lieux de rencontre et de compétition, où l'on pouvait défier ses amis et faire son meilleur score, inscrit en lettres capitales sur l'écran.

Qu'est-ce qui Définit une Machine d'Arcade ? La Forme et le Fond 🎮

Une machine d'arcade, c'est un ensemble harmonieux de plusieurs éléments clés :

  • Le Cabinet (la Borne) : C'est l'enveloppe extérieure, souvent en bois ou en métal, avec des illustrations vibrantes (le "side art", le "marquee" en haut, et le "control panel art"). Il existe de nombreuses formes : les bornes verticales classiques, les tables cocktail (où l'écran est plat et le jeu se joue de chaque côté), et les bornes assises pour les jeux de course ou de tir.

  • L'Écran : Historiquement, des écrans CRT (cathodiques) offraient ce look si particulier avec des couleurs vives et un léger balayage. Aujourd'hui, les répliques modernes utilisent souvent des écrans LCD avec des filtres pour recréer l'esthétique rétro.

  • Les Contrôles : Le joystick et les boutons sont les éléments les plus interactifs. Leur qualité et leur réactivité sont essentielles pour l'expérience de jeu. Selon le type de jeu, on peut trouver aussi des volants, des pistolets, des trackballs, ou des spinners.

  • Le Système Interne : C'est le cerveau de la machine, composé d'une carte mère (PCB) ou, pour les machines modernes et les systèmes multi-jeux, d'un PC ou d'un mini-ordinateur avec un émulateur.

  • Le Son : Les sons iconiques de l'arcade sont une part immense de l'expérience, des bips de Pac-Man aux explosions de Space Invaders, en passant par les voix digitalisées de Mortal Kombat.

Pourquoi l'Arcade Fascine Toujours Aujourd'hui ? ✨

Malgré l'avènement des consoles de salon et du jeu sur PC, l'attrait des machines d'arcade reste intact, et même grandissant :

  1. L'Expérience Unique : Rien ne remplace la sensation d'un joystick solide et de gros boutons physiques, ou l'immersion qu'offre une borne dédiée à un seul jeu.

  2. La Nostalgie : Pour beaucoup, c'est un voyage direct vers l'enfance, un rappel des après-midis passés dans les salles d'arcade.

  3. L'Objet de Collection : Une borne d'arcade est une pièce de design à part entière, un élément de décoration qui témoigne d'une époque révolue et d'une passion pour le jeu vidéo.

  4. La Simplicité et le "Pick-up and Play" : Souvent, les jeux d'arcade sont faciles à prendre en main mais difficiles à maîtriser, offrant un défi immédiat sans les complexités des jeux modernes.

  5. Le Plaisir du Partage : Les machines d'arcade sont faites pour le multijoueur local, le défi entre amis ou en famille, recréant l'ambiance conviviale des salles d'antan.

L'Ère du Rétrogaming : Faire Revivre les Classiques à la Maison 🏡

Le concept de "rétrogaming" a insufflé une nouvelle vie aux machines d'arcade. Il ne s'agit plus seulement de trouver une borne d'époque dans son jus, mais de recréer l'expérience arcade chez soi, souvent avec une touche de modernité.

Sur quelles machines s'opère le rétrogaming ?

  • Bornes "Multi-Jeux" Modernes : C'est la solution la plus populaire. Ces bornes, qu'elles soient achetées prêtes à l'emploi ou assemblées en kit, embarquent un système informatique (souvent un Raspberry Pi ou un mini-PC) et des émulateurs comme MAME, RetroPie ou Batocera. Elles peuvent contenir des milliers de jeux issus de l'âge d'or de l'arcade et même de consoles de salon rétro (NES, SNES, Megadrive, PlayStation 1, etc.).

  • Bornes "Plug-and-Play" (comme Arcade1Up) : Ces répliques à échelle réduite ou grandeur nature sont préchargées avec une sélection de jeux spécifiques. Elles sont parfaites pour ceux qui veulent une solution simple et un design authentique sans la complexité du montage ou de la configuration logicielle.

  • Restaurations de Bornes Originales : Pour les puristes, rien ne vaut la restauration d'une borne d'arcade d'époque. Cela demande souvent des compétences en électronique et en menuiserie, mais le résultat est une pièce d'histoire fonctionnelle.

  • "Bartop Arcades" et Mini-Bornes : Idéales pour les petits espaces, ces versions compactes reproduisent l'expérience arcade sur une table ou un comptoir.

Quels projets sont les plus populaires dans le rétrogaming ?

L'engouement pour le rétrogaming a donné naissance à de nombreux projets DIY (Do It Yourself) :

  • Construction de Bartops et de Bornes complètes : Des plans et des tutoriels abondent en ligne pour guider les passionnés dans la fabrication de leur propre borne d'arcade, du bois au câblage des contrôles.


     

  • Intégration de Raspberry Pi : Le petit ordinateur monocarte est devenu le cœur de la plupart des projets de rétrogaming, grâce à sa polyvalence et aux systèmes d'exploitation dédiés comme RetroPie ou Batocera.

  • Customisation et Décoration : Personnaliser l'apparence de sa borne avec des artworks inspirés de ses jeux préférés est une étape clé pour de nombreux passionnés.

  • Mise à jour de l'électronique : Remplacer les anciens écrans CRT par des LCD modernes tout en conservant l'esthétique rétro est un défi technique populaire.


Quelle est votre machine d'arcade préférée de tous les temps ? Partagez vos souvenirs et vos projets dans les commentaires ! 👇


lundi 16 juin 2025

Ramenez les Années 80 dans Votre Code avec un Clone de Missile Command en LÖVE !


Arcade-Forge : Plongée dans l'Histoire de Missile Command et Construction de Notre Propre Défense Atomique en LÖVE !

Salut les archivistes de l'arcade et les futurs développeurs de jeux !

Aujourd'hui, chez Arcade-Forge, nous allons rendre hommage à l'un des jeux les plus emblématiques et anxiogènes de l'histoire du jeu vidéo : Missile Command. Préparez-vous à un voyage dans le temps jusqu'à l'apogée de la Guerre Froide, puis à une plongée dans le code pour recréer ce classique avec le framework LÖVE2D.

L'Histoire Dramatique de Missile Command : Un Classique Né de la Peur

Missile Command, sorti en juillet 1980, n'est pas juste un jeu d'arcade, c'est un véritable témoignage de son époque.

  • Qui l'a créé ? Le jeu a été développé par Atari, avec une conception principale attribuée à Dave Theurer. L'idée lui est venue après une nuit d'anxiété et de cauchemars récurrents sur l'holocauste nucléaire, une peur palpable durant la Guerre Froide. Il voulait créer un jeu qui transmette cette tension et cette impuissance face à une menace écrasante.
  • Quand est-il sorti ? En plein été 1980. Le contexte géopolitique de l'époque (tensions entre les États-Unis et l'Union Soviétique, course aux armements nucléaires) a indéniablement contribué à la résonance du jeu auprès du public.
  • A-t-il eu du succès tout de suite ? Oui, un succès retentissant ! Missile Command a été un hit immédiat dans les salles d'arcade. Son gameplay unique, son thème intense et son ambiance sonore et visuelle (avec le célèbre "cloc" des missiles qui montent et le "bip" des alertes) ont captivé les joueurs. Il était non seulement divertissant mais aussi profondément marquant, les laissant souvent avec un sentiment d'urgence et d'inéluctabilité. Il est devenu l'un des jeux d'arcade les plus rentables d'Atari.

Missile Command est resté gravé dans les mémoires comme un chef-d'œuvre de design qui transcende le simple divertissement pour toucher à des thèmes plus profonds.

Qu'est-ce que Missile Command ? Le But, le Gameplay, la Tension

Le But du Jeu : Votre mission est simple, mais vitale : défendre six villes (ou bases) situées en bas de l'écran contre des vagues incessantes de missiles balistiques ennemis tombant du ciel.

Comment y Jouer : Le joueur dispose de trois bases de lancement de missiles (dans le jeu original, trois sites de lancement, un central et deux latéraux).

  1. Ciblage : À l'aide d'un trackball (ou de la souris dans notre version), vous déplacez un viseur sur l'écran.
  2. Tir : En cliquant, vous tirez un missile intercepteur depuis l'une de vos bases (la plus proche du point ciblé). Ce missile s'élève et explose à l'endroit que vous avez ciblé.
  3. L'Explosion Salvatrice : Le but est de faire exploser votre missile juste avant qu'un missile ennemi n'atteigne le sol. L'explosion de votre missile crée une boule de feu qui détruit tous les missiles ennemis qui la traversent.
  4. Gestion des Munitions : Dans l'original, chaque base avait un stock limité de missiles. Une fois vides, elles ne pouvaient plus tirer. Les bases pouvaient aussi être détruites par les missiles ennemis.
  5. Perte du Jeu : Le jeu se termine lorsque toutes vos villes (et vos bases dans certaines versions ou si vous manquez de munitions) sont détruites. La musique emblématique et l'écran final montrant l'annihilation de vos villes contribuent à la gravité du propos.

Missile Command est un jeu d'anticipation et de gestion de ressources sous pression, où chaque décision compte et le sentiment d'être submergé est quasi constant.

Notre Projet : Recréer Missile Command avec LÖVE2D

Inspirés par ce monument, nous avons entrepris de recréer une version simplifiée de Missile Command en utilisant LÖVE2D. LÖVE est un framework de développement de jeux léger et puissant, parfait pour les débutants grâce à sa simplicité et au langage Lua.

L'objectif de ce projet était double : rendre hommage à un classique et surtout, montrer comment les mécaniques de base d'un jeu peuvent être construites pas à pas. Nous avons rencontré des défis typiques, comme la gestion des collisions et la logique de suppression d'objets, mais chaque obstacle fut une occasion d'apprendre !


 

Le Code Commenté : Décortiquer la Défense Atomique pour les Débutants

Maintenant, il est temps de plonger dans le cœur de notre clone de Missile Command. Nous avons commenté le code de manière très détaillée pour que même un programmeur débutant puisse comprendre chaque ligne.

-- Arcade-forge 

-- main.lua
-- C'est le fichier principal de ton jeu LÖVE. Tout le code que LÖVE va exécuter est ici.

---
-- [[
--    ==========================
--    1. Définition des Constantes
--    ==========================
--    Les constantes sont des valeurs qui ne changent pas pendant l'exécution du jeu.
--    C'est une bonne pratique de les mettre en haut pour les modifier facilement si besoin.
-- ]]
---

local SCREEN_WIDTH = 800  -- Largeur de l'écran de jeu en pixels (points sur l'écran)
local SCREEN_HEIGHT = 600 -- Hauteur de l'écran de jeu en pixels

local BASE_COUNT = 3    -- Nombre de bases (lanceurs de missiles) du joueur
local CITY_COUNT = 6    -- Nombre de villes à protéger

local MISSILE_SPEED = 200     -- Vitesse des missiles ennemis (en pixels par seconde)
local PLAYER_MISSILE_SPEED = 400 -- Vitesse des missiles que le joueur tire

local EXPLOSION_LIFETIME = 0.5 -- Durée de vie d'une explosion en secondes
local EXPLOSION_RADIUS = 50   -- Rayon initial d'une explosion (quand elle apparaît)

-- Dimensions des éléments du jeu pour les collisions.
-- On les définit ici pour être cohérent avec le dessin et la détection.
local CITY_WIDTH = 30   -- Largeur d'une ville
local CITY_HEIGHT = 15  -- Hauteur d'une ville
local BASE_WIDTH = 40   -- Largeur d'une base
local BASE_HEIGHT = 20  -- Hauteur d'une base

---
-- [[
--    ==========================
--    2. Déclaration des Tables (Listes d'Objets)
--    ==========================
--    Les "tables" en Lua sont comme des listes ou des ensembles d'éléments.
--    Ici, chaque table va contenir tous les objets d'un certain type dans le jeu.
--    Par exemple, 'enemyMissiles' contiendra tous les missiles ennemis qui sont actuellement en l'air.
-- ]]
---

local bases = {}        -- Une table pour stocker toutes les bases du joueur
local cities = {}       -- Une table pour stocker toutes les villes à protéger
local enemyMissiles = {} -- Une table pour stocker tous les missiles ennemis
local playerMissiles = {} -- Une table pour stocker tous les missiles tirés par le joueur
local explosions = {}   -- Une table pour stocker toutes les explosions actives

---
-- [[
--    ==========================
--    3. Variables de Jeu Générales
--    ==========================
--    Ces variables gardent une trace de l'état global du jeu.
-- ]]
---

local score = 0      -- Le score du joueur, commence à zéro
local gameOver = false -- Un "drapeau" (flag) qui devient vrai (true) quand le jeu est terminé

---
-- [[
--    ==========================
--    4. love.load()
--    ==========================
--    Cette fonction est la première à être exécutée quand le jeu démarre.
--    C'est ici qu'on prépare tout : on initialise les éléments, on charge les ressources.
-- ]]
---
function love.load()
    -- Définit le titre de la fenêtre du jeu
    love.window.setTitle("LÖVE Missile Command")
    -- Configure la fenêtre : largeur, hauteur, non redimensionnable, et synchronisation verticale (pour éviter le "tearing")
    love.window.setMode(SCREEN_WIDTH, SCREEN_HEIGHT, {resizable = false, vsync = true})

    -- Initialisation des bases du joueur
    -- On calcule un espacement pour qu'elles soient bien réparties sur la largeur de l'écran
    local baseSpacing = SCREEN_WIDTH / (BASE_COUNT + 1)
    for i = 1, BASE_COUNT do -- Boucle pour créer chaque base
        -- Chaque base est une petite table avec sa position (x, y) et son état (vivante ou non)
        -- Les bases sont placées vers le bas de l'écran (SCREEN_HEIGHT - 50)
        bases[i] = {x = i * baseSpacing, y = SCREEN_HEIGHT - 50, alive = true}
    end

    -- Initialisation des villes
    -- Même principe que pour les bases, mais plus près du bas de l'écran
    local citySpacing = SCREEN_WIDTH / (CITY_COUNT + 1)
    for i = 1, CITY_COUNT do -- Boucle pour créer chaque ville
        -- Les villes sont placées encore plus près du bas de l'écran (SCREEN_HEIGHT - 20)
        cities[i] = {x = i * citySpacing, y = SCREEN_HEIGHT - 20, alive = true}
    end

    -- Charge une police de caractères par défaut pour afficher le texte (score, Game Over)
    love.graphics.setFont(love.graphics.newFont(20)) -- Taille de la police 20
end

---
-- [[
--    ==========================
--    5. love.update(dt)
--    ==========================
--    Cette fonction est appelée à *chaque image* du jeu.
--    C'est ici que toute la logique du jeu est mise à jour : les mouvements, les collisions, l'apparition des ennemis, etc.
--    'dt' (delta time) est un paramètre très important : c'est le temps écoulé depuis la dernière fois que love.update a été appelée.
--    Utiliser 'dt' permet de rendre la vitesse des objets indépendante de la vitesse de ton ordinateur (du nombre d'images par seconde).
-- ]]
---
function love.update(dt)
    if gameOver then return end -- Si le jeu est terminé, on ne fait rien dans la fonction update

    -- Mise à jour des missiles ennemis
    -- On parcourt la table 'enemyMissiles' à l'envers (de la fin vers le début).
    -- C'est important de faire ça quand on supprime des éléments d'une table pendant qu'on la parcourt,
    -- pour éviter des erreurs d'index.
    for i = #enemyMissiles, 1, -1 do
        local missile = enemyMissiles[i] -- On prend le missile actuel
        missile.y = missile.y + MISSILE_SPEED * dt -- Le missile descend (sa position y augmente)

        local targetHit = false -- Un drapeau pour savoir si ce missile a touché une cible au sol (base ou ville)

        -- 1. Vérifier les collisions avec les BASES
        -- On vérifie seulement si le missile est arrivé assez bas pour potentiellement toucher une base.
        -- 'bases[1].y - BASE_HEIGHT / 2 - 15' est une estimation du haut de la zone des bases.
        if missile.y >= (bases[1].y - BASE_HEIGHT / 2 - 15) then
            -- Cette boucle parcourt toutes les 'bases'.
            -- 'ipairs(bases)' : C'est une fonction qui parcourt les éléments d'une table numérotée (comme une liste).
            --                  Elle renvoie à chaque fois deux choses : l'index (1, 2, 3...) et la valeur de l'élément.
            -- '_' (underscore) : Ici, on n'a pas besoin de l'index numérique de la base (est-ce la 1ère, la 2ème ? Peu importe).
            --                    L'underscore est une convention en Lua pour dire "j'ignore cette valeur".
            -- 'base' : Cette variable va contenir l'objet 'base' lui-même (la table {x=..., y=..., alive=...}) à chaque tour de boucle.
            --          C'est cette variable 'base' qu'on utilisera ensuite pour vérifier ses propriétés.
            for _, base in ipairs(bases) do
                local base = base -- On prend la base actuelle (oui, le nom de la variable est le même que le paramètre de la boucle, c'est OK ici)
                -- Si la base est vivante ET que le missile est dans la zone de collision de la base
                if base.alive and
                   missile.x > base.x - BASE_WIDTH / 2 and missile.x < base.x + BASE_WIDTH / 2 and
                   missile.y > base.y - BASE_HEIGHT / 2 and missile.y < base.y + BASE_HEIGHT / 2 then
                    base.alive = false -- La base est détruite (elle n'est plus vivante)
                    targetHit = true   -- On indique que ce missile a touché quelque chose
                    break              -- Cesser de vérifier les autres bases pour ce missile (il n'en détruit qu'une)
                end
            end
        end

        -- 2. Vérifier les collisions avec les VILLES
        -- On ne vérifie les villes QUE SI le missile n'a PAS déjà touché une base.
        -- Et si le missile est arrivé assez bas pour potentiellement toucher une ville.
        if not targetHit and missile.y >= (cities[1].y - CITY_HEIGHT / 2 - 15) then
            for _, city in ipairs(cities) do -- Même logique de parcours que pour les bases
                local city = city -- On prend la ville actuelle
                -- Si la ville est vivante ET que le missile est dans la zone de collision de la ville
                if city.alive and
                   missile.x > city.x - CITY_WIDTH / 2 and missile.x < city.x + CITY_WIDTH / 2 and
                   missile.y > city.y - CITY_HEIGHT / 2 and missile.y < city.y + CITY_HEIGHT / 2 then
                    city.alive = false -- La ville est détruite
                    targetHit = true   -- On indique que ce missile a touché quelque chose
                    break              -- Cesser de vérifier les autres villes pour ce missile
                end
            end
        end

        -- 3. Retirer le missile
        -- Si le missile a touché une cible OU s'il a dépassé le bas de l'écran, on le retire.
        if targetHit or missile.y >= SCREEN_HEIGHT then
            table.remove(enemyMissiles, i) -- Retire le missile de la table
        end
    end

    -- Mise à jour des missiles du joueur
    for i = #playerMissiles, 1, -1 do
        local missile = playerMissiles[i] -- Prend le missile du joueur actuel
        -- Calcule la direction vers la cible (où le joueur a cliqué/touché)
        local dx = missile.targetX - missile.x
        local dy = missile.targetY - missile.y
        local dist = math.sqrt(dx*dx + dy*dy) -- Calcule la distance restante jusqu'à la cible

        if dist > 0 then -- Si le missile n'est pas encore sur sa cible
            -- Calcule comment le missile doit se déplacer pour atteindre la cible en fonction de sa vitesse et du temps écoulé (dt)
            local speedX = (dx / dist) * PLAYER_MISSILE_SPEED * dt
            local speedY = (dy / dist) * PLAYER_MISSILE_SPEED * dt

            missile.x = missile.x + speedX -- Déplace le missile horizontalement
            missile.y = missile.y + speedY -- Déplace le missile verticalement

            -- Vérifie si le missile a atteint ou dépassé sa cible.
            -- C'est une façon robuste de le faire, même si le missile "saute" la cible à cause de sa vitesse.
            if (dx * (missile.targetX - missile.x) <= 0 and dy * (missile.targetY - missile.y) <= 0) then
                -- Si oui, crée une explosion à l'endroit de la cible
                table.insert(explosions, {x = missile.targetX, y = missile.targetY, timer = EXPLOSION_LIFETIME, radius = EXPLOSION_RADIUS})
                table.remove(playerMissiles, i) -- Retire le missile du joueur (il a explosé)
            end
        else -- Si le missile est déjà pile sur sa cible (distance est zéro)
            table.insert(explosions, {x = missile.targetX, y = missile.targetY, timer = EXPLOSION_LIFETIME, radius = EXPLOSION_RADIUS})
            table.remove(playerMissiles, i)
        end
    end

    -- Mise à jour des explosions
    for i = #explosions, 1, -1 do
        local explosion = explosions[i] -- Prend l'explosion actuelle
        explosion.timer = explosion.timer - dt -- Le compte à rebours de l'explosion diminue
        explosion.radius = explosion.radius + 100 * dt -- L'explosion grandit visuellement

        -- Vérifier les collisions entre explosions et missiles ennemis
        for j = #enemyMissiles, 1, -1 do
            local enemyMissile = enemyMissiles[j] -- Prend le missile ennemi actuel
            -- Calcule la distance entre le centre de l'explosion et le missile ennemi
            local dist = math.sqrt((explosion.x - enemyMissile.x)^2 + (explosion.y - enemyMissile.y)^2)
            if dist < explosion.radius then -- Si le missile ennemi est dans le rayon de l'explosion
                score = score + 10 -- Le joueur gagne des points
                table.remove(enemyMissiles, j) -- Le missile ennemi est détruit par l'explosion
            end
        end

        if explosion.timer <= 0 then -- Si le compte à rebours de l'explosion est terminé
            table.remove(explosions, i) -- L'explosion disparaît
        end
    end

    -- Faire apparaître de nouveaux missiles ennemis (très basique pour l'instant)
    -- math.random() génère un nombre aléatoire entre 0 et 1.
    -- Si ce nombre est très petit (0.015), un nouveau missile apparaît.
    -- Cela crée une apparition aléatoire.
    if math.random() < 0.015 then
        -- Ajoute un nouveau missile ennemi, avec une position X aléatoire en haut de l'écran (y=0)
        table.insert(enemyMissiles, {x = math.random(SCREEN_WIDTH), y = 0})
    end

    -- Vérifier la condition de "Game Over"
    -- On suppose que toutes les villes sont détruites au début de la vérification
    local allCitiesDestroyed = true
    for _, city in ipairs(cities) do -- On parcourt toutes les villes
        if city.alive then -- Si on trouve au moins une ville vivante
            allCitiesDestroyed = false -- Alors toutes les villes ne sont pas détruites
            break                      -- On peut arrêter de vérifier les villes
        end
    end

    -- On fait la même chose pour les bases
    local allBasesDestroyed = true
    for _, base in ipairs(bases) do
        if base.alive then
            allBasesDestroyed = false
            break
        end
    end

    -- Si toutes les villes ET toutes les bases sont détruites, c'est "Game Over"
    if allCitiesDestroyed and allBasesDestroyed then
        gameOver = true
    end
end

---
-- [[
--    ==========================
--    6. love.draw()
--    ==========================
--    Cette fonction est appelée à *chaque image* juste après love.update.
--    C'est ici qu'on dessine tout ce qui doit apparaître à l'écran.
--    Il est important que love.draw() ne contienne pas de logique de jeu (mouvement, collisions)
--    car elle peut être appelée plus ou moins souvent que love.update selon la performance.
-- ]]
---
function love.draw()
    -- Dessiner les bases
    love.graphics.setColor(0.2, 0.8, 0.2) -- Définit la couleur de dessin en vert foncé (RVB : 0 à 1)
    for _, base in ipairs(bases) do -- Pour chaque base
        if base.alive then -- Si la base est vivante
            -- Dessine un rectangle plein. Les coordonnées sont ajustées pour que x,y soit le centre de la base.
            love.graphics.rectangle("fill", base.x - BASE_WIDTH / 2, base.y - BASE_HEIGHT / 2, BASE_WIDTH, BASE_HEIGHT)
        end
    end

    -- Dessiner les villes
    love.graphics.setColor(0.8, 0.8, 0.2) -- Couleur jaune pour les villes
    for _, city in ipairs(cities) do
        if city.alive then
            love.graphics.rectangle("fill", city.x - CITY_WIDTH / 2, city.y - CITY_HEIGHT / 2, CITY_WIDTH, CITY_HEIGHT)
        end
    end

    -- Dessiner les missiles ennemis
    love.graphics.setColor(1, 0, 0) -- Couleur rouge
    love.graphics.setLineWidth(3)   -- Définit l'épaisseur de ligne à 3 pixels (pour les rendre plus visibles)
    for _, missile in ipairs(enemyMissiles) do
        -- Dessine une ligne pour représenter le missile. 'missile.y - 15' rend la ligne un peu plus longue.
        love.graphics.line(missile.x, missile.y, missile.x, missile.y - 15)
    end
    love.graphics.setLineWidth(1) -- Très important : Réinitialise l'épaisseur de ligne à la valeur par défaut (1)
                                  -- pour que les prochains dessins (comme les missiles du joueur) n'aient pas cette épaisseur.

    -- Dessiner les missiles du joueur
    love.graphics.setColor(0, 0, 1) -- Couleur bleue
    for _, missile in ipairs(playerMissiles) do
        love.graphics.line(missile.x, missile.y, missile.x, missile.y - 10)
    end

    -- Dessiner les explosions
    love.graphics.setColor(1, 0.5, 0, 0.5) -- Couleur orange avec une transparence (0.5)
    for _, explosion in ipairs(explosions) do
        -- Dessine un cercle plein au centre de l'explosion, avec son rayon actuel
        love.graphics.circle("fill", explosion.x, explosion.y, explosion.radius)
    end

    -- Afficher le score
    love.graphics.setColor(1, 1, 1) -- Couleur blanche pour le texte
    -- Affiche le texte "Score: " suivi de la valeur de la variable 'score'
    love.graphics.print("Score: " .. score, 10, 10) -- Affiche en haut à gauche (position 10, 10)

    -- Afficher "Game Over" si le jeu est terminé
    if gameOver then
        love.graphics.setColor(1, 0, 0) -- Couleur rouge pour le texte Game Over
        -- Affiche "GAME OVER" centré sur l'écran
        love.graphics.printf("GAME OVER", 0, SCREEN_HEIGHT / 2 - 20, SCREEN_WIDTH, "center")
    end
end

---
-- [[
--    ==========================
--    7. love.mousepressed(x, y, button, istouch)
--    ==========================
--    Cette fonction est appelée chaque fois qu'un bouton de la souris est pressé.
--    Elle est aussi appelée lors d'un toucher sur un écran tactile !
--    x, y: position du clic ou du toucher.
--    button: quel bouton a été cliqué (1 pour le gauche/principal).
--    istouch: indique si l'événement vient d'un écran tactile (vrai/true) ou d'une souris (faux/false).
-- ]]
---
function love.mousepressed(x, y, button, istouch)
    if gameOver then return end -- Si le jeu est terminé, les clics/touchers n'ont plus d'effet

    -- On vérifie si c'est un clic gauche de souris (button == 1) OU un toucher d'écran (istouch == true).
    -- Cela rend le jeu jouable avec les deux types d'entrée.
    if button == 1 or istouch then
        -- On doit trouver la base du joueur la plus proche pour qu'elle tire le missile.
        local closestBase = nil -- Variable pour stocker la base la plus proche trouvée
        local minDist = math.huge -- Initialise la distance minimale à un très grand nombre (l'infini)

        -- Cette boucle parcourt toutes les 'bases'.
        -- 'ipairs(bases)' : C'est une fonction qui parcourt les éléments d'une table numérotée (comme une liste).
        --                  Elle renvoie à chaque fois deux choses : l'index (1, 2, 3...) et la valeur de l'élément.
        -- '_' (underscore) : Ici, on n'a pas besoin de l'index numérique de la base (est-ce la 1ère, la 2ème ? Peu importe).
        --                    L'underscore est une convention en Lua pour dire "j'ignore cette valeur".
        -- 'base' : Cette variable va contenir l'objet 'base' lui-même (la table {x=..., y=..., alive=...}) à chaque tour de boucle.
        --          C'est cette variable 'base' qu'on utilisera ensuite pour vérifier ses propriétés.
        for _, base in ipairs(bases) do
            if base.alive then -- Seulement si la base est encore vivante
                -- Calcule la distance entre la position du clic/toucher (x,y) et le centre de la base
                local dist = math.sqrt((x - base.x)^2 + (y - base.y)^2)
                if dist < minDist then -- Si cette distance est plus petite que la distance minimale trouvée jusqu'à présent
                    minDist = dist      -- Met à jour la distance minimale
                    closestBase = base  -- C'est cette base qui est la plus proche
                end
            end
        end

        if closestBase then -- Si une base vivante la plus proche a été trouvée
            -- Crée un nouveau missile du joueur. Il part de la position de la base
            -- et a pour cible la position où le joueur a cliqué/touché (x, y).
            table.insert(playerMissiles, {x = closestBase.x, y = closestBase.y, targetX = x, targetY = y})
        end
    end
end 

Les Piliers de Notre Code : Concepts Fondamentaux pour la Création de Jeux

En parcourant le code, vous avez découvert ou redécouvert des notions essentielles :

  • Variables (ex: score, gameOver) : Ce sont des conteneurs pour stocker des informations (nombres, états vrai/faux).
  • Tables (ex: enemyMissiles, bases) : Des collections d'objets. Elles sont vitales pour gérer tous les éléments dynamiques de votre jeu.
  • Fonctions LÖVE clés :
    • love.load() : La phase de préparation du jeu. C'est ici que vous initialisez vos variables, créez vos bases et villes, et chargez ce dont vous avez besoin.
    • love.update(dt) : Le moteur du jeu. Cette fonction est appelée continuellement et gère la logique : mouvement des missiles, détection des collisions, destruction des éléments, apparition de nouveaux ennemis. Le dt (delta time) est crucial pour que le jeu tourne à la même vitesse sur tous les ordinateurs.
    • love.draw() : Le pinceau du jeu. Appelé après update, il se charge uniquement de dessiner tous les éléments à l'écran à leurs positions mises à jour.
  • Boucles (for ... do ... end) : Permettent de répéter des actions pour chaque élément d'une liste (par exemple, déplacer tous les missiles ennemis). La technique de boucler à l'envers (for i = #table, 1, -1 do) est fondamentale quand vous retirez des éléments d'une liste pendant que vous la parcourez.
  • Conditions (if ... then ... end) : Le cœur de la logique décisionnelle du jeu. "Si ceci se produit, alors fais cela." (Ex: si un missile touche, alors détruis-le).
  • Système de Coordonnées (X, Y) : En LÖVE, le point (0,0) est en haut à gauche de votre fenêtre. L'axe X va vers la droite, et l'axe Y va vers le bas.
  • Détection de Collision (Point dans Rectangle) : Une méthode simple pour savoir si un missile (représenté par un point) touche une base ou une ville (représentées par des rectangles).

Ce projet n'est pas seulement une recréation d'un classique, c'est aussi un excellent tremplin pour comprendre les principes fondamentaux de la création de jeux. Chaque ligne de code, chaque ajustement pour corriger un bug, est une leçon.

N'attendez plus ! Téléchargez LÖVE2D, copiez ce code, et amusez-vous à le modifier, à y ajouter des fonctionnalités (sons, graphismes améliorés, vagues d'ennemis plus complexes...). Le monde du développement de jeux vous attend !

Rendez-vous sur Arcade-Forge pour de nouvelles aventures pixelisées !

 

Et voici le jeu :

 

Le Phénomène Pac-Man : L'icône qui a dévoré le monde de l'arcade

À l'époque où les bornes d'arcade étaient dominées par les vaisseaux spatiaux et les tirs de laser, un petit personnage rond et jaun...