Lights Out
Lights Out est un jeu de puzzle électronique inventé par Tiger Electronics en 1995.
La grille est composée de cellules pouvant être allumées ou éteintes. Cliquer sur une cellule inverse son état et celui de ses 4 voisines directes (haut, bas, gauche, droite). Le but est d'éteindre toutes les lumières.
Pré-requis
Cette activité fait suite à l'activité sur le voisinage. On réutilise ici les mêmes notions : grille (i, j), déplacements (di, dj), et vérification des bords.
Le code est organisé autour de la bibliothèque metagrid, qui prend en charge l'affichage et les interactions. Dans draw, on colorie chaque cellule avec jeu.set_cell_color(i, j, "#RRGGBB").
Installer metagrid dans le terminal: uv add metagrid
Crée un fichier lightsout.py avec ce squelette complet :
"""
Lights Out
Chaque cellule de la grille est soit allumée, soit éteinte.
Cliquer une cellule inverse son état et celui de ses 4 voisines directes.
But : éteindre toutes les lumières.
Touche r : recommencer.
"""
import metagrid
from random import randint
NB_LIGNES = 5
NB_COLONNES = 5
TAILLE_CASE = 100
COULEUR_ALLUMEE = "#FFDD00"
COULEUR_ETEINTE = "#222222"
COULEUR_VICTOIRE = "#44FF88"
grille: list[list[bool]] # True = allumée, False = éteinte
flag_game_over: bool
jeu: metagrid.AbstractEngine
def toggle(i: int, j: int):
"""Inverse la cellule (i, j) et ses 4 voisines directes."""
pass
def gagne() -> bool:
"""Renvoie True si toutes les cellules sont éteintes."""
pass
def init():
"""Callback - appelé au démarrage. Génère un nouveau puzzle garanti soluble."""
global grille, flag_game_over
pass
def cliquer(i: int, j: int, _modif: str):
"""Callback - appelé avec (i, j) quand le joueur clique une cellule."""
global flag_game_over
pass
def touche(key: str):
"""Callback - appelé avec la touche pressée. r pour recommencer."""
pass
def draw():
"""Callback - appelé à chaque frame. Colorie chaque cellule selon son état."""
pass
if __name__ == "__main__":
jeu = metagrid.create(NB_LIGNES, NB_COLONNES, TAILLE_CASE, 4)
jeu.on_init(init)
jeu.on_click(cliquer)
jeu.on_key(touche)
jeu.on_draw(draw)
jeu.start()
Étape 1 - La grille booléenne
Représentation
Contrairement à l'activité sur le voisinage où les cellules contenaient des nombres, ici chaque cellule est un booléen : True si la cellule est allumée, False si elle est éteinte.
-
Comment représente-t-on en Python une grille de 5 lignes et 5 colonnes, entièrement éteinte ?
-
Que vaut
[False] * 3? Vérifiez dans l'interpréteur. -
On veut inverser l'état d'une cellule. Que vaut
not True?not False? Comment inversergrille[i][j]en une instruction ?
Étape 2 - Afficher la grille
Avant d'ajouter toute interaction, on veut juste voir quelque chose à l'écran.
Fonctions init et draw
Complétez init pour créer une grille entièrement éteinte, et draw pour colorier chaque cellule selon son état.
def init():
global grille, flag_game_over
grille = ...
flag_game_over = ...
def draw():
for i in range(NB_LIGNES):
for j in range(NB_COLONNES):
if ... :
couleur = COULEUR_ALLUMEE
else:
...
jeu.set_cell_color(i, j, couleur)
Lancez le programme. Vous devez voir une grille vide. Rien ne se passe encore au clic, c'est normal.
Étape 3 - Toggle simple (sans voisinage)
On va maintenant réagir aux clics : cliquer sur une cellule inverse uniquement cette cellule, sans toucher aux voisines.
Fonction cliquer simple
Complétez cliquer pour inverser l'état de la cellule (i, j) :
Testez : cliquer sur une cellule doit l'allumer, recliquer doit l'éteindre.
Vous pouvez dessiner des motifs librement sur la grille.
Étape 4 - Toggle Lights Out (avec voisinage en croix)
La vraie règle de Lights Out : cliquer sur (i, j) inverse cette cellule et ses 4 voisines directes (haut, bas, gauche, droite).
Croix ou diagonales ?
Dans l'activité voisinage, on utilisait range(-1, 2) pour di et dj, ce qui donnait 9 couples. Voici les 9 couples classés :
| dj = -1 | dj = 0 | dj = +1 | |
|---|---|---|---|
| di = -1 | diag | haut | diag |
| di = 0 | gauche | centre | droite |
| di = +1 | diag | bas | diag |
-
Quels couples correspondent à la croix (centre + 4 voisins directs) ?
-
Quelle propriété distingue les couples de la croix ? Observez les valeurs de
dietdjpour chaque case en gras. -
Complétez la condition pour ne garder que la croix :
Écrire toggle et mettre à jour cliquer
Complétez la fonction toggle avec la double boucle et la condition trouvée, puis faites appeler toggle depuis cliquer :
Étape 5 - La fonction gagne
Vérifier la victoire
On gagne quand toutes les cellules sont éteintes (False).
Complétez la fonction : dès qu'on trouve une cellule allumée, on sait que la partie n'est pas gagnée.
Étape 6 - Vérifier la victoire
Gérer la victoire dans cliquer
Quand le joueur clique sur (i, j) :
- si la partie est terminée (flag_game_over est True), on ne fait rien,
- sinon, on applique toggle(i, j),
- puis on vérifie si la partie est gagnée avec gagne().
Étape 7 - Générer un puzzle soluble
Initialisation aléatoire
Une idée naïve serait de remplir la grille aléatoirement. Mais certaines configurations de Lights Out sont insolubles.
Voici une astuce : on part d'une grille entièrement éteinte (état gagnant), puis on applique un certain nombre de toggle aléatoires. Comme toggle est sa propre inverse (deux toggles au même endroit s'annulent), on est certain que le puzzle obtenu est soluble.
-
Pourquoi cette approche garantit-elle que le puzzle est soluble ?
-
Mettez à jour
init:
Étape 8 - Touche finale
Touche r pour recommencer
Si la touche appuyée est "r", alors on initialise le jeu.
Afficher la victoire dans draw
Ajoutez le cas victoire : si flag_game_over est True, toutes les cellules prennent COULEUR_VICTOIRE.
def draw():
for i in range(NB_LIGNES):
for j in range(NB_COLONNES):
if ...:
couleur = ...
elif ...:
couleur = ...
else:
...
jeu.set_cell_color(i, j, couleur)
Lancez le programme et jouez quelques parties. Appuyez sur r pour générer un nouveau puzzle.
Pour aller plus loin
- Ajoutez un compteur de coups affiché dans le titre de la fenêtre.
- Ajoutez un mode "aide" qui met en surbrillance la prochaine cellule à cliquer pour résoudre le puzzle (indice : cherchez "Lights Out solver").
- Essayez avec une grille de taille différente (6x6, 3x3). Toutes les tailles produisent-elles des puzzles solubles avec la méthode des toggles aléatoires ?