Le Pong de 1972 sans écran : une bande de 72 LED dans un couvercle de bois, un bouton à chaque bout, une balle rouge qui va et vient. Chaque joueur défend un camp de dix LED ; qui appuie trop tôt ou trop tard perd un point, et la balle accélère à chaque échange. Un Arduino Nano, un buzzer, et un chargeur de téléphone pour alimenter le tout.
Le besoin de départ
Pong : deux traits, un point, deux chiffres, et c’est un jeu. Écran d’après la réédition du jeu par Atari ; image du domaine public, Wikimedia Commons (Bumm13), tramée et glitchée pour l’occasion.
Pong, c’est le premier jeu vidéo à avoir eu du succès : deux raquettes, une balle, un téléviseur, 1972. Tout ce qui fait un jeu y est déjà, et rien de plus.
L’idée est de garder le jeu et de retirer l’écran. Un écran, c’est une carte plus grosse, une bibliothèque graphique, un câble de plus, et un objet qui redevient un ordinateur. Une bande de LED adressables est un écran d’une seule ligne : soixante-douze pixels et trois fils. Sur une ligne, Pong se réduit à ce qui compte, une balle qui va et vient, deux réflexes, un score.
Ce qu’on obtient tient sur une table et se comprend sans notice. Deux personnes qui ne se connaissent pas y jouent en dix secondes, et l’objet entier, programme compris, est à la portée d’une première initiation Arduino : des entrées, une sortie, une bande de LED, un peu de temps à compter.
Comment ça marche
Juin 2026 : trois Pong côte à côte, dans leur version réduite, trois parties à la fois. Chaque couvercle a sa gravure.
Une ligne de 72 LED, deux boutons, une balle.
La bande est figurée dans l’axe du couvercle pour la lisibilité. Sur l’objet, elle le traverse légèrement en biais.
Chaque joueur a son camp : les dix LED de son bout de bande, vertes à gauche, jaunes à droite. Entre les deux, cinquante-deux LED éteintes. La balle est une LED rouge qui part d’un bout et avance d’une LED toutes les 70 millisecondes, cinq secondes d’un bout à l’autre au départ.
Pour renvoyer la balle, on appuie pendant qu’elle est dans son camp. Dix LED, c’est sept dixièmes de seconde au départ. Trop tard, la balle sort par le bout : un point de perdu. Trop tôt, c’est-à-dire le doigt déjà appuyé quand la balle se présente à l’entrée du camp, et l’appui ne compte pas : la balle traverse, le point est perdu de la même façon. C’est ce qui empêche de gagner en gardant le bouton enfoncé.
À chaque aller-retour réussi, la balle accélère d’un cinquième. Le camp ne change pas, mais le temps pour réagir fond : après cinq échanges, il reste un quart de seconde.
Le score est sur la bande elle-même. Chaque point perdu vire au rouge une LED du camp, en partant du milieu ; quand les dix sont rouges, la partie est finie et une nouvelle commence aussitôt. Appuyer sur les deux boutons en même temps remet aussi les compteurs à zéro.
Le buzzer ponctue : un bip bref et aigu au renvoi, un son plus grave et plus long au point perdu.
L’alimentation vient d’un chargeur de téléphone, par une prise USB-C sur le petit côté du coffre. Ça suffit parce que la bande est loin de sa pleine puissance : soixante-douze LED allumées à fond en blanc tireraient plus de quatre ampères ; ici, les camps sont allumés au dixième et une seule LED est vraiment vive. Le montage entier reste sous le demi-ampère.
Comment c’est fabriqué
Machines
découpe laser, fer à souder
Matériaux
contreplaqué 6 mm et planches pour le coffre, Arduino Nano, bande de 72 LED WS2812 (144 LED par mètre, soit 50 cm), deux boutons poussoirs de 12 mm, buzzer, plaque à pastilles 5 x 7 cm, borniers à vis, prise USB-C
Coût
une trentaine d’euros de composants, le bois en plus
Le camp vert, la balle qui repart vers l’autre bout, le bouton du joueur au-dessus de son camp. La bande traverse le couvercle en biais ; le câble entre par le petit côté.
Le montage, d’abord sur plaque d’essai. Six fils sur le Nano : les deux boutons sur D3 et D4, la bande sur D2, le buzzer sur D5, plus le 5 V et la masse. Les boutons se câblent entre la broche et la masse, sans résistance : le programme active la résistance de rappel interne. On charge le programme et on joue sur la table avant de couper le moindre bout de bois ; c’est là qu’on règle la vitesse de départ et la taille des camps, deux constantes en tête du programme.
La carte définitive. Le Nano et le buzzer sont soudés sur une plaque à pastilles de 5 x 7 cm. Les boutons, la bande et l’alimentation arrivent sur des borniers à vis : le couvercle se démonte sans dessouder.
Le couvercle. Contreplaqué de 6 mm, 600 x 110 mm, angles abattus. Au laser : la fente de 500 x 7 mm, posée légèrement en biais, et les deux trous de 13 mm des boutons, aux angles opposés. Le fichier ne découpe que la fente et les trous ; le contour n’y est que marqué, le couvercle se coupe à part.
Le coffre. Un cadre de planches sur lequel le couvercle vient se visser, la prise USB-C sur un petit côté. La bande se fixe sous le couvercle, les LED dans la fente : rien ne dépasse, rien ne s’arrache. Les boutons passent dans leurs trous et se bloquent par leur écrou, et tout se raccorde aux borniers.
Conçu et fabriqué par un membre de l’Atelier Partagé (La Traverse, Bergerac), en avril 2026.
Fichiers et plans
La carte telle qu’elle est montée dans le coffre. Tout ce qui va au couvercle passe par les borniers.
Le programme, cent cinquante lignes : le déplacement de la balle au rythme de millis(), les deux camps, la détection du trop-tôt, le score en rouge et les deux sons. Il demande la bibliothèque Adafruit NeoPixel, qui s’installe depuis le gestionnaire de bibliothèques de l’IDE. Une mélodie de défaite y est écrite mais jamais appelée : ses deux appels sont en commentaire, il suffit de les rétablir.
La version réduite, dessinée dans la foulée : la face, 390 x 123 mm dans du 5 mm, et le support de la bande, dans du 3 mm. Voir plus bas.
Le câblage tient en cinq lignes :
Broche du Nano
Vers
D2
l’entrée de données de la bande
D3
le bouton vert, l’autre borne à la masse
D4
le bouton jaune, l’autre borne à la masse
D5
la borne + du buzzer, l’autre à la masse
5V et GND
la prise USB-C, et l’alimentation de la bande
Le programme complet (C++)
#include <Adafruit_NeoPixel.h>#define LED_PIN 2#define LED_COUNT 72#define BOUTON1_PIN 3 // Bouton Vert#define BOUTON2_PIN 4 // Bouton Jaune#define BUZZER_PIN 5#define TEMPO 70Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);// Configuration jeuconst int tailleTerritoire = 10;int positionBalle = 0;int directionBalle = 1; // 1 = vers la droite, -1 = vers la gaucheunsigned long dernierMouvement = 0;int cadencement = TEMPO; // temps de cycle de la led rougeint boutonInterdit = 0; // 1 = le bouton a été pressé trop tôtint partiePerduJaune = 0;int partiePerduVert = 0;void setup() { pinMode(BOUTON1_PIN, INPUT_PULLUP); // Bouton Vert pinMode(BOUTON2_PIN, INPUT_PULLUP); // Bouton Jaune pinMode(BUZZER_PIN, OUTPUT); strip.begin(); strip.show(); // Éteint toutes les LEDs startBall();}void loop() { if ((digitalRead(BOUTON1_PIN) == LOW && digitalRead(BOUTON2_PIN) == LOW) || partiePerduJaune ==10 || partiePerduVert ==10) { strip.begin(); strip.show(); // Éteint toutes les LEDs startBall(); partiePerduJaune = 0; partiePerduVert = 0; } else { unsigned long now = millis(); if (now - dernierMouvement >= cadencement) { dernierMouvement = now; moveBall(); } }}void startBall() { // Si direction 1 alors positionBalle=0 sinon positionBalle part de l'autre extremité if (directionBalle == 1) { positionBalle = 0; } else { positionBalle = LED_COUNT - 1; } cadencement = TEMPO;}void moveBall() { strip.clear(); // Affichage zones des joueurs - partie 1 sans tenir compte des parties perdues for (int i = 0; i <= LED_COUNT; i++) { if (i < tailleTerritoire - partiePerduVert) { // zone verte strip.setPixelColor(i, strip.Color(0, 25, 0)); } if (i >= tailleTerritoire - partiePerduVert && i <= tailleTerritoire - 1) { //Zone des points perdu du joueur vert strip.setPixelColor(i, strip.Color(20, 0, 0)); } if (i > tailleTerritoire && i >= LED_COUNT - tailleTerritoire ) { // partie sans lumiere strip.setPixelColor(i, strip.Color(0, 0, 0)); } if (i <= LED_COUNT - tailleTerritoire -1 + partiePerduJaune && i > LED_COUNT - tailleTerritoire -1 ) { //Zone des points perdu du joueur jaune strip.setPixelColor(i, strip.Color(20 , 0 , 0)); } if (i >= LED_COUNT - tailleTerritoire + partiePerduJaune ) { // zone jaune strip.setPixelColor(i, strip.Color(25, 25, 0)); } } // Affichage balle strip.setPixelColor(positionBalle, strip.Color(100, 0, 0)); // LED rouge // Gérer la logique des rebonds // Test si le bouton est appuyé avant l'arrivé dans les territoires if (directionBalle == -1 && positionBalle == tailleTerritoire +1) { if (digitalRead(BOUTON1_PIN) == LOW) { boutonInterdit = 1; } else { boutonInterdit = 0; } } if (directionBalle == 1 && positionBalle == LED_COUNT - tailleTerritoire - 1) { if (digitalRead(BOUTON2_PIN) == LOW) { boutonInterdit = 1; } else { boutonInterdit = 0; } } if (directionBalle == 1 && positionBalle >= LED_COUNT - tailleTerritoire) { if (digitalRead(BOUTON2_PIN) == LOW && boutonInterdit == 0) { directionBalle = -1; tone(BUZZER_PIN, 1000, 100); cadencement = 0.8 * cadencement; // acceleration progressive } else if (positionBalle == LED_COUNT - 1) { // Le Joueur Jaune a raté partiePerduJaune = partiePerduJaune + 1; tone(BUZZER_PIN, 200, 300); //playDefeatTune(); directionBalle = -1; startBall(); delay(500); } } else if (directionBalle == -1 && positionBalle < tailleTerritoire) { if (digitalRead(BOUTON1_PIN) == LOW && boutonInterdit == 0) { directionBalle = 1; tone(BUZZER_PIN, 1000, 100); } else if (positionBalle == 0) { // Le Joueur Vert a raté partiePerduVert = partiePerduVert + 1; tone(BUZZER_PIN, 200, 300); //playDefeatTune(); directionBalle = 1; startBall(); delay(500); } } strip.show(); positionBalle = positionBalle + directionBalle;}// le prog ci dessous n ;'est pas necessaire. Sa fonction est de jouer une petite musique lorsqu'un joueur perd.void playDefeatTune() { // Notes de l'air (fréquences en Hz) int melody[] = {659, 622, 587, 554, 523, 0, 392, 370, 349, 330, 311, 0 }; // Durée de chaque note (en ms) int noteDurations[] = {200, 200, 200, 200, 300, 100, 200, 200, 200, 200, 400, 100 }; for (int i = 0; i < 12; i++) { int noteDuration = noteDurations[i]; if (melody[i] == 0) { // Silence delay(noteDuration); } else { tone(BUZZER_PIN, melody[i], noteDuration); delay(noteDuration * 1.3); // Petite pause entre les notes noTone(BUZZER_PIN); } }}
Ce qu’on ferait autrement
Le martèlement gagne trop souvent. Le « trop tôt » n’est jugé qu’à un seul instant, quand la balle se présente à l’entrée du camp : le doigt est appuyé ou il ne l’est pas. Qui tapote frénétiquement a de bonnes chances de passer ce contrôle, et renvoie ensuite à coup sûr. Deux corrections possibles : juger le trop-tôt sur toute l’approche, les cinq ou six LED avant le camp, ou n’accepter qu’un appui franc, le passage de relâché à appuyé, une fois la balle dans le camp.
La balle et les points perdus ont la même couleur. Rouge vif pour l’une, rouge sombre pour les autres : quand le camp est à moitié perdu, la balle qui le traverse se lit moins bien, au moment précis où il faut réagir. Une balle blanche ou bleue resterait visible jusqu’au bout.
Rien ne protège la bande. Ni condensateur sur son alimentation, ni résistance sur la ligne de données. Le montage marche, mais ces deux composants à cinquante centimes sont la recommandation constante d’Adafruit, qui édite la bibliothèque : un condensateur de 1000 µF entre le 5 V et la masse au plus près de la bande, une résistance de 300 à 500 Ω en série sur les données. C’est ce qui évite de griller la première LED au branchement.
Soixante centimètres, c’est long sur une table. Une version réduite a été dessinée dans la foulée, au printemps 2026 : face de 390 x 123 mm, cinquante LED, boutons de 16 mm, et une plaque de 3 mm percée qui tient la bande sous la face. Le programme est le même, avec LED_COUNT à 50. Ses deux fichiers sont dans la liste ci-dessus.
Cette version a été construite, et en plusieurs exemplaires : plus courte, elle tient à trois sur une table, comme sur la photo du haut de la fiche.