Mystify Demo, TwoEleven updates & Snake (#21)
* **New Features** * Added Mystify screensaver demo with animated polygons and trails * Added Snake game with multiple difficulties, high-score persistence, and multi-input (touch/keyboard) support * Added CLI tool for building, packaging, and deploying apps (end-to-end build/install/run workflow) * Per-grid-size high-score persistence added to 2048 app; expanded keyboard controls (WASD and device-specific mappings) * **Documentation** * Added Snake README with gameplay, controls, and usage instructions
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
#pragma once
|
||||
|
||||
#include "PixelBuffer.h"
|
||||
#include "drivers/DisplayDriver.h"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <esp_random.h>
|
||||
|
||||
/**
|
||||
* Mystify Screensaver Demo
|
||||
*
|
||||
* Classic Windows-style mystify screensaver with bouncing polygons and trailing edges.
|
||||
* Adapted to work with DisplayDriver and PixelBuffer abstractions.
|
||||
*
|
||||
* Usage:
|
||||
* MystifyDemo mystify;
|
||||
* mystify.init(display);
|
||||
*
|
||||
* while (!shouldExit) {
|
||||
* mystify.update();
|
||||
* }
|
||||
*/
|
||||
class MystifyDemo {
|
||||
public:
|
||||
static constexpr int NUM_POLYGONS = 2;
|
||||
static constexpr int NUM_VERTICES = 4;
|
||||
static constexpr int TRAIL_LENGTH = 8;
|
||||
static constexpr int COLOR_CHANGE_INTERVAL = 200; // Frames between color changes
|
||||
static constexpr int STRIP_HEIGHT = 16; // Draw in strips to avoid SPI buffer overflow
|
||||
|
||||
MystifyDemo() = default;
|
||||
~MystifyDemo() { deinit(); }
|
||||
|
||||
// Non-copyable, non-movable (owns PixelBuffer)
|
||||
MystifyDemo(const MystifyDemo&) = delete;
|
||||
MystifyDemo& operator=(const MystifyDemo&) = delete;
|
||||
MystifyDemo(MystifyDemo&&) = delete;
|
||||
MystifyDemo& operator=(MystifyDemo&&) = delete;
|
||||
|
||||
bool init(DisplayDriver* display) {
|
||||
if (!display) {
|
||||
return false;
|
||||
}
|
||||
|
||||
display_ = display;
|
||||
width_ = display->getWidth();
|
||||
height_ = display->getHeight();
|
||||
|
||||
if (width_ <= 0 || height_ <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Seed random generator with hardware entropy
|
||||
srand(static_cast<unsigned>(esp_random()));
|
||||
|
||||
// Allocate full-screen framebuffer
|
||||
void* mem = malloc(sizeof(PixelBuffer));
|
||||
if (!mem) {
|
||||
return false;
|
||||
}
|
||||
framebuffer_ = new(mem) PixelBuffer(width_, height_, display->getColorFormat());
|
||||
|
||||
initPolygons();
|
||||
return true;
|
||||
}
|
||||
|
||||
void deinit() {
|
||||
if (framebuffer_) {
|
||||
framebuffer_->~PixelBuffer();
|
||||
free(framebuffer_);
|
||||
framebuffer_ = nullptr;
|
||||
}
|
||||
display_ = nullptr;
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (!framebuffer_ || !display_) return;
|
||||
|
||||
// Clear framebuffer to black
|
||||
framebuffer_->clear();
|
||||
|
||||
// Update and draw each polygon
|
||||
for (int p = 0; p < NUM_POLYGONS; p++) {
|
||||
updatePolygon(polygons_[p]);
|
||||
drawPolygon(polygons_[p]);
|
||||
}
|
||||
|
||||
// Send framebuffer to display in strips (full screen is too large for single SPI transaction)
|
||||
display_->lock();
|
||||
for (int y = 0; y < height_; y += STRIP_HEIGHT) {
|
||||
int stripEnd = (y + STRIP_HEIGHT > height_) ? height_ : y + STRIP_HEIGHT;
|
||||
display_->drawBitmap(0, y, width_, stripEnd, framebuffer_->getDataAtRow(y));
|
||||
}
|
||||
display_->unlock();
|
||||
}
|
||||
|
||||
private:
|
||||
// Smooth sub-pixel movement with floats
|
||||
struct Vertex {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
float dx = 0;
|
||||
float dy = 0;
|
||||
};
|
||||
|
||||
struct Polygon {
|
||||
Vertex vertices[NUM_VERTICES];
|
||||
// History: [trail_index][vertex_index] = {x, y}
|
||||
int16_t historyX[TRAIL_LENGTH][NUM_VERTICES];
|
||||
int16_t historyY[TRAIL_LENGTH][NUM_VERTICES];
|
||||
uint8_t colorIndex;
|
||||
int historyHead = 0;
|
||||
bool historyFull = false;
|
||||
int colorChangeCounter = 0;
|
||||
};
|
||||
|
||||
// Vibrant colors as RGB888 for format-agnostic rendering
|
||||
struct Color {
|
||||
uint8_t r, g, b;
|
||||
};
|
||||
|
||||
static constexpr Color COLOR_POOL[] = {
|
||||
{255, 0, 255}, // Magenta
|
||||
{0, 255, 255}, // Cyan
|
||||
{255, 255, 0}, // Yellow
|
||||
{255, 128, 0}, // Orange
|
||||
{0, 255, 128}, // Spring green
|
||||
{128, 0, 255}, // Purple
|
||||
{255, 64, 128}, // Hot pink
|
||||
{128, 255, 0}, // Lime
|
||||
};
|
||||
static constexpr int COLOR_POOL_SIZE = sizeof(COLOR_POOL) / sizeof(COLOR_POOL[0]);
|
||||
|
||||
DisplayDriver* display_ = nullptr;
|
||||
PixelBuffer* framebuffer_ = nullptr;
|
||||
uint16_t width_ = 0;
|
||||
uint16_t height_ = 0;
|
||||
Polygon polygons_[NUM_POLYGONS];
|
||||
|
||||
static float randomFloat(float min, float max) {
|
||||
return min + (max - min) * (static_cast<float>(rand()) / static_cast<float>(RAND_MAX));
|
||||
}
|
||||
|
||||
void initPolygons() {
|
||||
for (int p = 0; p < NUM_POLYGONS; p++) {
|
||||
Polygon& polygon = polygons_[p];
|
||||
|
||||
// Pick random color from pool
|
||||
polygon.colorIndex = rand() % COLOR_POOL_SIZE;
|
||||
polygon.historyHead = 0;
|
||||
polygon.historyFull = false;
|
||||
// Stagger color changes so polygons don't change simultaneously
|
||||
polygon.colorChangeCounter = rand() % COLOR_CHANGE_INTERVAL;
|
||||
|
||||
// Initialize vertices with random positions and velocities
|
||||
for (int v = 0; v < NUM_VERTICES; v++) {
|
||||
Vertex& vertex = polygon.vertices[v];
|
||||
vertex.x = static_cast<float>(rand() % width_);
|
||||
vertex.y = static_cast<float>(rand() % height_);
|
||||
|
||||
// Speed range for smooth movement
|
||||
vertex.dx = randomFloat(0.8f, 2.0f);
|
||||
vertex.dy = randomFloat(0.8f, 2.0f);
|
||||
if (rand() % 2) vertex.dx = -vertex.dx;
|
||||
if (rand() % 2) vertex.dy = -vertex.dy;
|
||||
|
||||
// Ensure dx != dy for more interesting movement patterns
|
||||
if (std::fabs(vertex.dx - vertex.dy) < 0.3f) {
|
||||
vertex.dy += (vertex.dy > 0 ? 0.5f : -0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize history with current positions
|
||||
for (int t = 0; t < TRAIL_LENGTH; t++) {
|
||||
for (int v = 0; v < NUM_VERTICES; v++) {
|
||||
polygon.historyX[t][v] = static_cast<int16_t>(polygon.vertices[v].x);
|
||||
polygon.historyY[t][v] = static_cast<int16_t>(polygon.vertices[v].y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updatePolygon(Polygon& polygon) {
|
||||
constexpr float minSpeed = 0.5f;
|
||||
constexpr float maxSpeed = 2.5f;
|
||||
|
||||
// Periodic color change
|
||||
polygon.colorChangeCounter++;
|
||||
if (polygon.colorChangeCounter >= COLOR_CHANGE_INTERVAL) {
|
||||
polygon.colorChangeCounter = 0;
|
||||
// Pick a different color
|
||||
uint8_t newColor;
|
||||
do {
|
||||
newColor = rand() % COLOR_POOL_SIZE;
|
||||
} while (newColor == polygon.colorIndex && COLOR_POOL_SIZE > 1);
|
||||
polygon.colorIndex = newColor;
|
||||
}
|
||||
|
||||
// Move vertices
|
||||
for (int v = 0; v < NUM_VERTICES; v++) {
|
||||
Vertex& vertex = polygon.vertices[v];
|
||||
vertex.x += vertex.dx;
|
||||
vertex.y += vertex.dy;
|
||||
|
||||
// Bounce off edges with slight angle variation for organic movement
|
||||
if (vertex.x <= 0) {
|
||||
vertex.x = 0;
|
||||
vertex.dx = std::fabs(vertex.dx);
|
||||
vertex.dy *= (1.0f + randomFloat(-0.1f, 0.1f));
|
||||
} else if (vertex.x >= width_ - 1) {
|
||||
vertex.x = static_cast<float>(width_ - 1);
|
||||
vertex.dx = -std::fabs(vertex.dx);
|
||||
vertex.dy *= (1.0f + randomFloat(-0.1f, 0.1f));
|
||||
}
|
||||
|
||||
if (vertex.y <= 0) {
|
||||
vertex.y = 0;
|
||||
vertex.dy = std::fabs(vertex.dy);
|
||||
vertex.dx *= (1.0f + randomFloat(-0.1f, 0.1f));
|
||||
} else if (vertex.y >= height_ - 1) {
|
||||
vertex.y = static_cast<float>(height_ - 1);
|
||||
vertex.dy = -std::fabs(vertex.dy);
|
||||
vertex.dx *= (1.0f + randomFloat(-0.1f, 0.1f));
|
||||
}
|
||||
|
||||
// Clamp speeds to prevent runaway acceleration or stalling
|
||||
auto clampSpeed = [minSpeed, maxSpeed](float& speed) {
|
||||
float sign = (speed >= 0) ? 1.0f : -1.0f;
|
||||
float absSpeed = std::fabs(speed);
|
||||
if (absSpeed < minSpeed) absSpeed = minSpeed;
|
||||
if (absSpeed > maxSpeed) absSpeed = maxSpeed;
|
||||
speed = sign * absSpeed;
|
||||
};
|
||||
clampSpeed(vertex.dx);
|
||||
clampSpeed(vertex.dy);
|
||||
}
|
||||
|
||||
// Advance history ring buffer
|
||||
polygon.historyHead = (polygon.historyHead + 1) % TRAIL_LENGTH;
|
||||
if (polygon.historyHead == 0) {
|
||||
polygon.historyFull = true;
|
||||
}
|
||||
|
||||
// Store current positions
|
||||
for (int v = 0; v < NUM_VERTICES; v++) {
|
||||
polygon.historyX[polygon.historyHead][v] = static_cast<int16_t>(polygon.vertices[v].x);
|
||||
polygon.historyY[polygon.historyHead][v] = static_cast<int16_t>(polygon.vertices[v].y);
|
||||
}
|
||||
}
|
||||
|
||||
void drawPolygon(const Polygon& polygon) {
|
||||
const Color& baseColor = COLOR_POOL[polygon.colorIndex];
|
||||
|
||||
// Draw trail from oldest to newest (so newest is on top)
|
||||
for (int t = TRAIL_LENGTH - 1; t >= 0; t--) {
|
||||
int histIndex = polygon.historyHead - t;
|
||||
if (histIndex < 0) histIndex += TRAIL_LENGTH;
|
||||
|
||||
// Skip if we don't have enough history yet
|
||||
if (!polygon.historyFull && histIndex > polygon.historyHead) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate brightness for this trail frame (older = dimmer)
|
||||
int brightness = 255 - (t * 230 / TRAIL_LENGTH);
|
||||
if (brightness < 25) brightness = 25;
|
||||
|
||||
// Scale color by brightness
|
||||
uint8_t r = (baseColor.r * brightness) / 255;
|
||||
uint8_t g = (baseColor.g * brightness) / 255;
|
||||
uint8_t b = (baseColor.b * brightness) / 255;
|
||||
|
||||
// Draw edges connecting vertices
|
||||
for (int e = 0; e < NUM_VERTICES; e++) {
|
||||
int nextVertex = (e + 1) % NUM_VERTICES;
|
||||
|
||||
int x0 = polygon.historyX[histIndex][e];
|
||||
int y0 = polygon.historyY[histIndex][e];
|
||||
int x1 = polygon.historyX[histIndex][nextVertex];
|
||||
int y1 = polygon.historyY[histIndex][nextVertex];
|
||||
|
||||
drawLine(x0, y0, x1, y1, r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bresenham's line algorithm
|
||||
void drawLine(int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) {
|
||||
int dx = std::abs(x1 - x0);
|
||||
int dy = std::abs(y1 - y0);
|
||||
int sx = (x0 < x1) ? 1 : -1;
|
||||
int sy = (y0 < y1) ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
while (true) {
|
||||
setPixel(x0, y0, r, g, b);
|
||||
|
||||
if (x0 == x1 && y0 == y1) break;
|
||||
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setPixel(int x, int y, uint8_t r, uint8_t g, uint8_t b) {
|
||||
if (x >= 0 && x < width_ && y >= 0 && y < height_) {
|
||||
framebuffer_->setPixel(x, y, r, g, b);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user