This project transforms basic components like LED strips and an Arduino into a professional-grade arcade experience that tests the limits of human reaction time.
Arcade reaction game is a genre of play designed to test a player's physical response time and hand-eye coordination against increasingly difficult visual or auditory cues. These games are usually dead simple (a single button, a joystick, or a touch screen), and anyone can play immediately, but mastering the timing is the real challenge.

This time I will present you with a way to make such a game using an Arduino microcontroller and a WS2812B type LED Strip. This game is designed for two players and the basic goal is to press a button as quickly as possible immediately after a certain color appears on the LED strips. It is the simplicity and speed of play that make this game very addictive. In fact, I used the complete hardware from one of my previous projects ), adapting the code for this purpose.
The device is really simple to make and consists of only a few components:
- Arduino Nano microcontroller Board
- Two LED strips with 50 LEDs each of the WS2812B type
- Three Buttons (Player 1, Player 2 and Start)
- Speaker
- and Two Potentiometers
This project is sponsored by PCBWay . From concept to production, PCBWay provide cutting-edge electronic design solutions for global innovators, Including hardware design, software development, mechanical design, product testing and certification. PCBWay engineering team consists of experienced engineers in electronics, embedded systems, and product development. They successfully delivered hundreds of projects across industries such as medical devices, industrial automation, consumer electronics, smart home, and IoT.

The entire device is mounted in PVC housings, and the two buttons for the players are large, robust ones that I made specifically for these kinds of arcade games.
The code uses the Adafruit_NeoPixel library and is designed in a way that allows you to change all settings and options very easily. Specifically, in this case I used Arduino IDE ver. 1.8.16 and the latest version of the Adafruit_NeoPixel library.

First, let me describe the method and rules of the game. At the beginning, when starting both strips, a simple animation appears accompanied by appropriate sounds. The left bar and button represent player 1, and the right side represents player 2. By pressing the Start button, the first three LEDs on both strips begin to alternately light up with green and blue, which is a sign that the game has started.

After this, from the third to the eighth second, at some point all the LEDs will light up with red. This is the moment when the players should press the button. The first to press the button is the winner of the individual game. If any of the players presses the button before the red LEDs appear, they automatically lose the game. The next game begins by pressing the start button again. A complete game ends when one player is the first to win five individual games. The result is displayed by the top five LEDs with the appropriate color, player 1 yellow, and player 2 magenta. The winner of the complete game is indicated by flashing all the LEDs with the winning color and the corresponding winning sound sequence.

Since the game is based on reaction speed, it is desirable that the reaction time can also be displayed somehow on the strips. For this purpose, I used the Select potentiometer. In the extreme left position, the active scale (45 LEDs) represents a time of 10 milliseconds (4 LEDs approximately indicate 1 millisecond), and in the extreme right position, the scale displays a time of 1 second (one LED indicates 22 milliseconds). At the end of any individual game, we can "Zoom" the time. So we can very precisely determine the reaction time of the two players (theoretically with an accuracy of 0.25mS !!!). This will become clearer to you during the specific demonstration game that you will watch below.

We set the left potentiometer approximately in the middle, where the entire scale represents a time of half a second. The right potentiometer changes the intensity of the LEDs from 0 to maximum.
Finally a short conclusion. This project transforms basic components like LED strips and an Arduino into a professional-grade arcade experience that tests the limits of human reaction time. It is the perfect blend of simple mechanics and high-precision engineering, making it a great addition to any DIY game collection.

// by mircemk, 2026
#include <Adafruit_NeoPixel.h>
#define PIN_L 5
#define PIN_R 6
#define PIN_START 7
#define PIN_BTN_L 8
#define PIN_BTN_R 9
#define PIN_SPK 10
#define PIN_BRIGHT A4
#define PIN_ZOOM A5
const int LEDS = 50;
const uint8_t BRIGHT_MAX = 100; // максимум наместо 255
const unsigned long BRIGHT_UPDATE_MS = 25;
const uint8_t BRIGHT_SMOOTH_SHIFT = 3; // 1/8 smoothing (поголемо = помазно)
const uint8_t BRIGHT_DEADBAND = 1; // игнорирај +/-1 чекор
// ---- LAYOUT ----
const int SCORE_LEDS = 5; // 45..49
const int GAP_LEDS = 5; // 40..44 (always OFF)
const int PLAY_LEDS = LEDS - SCORE_LEDS - GAP_LEDS; // 40 (0..39)
const int GAP_BOTTOM = PLAY_LEDS; // 40
const int GAP_TOP = PLAY_LEDS + GAP_LEDS - 1; // 44
const int SCORE_BOTTOM = PLAY_LEDS + GAP_LEDS; // 45
const int SCORE_TOP = LEDS - 1; // 49
unsigned long lastBrightUpdate = 0;
uint16_t brightFilt = 0; // работи во 0..(BRIGHT_MAX<<8)
uint8_t brightApplied = 255; // force first apply
// ---- TIMING ----
const unsigned long IDLE_STEP_MS = 30;
const unsigned long BLINK_MS = 180;
const unsigned long RED_MIN_MS = 4000;
const unsigned long RED_MAX_MS = 8000;
const unsigned long BAR_STEP_MS = 10;
const unsigned long SECOND_PRESS_TIMEOUT_MS = 2000;
// Winner baseline scale (fixed)
const unsigned long ABS_SCALE_MS = 1000; // 0..1000ms -> 0..40 LEDs
// MATCH FLASH (only first 40 LEDs)
const byte MATCH_FLASH_TIMES = 3;
const unsigned long MATCH_FLASH_PERIOD = 220;
// After match flash: show solid winner color for 5 seconds, then go to idle animation
const unsigned long FINAL_HOLD_MS = 5000;
// Debounce
const unsigned long DEBOUNCE_MS = 20;
// ---- SOUND ----
const uint16_t IDLE_F_MIN = 100;
const uint16_t IDLE_F_MAX = 1200;
const uint16_t BLINK_F1 = 300;
const uint16_t BLINK_F2 = 500;
const uint16_t RED_F = 1000;
const unsigned long ROUND_WIN_DELAY_MS = 500;
// Round win (first 4 notes of match theme) + pause
const uint16_t ROUND_WIN_FREQS[] = { 523, 659, 784, 1046, 0 };
const uint16_t ROUND_WIN_DURS[] = { 180, 180, 200, 260, 120 };
// False-start buzzer (low)
const uint16_t FALSE_FREQS[] = { 160, 120, 90, 0, 90, 0 };
const uint16_t FALSE_DURS[] = { 180, 180, 220, 80, 220, 140 };
// Match win sequence (longer)
const uint16_t MATCH_FREQS[] = { 523, 659, 784, 1046, 784, 659, 523, 0, 880, 0 };
const uint16_t MATCH_DURS[] = { 120, 120, 140, 200, 120, 120, 180, 90, 260, 150 };
Adafruit_NeoPixel stripL(LEDS, PIN_L, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel stripR(LEDS, PIN_R, NEO_GRB + NEO_KHZ800);
uint32_t OFF, RED, GREEN, BLUE;
uint32_t L_COL, R_COL;
enum State {
IDLE,
BLINK,
REDGO,
SHOWBARS_ANIM,
RESULT_WAIT_START,
MATCHFLASH,
MATCH_HOLD,
MATCH_IDLE
};
State state = IDLE;
// ---- debounce state ----
bool stableStart = LOW, stableL = LOW, stableR = LOW;
bool lastReadStart = LOW, lastReadL = LOW, lastReadR = LOW;
unsigned long lastChangeStart = 0;
unsigned long lastChangeL = 0;
unsigned long lastChangeR = 0;
// ---- idle chase ----
unsigned long lastIdle = 0;
int chasePos = 0;
byte chaseHue = 0;
// ---- blink ----
unsigned long lastBlink = 0;
bool blinkToggle = false;
unsigned long redGoTime = 0;
// ---- round ----
bool lUsed = false, rUsed = false;
bool gotL = false, gotR = false;
unsigned long tRed = 0;
unsigned long reactL = 0, reactR = 0;
byte winner = 0; // 1 left, 2 right
int targetBarL = 0, targetBarR = 0;
int curBarL = 0, curBarR = 0;
unsigned long lastBarStep = 0;
// ---- score ----
byte scoreL = 0, scoreR = 0;
// ---- match flash ----
uint32_t matchWinnerColor = 0;
unsigned long lastFlashT = 0;
bool flashOn = false;
byte flashCount = 0;
// ---- final hold timer ----
unsigned long finalHoldStart = 0;
// ---- delayed round-win sound trigger ----
bool roundWinSoundPending = false;
unsigned long roundWinSoundAt = 0;
// ---- LED FREEZE DURING ROUND WIN SOUND ----
bool freezeLeds = false; // when true: DO NOT call show() anywhere
int frozenBarL = 0;
int frozenBarR = 0;
// ================= SOUND PLAYER (non-blocking) =================
struct BeepSeq {
const uint16_t* freqs = nullptr;
const uint16_t* durs = nullptr; // ms per tone
uint8_t len = 0;
uint8_t idx = 0;
bool active = false;
unsigned long nextT = 0;
};
BeepSeq seq;
int continuousFreq = 0;
int currentToneFreq = -1;
void soundStop() {
noTone(PIN_SPK);
currentToneFreq = -1;
}
void soundSetContinuous(int f) {
continuousFreq = f;
}
void soundStartSeq(const uint16_t* freqs, const uint16_t* durs, uint8_t len) {
seq.freqs = freqs;
seq.durs = durs;
seq.len = len;
seq.idx = 0;
seq.active = true;
seq.nextT = 0;
}
bool soundSeqActive() { return seq.active; }
void soundUpdate(unsigned long now) {
if (seq.active) {
if (seq.nextT == 0 || (long)(now - seq.nextT) >= 0) {
if (seq.idx >= seq.len) {
seq.active = false;
} else {
uint16_t f = seq.freqs[seq.idx];
uint16_t d = seq.durs[seq.idx];
if (f == 0) {
noTone(PIN_SPK);
currentToneFreq = -1;
} else {
// fixed duration per note
tone(PIN_SPK, f, d);
currentToneFreq = (int)f;
}
seq.nextT = now + d;
seq.idx++;
}
}
return;
}
// No sequence active -> continuous tone
if (continuousFreq <= 0) {
if (currentToneFreq != -1) soundStop();
} else {
if (currentToneFreq != continuousFreq) {
tone(PIN_SPK, (unsigned int)continuousFreq);
currentToneFreq = continuousFreq;
}
}
}
// ================= HELPERS =================
uint32_t wheel(byte p) {
p = 255 - p;
if (p < 85) return stripL.Color(255 - p * 3, 0, p * 3);
if (p < 170) { p -= 85; return stripL.Color(0, p * 3, 255 - p * 3); }
p -= 170; return stripL.Color(p * 3, 255 - p * 3, 0);
}
void showBoth() {
if (freezeLeds) return;
stripL.show();
stripR.show();
}
void clearPlayfield() {
for (int i = 0; i < PLAY_LEDS; i++) {
stripL.setPixelColor(i, OFF);
stripR.setPixelColor(i, OFF);
}
}
void clearGap() {
for (int i = GAP_BOTTOM; i <= GAP_TOP; i++) {
stripL.setPixelColor(i, OFF);
stripR.setPixelColor(i, OFF);
}
}
void setPlayfield(uint32_t c) {
for (int i = 0; i < PLAY_LEDS; i++) {
stripL.setPixelColor(i, c);
stripR.setPixelColor(i, c);
}
}
void drawScore() {
clearGap();
for (int i = SCORE_BOTTOM; i <= SCORE_TOP; i++) {
stripL.setPixelColor(i, OFF);
stripR.setPixelColor(i, OFF);
}
for (byte i = 0; i < scoreL && i < SCORE_LEDS; i++) stripL.setPixelColor(SCORE_TOP - i, L_COL);
for (byte i = 0; i < scoreR && i < SCORE_LEDS; i++) stripR.setPixelColor(SCORE_TOP - i, R_COL);
}
unsigned long readZoom() {
int v = analogRead(PIN_ZOOM);
return 10UL + (unsigned long)((v * 990UL) / 1023UL);
}
int mapAbs(unsigned long ms) {
if (ms > ABS_SCALE_MS) ms = ABS_SCALE_MS;
unsigned long bar = (ms * (unsigned long)PLAY_LEDS + (ABS_SCALE_MS / 2)) / ABS_SCALE_MS;
if (bar > (unsigned long)PLAY_LEDS) bar = PLAY_LEDS;
return (int)bar;
}
int mapDiff(unsigned long diff, unsigned long zoom, int maxExtra) {
if (zoom < 10) zoom = 10;
if (diff > zoom) diff = zoom;
unsigned long extra = (diff * (unsigned long)maxExtra + (zoom / 2)) / zoom;
if ((int)extra > maxExtra) extra = maxExtra;
return (int)extra;
}
void resetRoundVars() {
lUsed = rUsed = false;
gotL = gotR = false;
reactL = reactR = 0;
winner = 0;
targetBarL = targetBarR = 0;
curBarL = curBarR = 0;
roundWinSoundPending = false;
freezeLeds = false;
}
void startRoundKeepScore(unsigned long now) {
state = BLINK;
lastBlink = now;
blinkToggle = false;
redGoTime = now + random(RED_MIN_MS, RED_MAX_MS + 1);
resetRoundVars();
clearPlayfield();
drawScore();
showBoth();
soundSetContinuous(BLINK_F1);
}
void startMatchFlash(uint32_t winColor) {
matchWinnerColor = winColor;
state = MATCHFLASH;
lastFlashT = millis();
flashOn = false;
flashCount = 0;
soundStartSeq(MATCH_FREQS, MATCH_DURS, sizeof(MATCH_FREQS) / sizeof(MATCH_FREQS[0]));
soundSetContinuous(0);
}
// Draw bars once (used when freezing)
void drawBarsStatic(int barL, int barR) {
clearPlayfield();
for (int i = 0; i < barL && i < PLAY_LEDS; i++) stripL.setPixelColor(i, L_COL);
for (int i = 0; i < barR && i < PLAY_LEDS; i++) stripR.setPixelColor(i, R_COL);
drawScore();
showBoth();
}
// ---------- setup ----------
void setup() {
pinMode(PIN_START, INPUT);
pinMode(PIN_BTN_L, INPUT);
pinMode(PIN_BTN_R, INPUT);
pinMode(PIN_SPK, OUTPUT);
pinMode(PIN_BRIGHT, INPUT);
pinMode(PIN_ZOOM, INPUT);
stripL.begin();
stripR.begin();
OFF = stripL.Color(0, 0, 0);
RED = stripL.Color(255, 0, 0);
GREEN = stripL.Color(0, 255, 0);
BLUE = stripL.Color(0, 0, 255);
L_COL = stripL.Color(255, 180, 0);
R_COL = stripL.Color(255, 0, 180);
randomSeed(analogRead(A0));
state = IDLE;
clearPlayfield();
drawScore();
showBoth();
soundSetContinuous(IDLE_F_MIN);
}
// ---------- loop ----------
void loop() {
unsigned long now = millis();
// Brightness
// Brightness (filtered + capped + rate limited)
if (now - lastBrightUpdate >= BRIGHT_UPDATE_MS) {
lastBrightUpdate = now;
// map pot -> 0..BRIGHT_MAX
uint16_t raw = analogRead(PIN_BRIGHT); // 0..1023
uint16_t target = (raw * BRIGHT_MAX + 511) / 1023; // 0..BRIGHT_MAX
// IIR smoothing in fixed point (<<8)
uint16_t targetFP = (uint16_t)(target << 8);
brightFilt = brightFilt + ((int32_t)targetFP - (int32_t)brightFilt) / (1 << BRIGHT_SMOOTH_SHIFT);
uint8_t b = (uint8_t)(brightFilt >> 8);
// deadband to avoid flicker from tiny pot noise
if (b > brightApplied + BRIGHT_DEADBAND || b + BRIGHT_DEADBAND < brightApplied) {
brightApplied = b;
stripL.setBrightness(brightApplied);
stripR.setBrightness(brightApplied);
}
}
// ---- DEBOUNCE -> edges ----
bool startEdge = false, lEdge = false, rEdge = false;
bool readStart = digitalRead(PIN_START);
if (readStart != lastReadStart) { lastChangeStart = now; lastReadStart = readStart; }
if (now - lastChangeStart > DEBOUNCE_MS) {
if (stableStart != readStart) { stableStart = readStart; if (stableStart == HIGH) startEdge = true; }
}
bool readL = digitalRead(PIN_BTN_L);
if (readL != lastReadL) { lastChangeL = now; lastReadL = readL; }
if (now - lastChangeL > DEBOUNCE_MS) {
if (stableL != readL) { stableL = readL; if (stableL == HIGH) lEdge = true; }
}
bool readR = digitalRead(PIN_BTN_R);
if (readR != lastReadR) { lastChangeR = now; lastReadR = readR; }
if (now - lastChangeR > DEBOUNCE_MS) {
if (stableR != readR) { stableR = readR; if (stableR == HIGH) rEdge = true; }
}
// Update sound engine
soundUpdate(now);
// When round-win sound is active, freeze LED updates completely for clean tone
if (freezeLeds) {
if (!soundSeqActive()) {
// sequence ended -> unfreeze
freezeLeds = false;
// redraw once (so zoom changes etc. can resume)
drawBarsStatic(curBarL, curBarR);
}
}
// Trigger delayed round-win sound ONLY when time comes and nothing else is playing
if (roundWinSoundPending && (long)(now - roundWinSoundAt) >= 0 && !soundSeqActive()) {
// Freeze LEDs for the whole round-win sequence
freezeLeds = true;
// show the bars ONCE, then stop calling show()
drawBarsStatic(curBarL, curBarR);
soundStartSeq(ROUND_WIN_FREQS, ROUND_WIN_DURS, sizeof(ROUND_WIN_FREQS) / sizeof(ROUND_WIN_FREQS[0]));
soundSetContinuous(0);
roundWinSoundPending = false;
}
// ---- START behavior ----
if (startEdge) {
if (state == MATCH_HOLD || state == MATCH_IDLE) {
scoreL = 0;
scoreR = 0;
startRoundKeepScore(now);
return;
}
if (state != MATCHFLASH) {
startRoundKeepScore(now);
return;
}
}
// ---- IDLE / MATCH_IDLE ----
if (state == IDLE || state == MATCH_IDLE) {
if (now - lastIdle >= IDLE_STEP_MS) {
lastIdle = now;
clearPlayfield();
uint32_t c = wheel(chaseHue);
stripL.setPixelColor(chasePos, c);
stripR.setPixelColor(chasePos, c);
drawScore();
showBoth();
uint16_t f = (uint16_t)(IDLE_F_MIN + (uint32_t)(IDLE_F_MAX - IDLE_F_MIN) * (uint32_t)chasePos / (uint32_t)(PLAY_LEDS - 1));
soundSetContinuous(f);
chasePos++;
if (chasePos >= PLAY_LEDS) chasePos = 0;
chaseHue += 3;
}
return;
}
// ---- BLINK ----
if (state == BLINK) {
// false start => lose + low buzzer
if (lEdge && !lUsed) {
lUsed = true;
winner = 2;
gotL = gotR = true;
reactL = reactR = 0;
soundStartSeq(FALSE_FREQS, FALSE_DURS, sizeof(FALSE_FREQS) / sizeof(FALSE_FREQS[0]));
soundSetContinuous(0);
if (scoreR < SCORE_LEDS) scoreR++;
if (scoreR >= 5) { startMatchFlash(R_COL); return; }
unsigned long zoom = readZoom();
int base = mapAbs(0);
int extra = mapDiff(0, zoom, PLAY_LEDS - base);
targetBarR = base;
targetBarL = base + extra;
curBarL = curBarR = 0;
lastBarStep = now;
state = SHOWBARS_ANIM;
return;
}
if (rEdge && !rUsed) {
rUsed = true;
winner = 1;
gotL = gotR = true;
reactL = reactR = 0;
soundStartSeq(FALSE_FREQS, FALSE_DURS, sizeof(FALSE_FREQS) / sizeof(FALSE_FREQS[0]));
soundSetContinuous(0);
if (scoreL < SCORE_LEDS) scoreL++;
if (scoreL >= 5) { startMatchFlash(L_COL); return; }
unsigned long zoom = readZoom();
int base = mapAbs(0);
int extra = mapDiff(0, zoom, PLAY_LEDS - base);
targetBarL = base;
targetBarR = base + extra;
curBarL = curBarR = 0;
lastBarStep = now;
state = SHOWBARS_ANIM;
return;
}
if (now - lastBlink >= BLINK_MS) {
lastBlink = now;
blinkToggle = !blinkToggle;
clearPlayfield();
uint32_t c = blinkToggle ? GREEN : BLUE;
for (int i = 0; i < 3; i++) {
stripL.setPixelColor(i, c);
stripR.setPixelColor(i, c);
}
drawScore();
showBoth();
soundSetContinuous(blinkToggle ? BLINK_F1 : BLINK_F2);
}
if ((long)(now - redGoTime) >= 0) {
tRed = now;
clearPlayfield();
for (int i = 0; i < 3; i++) {
stripL.setPixelColor(i, RED);
stripR.setPixelColor(i, RED);
}
drawScore();
showBoth();
state = REDGO;
lUsed = rUsed = false;
gotL = gotR = false;
soundSetContinuous(RED_F);
return;
}
return;
}
// ---- REDGO ----
if (state == REDGO) {
if (lEdge && !lUsed) { lUsed = true; gotL = true; reactL = now - tRed; if (winner == 0) winner = 1; }
if (rEdge && !rUsed) { rUsed = true; gotR = true; reactR = now - tRed; if (winner == 0) winner = 2; }
bool timeout = false;
if (winner != 0 && (now - tRed >= SECOND_PRESS_TIMEOUT_MS)) timeout = true;
if ((gotL && gotR) || timeout) {
if (!gotL) reactL = SECOND_PRESS_TIMEOUT_MS;
if (!gotR) reactR = SECOND_PRESS_TIMEOUT_MS;
if (winner == 1 && scoreL < SCORE_LEDS) scoreL++;
if (winner == 2 && scoreR < SCORE_LEDS) scoreR++;
soundSetContinuous(0);
if (scoreL >= 5) { startMatchFlash(L_COL); return; }
if (scoreR >= 5) { startMatchFlash(R_COL); return; }
unsigned long zoom = readZoom();
unsigned long wMs = (winner == 1) ? reactL : reactR;
unsigned long lMs = (winner == 1) ? reactR : reactL;
int base = mapAbs(wMs);
int maxExtra = PLAY_LEDS - base;
if (maxExtra < 0) maxExtra = 0;
int extra = mapDiff((lMs > wMs) ? (lMs - wMs) : 0, zoom, maxExtra);
if (winner == 1) { targetBarL = base; targetBarR = base + extra; }
else { targetBarR = base; targetBarL = base + extra; }
curBarL = curBarR = 0;
lastBarStep = now;
state = SHOWBARS_ANIM;
}
return;
}
// ---- SHOWBARS_ANIM ----
if (state == SHOWBARS_ANIM) {
if (winner != 0) {
unsigned long zoom = readZoom();
unsigned long wMs = (winner == 1) ? reactL : reactR;
unsigned long lMs = (winner == 1) ? reactR : reactL;
int base = mapAbs(wMs);
int maxExtra = PLAY_LEDS - base;
if (maxExtra < 0) maxExtra = 0;
int extra = mapDiff((lMs > wMs) ? (lMs - wMs) : 0, zoom, maxExtra);
if (winner == 1) { targetBarL = base; targetBarR = base + extra; }
else { targetBarR = base; targetBarL = base + extra; }
}
if (now - lastBarStep >= BAR_STEP_MS) {
lastBarStep = now;
if (curBarL < targetBarL) curBarL++;
if (curBarR < targetBarR) curBarR++;
clearPlayfield();
for (int i = 0; i < curBarL && i < PLAY_LEDS; i++) stripL.setPixelColor(i, L_COL);
for (int i = 0; i < curBarR && i < PLAY_LEDS; i++) stripR.setPixelColor(i, R_COL);
drawScore();
showBoth();
if (curBarL >= targetBarL && curBarR >= targetBarR) {
state = RESULT_WAIT_START;
// schedule clean round win sound after bars shown
roundWinSoundPending = true;
roundWinSoundAt = now + ROUND_WIN_DELAY_MS;
}
}
return;
}
// ---- RESULT_WAIT_START ----
if (state == RESULT_WAIT_START) {
// If LEDs are frozen due to sound -> do not update visuals
if (freezeLeds) return;
unsigned long zoom = readZoom();
unsigned long wMs = (winner == 1) ? reactL : reactR;
unsigned long lMs = (winner == 1) ? reactR : reactL;
int base = mapAbs(wMs);
int maxExtra = PLAY_LEDS - base;
if (maxExtra < 0) maxExtra = 0;
int extra = mapDiff((lMs > wMs) ? (lMs - wMs) : 0, zoom, maxExtra);
if (winner == 1) { targetBarL = base; targetBarR = base + extra; }
else { targetBarR = base; targetBarL = base + extra; }
static unsigned long lastT = 0;
if (now - lastT >= BAR_STEP_MS) {
lastT = now;
if (curBarL < targetBarL) curBarL++;
else if (curBarL > targetBarL) curBarL--;
if (curBarR < targetBarR) curBarR++;
else if (curBarR > targetBarR) curBarR--;
clearPlayfield();
for (int i = 0; i < curBarL && i < PLAY_LEDS; i++) stripL.setPixelColor(i, L_COL);
for (int i = 0; i < curBarR && i < PLAY_LEDS; i++) stripR.setPixelColor(i, R_COL);
drawScore();
showBoth();
}
return;
}
// ---- MATCHFLASH ----
if (state == MATCHFLASH) {
if (now - lastFlashT >= MATCH_FLASH_PERIOD) {
lastFlashT = now;
flashOn = !flashOn;
if (flashOn) setPlayfield(matchWinnerColor);
else clearPlayfield();
drawScore();
showBoth();
if (!flashOn) {
flashCount++;
if (flashCount >= MATCH_FLASH_TIMES) {
setPlayfield(matchWinnerColor);
drawScore();
showBoth();
finalHoldStart = now;
state = MATCH_HOLD;
}
}
}
return;
}
// ---- MATCH_HOLD ----
if (state == MATCH_HOLD) {
if (now - finalHoldStart >= FINAL_HOLD_MS) {
state = MATCH_IDLE;
lastIdle = now;
chasePos = 0;
chaseHue = 0;
clearPlayfield();
drawScore();
showBoth();
soundSetContinuous(IDLE_F_MIN);
}
return;
}
}









