The Button Battle program is a two-player reaction/tapping game built on a finite state machine (FSM) with four states: MENU, READY, GO, and RESULT. The main loop uses a switch/case structure to delegate behavior to state-specific handler functions, keeping the logic organized and modular.
Sample Code:
/**************************************************
* Program: Button_Battle.ino
* Copyright (C) 李宗瑞全集, Inc. 2026
*
* Description:
* Two players take turns mashing their button as
* many times as possible before time runs out.
* Player 1 uses SW1; Player 2 uses SW2. The pot
* sets the round length (3, 5, or 10 seconds).
* After both players have gone, the OLED shows
* the scores and announces the winner. Press
* either button to play again.
*
* Hardware:
* - 1ST Maker Frog board
* - OLED display (SSD1306)
* - Two buttons (BUTTON_ONE, BUTTON_TWO)
* - Two NeoPixel eyes (NEO_PIN)
* - Four cheek LEDs (LEDs[])
* - Piezo buzzer (PIEZO)
* - Potentiometer (POT_PIN)
**************************************************/
#include "1ST_Maker_Frog.h"
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
const uint8_t ROUND_COUNT = 3;
const uint8_t ROUND_SECONDS[ROUND_COUNT] = {3, 5, 10};
const uint16_t DEBOUNCE_MS = 30;
const uint16_t DISPLAY_FPS_MS = 80;
// Piezo tones
const uint16_t TONE_READY_HZ = 1200;
const uint16_t TONE_GO_HZ = 1800;
const uint16_t TONE_STOP_HZ = 600;
const uint16_t TONE_WIN_HZ = 2000;
const uint16_t TONE_TIE_HZ = 1000;
const uint16_t TONE_TICK_HZ = 1400;
// NeoPixel eye indices
const uint8_t EYE_LEFT = 0;
const uint8_t EYE_RIGHT = 1;
// ---------------------------------------------------------------------------
// Game states
// ---------------------------------------------------------------------------
const uint8_t STATE_MENU = 0;
const uint8_t STATE_READY = 1; // "Get ready, P1 / P2…"
const uint8_t STATE_GO = 2; // actively mashing
const uint8_t STATE_RESULT = 3; // show winner screen
// ---------------------------------------------------------------------------
// Global variables
// ---------------------------------------------------------------------------
uint8_t gameState = STATE_MENU;
uint8_t currentPlayer = 1; // 1 or 2
uint8_t roundSeconds = 5;
int scoreP1 = 0;
int scoreP2 = 0;
unsigned long roundStartMs = 0;
unsigned long lastDisplayMs = 0;
unsigned long lastTickMs = 0;
// Button debounce for the active player's button
bool btnDownLast = false;
bool btnDownStable = false;
unsigned long btnChangeMs = 0;
// Menu / non-game button handling (raw read, wait for release)
bool anyBtnWasDown = false;
// Cheek LED animation index during GO phase
uint8_t ledChaser = 0;
unsigned long lastChaserMs = 0;
// ---------------------------------------------------------------------------
// Function prototypes
// ---------------------------------------------------------------------------
void showMenu();
void showReady();
void runGoPhase();
void showResult();
uint8_t readRoundIndex();
void applyRoundPreset();
bool readActiveButton();
bool updateDebounce(bool rawDown, unsigned long nowMs);
void flashStartSequence();
void beepStop();
void beepWin();
void beepTie();
void beepTick(int secondsLeft);
void setEyes(uint8_t r0, uint8_t g0, uint8_t b0,
uint8_t r1, uint8_t g1, uint8_t b1);
void chaseLEDs(unsigned long nowMs);
void allLEDsOff();
void allLEDsOn();
void drawCountdown(int secondsLeft, int tapsNow);
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
/**************************************************
* Function: setup
* Purpose: Initializes hardware and shows the menu.
**************************************************/
void setup() {
Serial.begin(9600);
// Cheek LEDs
for (uint8_t i = 0; i < 4; i++) {
pinMode(LEDs[i], OUTPUT);
digitalWrite(LEDs[i], LOW);
}
// Buttons active-LOW
pinMode(BUTTON_ONE, INPUT_PULLUP);
pinMode(BUTTON_TWO, INPUT_PULLUP);
pinMode(PIEZO, OUTPUT);
pixel.begin();
pixel.show();
Wire.begin();
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
display.display();
randomSeed(analogRead(A1));
gameState = STATE_MENU;
showMenu();
}
// ---------------------------------------------------------------------------
// Main loop
// ---------------------------------------------------------------------------
/**************************************************
* Function: loop
* Purpose: Dispatches to the correct state handler
* every iteration.
**************************************************/
void loop() {
switch (gameState) {
case STATE_MENU:
handleMenu();
break;
case STATE_READY:
handleReady();
break;
case STATE_GO:
handleGo();
break;
case STATE_RESULT:
handleResult();
break;
}
}
// ---------------------------------------------------------------------------
// State: MENU
// ---------------------------------------------------------------------------
/**************************************************
* Function: showMenu
* Purpose: Draws the title screen and instructions
* on the OLED.
**************************************************/
void showMenu() {
applyRoundPreset();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
// Title
display.setTextSize(2);
display.setCursor(4, 0);
display.print("BUTTON");
display.setCursor(4, 16);
display.print("BATTLE!");
// Divider
display.drawFastHLine(0, 33, 128, SSD1306_WHITE);
// Instructions
display.setTextSize(1);
display.setCursor(0, 36);
display.print("Knob = time (");
display.print(roundSeconds);
display.print("s)");
display.setCursor(0, 46);
display.print("Tap your button fast!");
display.setCursor(0, 56);
display.print("SW1 or SW2 to start");
display.display();
// Eyes idle: dim blue
setEyes(0, 0, 20, 0, 0, 20);
allLEDsOff();
}
/**************************************************
* Function: handleMenu
* Purpose: Waits for either button press to start
* the game. Also refreshes the time
* display if the pot is turned.
**************************************************/
void handleMenu() {
// Refresh knob selection live
unsigned long nowMs = millis();
if (nowMs - lastDisplayMs >= 150) {
lastDisplayMs = nowMs;
uint8_t idx = readRoundIndex();
if (ROUND_SECONDS[idx] != roundSeconds) {
applyRoundPreset();
showMenu();
}
}
bool b1 = (digitalRead(BUTTON_ONE) == PRESSED);
bool b2 = (digitalRead(BUTTON_TWO) == PRESSED);
bool anyDown = b1 || b2;
if (anyDown && !anyBtnWasDown) {
// Wait for release before proceeding
anyBtnWasDown = true;
}
if (!anyDown && anyBtnWasDown) {
anyBtnWasDown = false;
scoreP1 = 0;
scoreP2 = 0;
currentPlayer = 1;
gameState = STATE_READY;
showReady();
}
}
// ---------------------------------------------------------------------------
// State: READY
// ---------------------------------------------------------------------------
/**************************************************
* Function: showReady
* Purpose: Tells the current player to get ready
* and wait for the GO signal.
**************************************************/
void showReady() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(0, 0);
display.print("Player ");
display.print(currentPlayer);
display.setTextSize(1);
display.setCursor(0, 22);
display.print("Your turn!");
display.setCursor(0, 33);
if (currentPlayer == 1) {
display.print("Use SW1 (left)");
} else {
display.print("Use SW2 (right)");
}
display.setCursor(0, 44);
display.print("Press your button");
display.setCursor(0, 54);
display.print("when ready...");
display.display();
// Eyes: player colour
if (currentPlayer == 1) {
setEyes(0, 80, 0, 0, 0, 0); // left eye green
} else {
setEyes(0, 0, 0, 0, 0, 80); // right eye blue
}
allLEDsOff();
anyBtnWasDown = true; // ignore button still held from menu
}
/**************************************************
* Function: handleReady
* Purpose: Waits for the current player to press
* their button, then kicks off the round
* with a start sequence.
**************************************************/
void handleReady() {
bool b1 = (digitalRead(BUTTON_ONE) == PRESSED);
bool b2 = (digitalRead(BUTTON_TWO) == PRESSED);
bool myBtn = (currentPlayer == 1) ? b1 : b2;
bool anyDown = b1 || b2;
// Release guard: wait for button to come up first
if (anyBtnWasDown) {
if (!anyDown) anyBtnWasDown = false;
return;
}
if (myBtn) {
// Wait for release before starting so the press
// doesn't count as the first tap.
while ((currentPlayer == 1 ? digitalRead(BUTTON_ONE)
: digitalRead(BUTTON_TWO)) == PRESSED) {
delay(5);
}
flashStartSequence();
gameState = STATE_GO;
roundStartMs = millis();
lastChaserMs = roundStartMs;
lastTickMs = roundStartMs;
btnDownLast = false;
btnDownStable = false;
btnChangeMs = roundStartMs;
}
}
// ---------------------------------------------------------------------------
// State: GO
// ---------------------------------------------------------------------------
/**************************************************
* Function: handleGo
* Purpose: Counts taps from the current player
* until time runs out. Updates the OLED
* and LEDs continuously.
**************************************************/
void handleGo() {
unsigned long nowMs = millis();
unsigned long elapsed = nowMs - roundStartMs;
unsigned long totalMs = (unsigned long)roundSeconds * 1000UL;
// Time up?
if (elapsed >= totalMs) {
beepStop();
allLEDsOff();
if (currentPlayer == 1) {
// P1 done — show score briefly, then move to P2
showInterimScore();
delay(2000);
currentPlayer = 2;
gameState = STATE_READY;
showReady();
} else {
// Both done — show result
gameState = STATE_RESULT;
showResult();
}
return;
}
int secondsLeft = (int)(((totalMs - elapsed) + 999) / 1000);
// Debounce and count taps
bool rawDown = readActiveButton();
bool wasStable = btnDownStable;
btnDownStable = updateDebounce(rawDown, nowMs);
if (btnDownStable && !wasStable) {
// Rising edge: count the tap
if (currentPlayer == 1) scoreP1++;
else scoreP2++;
tone(PIEZO, TONE_TICK_HZ, 15);
}
// Chase LEDs
chaseLEDs(nowMs);
// Refresh display
if (nowMs - lastDisplayMs >= DISPLAY_FPS_MS) {
lastDisplayMs = nowMs;
int tapsNow = (currentPlayer == 1) ? scoreP1 : scoreP2;
drawCountdown(secondsLeft, tapsNow);
}
// Beep at each whole second boundary
if (nowMs - lastTickMs >= 1000) {
lastTickMs += 1000;
if (secondsLeft <= 3 && secondsLeft >= 1) {
tone(PIEZO, TONE_READY_HZ, 60);
}
}
}
/**************************************************
* Function: showInterimScore
* Purpose: Briefly shows Player 1's score before
* Player 2's turn begins.
**************************************************/
void showInterimScore() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Player 1 done!");
display.setTextSize(3);
display.setCursor(44, 20);
display.print(scoreP1);
display.setTextSize(1);
display.setCursor(0, 55);
display.print("taps in ");
display.print(roundSeconds);
display.print(" seconds");
display.display();
}
// ---------------------------------------------------------------------------
// State: RESULT
// ---------------------------------------------------------------------------
/**************************************************
* Function: showResult
* Purpose: Displays both scores and declares the
* winner (or a tie).
**************************************************/
void showResult() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
// Scores side by side
display.setTextSize(1);
display.setCursor(0, 0);
display.print("P1");
display.setCursor(64, 0);
display.print("P2");
display.setTextSize(3);
display.setCursor(0, 12);
display.print(scoreP1);
display.setCursor(64, 12);
display.print(scoreP2);
display.drawFastHLine(0, 42, 128, SSD1306_WHITE);
display.setTextSize(1);
if (scoreP1 > scoreP2) {
display.setCursor(20, 47);
display.print("Player 1 WINS!");
beepWin();
setEyes(0, 120, 0, 120, 0, 0); // P1 eye green, P2 eye red
} else if (scoreP2 > scoreP1) {
display.setCursor(20, 47);
display.print("Player 2 WINS!");
beepWin();
setEyes(120, 0, 0, 0, 120, 0); // P1 eye red, P2 eye green
} else {
display.setCursor(28, 47);
display.print("It's a TIE!");
beepTie();
setEyes(0, 80, 0, 0, 80, 0); // both eyes green for a tie
}
display.setCursor(16, 57);
display.print("Press any button");
display.display();
allLEDsOn();
anyBtnWasDown = true; // ignore button still held
}
/**************************************************
* Function: handleResult
* Purpose: Waits for a button press to return
* to the menu.
**************************************************/
void handleResult() {
bool b1 = (digitalRead(BUTTON_ONE) == PRESSED);
bool b2 = (digitalRead(BUTTON_TWO) == PRESSED);
bool anyDown = b1 || b2;
if (anyBtnWasDown) {
if (!anyDown) anyBtnWasDown = false;
return;
}
if (anyDown) {
anyBtnWasDown = true;
while (digitalRead(BUTTON_ONE) == PRESSED ||
digitalRead(BUTTON_TWO) == PRESSED) {
delay(5);
}
gameState = STATE_MENU;
showMenu();
}
}
// ---------------------------------------------------------------------------
// Drawing helpers
// ---------------------------------------------------------------------------
/**************************************************
* Function: drawCountdown
* Purpose: Updates the OLED during the GO phase,
* showing the time remaining and tap count.
*
* Parameters:
* secondsLeft - whole seconds remaining in the round
* tapsNow - current tap count for this player
**************************************************/
void drawCountdown(int secondsLeft, int tapsNow) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
// Player label
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Player ");
display.print(currentPlayer);
display.print(" TAP!");
// Big tap counter
display.setTextSize(3);
if (tapsNow < 10) {
display.setCursor(52, 16);
} else if (tapsNow < 100) {
display.setCursor(34, 16);
} else {
display.setCursor(16, 16);
}
display.print(tapsNow);
display.setTextSize(1);
display.setCursor(0, 46);
display.print("taps");
// Countdown bar on the right
display.setCursor(90, 46);
display.print(secondsLeft);
display.print("s left");
// Progress bar at the bottom
int barWidth = map(secondsLeft, 0, roundSeconds, 0, 126);
display.drawRect(1, 57, 126, 6, SSD1306_WHITE);
display.fillRect(1, 57, barWidth, 6, SSD1306_WHITE);
display.display();
}
// ---------------------------------------------------------------------------
// Hardware helpers
// ---------------------------------------------------------------------------
/**************************************************
* Function: readRoundIndex
* Purpose: Maps the potentiometer to one of the
* available round-length presets.
*
* Returns:
* Index into ROUND_SECONDS array.
**************************************************/
uint8_t readRoundIndex() {
int raw = analogRead(POT_PIN);
int binSize = 1024 / ROUND_COUNT;
uint8_t idx = raw / binSize;
if (idx >= ROUND_COUNT) idx = ROUND_COUNT - 1;
return idx;
}
/**************************************************
* Function: applyRoundPreset
* Purpose: Reads the pot and sets roundSeconds.
**************************************************/
void applyRoundPreset() {
roundSeconds = ROUND_SECONDS[readRoundIndex()];
}
/**************************************************
* Function: readActiveButton
* Purpose: Returns true if the current player's
* button is physically pressed.
*
* Returns:
* true if pressed, false otherwise.
**************************************************/
bool readActiveButton() {
if (currentPlayer == 1) {
return (digitalRead(BUTTON_ONE) == PRESSED);
}
return (digitalRead(BUTTON_TWO) == PRESSED);
}
/**************************************************
* Function: updateDebounce
* Purpose: Simple time-based debounce filter.
*
* Parameters:
* rawDown - raw button state (true = pressed)
* nowMs - current millis() value
*
* Returns:
* Debounced stable state.
**************************************************/
bool updateDebounce(bool rawDown, unsigned long nowMs) {
if (rawDown != btnDownLast) {
btnChangeMs = nowMs;
btnDownLast = rawDown;
}
if (nowMs - btnChangeMs >= DEBOUNCE_MS) {
return rawDown;
}
return btnDownStable;
}
/**************************************************
* Function: flashStartSequence
* Purpose: Plays a short ready/set/go sequence
* with LEDs and tone before the round.
**************************************************/
void flashStartSequence() {
// Three quick beeps then a high GO tone
for (uint8_t i = 0; i < 3; i++) {
allLEDsOn();
tone(PIEZO, TONE_READY_HZ, 80);
delay(150);
allLEDsOff();
delay(100);
}
allLEDsOn();
tone(PIEZO, TONE_GO_HZ, 200);
delay(200);
allLEDsOff();
// Eyes: active glow for current player
if (currentPlayer == 1) {
setEyes(0, 120, 0, 0, 0, 0);
} else {
setEyes(0, 0, 0, 0, 0, 120);
}
}
/**************************************************
* Function: beepStop
* Purpose: Plays a descending tone to signal that
* time has run out.
**************************************************/
void beepStop() {
tone(PIEZO, TONE_STOP_HZ + 200, 100);
delay(110);
tone(PIEZO, TONE_STOP_HZ, 200);
delay(210);
}
/**************************************************
* Function: beepWin
* Purpose: Plays a short victory fanfare.
**************************************************/
void beepWin() {
tone(PIEZO, TONE_WIN_HZ, 80);
delay(100);
tone(PIEZO, TONE_WIN_HZ + 200, 80);
delay(100);
tone(PIEZO, TONE_WIN_HZ + 400, 150);
delay(160);
}
/**************************************************
* Function: beepTie
* Purpose: Plays a neutral two-tone tie sound.
**************************************************/
void beepTie() {
tone(PIEZO, TONE_TIE_HZ, 100);
delay(120);
tone(PIEZO, TONE_TIE_HZ, 100);
delay(120);
}
/**************************************************
* Function: setEyes
* Purpose: Sets both NeoPixel eyes to given colours.
*
* Parameters:
* r0, g0, b0 - RGB values for the left eye
* r1, g1, b1 - RGB values for the right eye
**************************************************/
void setEyes(uint8_t r0, uint8_t g0, uint8_t b0,
uint8_t r1, uint8_t g1, uint8_t b1) {
pixel.setPixelColor(EYE_LEFT, pixel.Color(r0, g0, b0));
pixel.setPixelColor(EYE_RIGHT, pixel.Color(r1, g1, b1));
pixel.show();
}
/**************************************************
* Function: chaseLEDs
* Purpose: Animates the four cheek LEDs in a
* chasing pattern during the GO phase.
*
* Parameters:
* nowMs - current millis() value
**************************************************/
void chaseLEDs(unsigned long nowMs) {
if (nowMs - lastChaserMs >= 120) {
lastChaserMs = nowMs;
for (uint8_t i = 0; i < 4; i++) {
digitalWrite(LEDs[i], (i == ledChaser) ? HIGH : LOW);
}
ledChaser = (ledChaser + 1) % 4;
}
}
/**************************************************
* Function: allLEDsOff
* Purpose: Turns all four cheek LEDs off.
**************************************************/
void allLEDsOff() {
for (uint8_t i = 0; i < 4; i++) {
digitalWrite(LEDs[i], LOW);
}
}
/**************************************************
* Function: allLEDsOn
* Purpose: Turns all four cheek LEDs on.
**************************************************/
void allLEDsOn() {
for (uint8_t i = 0; i < 4; i++) {
digitalWrite(LEDs[i], HIGH);
}
}
*If you’re copying and pasting the code, or typing from scratch, delete everything out of a new Arduino sketch and paste / type in the above text.