Build a Reaction Timer Game
DisplaysGamesInputs
Wait for the signal, then hit the button as fast as you can -- a small display and a stopwatch built entirely out of your own code.
7 real parts included
Total for this kit: 1,800.00 ฿
Doesn't include any order discount you may get at checkout.
What you'll need
ESP32-C3 SuperMini Development Board — Wi-Fi & Bluetooth, USB-C
670.00 ฿
0.96" I2C OLED Display Module — 128×64, White
285.00 ฿
Active Buzzer Module — 12mm
235.00 ฿
Tactile Push Button Switches — 12×12×7.3mm
85.00 ฿
830-Point Solderless Breadboard
125.00 ฿
Jumper Wire Kit — Male-Male, Male-Female, Female-Female
220.00 ฿
USB-C Data & Charging Cable — 1m
180.00 ฿
Wiring Diagram
Starter Code
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SDA_PIN 8 // ESP32-C3 default I2C SDA - adjust if your board differs
#define SCL_PIN 9 // ESP32-C3 default I2C SCL - adjust if your board differs
#define BUTTON_PIN 3 // Tactile button, other leg to GND - any digital input GPIO
#define BUZZER_PIN 4 // Active buzzer signal - any digital output GPIO
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
enum GameState { IDLE, GET_READY, ARMED, GO, SHOW_RESULT };
GameState state = IDLE;
unsigned long stateEnteredAt = 0;
unsigned long randomDelay = 0;
unsigned long goTime = 0;
bool rawPressed() {
return digitalRead(BUTTON_PIN) == LOW; // INPUT_PULLUP: LOW = pressed
}
// Returns true once, only on the release -> press transition (debounced).
bool pressedEdge() {
static bool lastStable = false;
static unsigned long lastChange = 0;
bool reading = rawPressed();
if (reading != lastStable && millis() - lastChange > 30) {
lastStable = reading;
lastChange = millis();
return lastStable; // true only on the press edge
}
return false;
}
void showMessage(const char* line1, const char* line2) {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 10);
display.println(line1);
display.setTextSize(1);
display.setCursor(0, 44);
display.println(line2);
display.display();
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
Wire.begin(SDA_PIN, SCL_PIN);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println("SSD1306 init failed - check wiring/address");
while (true) delay(1000);
}
randomSeed(analogRead(0)); // floating ADC pin as an entropy source
showMessage("Reaction Timer", "Press to start");
}
void loop() {
bool edge = pressedEdge();
switch (state) {
case IDLE:
if (edge) {
showMessage("Get ready...", "");
state = GET_READY;
stateEnteredAt = millis();
}
break;
case GET_READY:
// brief pause so the player lets go of the button before arming
if (millis() - stateEnteredAt > 500) {
randomDelay = random(1500, 5000); // 1.5-5s random wait before GO
stateEnteredAt = millis();
state = ARMED;
}
break;
case ARMED:
if (rawPressed()) {
showMessage("Too early!", "Press to retry");
digitalWrite(BUZZER_PIN, HIGH);
delay(200);
digitalWrite(BUZZER_PIN, LOW);
state = SHOW_RESULT;
} else if (millis() - stateEnteredAt >= randomDelay) {
showMessage("GO!", "");
digitalWrite(BUZZER_PIN, HIGH);
goTime = millis();
state = GO;
}
break;
case GO:
if (millis() - goTime > 150) {
digitalWrite(BUZZER_PIN, LOW); // stop the short GO beep after 150ms
}
if (rawPressed()) {
unsigned long reactionMs = millis() - goTime;
char line1[16];
snprintf(line1, sizeof(line1), "%lu ms", reactionMs);
showMessage(line1, "Press to retry");
Serial.printf("Reaction time: %lu ms\n", reactionMs);
state = SHOW_RESULT;
}
break;
case SHOW_RESULT:
if (edge) {
showMessage("Get ready...", "");
state = GET_READY;
stateEnteredAt = millis();
}
break;
}
}Not sure about a step? Ask us