Initial implementation of HeisserDraht buzz wire game

ESP32-C3 based hot wire game with WS2812B LED effects,
SSD1306 OLED display, debounced touch detection, and
full game state machine (IDLE → COUNTDOWN → PLAYING → GAME_OVER).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 00:24:52 +01:00
commit 01e79a4eb2
9 changed files with 619 additions and 0 deletions

132
led_effects.cpp Normal file
View File

@@ -0,0 +1,132 @@
#include "led_effects.h"
#include "config.h"
#include "game_logic.h"
#include <Adafruit_NeoPixel.h>
static Adafruit_NeoPixel strip(NUM_LEDS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
// ── Helpers ─────────────────────────────────────────────────────────
static uint32_t colorWheel(uint8_t pos) {
pos = 255 - pos;
if (pos < 85) return strip.Color(255 - pos * 3, 0, pos * 3);
if (pos < 170) { pos -= 85; return strip.Color(0, pos * 3, 255 - pos * 3); }
pos -= 170;
return strip.Color(pos * 3, 255 - pos * 3, 0);
}
// ── Animations ──────────────────────────────────────────────────────
static void animIdle() {
// Rainbow chase
static uint16_t hue = 0;
for (int i = 0; i < NUM_LEDS; i++) {
strip.setPixelColor(i, colorWheel((i * 256 / NUM_LEDS + hue) & 0xFF));
}
hue += 2;
strip.show();
}
static void animCountdown() {
GameData& gd = gameGetData();
uint32_t color;
switch (gd.countdownValue) {
case 3: // fall through
case 2: color = strip.Color(255, 0, 0); break; // Red
case 1: color = strip.Color(255, 180, 0); break; // Yellow
default: color = strip.Color(0, 255, 0); break; // Green (GO!)
}
for (int i = 0; i < NUM_LEDS; i++) {
strip.setPixelColor(i, color);
}
strip.show();
}
static void animPlaying() {
GameData& gd = gameGetData();
unsigned long totalMs = (unsigned long)GAME_DURATION_S * 1000UL;
float progress = 1.0f - (float)gd.elapsedMs / (float)totalMs;
if (progress < 0.0f) progress = 0.0f;
int litLeds = (int)(progress * NUM_LEDS + 0.5f);
for (int i = 0; i < NUM_LEDS; i++) {
if (i < litLeds) {
// Color transitions: green → yellow → red as time depletes
if (progress > 0.5f) strip.setPixelColor(i, strip.Color(0, 255, 0));
else if (progress > 0.2f) strip.setPixelColor(i, strip.Color(255, 180, 0));
else strip.setPixelColor(i, strip.Color(255, 0, 0));
} else {
strip.setPixelColor(i, 0);
}
}
strip.show();
}
static void animGameOverWin() {
// Green/gold sparkle
static unsigned long lastSparkle = 0;
unsigned long now = millis();
if (now - lastSparkle > 80) {
lastSparkle = now;
for (int i = 0; i < NUM_LEDS; i++) {
if (random(3) == 0)
strip.setPixelColor(i, strip.Color(255, 215, 0)); // Gold
else
strip.setPixelColor(i, strip.Color(0, 180, 0)); // Green
}
strip.show();
}
}
static void animGameOverLose() {
// Pulsing red breathing
static uint8_t breathVal = 0;
static int8_t breathDir = 2;
breathVal += breathDir;
if (breathVal >= 250 || breathVal <= 5) breathDir = -breathDir;
for (int i = 0; i < NUM_LEDS; i++) {
strip.setPixelColor(i, strip.Color(breathVal, 0, 0));
}
strip.show();
}
// ── Public API ──────────────────────────────────────────────────────
void ledsInit() {
strip.begin();
strip.setBrightness(LED_BRIGHTNESS);
strip.show();
}
void ledsSetBrightness(uint8_t b) {
strip.setBrightness(b);
}
void ledsTouchFlash() {
// Blocking 3x red blink (~300ms total)
for (int flash = 0; flash < 3; flash++) {
for (int i = 0; i < NUM_LEDS; i++)
strip.setPixelColor(i, strip.Color(255, 0, 0));
strip.show();
delay(50);
for (int i = 0; i < NUM_LEDS; i++)
strip.setPixelColor(i, 0);
strip.show();
delay(50);
}
}
void ledsUpdate() {
GameData& gd = gameGetData();
switch (gd.state) {
case STATE_IDLE: animIdle(); break;
case STATE_COUNTDOWN: animCountdown(); break;
case STATE_PLAYING: animPlaying(); break;
case STATE_GAME_OVER:
if (gd.won) animGameOverWin();
else animGameOverLose();
break;
}
}