commit 01e79a4eb2f1f828c47ad6ec86a3788ba6ad0c4d Author: Jan Remmer Siebels Date: Sun Feb 22 00:24:52 2026 +0100 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3bf25e --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Arduino build artifacts +build/ +*.elf +*.bin +*.hex +*.map + +# IDE files +.vscode/ +*.code-workspace +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Backup files +*~ +*.bak +*.swp diff --git a/HeisserDraht.ino b/HeisserDraht.ino new file mode 100644 index 0000000..a664da9 --- /dev/null +++ b/HeisserDraht.ino @@ -0,0 +1,54 @@ +/* + * HeisserDraht - ESP32-C3 Buzz Wire Game + * + * Hardware: + * - ESP32-C3 (RISC-V) + * - 16x WS2812B LEDs on GPIO 8 + * - SSD1306 128x64 OLED on I2C (SDA=4, SCL=5) + * - Wire touch sensor on GPIO 2 (INPUT_PULLUP, touch = GND) + * - Start button on GPIO 3 (INPUT_PULLUP, press = GND) + * - Optional passive buzzer on GPIO 10 + * + * Game flow: IDLE → COUNTDOWN → PLAYING → GAME_OVER → IDLE + */ + +#include "config.h" +#include "game_logic.h" +#include "led_effects.h" +#include "display.h" + +void setup() { + Serial.begin(115200); + delay(500); + Serial.println(); + Serial.println("=== HEISSER DRAHT " VERSION_STR " ==="); + Serial.println("[INIT] Starting..."); + + // Initialize subsystems + gameInit(); + Serial.println("[INIT] Game logic OK"); + + ledsInit(); + Serial.println("[INIT] LEDs OK"); + + displayInit(); + Serial.println("[INIT] Display OK"); + + Serial.println("[INIT] Ready! Press START to begin."); +} + +void loop() { + // Check inputs + gameCheckStartButton(); + gameCheckTouch(); + + // Update game state + gameUpdate(); + + // Update outputs + ledsUpdate(); + displayUpdate(); + + // Small delay for watchdog + delay(LOOP_DELAY_MS); +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..8ed8a1f --- /dev/null +++ b/config.h @@ -0,0 +1,50 @@ +#ifndef CONFIG_H +#define CONFIG_H + +// ── Pin Assignments ───────────────────────────────────────────────── +#define PIN_NEOPIXEL 8 // WS2812B data (330Ω series resistor recommended) +#define PIN_SDA 4 // I2C SDA (OLED) +#define PIN_SCL 5 // I2C SCL (OLED) +#define PIN_WIRE_TOUCH 2 // Wire touch detect (INPUT_PULLUP, loop-to-GND) +#define PIN_START_BTN 3 // Start button (INPUT_PULLUP, button-to-GND) +#define PIN_BUZZER 10 // Optional passive piezo buzzer + +// ── LED Configuration ─────────────────────────────────────────────── +#define NUM_LEDS 16 // Number of WS2812B LEDs (adjustable 10-20) +#define LED_BRIGHTNESS 60 // Default brightness (0-255) + +// ── OLED Configuration ────────────────────────────────────────────── +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_ADDR 0x3C +#define DISPLAY_UPDATE_MS 150 // Throttle display updates to avoid I2C flicker + +// ── Game Timing ───────────────────────────────────────────────────── +#define GAME_DURATION_S 60 // Game duration in seconds +#define COUNTDOWN_SECS 3 // Countdown before game starts +#define MAX_STRIKES 10 // Maximum strikes before game over + +// ── Debounce ──────────────────────────────────────────────────────── +#define TOUCH_DEBOUNCE_MS 200 // Touch detection debounce +#define BUTTON_DEBOUNCE_MS 50 // Start button debounce + +// ── Scoring ───────────────────────────────────────────────────────── +#define SCORE_BASE 1000 // Starting score +#define SCORE_STRIKE_PENALTY 50 // Points lost per strike +#define SCORE_TIME_DIVISOR 100 // elapsed_ms / divisor = time penalty + +// ── Buzzer Tones ──────────────────────────────────────────────────── +#define TONE_COUNTDOWN_HZ 800 +#define TONE_GO_HZ 1200 +#define TONE_TOUCH_HZ 1000 +#define TONE_WIN_HZ 1500 +#define TONE_LOSE_HZ 400 +#define TONE_DURATION_MS 150 + +// ── Loop Timing ───────────────────────────────────────────────────── +#define LOOP_DELAY_MS 10 // Main loop delay for watchdog + +// ── Version ───────────────────────────────────────────────────────── +#define VERSION_STR "v1.0" + +#endif // CONFIG_H diff --git a/display.cpp b/display.cpp new file mode 100644 index 0000000..d6be2d0 --- /dev/null +++ b/display.cpp @@ -0,0 +1,164 @@ +#include "display.h" +#include "config.h" +#include "game_logic.h" +#include +#include +#include + +static Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); +static unsigned long lastUpdateMs = 0; + +// ── Screens ───────────────────────────────────────────────────────── + +static void screenSplash() { + oled.clearDisplay(); + oled.setTextColor(SSD1306_WHITE); + + oled.setTextSize(2); + oled.setCursor(8, 10); + oled.print("HEISSER"); + oled.setCursor(20, 30); + oled.print("DRAHT"); + + oled.setTextSize(1); + oled.setCursor(48, 52); + oled.print(VERSION_STR); + + oled.display(); +} + +static void screenIdle() { + oled.clearDisplay(); + oled.setTextColor(SSD1306_WHITE); + + oled.setTextSize(2); + oled.setCursor(8, 5); + oled.print("HEISSER"); + oled.setCursor(20, 23); + oled.print("DRAHT"); + + // Blinking "Druecke START" + if ((millis() / 500) % 2 == 0) { + oled.setTextSize(1); + oled.setCursor(12, 50); + oled.print("Druecke START"); + } + + oled.display(); +} + +static void screenCountdown() { + GameData& gd = gameGetData(); + oled.clearDisplay(); + oled.setTextColor(SSD1306_WHITE); + + if (gd.countdownValue > 0) { + oled.setTextSize(4); + oled.setCursor(52, 16); + oled.print(gd.countdownValue); + } else { + oled.setTextSize(3); + oled.setCursor(36, 20); + oled.print("GO!"); + } + + oled.display(); +} + +static void screenPlaying() { + GameData& gd = gameGetData(); + oled.clearDisplay(); + oled.setTextColor(SSD1306_WHITE); + + // Timer MM:SS + unsigned long remainMs = 0; + unsigned long totalMs = (unsigned long)GAME_DURATION_S * 1000UL; + if (gd.elapsedMs < totalMs) remainMs = totalMs - gd.elapsedMs; + uint8_t mins = (remainMs / 1000) / 60; + uint8_t secs = (remainMs / 1000) % 60; + + oled.setTextSize(1); + oled.setCursor(0, 0); + oled.print("Zeit: "); + if (mins < 10) oled.print('0'); + oled.print(mins); + oled.print(':'); + if (secs < 10) oled.print('0'); + oled.print(secs); + + // Strikes + oled.setCursor(0, 14); + oled.print("Fehler: "); + oled.print(gd.strikes); + oled.print('/'); + oled.print(MAX_STRIKES); + + // Score (large) + oled.setTextSize(3); + oled.setCursor(10, 34); + oled.print(gd.score); + + oled.display(); +} + +static void screenGameOver() { + GameData& gd = gameGetData(); + oled.clearDisplay(); + oled.setTextColor(SSD1306_WHITE); + + oled.setTextSize(1); + oled.setCursor(20, 0); + oled.print(gd.won ? "GESCHAFFT!" : "GAME OVER!"); + + // Final score + oled.setTextSize(2); + oled.setCursor(10, 14); + oled.print("Score:"); + oled.print(gd.score); + + // Stats + oled.setTextSize(1); + unsigned long totalSec = gd.elapsedMs / 1000; + oled.setCursor(0, 36); + oled.print("Zeit: "); + oled.print(totalSec); + oled.print("s Fehler: "); + oled.print(gd.strikes); + + // Restart hint (blinking) + if ((millis() / 600) % 2 == 0) { + oled.setCursor(8, 54); + oled.print("START = Nochmal"); + } + + oled.display(); +} + +// ── Public API ────────────────────────────────────────────────────── + +void displayInit() { + Wire.begin(PIN_SDA, PIN_SCL); + + if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { + Serial.println("[OLED] Init FAILED"); + return; + } + + Serial.println("[OLED] Init OK"); + screenSplash(); + delay(1500); +} + +void displayUpdate() { + unsigned long now = millis(); + if (now - lastUpdateMs < DISPLAY_UPDATE_MS) return; + lastUpdateMs = now; + + GameData& gd = gameGetData(); + switch (gd.state) { + case STATE_IDLE: screenIdle(); break; + case STATE_COUNTDOWN: screenCountdown(); break; + case STATE_PLAYING: screenPlaying(); break; + case STATE_GAME_OVER: screenGameOver(); break; + } +} diff --git a/display.h b/display.h new file mode 100644 index 0000000..39ce89b --- /dev/null +++ b/display.h @@ -0,0 +1,9 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#include + +void displayInit(); +void displayUpdate(); // Called every loop, dispatches based on game state (throttled) + +#endif // DISPLAY_H diff --git a/game_logic.cpp b/game_logic.cpp new file mode 100644 index 0000000..45995b0 --- /dev/null +++ b/game_logic.cpp @@ -0,0 +1,142 @@ +#include "game_logic.h" +#include "config.h" +#include "led_effects.h" + +static GameData gd; + +void gameInit() { + pinMode(PIN_WIRE_TOUCH, INPUT_PULLUP); + pinMode(PIN_START_BTN, INPUT_PULLUP); + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); + + gd.state = STATE_IDLE; + gd.strikes = 0; + gd.score = SCORE_BASE; + gd.gameStartMs = 0; + gd.elapsedMs = 0; + gd.countdownValue = COUNTDOWN_SECS; + gd.countdownStepMs = 0; + gd.touchActive = false; + gd.lastTouchMs = 0; + gd.lastButtonMs = 0; + gd.won = false; +} + +GameData& gameGetData() { + return gd; +} + +void gameCheckStartButton() { + if (digitalRead(PIN_START_BTN) == LOW) { + unsigned long now = millis(); + if (now - gd.lastButtonMs < BUTTON_DEBOUNCE_MS) return; + gd.lastButtonMs = now; + + if (gd.state == STATE_IDLE || gd.state == STATE_GAME_OVER) { + // Reset game data + gd.strikes = 0; + gd.score = SCORE_BASE; + gd.elapsedMs = 0; + gd.countdownValue = COUNTDOWN_SECS; + gd.touchActive = false; + gd.won = false; + + // Start countdown + gd.state = STATE_COUNTDOWN; + gd.countdownStepMs = millis(); + + Serial.println("[GAME] START pressed -> COUNTDOWN"); + } + } +} + +void gameCheckTouch() { + if (gd.state != STATE_PLAYING) return; + + bool touching = (digitalRead(PIN_WIRE_TOUCH) == LOW); + unsigned long now = millis(); + + if (touching && !gd.touchActive) { + if (now - gd.lastTouchMs < TOUCH_DEBOUNCE_MS) return; + gd.lastTouchMs = now; + gd.touchActive = true; + gd.strikes++; + + Serial.print("[GAME] TOUCH! Strikes: "); + Serial.println(gd.strikes); + + // Buzzer alert + tone(PIN_BUZZER, TONE_TOUCH_HZ, TONE_DURATION_MS); + + // Blocking LED flash + ledsTouchFlash(); + + // Check for game over by max strikes + if (gd.strikes >= MAX_STRIKES) { + gd.won = false; + gd.state = STATE_GAME_OVER; + // Lose tone + tone(PIN_BUZZER, TONE_LOSE_HZ, 500); + Serial.println("[GAME] MAX STRIKES -> GAME_OVER (lose)"); + } + } else if (!touching) { + gd.touchActive = false; + } +} + +void gameUpdate() { + unsigned long now = millis(); + + switch (gd.state) { + case STATE_IDLE: + // Nothing to update, waiting for start button + break; + + case STATE_COUNTDOWN: + if (now - gd.countdownStepMs >= 1000) { + gd.countdownStepMs = now; + if (gd.countdownValue > 0) { + // Countdown beep + tone(PIN_BUZZER, TONE_COUNTDOWN_HZ, 100); + Serial.print("[GAME] Countdown: "); + Serial.println(gd.countdownValue); + gd.countdownValue--; + } else { + // GO! + tone(PIN_BUZZER, TONE_GO_HZ, 200); + gd.state = STATE_PLAYING; + gd.gameStartMs = millis(); + Serial.println("[GAME] GO! -> PLAYING"); + } + } + break; + + case STATE_PLAYING: + gd.elapsedMs = now - gd.gameStartMs; + + // Calculate live score + gd.score = SCORE_BASE + - (gd.strikes * SCORE_STRIKE_PENALTY) + - (int)(gd.elapsedMs / SCORE_TIME_DIVISOR); + if (gd.score < 0) gd.score = 0; + + // Check time up + if (gd.elapsedMs >= (unsigned long)GAME_DURATION_S * 1000UL) { + gd.won = (gd.strikes < MAX_STRIKES); + gd.state = STATE_GAME_OVER; + if (gd.won) { + tone(PIN_BUZZER, TONE_WIN_HZ, 300); + Serial.println("[GAME] TIME UP -> GAME_OVER (win)"); + } else { + tone(PIN_BUZZER, TONE_LOSE_HZ, 500); + Serial.println("[GAME] TIME UP -> GAME_OVER (lose)"); + } + } + break; + + case STATE_GAME_OVER: + // Waiting for start button to restart + break; + } +} diff --git a/game_logic.h b/game_logic.h new file mode 100644 index 0000000..d26dc84 --- /dev/null +++ b/game_logic.h @@ -0,0 +1,37 @@ +#ifndef GAME_LOGIC_H +#define GAME_LOGIC_H + +#include + +// Game states +enum GameState { + STATE_IDLE, + STATE_COUNTDOWN, + STATE_PLAYING, + STATE_GAME_OVER +}; + +// Game data +struct GameData { + GameState state; + uint8_t strikes; + int score; + unsigned long gameStartMs; + unsigned long elapsedMs; + uint8_t countdownValue; // 3, 2, 1, 0(GO!) + unsigned long countdownStepMs; + bool touchActive; // true while wire is being touched + unsigned long lastTouchMs; + unsigned long lastButtonMs; + bool won; // true if timer ran out with strikes < MAX +}; + +void gameInit(); +void gameUpdate(); +GameData& gameGetData(); + +// Called by main loop to check inputs +void gameCheckStartButton(); +void gameCheckTouch(); + +#endif // GAME_LOGIC_H diff --git a/led_effects.cpp b/led_effects.cpp new file mode 100644 index 0000000..cccf8cd --- /dev/null +++ b/led_effects.cpp @@ -0,0 +1,132 @@ +#include "led_effects.h" +#include "config.h" +#include "game_logic.h" +#include + +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; + } +} diff --git a/led_effects.h b/led_effects.h new file mode 100644 index 0000000..fc4bf27 --- /dev/null +++ b/led_effects.h @@ -0,0 +1,11 @@ +#ifndef LED_EFFECTS_H +#define LED_EFFECTS_H + +#include + +void ledsInit(); +void ledsUpdate(); // Called every loop, dispatches based on game state +void ledsTouchFlash(); // Blocking 3x red blink (~300ms) +void ledsSetBrightness(uint8_t b); + +#endif // LED_EFFECTS_H