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 :
Le jeu montre une séquence de lumières et de sons.
Le joueur doit la reproduire en appuyant sur les boutons dans le bon ordre.
À 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.
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.xest beaucoup plus parlant quebtn[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 utilisemath.random(1, #game.buttons)pour choisir un nouveau bouton aléatoire et l'ajoute à lagame.sequenceexistante.math.randomseed(os.time()): C'est une ligne cruciale située danslove.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 utilisantos.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 tableCONSTANTSau 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'étatWAITING_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
-- 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


