This commit is contained in:
2026-05-23 22:36:06 +02:00
parent 489e670bdc
commit ca0e3515be
629 changed files with 2558 additions and 103525 deletions
+1 -1
View File
@@ -1 +1 @@
7bad751cc212e33e418ee5528532e97d795b8d82
11b43f3b2b511fe7454baf368b17d1fda87cd079
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Written by Carl Vargklint 2026-05-19
# Install platformio CLI then run the script from this folder.
pio run -t upload
+74
View File
@@ -0,0 +1,74 @@
// ================================================================
// badge_config.h — Secfest 2026 Badge
// ================================================================
// Shared badge configuration: EEPROM layout, types, and globals
// accessed by both main.cpp and serial_games.cpp.
//
// EEPROM map (32 bytes):
// Byte 0 : matrix/global brightness (1-8)
// Byte 1 : flashlight brightness (1-8)
// Bytes 2-25 : IR codes for 6 buttons × 4 bytes each
// Layout per button: [protocol][addr_lo][addr_hi][command]
// ================================================================
#pragma once
#include <Arduino.h>
// ── EEPROM ──────────────────────────────────────────────────────
#define EEPROM_CFG_SIZE 32
#define EEPROM_ADDR_BRIGHT 0
#define EEPROM_ADDR_FLASH 1
#define EEPROM_ADDR_IR_BASE 2 // 4 bytes × 6 buttons = 24 bytes
// ── IR protocol IDs ─────────────────────────────────────────────
#define IR_PROTO_NEC 0
#define IR_PROTO_SAMSUNG 1
#define IR_PROTO_SONY 2
#define IR_PROTO_RC5 3
// ── IR code record (4 bytes on EEPROM) ──────────────────────────
struct IrCode {
uint8_t protocol; // IR_PROTO_*
uint8_t addr_lo; // address low byte
uint8_t addr_hi; // address high byte
uint8_t command; // command byte
};
static inline uint16_t irCodeAddr(const IrCode& c) {
return (uint16_t)c.addr_lo | ((uint16_t)c.addr_hi << 8);
}
// ── Button indices (must match BtnIdx in main.cpp) ───────────────
// BI_A=0, BI_B=1, BI_UP=2, BI_DOWN=3, BI_LEFT=4, BI_RIGHT=5
#define IR_BTN_COUNT 6
// ── Full badge configuration ─────────────────────────────────────
struct BadgeConfig {
uint8_t brightness; // matrix + front LED depth, 1-8
uint8_t flashBright; // flashlight brightness, 1-8
IrCode ir[IR_BTN_COUNT];// per-button IR codes
};
// Default IR codes — Samsung TV (NEC-compatible)
// Power=0x02, Mute=0x0F, VolumeUp=0x07, VolDown=0x0B, ChUp=0x12, ChDn=0x10
static const BadgeConfig BADGE_CONFIG_DEFAULT = {
8, // brightness
8, // flashBright
{
{ IR_PROTO_NEC, 0x07, 0x07, 0x07 }, // A → Volume Up
{ IR_PROTO_NEC, 0x07, 0x07, 0x0B }, // B → Volume Down
{ IR_PROTO_NEC, 0x07, 0x07, 0x12 }, // Up → Channel Up
{ IR_PROTO_NEC, 0x07, 0x07, 0x10 }, // Down → Channel Down
{ IR_PROTO_NEC, 0x07, 0x07, 0x0F }, // Left → Mute
{ IR_PROTO_NEC, 0x07, 0x07, 0x02 }, // Right → Power
}
};
// ── Global config — defined in main.cpp ──────────────────────────
extern BadgeConfig g_cfg;
// ── Persistence helpers — defined in main.cpp ────────────────────
void configLoad();
void configSave();
void configSaveBrightness();
void configSaveFlash();
void configSaveIr(uint8_t btnIdx);
+283 -35
View File
@@ -15,27 +15,75 @@
#include <Adafruit_IS31FL3731.h>
#include <EEPROM.h>
#include "font4x7.h"
#include "badge_config.h"
#include "serial_games.h"
static SerialGames serialGames;
// ============================================================ pin map =======
#define MATRIX_BRIGHTNESS 20
// ============================================================ brightness =====
// Global brightness scale: 1 (dim) … 8 (full).
// Stored in EEPROM byte 0; loaded at boot, saved whenever changed.
#define EEPROM_SIZE 4
#define EEPROM_ADDR_BRIGHT 0
static uint8_t g_brightness = 8; // 1..8
// ============================================================ config / EEPROM =
// g_cfg is the live badge configuration. Load on boot, save whenever changed.
// Helpers declared here, defined below (badge_config.h has extern declarations).
BadgeConfig g_cfg = BADGE_CONFIG_DEFAULT;
static void loadBrightness() {
EEPROM.begin(EEPROM_SIZE);
uint8_t v = EEPROM.read(EEPROM_ADDR_BRIGHT);
g_brightness = (v >= 1 && v <= 8) ? v : 8;
// Convenience shorthand — used throughout the file
#define g_brightness g_cfg.brightness
#define g_flashBright g_cfg.flashBright
void configLoad() {
EEPROM.begin(EEPROM_CFG_SIZE);
uint8_t bright = EEPROM.read(EEPROM_ADDR_BRIGHT);
g_cfg.brightness = (bright >= 1 && bright <= 8) ? bright : 8;
uint8_t flash = EEPROM.read(EEPROM_ADDR_FLASH);
g_cfg.flashBright = (flash >= 1 && flash <= 8) ? flash : 8;
for (uint8_t i = 0; i < IR_BTN_COUNT; i++) {
uint16_t base = EEPROM_ADDR_IR_BASE + i * 4;
uint8_t proto = EEPROM.read(base);
if (proto > IR_PROTO_RC5) {
// uninitialised — use default
g_cfg.ir[i] = BADGE_CONFIG_DEFAULT.ir[i];
} else {
g_cfg.ir[i].protocol = proto;
g_cfg.ir[i].addr_lo = EEPROM.read(base + 1);
g_cfg.ir[i].addr_hi = EEPROM.read(base + 2);
g_cfg.ir[i].command = EEPROM.read(base + 3);
}
}
}
static void saveBrightness(uint8_t v) {
g_brightness = v;
EEPROM.write(EEPROM_ADDR_BRIGHT, v);
void configSave() {
EEPROM.write(EEPROM_ADDR_BRIGHT, g_cfg.brightness);
EEPROM.write(EEPROM_ADDR_FLASH, g_cfg.flashBright);
for (uint8_t i = 0; i < IR_BTN_COUNT; i++) {
uint16_t base = EEPROM_ADDR_IR_BASE + i * 4;
EEPROM.write(base, g_cfg.ir[i].protocol);
EEPROM.write(base + 1, g_cfg.ir[i].addr_lo);
EEPROM.write(base + 2, g_cfg.ir[i].addr_hi);
EEPROM.write(base + 3, g_cfg.ir[i].command);
}
EEPROM.commit();
}
void configSaveBrightness() {
EEPROM.write(EEPROM_ADDR_BRIGHT, g_cfg.brightness);
EEPROM.commit();
}
void configSaveFlash() {
EEPROM.write(EEPROM_ADDR_FLASH, g_cfg.flashBright);
EEPROM.commit();
}
void configSaveIr(uint8_t idx) {
if (idx >= IR_BTN_COUNT) return;
uint16_t base = EEPROM_ADDR_IR_BASE + idx * 4;
EEPROM.write(base, g_cfg.ir[idx].protocol);
EEPROM.write(base + 1, g_cfg.ir[idx].addr_lo);
EEPROM.write(base + 2, g_cfg.ir[idx].addr_hi);
EEPROM.write(base + 3, g_cfg.ir[idx].command);
EEPROM.commit();
}
@@ -49,7 +97,7 @@ static void saveBrightness(uint8_t v) {
#define SAO_GP1 0
#define SAO_GP2 1
#define LED_FLASHLIGHT 15
#define LED_FLASHLIGHT 14
#define LED_FRONT_1 20
#define LED_FRONT_2 21
#define LED_FRONT_3 22
@@ -59,9 +107,9 @@ static void saveBrightness(uint8_t v) {
#define LED_MATRIX_SCL 5
#define IS31_ADDR 0x74
// Front LEDs are active-low (anode to 3V3, cathode to GPIO via series R).
const int FRONT_LED_ON = LOW;
const int FRONT_LED_OFF = HIGH;
// Front LEDs are active-high (anode to GPIO, cathode to GND).
const int FRONT_LED_ON = HIGH;
const int FRONT_LED_OFF = LOW;
const int FRONT_LEDS[] = { LED_FRONT_1, LED_FRONT_2, LED_FRONT_3, LED_FRONT_4 };
const int NUM_FRONT_LEDS = 4;
@@ -665,7 +713,7 @@ static void settingsStep() {
settingsCtx.brightness--;
changed = true;
}
if (changed) saveBrightness(settingsCtx.brightness);
if (changed) { g_brightness = settingsCtx.brightness; configSaveBrightness(); }
// — Draw brightness bar ——————————————————————————————
// Rows 3-5: filled bar, 1 column per level (1..8 → cols 0..7).
@@ -685,9 +733,174 @@ static void settingsStep() {
fbPush();
}
// ============================================================ IR sender ======
//
// Software IR modulation on LED_FLASHLIGHT (GPIO15 → MOSFET → IR/white LEDs).
// Implements NEC (32-bit) and Samsung (which is NEC-compatible but with a
// repeated address byte). Sony SIRC-12 is also supported.
//
// Blocking during transmission (~67 ms for NEC), but called only on user input.
#define IR_PIN LED_FLASHLIGHT
#define IR_FREQ_NEC 38000
#define IR_FREQ_SONY 40000
static void irOn(uint16_t us) {
// 33 % duty is standard for IR LEDs — avoids overheating
analogWrite(IR_PIN, 333);
delayMicroseconds(us);
}
static void irOff(uint16_t us) {
analogWrite(IR_PIN, 0);
delayMicroseconds(us);
}
static void irSendNEC(uint16_t address, uint8_t command, bool samsung = false) {
analogWriteFreq(IR_FREQ_NEC);
analogWriteRange(1000);
// Header: 9 ms burst + 4.5 ms space
irOn(9000);
irOff(samsung ? 4500 : 4500); // Samsung header is identical to NEC here
// 16-bit address (8-bit addr + ~addr for NEC; 8-bit addr repeated for Samsung)
uint8_t a1 = (uint8_t)(address & 0xFF);
uint8_t a2 = samsung ? a1 : (uint8_t)(~a1 & 0xFF);
uint8_t c1 = command;
uint8_t c2 = (uint8_t)(~c1 & 0xFF);
for (int i = 0; i < 8; i++) { irOn(562); irOff((a1 >> i) & 1 ? 1687 : 562); }
for (int i = 0; i < 8; i++) { irOn(562); irOff((a2 >> i) & 1 ? 1687 : 562); }
for (int i = 0; i < 8; i++) { irOn(562); irOff((c1 >> i) & 1 ? 1687 : 562); }
for (int i = 0; i < 8; i++) { irOn(562); irOff((c2 >> i) & 1 ? 1687 : 562); }
irOn(562);
analogWrite(IR_PIN, 0);
}
static void irSendSony(uint16_t address, uint8_t command) {
analogWriteFreq(IR_FREQ_SONY);
analogWriteRange(1000);
irOn(2400); irOff(600);
for (int i = 6; i >= 0; i--) { irOn((command >> i) & 1 ? 1200 : 600); irOff(600); }
for (int i = 4; i >= 0; i--) { irOn((address >> i) & 1 ? 1200 : 600); irOff(600); }
analogWrite(IR_PIN, 0);
}
static void irSendCode(const IrCode& c) {
uint16_t addr = irCodeAddr(c);
switch (c.protocol) {
case IR_PROTO_NEC: irSendNEC(addr, c.command, false); break;
case IR_PROTO_SAMSUNG: irSendNEC(addr, c.command, true); break;
case IR_PROTO_SONY: irSendSony(addr, c.command); break;
case IR_PROTO_RC5: irSendNEC(addr, c.command, false); break; // fallback
}
}
// ============================================================ flashlight =====
//
// All four front LEDs continue their Knight Rider animation in the background.
// LED_FLASHLIGHT (GPIO15) is PWM'd at variable duty for the flashlight.
// Level 8 → duty 1000 (constant on, maximum output)
// Level 1 → duty 125 (dim — still on, no strobe at minimum)
// Up/Down → adjust brightness level. B → exit to menu.
struct FlashState {
bool exitRequested;
};
static FlashState flashCtx;
static void flashApply() {
int duty = (int)((float)g_flashBright / 8.0f * 1000.0f);
analogWriteFreq(1000);
analogWriteRange(1000);
analogWrite(IR_PIN, duty);
}
static void flashReset() {
flashCtx.exitRequested = false;
flashApply();
}
static void flashStep() {
if (input.pressed[BI_B]) {
flashCtx.exitRequested = true;
analogWrite(IR_PIN, 0); // off when leaving
return;
}
bool changed = false;
if (input.pressed[BI_UP] && g_flashBright < 8) { g_flashBright++; changed = true; }
if (input.pressed[BI_DOWN] && g_flashBright > 1) { g_flashBright--; changed = true; }
if (changed) { configSaveFlash(); flashApply(); }
// Draw brightness level bar on matrix (rows 3-5, same style as Settings)
fbClear();
uint8_t bar = 180;
for (int x = 0; x < g_flashBright; x++) {
fbSet(x, 3, bar);
fbSet(x, 4, bar);
fbSet(x, 5, bar);
}
int edge = g_flashBright - 1;
fbSet(edge, 1, 255);
fbSet(edge, 7, 255);
// Torch icon — two pixels above bar
fbSet(3, 0, 200); fbSet(4, 0, 255); fbSet(5, 0, 200);
fbPush();
}
// ============================================================ IR remote ======
//
// Each directional/A/B button sends the configured IR code.
// A+B held simultaneously exits. The matrix shows a remote icon.
// The front LEDs continue Knight Rider in the background.
static unsigned long irLastSend = 0;
static bool irExitRequested = false;
static void irRemoteReset() {
irExitRequested = false;
irLastSend = 0;
}
static void irRemoteDrawIcon() {
fbClear();
// Stylised remote outline on 9×9 matrix
for (int y = 1; y <= 7; y++) { fbSet(2, y, 80); fbSet(6, y, 80); }
for (int x = 2; x <= 6; x++) { fbSet(x, 1, 80); fbSet(x, 7, 80); }
fbSet(4, 2, 200); // top button
fbSet(3, 4, 120); fbSet(5, 4, 120); // side buttons
fbSet(4, 5, 120); // bottom button
fbPush();
}
static void irRemoteStep() {
// Exit on A+B held together
if (input.down[BI_A] && input.down[BI_B]) {
irExitRequested = true;
analogWrite(IR_PIN, 0);
return;
}
unsigned long now = millis();
// 200 ms gap between sends to avoid flooding
if (now - irLastSend < 200) { irRemoteDrawIcon(); return; }
static const int BTN_MAP[IR_BTN_COUNT] = {
BI_A, BI_B, BI_UP, BI_DOWN, BI_LEFT, BI_RIGHT
};
for (int i = 0; i < IR_BTN_COUNT; i++) {
if (input.pressed[BTN_MAP[i]]) {
// Flash all front LEDs briefly to indicate send
for (int p : FRONT_LEDS) analogWrite(p, 1000);
irSendCode(g_cfg.ir[i]);
for (int p : FRONT_LEDS) analogWrite(p, 0);
irLastSend = now;
break;
}
}
irRemoteDrawIcon();
}
// ============================================================ menu =========
const char* MENU_ITEMS[] = { "SNAKE", "SIMON", "WELCOME", "SETTINGS" };
const char* MENU_ITEMS[] = { "SNAKE", "SIMON", "WELCOME", "SETTINGS", "FLASHLIGHT", "IR REMOTE" };
const int MENU_LEN = sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]);
static int menuIdx = 0;
@@ -725,6 +938,8 @@ enum AppState {
ST_SNAKE,
ST_SIMON,
ST_SETTINGS,
ST_FLASHLIGHT,
ST_IR_REMOTE,
};
static AppState appState = ST_BOOT_SWEEP;
@@ -742,6 +957,8 @@ static void enterState(AppState s) {
case ST_SNAKE: snakeReset(); break;
case ST_SIMON: simonReset(); break;
case ST_SETTINGS: settingsReset(); break;
case ST_FLASHLIGHT: flashReset(); break;
case ST_IR_REMOTE: irRemoteReset(); break;
default: break;
}
}
@@ -776,29 +993,42 @@ static void saoUpdate() {
// Passive background animation: a bright spot sweeps back and forth across
// the four front LEDs using hardware PWM.
//
// Front LEDs are active-low (anode → 3V3, cathode → GPIO):
// analogWrite duty 0 = pin always LOW = LED fully ON
// analogWrite duty 1000 = pin always HIGH = LED fully OFF
// Front LEDs are active-HIGH (anode → GPIO, cathode → GND via resistor):
// analogWrite duty 0 = pin always LOW = LED fully OFF
// analogWrite duty 1000 = pin always HIGH = LED fully ON
//
// g_brightness controls sweep depth:
// 8 → all LEDs constantly fully on (no animation)
// 1 → full Knight Rider sweep (background goes dark, peak is fully on)
// 2-7 → intermediate — background LEDs stay at a raised floor level
//
// The sweep is skipped while ST_SNAKE or ST_SIMON own the front LEDs.
static void frontKnightRiderUpdate() {
if (appState == ST_SNAKE || appState == ST_SIMON) return;
if (appState == ST_SNAKE || appState == ST_SIMON || appState == ST_IR_REMOTE) return;
if (g_brightness >= 8) {
// Brightness at maximum → all LEDs constantly on, no animation
for (int i = 0; i < NUM_FRONT_LEDS; i++) analogWrite(FRONT_LEDS[i], 1000);
return;
}
// Triangle wave, period 800 ms → position sweeps 0.0 → 3.0 → 0.0
const float period = 800.0f;
float t = fmodf((float)millis(), period) / period; // 0 .. 1
float tri = (t < 0.5f) ? (t * 2.0f) : (2.0f - t * 2.0f); // 0 .. 1 .. 0
float pos = tri * 3.0f; // 0.0 .. 3.0
float t = fmodf((float)millis(), period) / period;
float tri = (t < 0.5f) ? (t * 2.0f) : (2.0f - t * 2.0f);
float pos = tri * 3.0f;
// animDepth: 1 = full sweep at level 1, 0 = no sweep at level 8
float animDepth = (float)(8 - g_brightness) / 7.0f; // 1 at level 1, ~0 at level 7
// floor = minimum duty at the dark end of the sweep
float floor_val = 1.0f - animDepth;
for (int i = 0; i < NUM_FRONT_LEDS; i++) {
float dist = fabsf(pos - (float)i);
// Soft glow: full brightness within 0.5 px, fades out to 1.5 px
float brightness = (dist < 1.5f) ? (1.0f - dist / 1.5f) : 0.0f;
// Scale by global brightness setting
brightness *= (float)g_brightness / 8.0f;
// Invert for active-low: duty 0 = fully on, 1000 = fully off
int duty = 1000 - (int)(brightness * 1000.0f);
float dist = fabsf(pos - (float)i);
float peak = (dist < 1.5f) ? (1.0f - dist / 1.5f) : 0.0f;
float bright = floor_val + (1.0f - floor_val) * peak; // never below floor_val
int duty = (int)(bright * 1000.0f);
analogWrite(FRONT_LEDS[i], duty);
}
}
@@ -834,8 +1064,11 @@ void setup() {
// Seed RNG from a floating ADC pin if available; otherwise use micros().
randomSeed(micros() ^ analogRead(A0));
// Load persisted brightness from flash-backed EEPROM
loadBrightness();
// Load persisted config (brightness, flashlight brightness, IR codes)
configLoad();
// Serial terminal games (Mastermind, Hangman, Minesweeper)
serialGames.begin();
// SAO GPIO PWM (must come after randomSeed / analogRead so ADC init is done)
saoSetup();
@@ -846,6 +1079,7 @@ void setup() {
// ============================================================ loop =========
void loop() {
serialGames.update(); // USB-CDC terminal games (non-blocking)
saoUpdate(); // SAO GP1 sin-wave PWM, GP2 constant 10 %
frontKnightRiderUpdate(); // front LED Knight Rider sweep
inputUpdate();
@@ -897,6 +1131,8 @@ void loop() {
case 1: enterState(ST_SIMON); break;
case 2: enterState(ST_WELCOME_SCROLL); break;
case 3: enterState(ST_SETTINGS); break;
case 4: enterState(ST_FLASHLIGHT); break;
case 5: enterState(ST_IR_REMOTE); break;
}
}
break;
@@ -919,5 +1155,17 @@ void loop() {
if (settingsCtx.exitRequested) enterState(ST_MENU);
break;
}
case ST_FLASHLIGHT: {
flashStep();
if (flashCtx.exitRequested) enterState(ST_MENU);
break;
}
case ST_IR_REMOTE: {
irRemoteStep();
if (irExitRequested) enterState(ST_MENU);
break;
}
}
}
+770
View File
@@ -0,0 +1,770 @@
// ================================================================
// serial_games.cpp — Secfest 2026 Badge
// ================================================================
#include "serial_games.h"
// ── VT100 / ANSI helpers ─────────────────────────────────────────
#define CLS "\033[2J\033[H" // clear screen + home cursor
#define RST "\033[0m" // reset all attributes
#define BLD "\033[1m" // bold
#define DIM "\033[2m" // dim
#define RED "\033[91m" // bright red
#define GRN "\033[92m" // bright green
#define YLW "\033[93m" // bright yellow
#define BLU "\033[94m" // bright blue
#define MAG "\033[95m" // bright magenta
#define CYN "\033[96m" // bright cyan
#define GRY "\033[90m" // dark grey
// ── Hangman word list (security / hacking themed) ───────────────
const char* const SerialGames::_hmWords[] = {
"firewall", "exploit", "malware", "phishing", "rootkit",
"payload", "sandbox", "fuzzing", "honeypot", "cipher",
"botnet", "spoofing", "backdoor", "shellcode", "keylogger",
"pivoting", "injection", "sniffing", "ransomware", "escalation",
"obfuscation", "enumeration", "stealthy", "exfiltrate", "wireshark",
"shodan", "zerotrust", "darknet", "privilege", "pentest",
};
const uint8_t SerialGames::_hmWordCount =
sizeof(SerialGames::_hmWords) / sizeof(SerialGames::_hmWords[0]);
// Gallows rows [stage 0..6][row 0..6] — stage = number of wrong guesses
static const char* const GALLOWS[7][7] = {
{" +---+", " | |", " |", " |", " |", " |", "========="},
{" +---+", " | |", " O |", " |", " |", " |", "========="},
{" +---+", " | |", " O |", " | |", " |", " |", "========="},
{" +---+", " | |", " O |", " /| |", " |", " |", "========="},
{" +---+", " | |", " O |", " /|\\ |", " |", " |", "========="},
{" +---+", " | |", " O |", " /|\\ |", " / |", " |", "========="},
{" +---+", " | |", " O |", " /|\\ |", " / \\ |", " |", "========="},
};
// ================================================================
// Public API
// ================================================================
void SerialGames::begin() {
_st = St::IDLE;
_prevConn = false;
_clearLine();
}
void SerialGames::update() {
bool conn = (bool)Serial;
// ── detect connect / disconnect ──────────────────────────
if (conn && !_prevConn) {
delay(200); // let the terminal finish its handshake
_drawMenu();
_st = St::MENU;
_lineMode = false;
}
if (!conn) {
_prevConn = false;
_st = St::IDLE;
return;
}
_prevConn = conn;
if (_st == St::IDLE) return;
// ── drain available bytes ────────────────────────────────
while (Serial.available()) {
char c = (char)Serial.read();
// Swallow VT100 escape sequences (arrow keys etc.)
if (c == '\033') {
unsigned long t = millis();
while (Serial.available() && (millis() - t) < 25) Serial.read();
continue;
}
if (_lineMode) {
_feedChar(c);
} else {
if (c >= 'A' && c <= 'Z') c += 32; // normalise to lower-case
_charDispatch(c);
}
}
}
// ================================================================
// Input helpers
// ================================================================
void SerialGames::_clearLine() {
memset(_line, 0, sizeof(_line));
_lineLen = 0;
}
// Called for every byte in line-mode. Echoes characters, handles
// backspace, and dispatches to the relevant game on Enter.
void SerialGames::_feedChar(char c) {
if (c == '\r' || c == '\n') {
// Ignore the bare \n of a \r\n pair
if (c == '\n' && _lineLen == 0) return;
Serial.print("\r\n");
_line[_lineLen] = '\0';
St prevSt = _st;
bool redraw = false;
if (_st == St::MASTERMIND) redraw = _mmOnLine();
else if (_st == St::MINESWEEPER) redraw = _msOnLine();
else if (_st == St::ADVENTURE) {
_adv.feedLine(_line);
if (_adv.quitToMenu) {
_adv.quitToMenu = false;
_st = St::MENU;
_lineMode = false;
_drawMenu();
}
}
else if (_st == St::CONFIG) {
_cfgOnLine();
}
_clearLine();
// Only redraw if still in the same state (handlers that exit to menu
// have already redrawn the menu themselves)
if (_st == prevSt && redraw) {
if (_st == St::MASTERMIND) _mmDraw();
if (_st == St::MINESWEEPER) _msDraw();
}
} else if ((c == '\b' || c == 127) && _lineLen > 0) {
_lineLen--;
Serial.print("\b \b"); // erase the echoed character
} else if (c >= 32 && _lineLen < 31) {
_line[_lineLen++] = c;
Serial.print(c); // local echo
}
}
// Called for every byte in char-mode (Menu, Hangman).
void SerialGames::_charDispatch(char c) {
switch (_st) {
case St::MENU:
if (c == '1') _mmStart();
else if (c == '2') _hmStart();
else if (c == '3') _msStart();
else if (c == '4') _advStart();
else if (c == '5') _cfgStart();
break;
case St::HANGMAN:
if (c == 'q') { _drawMenu(); _st = St::MENU; }
else if (c >= 'a' && c <= 'z') _hmOnChar(c);
break;
default: break;
}
}
// ================================================================
// Screen helpers
// ================================================================
void SerialGames::_cls() { Serial.print(CLS); }
void SerialGames::_drawMenu() {
_cls();
Serial.print(
BLD CYN
" ╔══════════════════════════════════════╗\r\n"
" ║ SECFEST 2026 BADGE GAMES ║\r\n"
" ╚══════════════════════════════════════╝\r\n"
RST "\r\n"
" " BLD "[1]" RST " MASTERMIND — Crack the 4-digit code\r\n"
" " BLD "[2]" RST " HANGMAN — Security vocabulary edition\r\n"
" " BLD "[3]" RST " MINESWEEPER — 8 × 8 grid, 10 mines\r\n"
" " BLD "[4]" RST " ZERO DAY — Text adventure\r\n"
" " BLD "[5]" RST " CONFIG — Badge settings & IR codes\r\n"
"\r\n"
GRY " Press 1–5 to start. Q inside any game returns here.\r\n" RST
);
}
// ================================================================
// MASTERMIND
// ================================================================
void SerialGames::_mmStart() {
for (uint8_t i = 0; i < 4; i++) _mm.secret[i] = (uint8_t)random(1, 7);
_mm.nGuess = 0;
_mm.won = false;
_mm.over = false;
_st = St::MASTERMIND;
_lineMode = true;
_clearLine();
_mmDraw();
}
void SerialGames::_mmDraw() {
_cls();
Serial.print(
BLD YLW
" ╔══════════════════════════════════════╗\r\n"
" ║ CRACK THE CODE ║\r\n"
" ║ Guess the 4-digit code (1 – 6) ║\r\n"
" ╚══════════════════════════════════════╝\r\n"
RST "\r\n"
" # GUESS " BLD GRN "●" RST " bulls " BLD CYN "○" RST " cows\r\n"
GRY " ──────────────────────────────────────\r\n" RST
);
for (uint8_t i = 0; i < 10; i++) {
if (i < _mm.nGuess) {
Serial.printf(" %2u ", i + 1);
for (uint8_t j = 0; j < 4; j++) Serial.printf("%u ", _mm.guess[i][j]);
Serial.printf(" " BLD GRN "%u" RST " " BLD CYN "%u" RST "\r\n",
_mm.bulls[i], _mm.cows[i]);
} else {
Serial.print(GRY " . _ _ _ _\r\n" RST);
}
}
Serial.print("\r\n");
if (_mm.won) {
Serial.printf(BLD GRN " ✓ Cracked in %u guess%s! Well played.\r\n" RST,
_mm.nGuess, _mm.nGuess == 1 ? "" : "es");
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
return;
}
if (_mm.over) {
Serial.printf(BLD RED " ✗ Out of guesses. Code was: %u %u %u %u\r\n" RST,
_mm.secret[0], _mm.secret[1], _mm.secret[2], _mm.secret[3]);
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
return;
}
Serial.printf(" Attempts remaining: " BLD "%u" RST "\r\n\r\n", 10 - _mm.nGuess);
Serial.print(" Enter 4 digits (1-6), e.g. 1234. Q = quit: ");
}
// Returns true if the screen needs a full redraw.
bool SerialGames::_mmOnLine() {
// Quit
if (_line[0] == 'q' || _line[0] == 'Q') {
_drawMenu();
_st = St::MENU;
_lineMode = false;
return false;
}
// Allow new game when already over
if (_mm.won || _mm.over) return false;
// Collect exactly 4 digits 1-6, ignoring spaces and other noise
uint8_t digits[4], n = 0;
for (uint8_t i = 0; _line[i] && n < 4; i++) {
if (_line[i] >= '1' && _line[i] <= '6') digits[n++] = (uint8_t)(_line[i] - '0');
}
if (n < 4) {
Serial.print(RED " ✗ Need exactly 4 digits in range 1-6 (e.g. 1234).\r\n" RST);
Serial.print(" Enter 4 digits (1-6), e.g. 1234. Q = quit: ");
return false; // bad input — don't redraw; we already printed a message
}
uint8_t r = _mm.nGuess;
for (uint8_t i = 0; i < 4; i++) _mm.guess[r][i] = digits[i];
// Score: bulls (right digit + right position), cows (right digit, wrong position)
uint8_t bulls = 0, cows = 0;
uint8_t sc[7] = {}, gc[7] = {};
for (uint8_t i = 0; i < 4; i++) {
if (_mm.secret[i] == digits[i]) {
bulls++;
} else {
sc[_mm.secret[i]]++;
gc[digits[i]]++;
}
}
for (uint8_t d = 1; d <= 6; d++) cows += min(sc[d], gc[d]);
_mm.bulls[r] = bulls;
_mm.cows[r] = cows;
_mm.nGuess++;
if (bulls == 4) _mm.won = true;
else if (_mm.nGuess >= 10) _mm.over = true;
return true; // valid input processed — caller will redraw
}
// ================================================================
// HANGMAN
// ================================================================
void SerialGames::_hmStart() {
_hm.word = _hmWords[random(_hmWordCount)];
_hm.nWrong = 0;
_hm.won = false;
_hm.over = false;
memset(_hm.guessed, 0, sizeof(_hm.guessed));
_st = St::HANGMAN;
_lineMode = false;
_hmDraw();
}
void SerialGames::_hmDraw() {
_cls();
Serial.print(
BLD MAG
" ╔══════════════════════════════════════╗\r\n"
" ║ HANGMAN — Security Edition ║\r\n"
" ╚══════════════════════════════════════╝\r\n"
RST "\r\n"
);
// Gallows — figure turns red at maximum wrong guesses
uint8_t stage = (_hm.nWrong > 6) ? 6 : _hm.nWrong;
for (uint8_t row = 0; row < 7; row++) {
bool isBody = (row >= 2 && row <= 5);
const char* col = (isBody && _hm.nWrong >= 6) ? RED : GRY;
Serial.printf(" %s%s" RST "\r\n", col, GALLOWS[stage][row]);
}
Serial.print("\r\n");
// Word — revealed letters in green, blanks in white
Serial.print(" ");
uint8_t missing = 0;
for (uint8_t i = 0; _hm.word[i]; i++) {
uint8_t idx = (uint8_t)(_hm.word[i] - 'a');
if (_hm.guessed[idx]) {
Serial.printf(BLD GRN "%c " RST, _hm.word[i]);
} else {
Serial.print(BLD "_ " RST);
missing++;
}
}
Serial.print("\r\n\r\n");
// Wrong-guess list
Serial.print(" Wrong: ");
bool any = false;
for (uint8_t i = 0; i < 26; i++) {
if (!_hm.guessed[i]) continue;
bool inWord = false;
for (uint8_t j = 0; _hm.word[j]; j++)
if (_hm.word[j] == (char)('a' + i)) { inWord = true; break; }
if (!inWord) { Serial.printf(RED "%c " RST, 'a' + i); any = true; }
}
if (!any) Serial.print(GRY "none" RST);
Serial.printf(" " GRY "(%u / 6 wrong)" RST "\r\n\r\n", _hm.nWrong);
// Win / lose check
if (missing == 0 && !_hm.over) {
_hm.won = true;
_hm.over = true;
}
if (_hm.nWrong >= 6 && !_hm.won) {
_hm.over = true;
}
if (_hm.won) {
Serial.print(BLD GRN " ✓ You survived! Well played, hacker.\r\n" RST);
Serial.print(GRY " Press any key to return to the menu.\r\n" RST);
return;
}
if (_hm.over) {
Serial.printf(BLD RED " ✗ Caught! The word was: " YLW "%s\r\n" RST, _hm.word);
Serial.print(GRY " Press any key to return to the menu.\r\n" RST);
return;
}
Serial.print(" Guess a letter (Q = quit): ");
}
void SerialGames::_hmOnChar(char c) {
// After game over, any key returns to menu
if (_hm.over) { _drawMenu(); _st = St::MENU; return; }
uint8_t idx = (uint8_t)(c - 'a');
if (_hm.guessed[idx]) return; // already guessed — ignore silently
_hm.guessed[idx] = true;
// Count miss
bool hit = false;
for (uint8_t i = 0; _hm.word[i]; i++)
if (_hm.word[i] == c) { hit = true; break; }
if (!hit) _hm.nWrong++;
_hmDraw();
}
// ================================================================
// MINESWEEPER
// ================================================================
void SerialGames::_msStart() {
memset(&_ms, 0, sizeof(_ms));
_st = St::MINESWEEPER;
_lineMode = true;
_clearLine();
_msDraw();
}
void SerialGames::_msSeed(uint8_t safeR, uint8_t safeC) {
_ms.seeded = true;
uint8_t placed = 0;
while (placed < MSN) {
uint8_t r = (uint8_t)random(MSR);
uint8_t c = (uint8_t)random(MSC);
if (_ms.mine[r][c]) continue;
// Keep a 1-cell safety radius around the first click
if (abs((int)r - safeR) <= 1 && abs((int)c - safeC) <= 1) continue;
_ms.mine[r][c] = true;
placed++;
}
// Pre-compute neighbour mine counts
for (uint8_t r = 0; r < MSR; r++)
for (uint8_t c = 0; c < MSC; c++) {
uint8_t n = 0;
for (int8_t dr = -1; dr <= 1; dr++)
for (int8_t dc = -1; dc <= 1; dc++) {
int nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < MSR && nc >= 0 && nc < MSC && _ms.mine[nr][nc]) n++;
}
_ms.count[r][c] = n;
}
}
// Recursive flood-fill reveal — safe on an 8×8 grid
void SerialGames::_msReveal(int r, int c) {
if (r < 0 || r >= MSR || c < 0 || c >= MSC) return;
if (_ms.revealed[r][c] || _ms.flagged[r][c]) return;
if (_ms.mine[r][c]) { _ms.over = true; return; }
_ms.revealed[r][c] = true;
_ms.nRevealed++;
if (_ms.count[r][c] == 0) {
for (int8_t dr = -1; dr <= 1; dr++)
for (int8_t dc = -1; dc <= 1; dc++)
if (dr || dc) _msReveal(r + dr, c + dc);
}
}
void SerialGames::_msDraw() {
_cls();
uint8_t flags = 0;
for (uint8_t r = 0; r < MSR; r++)
for (uint8_t c = 0; c < MSC; c++)
if (_ms.flagged[r][c]) flags++;
Serial.print(
BLD GRN
" ╔══════════════════════════════════════╗\r\n"
" ║ MINESWEEPER — 8×8, 10 mines ║\r\n"
" ╚══════════════════════════════════════╝\r\n"
RST "\r\n"
);
// Column header + top border
Serial.print(" 1 2 3 4 5 6 7 8\r\n");
Serial.print(" +-----------------+\r\n");
for (uint8_t r = 0; r < MSR; r++) {
Serial.printf(" %u | ", r + 1);
for (uint8_t c = 0; c < MSC; c++) {
if (_ms.flagged[r][c]) {
Serial.print(YLW "F" RST " ");
} else if (!_ms.revealed[r][c]) {
// On game over, show all mine locations
if (_ms.over && _ms.mine[r][c])
Serial.print(RED "*" RST " ");
else
Serial.print(GRY "?" RST " ");
} else if (_ms.count[r][c] == 0) {
Serial.print(" "); // clear / empty cell
} else {
// Colour-code by danger: 1-2 cyan, 3-4 yellow, 5+ red
const char* nc =
(_ms.count[r][c] <= 2) ? CYN :
(_ms.count[r][c] <= 4) ? YLW : RED;
Serial.printf("%s%u" RST " ", nc, _ms.count[r][c]);
}
}
Serial.print("|\r\n");
}
Serial.print(" +-----------------+\r\n\r\n");
Serial.printf(" " BLD "Mines left: %d" RST
GRY " (F=flag ?=hidden blank=safe)" RST "\r\n\r\n",
(int)MSN - (int)flags);
if (_ms.won) {
Serial.print(BLD GRN " ✓ Cleared! Every mine avoided.\r\n" RST);
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
return;
}
if (_ms.over) {
Serial.print(BLD RED " ✗ BOOM! You hit a mine.\r\n" RST);
Serial.print(GRY " Type Q + Enter to return to the menu.\r\n" RST);
return;
}
Serial.print(
" " GRY "r ROW COL" RST " = reveal "
GRY "f ROW COL" RST " = flag/unflag "
GRY "Q" RST " = quit\r\n"
" > "
);
}
// Returns true if the screen needs a full redraw.
bool SerialGames::_msOnLine() {
// Quit (works mid-game and on win/lose screen)
if (_line[0] == 'q' || _line[0] == 'Q') {
_drawMenu();
_st = St::MENU;
_lineMode = false;
return false;
}
if (_ms.won || _ms.over) return false;
char cmd = _line[0];
if (cmd != 'r' && cmd != 'R' && cmd != 'f' && cmd != 'F') {
Serial.print(RED " ✗ Unknown command. Use: r ROW COL or f ROW COL\r\n" RST " > ");
return false;
}
// Parse two integers following the command letter
const char* p = _line + 1;
while (*p == ' ') p++;
int row = 0;
while (*p >= '0' && *p <= '9') row = row * 10 + (*p++ - '0');
while (*p == ' ') p++;
int col = 0;
while (*p >= '0' && *p <= '9') col = col * 10 + (*p++ - '0');
row--; col--; // convert 1-indexed user input to 0-indexed
if (row < 0 || row >= MSR || col < 0 || col >= MSC) {
Serial.print(RED " ✗ Row and column must each be 1–8.\r\n" RST " > ");
return false;
}
if (cmd == 'f' || cmd == 'F') {
if (!_ms.revealed[row][col])
_ms.flagged[row][col] = !_ms.flagged[row][col];
} else {
if (!_ms.seeded) _msSeed((uint8_t)row, (uint8_t)col);
_msReveal(row, col);
}
if (_ms.nRevealed == MSR * MSC - MSN) _ms.won = true;
return true;
}
// ================================================================
// TEXT ADVENTURE
// ================================================================
void SerialGames::_advStart() {
_st = St::ADVENTURE;
_lineMode = true;
_clearLine();
_adv.begin();
}
// ================================================================
// CONFIG — badge settings over serial CLI
// ================================================================
static const char* _protoName(uint8_t p) {
switch (p) {
case IR_PROTO_NEC: return "NEC";
case IR_PROTO_SAMSUNG: return "SAMSUNG";
case IR_PROTO_SONY: return "SONY";
case IR_PROTO_RC5: return "RC5";
}
return "?";
}
static const char* _btnName(uint8_t i) {
switch (i) {
case 0: return "A";
case 1: return "B";
case 2: return "UP";
case 3: return "DOWN";
case 4: return "LEFT";
case 5: return "RIGHT";
}
return "?";
}
uint8_t SerialGames::_cfgParseBtn(const char* s) {
if (strcmp(s,"a")==0) return 0;
if (strcmp(s,"b")==0) return 1;
if (strcmp(s,"up")==0) return 2;
if (strcmp(s,"down")==0) return 3;
if (strcmp(s,"left")==0) return 4;
if (strcmp(s,"right")==0) return 5;
return 0xFF;
}
uint8_t SerialGames::_cfgParseProto(const char* s) {
if (strcmp(s,"nec")==0) return IR_PROTO_NEC;
if (strcmp(s,"samsung")==0) return IR_PROTO_SAMSUNG;
if (strcmp(s,"sony")==0) return IR_PROTO_SONY;
if (strcmp(s,"rc5")==0) return IR_PROTO_RC5;
return 0xFF;
}
void SerialGames::_cfgStart() {
_st = St::CONFIG;
_lineMode = true;
_clearLine();
_cfgDraw();
}
void SerialGames::_cfgDraw() {
_cls();
Serial.print(
BLD CYN
" ╔══════════════════════════════════════════════╗\r\n"
" ║ BADGE CONFIG ║\r\n"
" ╚══════════════════════════════════════════════╝\r\n"
RST "\r\n"
GRY
" Commands (case-insensitive):\r\n"
" " RST BLD "show" RST GRY " — print current settings\r\n"
" " RST BLD "bright matrix <1-8>" RST GRY " — set matrix / LED animation depth\r\n"
" " RST BLD "bright flash <1-8>" RST GRY " — set flashlight brightness\r\n"
" " RST BLD "ir <btn> <proto> <addr> <cmd>" RST GRY "\r\n"
" btn = A B UP DOWN LEFT RIGHT\r\n"
" proto = NEC SAMSUNG SONY RC5\r\n"
" addr = 0x… (16-bit hex)\r\n"
" cmd = 0x… (8-bit hex)\r\n"
" " RST BLD "default" RST GRY " — restore factory defaults\r\n"
" " RST BLD "q / back" RST GRY " — return to main menu\r\n"
RST "\r\n"
" > "
);
}
void SerialGames::_cfgShowConfig() {
Serial.print("\r\n");
Serial.printf(" Matrix brightness : " BLD "%d" RST "/8\r\n", g_cfg.brightness);
Serial.printf(" Flash brightness : " BLD "%d" RST "/8\r\n", g_cfg.flashBright);
Serial.print("\r\n");
Serial.print(" " BLD "Button Protocol Address Command\r\n" RST);
Serial.print(GRY " ────────────────────────────────────\r\n" RST);
for (uint8_t i = 0; i < IR_BTN_COUNT; i++) {
const IrCode& c = g_cfg.ir[i];
Serial.printf(" %-6s %-8s 0x%04X 0x%02X\r\n",
_btnName(i), _protoName(c.protocol),
(unsigned)(irCodeAddr(c)), (unsigned)c.command);
}
Serial.print("\r\n > ");
}
void SerialGames::_cfgOnLine() {
// Lower-case
char buf[64];
uint8_t len = 0;
while (_line[len] && len < 63) { buf[len] = _line[len]; len++; }
buf[len] = '\0';
for (uint8_t i = 0; i < len; i++)
if (buf[i] >= 'A' && buf[i] <= 'Z') buf[i] += 32;
// Tokenise (up to 5 tokens)
char* toks[5] = {nullptr,nullptr,nullptr,nullptr,nullptr};
uint8_t tc = 0;
char* p = buf;
while (*p && tc < 5) {
while (*p == ' ') p++;
if (!*p) break;
toks[tc++] = p;
while (*p && *p != ' ') p++;
if (*p) { *p = '\0'; p++; }
}
if (tc == 0) { Serial.print(" > "); return; }
// q / back
if (strcmp(toks[0],"q")==0 || strcmp(toks[0],"back")==0) {
_st = St::MENU;
_lineMode = false;
_drawMenu();
return;
}
// help
if (strcmp(toks[0],"help")==0) {
_cfgDraw();
return;
}
// show
if (strcmp(toks[0],"show")==0) {
_cfgShowConfig();
return;
}
// default
if (strcmp(toks[0],"default")==0) {
g_cfg = BADGE_CONFIG_DEFAULT;
configSave();
Serial.print(" " GRN "Factory defaults restored and saved.\r\n" RST "\r\n > ");
return;
}
// bright matrix|flash <1-8>
if (strcmp(toks[0],"bright")==0 && tc >= 3) {
uint8_t val = (uint8_t)atoi(toks[2]);
if (val < 1 || val > 8) {
Serial.print(" " RED "Value must be 1-8.\r\n" RST " > ");
return;
}
if (strcmp(toks[1],"matrix")==0) {
g_cfg.brightness = val;
configSaveBrightness();
Serial.printf(" " GRN "Matrix brightness set to %d.\r\n" RST " > ", val);
} else if (strcmp(toks[1],"flash")==0) {
g_cfg.flashBright = val;
configSaveFlash();
Serial.printf(" " GRN "Flashlight brightness set to %d.\r\n" RST " > ", val);
} else {
Serial.print(" " RED "Unknown target. Use 'matrix' or 'flash'.\r\n" RST " > ");
}
return;
}
// ir <btn> <proto> <addr> <cmd>
if (strcmp(toks[0],"ir")==0) {
if (tc < 5) {
Serial.print(" " RED "Usage: ir <btn> <proto> <addr> <cmd>\r\n" RST " > ");
return;
}
uint8_t btn = _cfgParseBtn(toks[1]);
uint8_t proto = _cfgParseProto(toks[2]);
if (btn == 0xFF) {
Serial.print(" " RED "Unknown button. Use A B UP DOWN LEFT RIGHT.\r\n" RST " > ");
return;
}
if (proto == 0xFF) {
Serial.print(" " RED "Unknown protocol. Use NEC SAMSUNG SONY RC5.\r\n" RST " > ");
return;
}
uint32_t addr = (uint32_t)strtoul(toks[3], nullptr, 16);
uint32_t cmd = (uint32_t)strtoul(toks[4], nullptr, 16);
if (addr > 0xFFFF || cmd > 0xFF) {
Serial.print(" " RED "Address must be ≤ 0xFFFF, command ≤ 0xFF.\r\n" RST " > ");
return;
}
g_cfg.ir[btn].protocol = proto;
g_cfg.ir[btn].addr_lo = (uint8_t)(addr & 0xFF);
g_cfg.ir[btn].addr_hi = (uint8_t)(addr >> 8);
g_cfg.ir[btn].command = (uint8_t)cmd;
configSaveIr(btn);
Serial.printf(" " GRN "%s → %s addr=0x%04lX cmd=0x%02lX — saved.\r\n" RST " > ",
_btnName(btn), _protoName(proto), addr, cmd);
return;
}
Serial.print(" " DIM "Unrecognised command. Type 'help'.\r\n" RST " > ");
}
+129
View File
@@ -0,0 +1,129 @@
// ================================================================
// serial_games.h — Secfest 2026 Badge
// ================================================================
// Text-based games over USB-CDC serial.
// Connect with PuTTY or Minicom; the terminal must support
// VT100 / ANSI escape codes (colour + clear-screen).
//
// Usage in main.cpp:
// #include "serial_games.h"
// static SerialGames serialGames;
// // in setup(): serialGames.begin();
// // in loop(): serialGames.update();
// ================================================================
#pragma once
#include <Arduino.h>
#include "badge_config.h"
#include "text_adventure.h"
class SerialGames {
public:
void begin();
void update(); // call every loop() iteration — fully non-blocking
private:
// ── top-level state ─────────────────────────────────────
enum class St : uint8_t { IDLE, MENU, MASTERMIND, HANGMAN, MINESWEEPER, ADVENTURE, CONFIG };
St _st = St::IDLE;
bool _prevConn = false;
// ── shared line-input buffer ────────────────────────────
// lineMode=true → buffer chars until Enter, then dispatch
// lineMode=false → dispatch each printable char immediately
bool _lineMode = false;
char _line[32] = {};
uint8_t _lineLen = 0;
void _clearLine();
void _feedChar(char c); // line-mode byte handler
void _charDispatch(char c); // char-mode byte handler
// ── screen helpers ──────────────────────────────────────
static void _cls();
void _drawMenu();
// ── MASTERMIND ──────────────────────────────────────────
// Guess a secret 4-digit code (each digit 1-6) in ≤10 tries.
// Feedback: ● (bull) = right digit, right place;
// ○ (cow) = right digit, wrong place.
struct MMState {
uint8_t secret[4];
uint8_t guess[10][4];
uint8_t bulls[10];
uint8_t cows[10];
uint8_t nGuess;
bool won, over;
} _mm;
void _mmStart();
bool _mmOnLine(); // returns true if a full redraw is needed
void _mmDraw();
// ── HANGMAN ─────────────────────────────────────────────
// Guess a security-themed word before the figure is complete
// (≤6 wrong guesses). Single-keystroke input — no Enter needed.
struct HMState {
const char* word;
bool guessed[26];
uint8_t nWrong;
bool won, over;
} _hm;
static const char* const _hmWords[];
static const uint8_t _hmWordCount;
void _hmStart();
void _hmOnChar(char c);
void _hmDraw();
// ── MINESWEEPER ─────────────────────────────────────────
// 8 × 8 grid, 10 mines. First reveal is always safe.
// Commands: r ROW COL (reveal)
// f ROW COL (flag / unflag)
// q (quit to menu)
static constexpr uint8_t MSR = 8;
static constexpr uint8_t MSC = 8;
static constexpr uint8_t MSN = 10;
struct MSState {
bool mine [MSR][MSC];
bool revealed[MSR][MSC];
bool flagged [MSR][MSC];
uint8_t count [MSR][MSC];
bool seeded, won, over;
uint8_t nRevealed;
} _ms;
void _msStart();
void _msSeed(uint8_t safeR, uint8_t safeC);
void _msReveal(int r, int c);
bool _msOnLine(); // returns true if a full redraw is needed
void _msDraw();
// ── TEXT ADVENTURE ──────────────────────────────────────
// ZERO DAY — hacker infiltration story.
// Full line-based input; adventure manages its own prompts.
TextAdventure _adv;
void _advStart();
// ── CONFIG ──────────────────────────────────────────────
// Line-mode CLI for editing badge_config (brightness, IR codes).
// Commands (not case-sensitive):
// show — print current config
// bright matrix <1-8> — set matrix brightness
// bright flash <1-8> — set flashlight brightness
// ir <btn> <proto> <addr> <cmd>
// btn = a b up down left right
// proto = nec samsung sony rc5
// addr = 0x… (16-bit hex)
// cmd = 0x… (8-bit hex)
// help — show commands
// q / back — return to main menu
void _cfgStart();
void _cfgOnLine(); // called from _feedChar on Enter
void _cfgDraw();
void _cfgShowConfig();
static uint8_t _cfgParseBtn(const char* s); // BI_* or 0xFF
static uint8_t _cfgParseProto(const char* s);
};
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
// ================================================================
// text_adventure.h — Secfest 2026 Badge
// ================================================================
// ZERO DAY — A hacker text adventure.
//
// You are a security operative sent to extract stolen research data
// from NovaCorp HQ before the building goes into full lockdown.
// Uncover the truth, free a trapped contact, and vanish without
// a trace — or blow your cover trying.
//
// Play over USB-CDC serial (PuTTY / Minicom).
// All interaction is line-based: type a command and press Enter.
//
// Commands: go/move DIRECTION, look, examine ITEM,
// take/get ITEM, drop ITEM, use ITEM,
// use ITEM on TARGET, talk NPC,
// inventory/inv, score, help, quit
// ================================================================
#pragma once
#include <Arduino.h>
class TextAdventure {
public:
void begin(); // reset to start state
void update(); // call from SerialGames::update() — non-blocking
void feedLine(const char* line); // dispatch a completed input line
bool isActive() const { return _active; }
// Called when 'q'/'quit' should return to the SerialGames menu
// — caller checks _quitToMenu and resets it after reading
bool quitToMenu = false;
private:
bool _active = false;
// ── Rooms ────────────────────────────────────────────────────────
enum Room : uint8_t {
PARKING=0, LOBBY, STAIRWELL, BREAKROOM, OFFICE,
SERVERCLOSET, HALL3, MEETINGROOM, MANAGERSOFFICE, SECURITYOFFICE,
DATACENTER, ROOF, BASEMENT, POWERROOM, TUNNEL,
STREET, // win-condition room — not truly reachable as normal room
ROOM_COUNT
};
// ── Items ────────────────────────────────────────────────────────
enum Item : uint8_t {
BADGE=0, LOCKPICK, MULTITOOL, STICKYNOTE, USB_DRIVE,
KEYCARD, MASTERKEY, EXTINGUISHER, THERMOS, DATA_CHIP,
ROPE, PHOTO,
ITEM_COUNT,
ITEM_NONE = 0xFF
};
// ── NPCs ─────────────────────────────────────────────────────────
enum NPC : uint8_t { BOB=0, JANET, GHOST, NPC_COUNT };
// ── Flags ────────────────────────────────────────────────────────
enum Flag : uint8_t {
FL_BADGE_SHOWN=0, FL_GUARD_DISTRACT, FL_CAMERAS_OFF, FL_ALARM,
FL_MANAGER_OPEN, FL_TERMINAL_USED, FL_GUARD_AWAKE, FL_DATA_COPIED,
FL_GHOST_FOUND, FL_GHOST_FREED, FL_ROPE_USED, FL_POWER_CUT,
FL_JANET_TALKED, FL_LAPTOP_OPEN,
FLAG_COUNT
};
// ── State ────────────────────────────────────────────────────────
Room _room = PARKING;
uint8_t _inv[ITEM_COUNT]; // 1 = in inventory
uint8_t _roomItem[ROOM_COUNT][ITEM_COUNT]; // items on floor per room
bool _flags[FLAG_COUNT];
uint8_t _bobTalks = 0; // how many times player talked to Glenn
uint8_t _janetTalks = 0;
uint8_t _ghostTalks = 0;
int16_t _score = 0;
bool _ended = false;
// ── Helpers ──────────────────────────────────────────────────────
void _printBanner();
void _printHelp();
void _printScore();
void _printInventory();
void _look();
void _describeRoom();
const char* _roomName(Room r);
const char* _itemName(Item it);
const char* _itemShort(Item it);
const char* _npcName(NPC n);
bool _hasItem(Item it) const { return _inv[it]; }
bool _roomHasItem(Item it) const { return _roomItem[_room][it]; }
bool _flag(Flag f) const { return _flags[f]; }
void _setFlag(Flag f) { if (!_flags[f]) { _flags[f] = true; } }
void _addScore(int16_t pts) { _score += pts; }
Item _parseItem(const char* word) const;
NPC _parseNPC(const char* word) const;
Room _parseDir(const char* word) const; // returns ROOM_COUNT on fail
void _cmdGo(const char* dir);
void _cmdLook();
void _cmdExamine(const char* what);
void _cmdTake(const char* what);
void _cmdDrop(const char* what);
void _cmdUse(const char* what, const char* on);
void _cmdTalk(const char* who);
void _triggerEnding();
void _endingGhostMode();
void _endingOperative();
void _endingBurned();
static void _p(const char* s); // Serial.print wrapper
static void _pl(const char* s); // print + \r\n
static void _pr(); // print \r\n
};