„Glass Maze” to minimalistyczny survival-horror zaprojektowany z myślą o mikrokontrolerach i małych ekranach OLED. Gracz trafia do proceduralnie generowanego, całkowicie pogrążonego w ciemności labiryntu, gdzie celem jest odnalezienie wyjścia i przejście na kolejny poziom.
Kluczową mechaniką jest latarka o ograniczonym zasięgu i baterii – oświetla jedynie wąski stożek przed postacią, zmuszając do ostrożnego i strategicznego korzystania z światła. Im mniej energii, tym słabsza widoczność, a jej całkowite wyczerpanie oznacza natychmiastową porażkę.
Każdy ukończony poziom odnawia część baterii, ale z czasem nagroda maleje, zwiększając trudność. Dodatkowe zagrożenie stanowi potwór, który pojawia się po pewnym czasie i ściga gracza, stając się coraz szybszy wraz z postępem. Choć ukrywa się w mroku, co kilka sekund zdradza swoją pozycję krótkim błyskiem.
Gra buduje napięcie poprzez ograniczoną widoczność, presję czasu i nieustanne zagrożenie, gdzie każdy błąd może zakończyć się śmiertelnym spotkaniem.
1. Płytka Arduino UNO
2. ekran oled 128x64
„Glass Maze” to trzymająca w napięciu gra survival-horror o minimalistycznej oprawie wizualnej, stworzona z myślą o mikrokontrolerach z niewielkim ekranem OLED, w której wcielasz się w postać uwięzioną w proceduralnie generowanym, całkowicie mrocznym labiryncie i musisz znaleźć wyjście, by przejść do kolejnego etapu. Najważniejszą mechaniką jest system pola widzenia oparty na latarce, która rozświetla mrok jedynie w wąskim stożku przed bohaterem, czyniąc resztę planszy niewidoczną, co w połączeniu z nieustannym zużyciem ograniczonej baterii – gdzie jej poziom naładowania bezpośrednio wpływa na zasięg światła – zmusza gracza do strategicznego zarządzania zasilaniem i włączania latarki tylko w ostateczności. Całkowite wyczerpanie zasilania oznacza natychmiastowy koniec gry, a choć dotarcie do wyjścia generuje nowy układ ścian i odnawia część baterii, z każdym zdanym poziomem nagroda ta maleje, podnosząc stopień trudności. Dodatkowe zagrożenie stanowi przerażający potwór, pojawiający się na planszy po upływie określonego czasu, który inteligentnie podąża śladem gracza i przyspiesza z każdym pokonanym etapem, bezlitośnie skracając czas na znalezienie celu. Wróg ten ukrywa się w ciemnościach, ale gra regularnie, co 3 sekundy, zdradza jego pozycję poprzez ułamkowe mignięcie, co potęguje klaustrofobiczny klimat i sprawia, że każdy fałszywy krok lub pusta bateria prowadzą do nieuchronnego, śmiertelnego spotkania z bestią.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
// ==========================================
// --- HARDWARE & DISPLAY CONFIGURATION ---
// ==========================================
#define i2c_Address 0x3c
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Button Pin Definitions (Connect to Ground, using INPUT_PULLUP)
#define BTN_UP 7
#define BTN_DOWN 9
#define BTN_LEFT 10
#define BTN_RIGHT 8
#define BTN_X 4
#define BTN_Y 5
// ==========================================
// --- TITLE SCREEN BITMAP (128x64) ---
// ==========================================
const unsigned char epd_bitmap_obraz [] PROGMEM = {
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0xbf, 0xff, 0xff, 0xfe, 0x09, 0xf8, 0xf0, 0xe1, 0xff, 0x33, 0x0c, 0x00, 0x1f, 0xff, 0xff, 0xfd,
0xff, 0xff, 0xff, 0xfc, 0xf9, 0xf2, 0x66, 0x4c, 0xff, 0x33, 0x27, 0xe1, 0xff, 0xff, 0xff, 0xfd,
0xdf, 0xff, 0xff, 0xf9, 0xf9, 0xf3, 0x27, 0xcf, 0xff, 0x02, 0x67, 0x89, 0xff, 0xff, 0xff, 0xfb,
0xff, 0xff, 0xff, 0xf9, 0x09, 0xf3, 0x30, 0xe1, 0xff, 0x02, 0x67, 0x18, 0x3f, 0xff, 0xff, 0xfb,
0xef, 0xff, 0xff, 0xf9, 0x89, 0xf0, 0x3e, 0x7c, 0xff, 0x32, 0x06, 0x79, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfc, 0x89, 0xf3, 0x26, 0x4c, 0xff, 0x32, 0x64, 0xf9, 0xff, 0xff, 0xff, 0xf7,
0xf7, 0xff, 0xff, 0xfe, 0x08, 0x13, 0x30, 0xe1, 0xff, 0x32, 0x64, 0x00, 0x1f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef,
0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xde,
0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3e, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0x67, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf,
0xfc, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x07, 0x7c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfd, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7d,
0xfd, 0x7f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x01, 0x7f, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
0xfd, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x01, 0x7f, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xfe, 0xfd,
0xfd, 0xbf, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x7f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xfd,
0xfd, 0xbf, 0xff, 0xff, 0xff, 0x90, 0x00, 0x00, 0x7f, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xfd, 0xff,
0xfd, 0xdf, 0xff, 0xff, 0xfc, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff,
0xfd, 0xff, 0xff, 0xff, 0xef, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xfb, 0xf9,
0xfd, 0xff, 0xff, 0xfe, 0x7f, 0xe0, 0x0c, 0x00, 0x7f, 0xff, 0xff, 0xef, 0x3f, 0xff, 0xff, 0xf9,
0xfd, 0xef, 0xff, 0xf3, 0x7f, 0xf0, 0x0f, 0x04, 0x7f, 0xff, 0xff, 0xdf, 0xe7, 0xff, 0xf7, 0xfb,
0xfd, 0xff, 0xff, 0x97, 0xff, 0xf0, 0x07, 0x1c, 0x7f, 0xff, 0xff, 0xaf, 0xbc, 0xff, 0xf7, 0xff,
0xfd, 0xf7, 0xfd, 0xf7, 0xff, 0xf8, 0x00, 0x19, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xef, 0xff,
0xfd, 0xff, 0xef, 0xf7, 0xff, 0xf8, 0x00, 0x01, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xef, 0xf7,
0xfd, 0xff, 0xff, 0xf7, 0xff, 0xfc, 0x1f, 0x83, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xfe,
0xfd, 0xff, 0xff, 0xf7, 0xff, 0xf8, 0x07, 0xc7, 0x7f, 0xff, 0xff, 0xff, 0xbf, 0xff, 0xef, 0xfe,
0xfd, 0xff, 0xff, 0xf5, 0xff, 0xf0, 0x00, 0x0f, 0x7f, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xef, 0xff,
0xfd, 0xff, 0xff, 0xf7, 0xff, 0xc0, 0x00, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff,
0xfd, 0xff, 0xff, 0xf5, 0xff, 0x04, 0x61, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff,
0xfd, 0xff, 0xff, 0xf5, 0xfc, 0x0c, 0x70, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff,
0xfd, 0xff, 0xff, 0xf5, 0xf0, 0x38, 0xf8, 0x7f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff,
0xfd, 0xff, 0xff, 0xf5, 0xe0, 0xf8, 0xfc, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff,
0xfd, 0xff, 0xef, 0xf7, 0x83, 0xf8, 0xfe, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xfb, 0xef, 0xff,
0xfd, 0xf7, 0xfd, 0xf7, 0x0f, 0xf1, 0xfe, 0x3f, 0x79, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
0xfd, 0xf0, 0x00, 0x37, 0x1f, 0xf1, 0xff, 0x3f, 0x76, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x07, 0xff,
0xfd, 0xef, 0xff, 0xe7, 0x1f, 0xf1, 0xff, 0x17, 0x6f, 0x7f, 0xff, 0xff, 0xf3, 0xff, 0xf7, 0xff,
0xfd, 0xef, 0xff, 0xfc, 0x0f, 0xf3, 0xff, 0x03, 0x6f, 0x7f, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff,
0xfd, 0xe0, 0xff, 0xfe, 0x03, 0xf3, 0xfc, 0x07, 0x6f, 0x7f, 0xff, 0xfc, 0xff, 0xfc, 0x03, 0xff,
0x7d, 0xdf, 0xff, 0xfe, 0x00, 0xe3, 0xe0, 0x07, 0x66, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff,
0x7d, 0xff, 0xff, 0xfe, 0x00, 0x23, 0x80, 0x8f, 0x78, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xfd, 0xff,
0xfd, 0xbf, 0xff, 0xfc, 0x44, 0x00, 0x07, 0x8f, 0x70, 0x1f, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff,
0xbd, 0xff, 0xff, 0xfc, 0x63, 0x00, 0x3f, 0x1f, 0x71, 0xdf, 0xef, 0xff, 0xff, 0xff, 0xfe, 0xff,
0xfd, 0x7f, 0xff, 0xfc, 0x61, 0xc0, 0xfe, 0x3f, 0x69, 0xaf, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfd, 0xff, 0xff, 0xff, 0xf0, 0x77, 0xde, 0x3f, 0x60, 0x01, 0xf1, 0xff, 0xff, 0xff, 0xff, 0x7f,
0xdc, 0xff, 0xff, 0xff, 0x81, 0xff, 0xf0, 0x7f, 0x6d, 0x07, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff,
0xfc, 0xff, 0xff, 0xf3, 0xf8, 0xff, 0xf8, 0x7f, 0x7c, 0x17, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xbf,
0xad, 0xff, 0xfc, 0x7f, 0xf8, 0xff, 0xf8, 0xcf, 0x78, 0x13, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xbf,
0xfd, 0xff, 0x8f, 0xff, 0xfc, 0x7f, 0xf0, 0x79, 0x4c, 0x99, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff,
0x7f, 0xe3, 0xff, 0xff, 0xf8, 0x7f, 0xf0, 0x1f, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xdf,
0xf8, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xfc, 0x1f, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0xff, 0xff, 0xff, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef,
0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7,
0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb,
0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// ==========================================
// --- GAME SETTINGS & VARIABLES ---
// ==========================================
// Maze Settings
#define COLS 8
#define ROWS 4
#define CELL_SIZE 16
#define WALL_TOP 1
#define WALL_RIGHT 2
#define WALL_BOTTOM 4
#define WALL_LEFT 8
#define VISITED 16
byte maze[COLS][ROWS];
// State Machine
enum GameState { MENU, PLAYING, GAMEOVER };
GameState gameState = MENU;
// Player Settings
float playerX, playerY, playerAngle;
float moveSpeed = 1.5;
float turnSpeed = 0.15;
const float playerRadius = 3.0;
// Flashlight & Battery
bool flashlightOn = false;
bool lastBtnXState = HIGH;
float maxBattery = 45.0;
float currentBattery = maxBattery;
unsigned long lastDrainTime = 0;
// Game Stats & Menu Timing
int score = 0;
bool showStatus = false;
bool lastBtnYState = HIGH;
unsigned long blinkingInterval = 500; // Text is visible for 500ms, then invisible
// Enemy Settings
float enemyX, enemyY;
int enemyTargetCX, enemyTargetCY;
int enemyLastCX, enemyLastCY;
bool enemyActive = false;
unsigned long levelStartTime = 0;
// Player Sprite Definition ('i' shape to show direction)
const float iSprite[10][2] = {
{ 3, 0}, { 3, -1},
{ 0, 0}, { 0, -1},
{-1, 0}, {-1, -1},
{-2, 0}, {-2, -1},
{-3, 0}, {-3, -1}
};
// ==========================================
// --- SETUP & MAIN LOOP ---
// ==========================================
void setup() {
Serial.begin(115200);
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
pinMode(BTN_X, INPUT_PULLUP);
pinMode(BTN_Y, INPUT_PULLUP);
randomSeed(analogRead(A0));
delay(250); // Give OLED time to boot
if (!display.begin(i2c_Address, true)) {
Serial.println(F("SH1106 init failed"));
while (true);
}
display.setRotation(2);
}
void loop() {
switch (gameState) {
case MENU:
drawMenu(); // Draw static background elements
// Add blinking text separately after graphics are in buffer
if ((millis() / blinkingInterval) % 2 == 0) {
display.setTextSize(1);
// Flipped colors! White text on a Black highlight block
display.setTextColor(SH110X_WHITE, SH110X_BLACK);
display.setCursor(16, 54);
display.print(F("Press Y to Start"));
}
display.display(); // Push everything to the screen at once
handleMenuInput();
break;
case PLAYING:
handleInput();
if (!showStatus) {
updateGame();
updateEnemy();
}
drawGame(); // drawGame handles its own display.display()
break;
case GAMEOVER:
drawGameOver();
handleGameOverInput();
break;
}
}
// ==========================================
// --- MENU (GLASS MAZE TITLE SCREEN) ---
// ==========================================
void drawMenu() {
display.clearDisplay();
// Render the custom bitmap image in BLACK (0s) over a WHITE background (1s) to invert it
display.drawBitmap(0, 0, epd_bitmap_obraz, 128, 64, SH110X_BLACK, SH110X_WHITE);
}
void handleMenuInput() {
if (digitalRead(BTN_Y) == LOW) {
score = 0;
currentBattery = maxBattery;
enemyActive = false;
generateMaze();
playerX = CELL_SIZE / 2.0;
playerY = SCREEN_HEIGHT - (CELL_SIZE / 2.0);
playerAngle = -PI / 2.0;
levelStartTime = millis();
gameState = PLAYING;
delay(200);
}
}
// ==========================================
// --- CORE GAMEPLAY LOGIC ---
// ==========================================
void generateMaze() {
for (int x = 0; x < COLS; x++) {
for (int y = 0; y < ROWS; y++) {
maze[x][y] = WALL_TOP | WALL_RIGHT | WALL_BOTTOM | WALL_LEFT;
}
}
int stackX[COLS * ROWS];
int stackY[COLS * ROWS];
int stackSize = 0;
int cx = 0, cy = ROWS - 1;
maze[cx][cy] |= VISITED;
stackX[stackSize] = cx;
stackY[stackSize] = cy;
stackSize++;
while (stackSize > 0) {
cx = stackX[stackSize - 1];
cy = stackY[stackSize - 1];
int neighbors[4];
int numNeighbors = 0;
if (cy > 0 && !(maze[cx][cy - 1] & VISITED)) neighbors[numNeighbors++] = 0;
if (cx < COLS - 1 && !(maze[cx + 1][cy] & VISITED)) neighbors[numNeighbors++] = 1;
if (cy < ROWS - 1 && !(maze[cx][cy + 1] & VISITED)) neighbors[numNeighbors++] = 2;
if (cx > 0 && !(maze[cx - 1][cy] & VISITED)) neighbors[numNeighbors++] = 3;
if (numNeighbors > 0) {
int dir = neighbors[random(numNeighbors)];
int nx = cx, ny = cy;
if (dir == 0) { ny--; maze[cx][cy] &= ~WALL_TOP; maze[nx][ny] &= ~WALL_BOTTOM; }
else if (dir == 1) { nx++; maze[cx][cy] &= ~WALL_RIGHT; maze[nx][ny] &= ~WALL_LEFT; }
else if (dir == 2) { ny++; maze[cx][cy] &= ~WALL_BOTTOM; maze[nx][ny] &= ~WALL_TOP; }
else if (dir == 3) { nx--; maze[cx][cy] &= ~WALL_LEFT; maze[nx][ny] &= ~WALL_RIGHT; }
maze[nx][ny] |= VISITED;
stackX[stackSize] = nx;
stackY[stackSize] = ny;
stackSize++;
} else {
stackSize--;
}
}
}
bool checkCollision(float x, float y) {
if (x < playerRadius || x >= SCREEN_WIDTH - playerRadius) return true;
if (y < playerRadius || y >= SCREEN_HEIGHT - playerRadius) return true;
int cx = x / CELL_SIZE;
int cy = y / CELL_SIZE;
if (cx < 0 || cx >= COLS || cy < 0 || cy >= ROWS) return true;
float lx = x - (cx * CELL_SIZE);
float ly = y - (cy * CELL_SIZE);
if ((maze[cx][cy] & WALL_TOP) && ly < playerRadius) return true;
if ((maze[cx][cy] & WALL_BOTTOM) && ly > CELL_SIZE - playerRadius) return true;
if ((maze[cx][cy] & WALL_LEFT) && lx < playerRadius) return true;
if ((maze[cx][cy] & WALL_RIGHT) && lx > CELL_SIZE - playerRadius) return true;
return false;
}
void handleInput() {
bool currentBtnYState = digitalRead(BTN_Y);
if (lastBtnYState == HIGH && currentBtnYState == LOW) showStatus = !showStatus;
lastBtnYState = currentBtnYState;
if (showStatus) {
lastDrainTime = millis();
return;
}
if (digitalRead(BTN_LEFT) == LOW) playerAngle -= turnSpeed;
if (digitalRead(BTN_RIGHT) == LOW) playerAngle += turnSpeed;
float nextX = playerX;
float nextY = playerY;
if (digitalRead(BTN_UP) == LOW) {
nextX += cos(playerAngle) * moveSpeed;
nextY += sin(playerAngle) * moveSpeed;
}
if (digitalRead(BTN_DOWN) == LOW) {
nextX -= cos(playerAngle) * moveSpeed;
nextY -= sin(playerAngle) * moveSpeed;
}
if (!checkCollision(nextX, playerY)) playerX = nextX;
if (!checkCollision(playerX, nextY)) playerY = nextY;
bool currentBtnXState = digitalRead(BTN_X);
if (lastBtnXState == HIGH && currentBtnXState == LOW) flashlightOn = !flashlightOn;
lastBtnXState = currentBtnXState;
}
void updateGame() {
int cx = playerX / CELL_SIZE;
int cy = playerY / CELL_SIZE;
if (cx == COLS - 1 && cy == 0) {
generateMaze();
playerX = CELL_SIZE / 2.0;
playerY = SCREEN_HEIGHT - (CELL_SIZE / 2.0);
playerAngle = -PI / 2.0;
float scavengeAmount = maxBattery - (score * ((maxBattery - 10.0) / 7.0));
if (scavengeAmount < 10.0) scavengeAmount = 10.0;
currentBattery += scavengeAmount;
if (currentBattery > maxBattery) currentBattery = maxBattery;
score++;
levelStartTime = millis();
enemyActive = false;
}
if (flashlightOn) {
if (millis() - lastDrainTime > 150) {
currentBattery -= 0.15;
if (currentBattery <= 0.0) {
currentBattery = 0.0;
gameState = GAMEOVER;
}
lastDrainTime = millis();
}
} else {
lastDrainTime = millis();
}
}
// ==========================================
// --- ENEMY AI LOGIC ---
// ==========================================
void pickNextEnemyTarget() {
int cx = enemyTargetCX;
int cy = enemyTargetCY;
int candidatesX[4], candidatesY[4], numCandidates = 0;
if (!(maze[cx][cy] & WALL_TOP)) { candidatesX[numCandidates] = cx; candidatesY[numCandidates] = cy - 1; numCandidates++; }
if (!(maze[cx][cy] & WALL_RIGHT)) { candidatesX[numCandidates] = cx + 1; candidatesY[numCandidates] = cy; numCandidates++; }
if (!(maze[cx][cy] & WALL_BOTTOM)) { candidatesX[numCandidates] = cx; candidatesY[numCandidates] = cy + 1; numCandidates++; }
if (!(maze[cx][cy] & WALL_LEFT)) { candidatesX[numCandidates] = cx - 1; candidatesY[numCandidates] = cy; numCandidates++; }
if (numCandidates == 1) {
enemyLastCX = cx; enemyLastCY = cy;
enemyTargetCX = candidatesX[0]; enemyTargetCY = candidatesY[0];
return;
}
int validX[4], validY[4], validCount = 0;
for(int i = 0; i < numCandidates; i++) {
if (candidatesX[i] != enemyLastCX || candidatesY[i] != enemyLastCY) {
validX[validCount] = candidatesX[i]; validY[validCount] = candidatesY[i];
validCount++;
}
}
if (validCount > 0) {
int choice = random(validCount);
enemyLastCX = cx; enemyLastCY = cy;
enemyTargetCX = validX[choice]; enemyTargetCY = validY[choice];
}
}
void updateEnemy() {
if (!enemyActive) {
if (score >= 1 && (millis() - levelStartTime > 10000)) {
enemyActive = true;
enemyTargetCX = 0;
enemyTargetCY = ROWS - 1;
enemyLastCX = enemyTargetCX;
enemyLastCY = enemyTargetCY;
enemyX = enemyTargetCX * CELL_SIZE + CELL_SIZE / 2.0;
enemyY = enemyTargetCY * CELL_SIZE + CELL_SIZE / 2.0;
pickNextEnemyTarget();
}
return;
}
float enemySpeed = moveSpeed * min(1.25, 0.75 + (score * 0.05));
float targetPixelX = enemyTargetCX * CELL_SIZE + CELL_SIZE / 2.0;
float targetPixelY = enemyTargetCY * CELL_SIZE + CELL_SIZE / 2.0;
float dx = targetPixelX - enemyX;
float dy = targetPixelY - enemyY;
float dist = sqrt(dx * dx + dy * dy);
if (dist <= enemySpeed) {
enemyX = targetPixelX;
enemyY = targetPixelY;
pickNextEnemyTarget();
} else {
enemyX += (dx / dist) * enemySpeed;
enemyY += (dy / dist) * enemySpeed;
}
float pdx = playerX - enemyX;
float pdy = playerY - enemyY;
if (sqrt(pdx * pdx + pdy * pdy) < 7.0) gameState = GAMEOVER;
}
// ==========================================
// --- RENDERING & MATH ---
// ==========================================
bool isPointInCone(float x, float y) {
float dx = x - playerX;
float dy = y - playerY;
float distSq = dx * dx + dy * dy;
if (distSq > currentBattery * currentBattery) return false;
if (distSq == 0) return true;
float dirX = cos(playerAngle);
float dirY = sin(playerAngle);
float dot = dx * dirX + dy * dirY;
if (dot <= 0) return false;
float cosFovSq = 0.75;
return ((dot * dot) / distSq) >= cosFovSq;
}
bool isWallVisible(float x1, float y1, float x2, float y2) {
if (!flashlightOn) return false;
return isPointInCone(x1, y1) || isPointInCone(x2, y2) || isPointInCone((x1 + x2) / 2.0, (y1 + y2) / 2.0);
}
void drawGameOver() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(38, 20);
display.print(F("GAME OVER"));
display.setCursor(12, 40);
display.print(F("Press Y to Restart"));
display.display();
}
void handleGameOverInput() {
if (digitalRead(BTN_Y) == LOW) {
gameState = MENU;
delay(200);
}
}
void drawGame() {
display.clearDisplay();
if (showStatus) {
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(30, 8);
display.print(F("--- STATUS ---"));
display.setCursor(15, 25);
display.print(F("Mazes Beaten: "));
display.print(score);
int batPct = (int)((currentBattery / maxBattery) * 100);
display.setCursor(10, 40);
display.print(F("Power: "));
display.print(batPct);
display.print(F("%"));
display.drawRect(76, 39, 42, 9, SH110X_WHITE);
display.fillRect(78, 41, (int)(38.0 * (currentBattery / maxBattery)), 5, SH110X_WHITE);
display.display();
return;
}
for (int x = 0; x < COLS; x++) {
for (int y = 0; y < ROWS; y++) {
int px = x * CELL_SIZE;
int py = y * CELL_SIZE;
if (maze[x][y] & WALL_TOP) {
if (isWallVisible(px, py, px + CELL_SIZE, py)) display.drawLine(px, py, px + CELL_SIZE, py, SH110X_WHITE);
}
if (maze[x][y] & WALL_LEFT) {
if (isWallVisible(px, py, px, py + CELL_SIZE)) display.drawLine(px, py, px, py + CELL_SIZE, SH110X_WHITE);
}
if (y == ROWS - 1 && (maze[x][y] & WALL_BOTTOM)) {
if (isWallVisible(px, py + CELL_SIZE - 1, px + CELL_SIZE, py + CELL_SIZE - 1)) display.drawLine(px, py + CELL_SIZE - 1, px + CELL_SIZE, py + CELL_SIZE - 1, SH110X_WHITE);
}
if (x == COLS - 1 && (maze[x][y] & WALL_RIGHT)) {
if (isWallVisible(px + CELL_SIZE - 1, py, px + CELL_SIZE - 1, py + CELL_SIZE)) display.drawLine(px + CELL_SIZE - 1, py, px + CELL_SIZE - 1, py + CELL_SIZE, SH110X_WHITE);
}
}
}
if ((millis() / 250) % 2 == 0) {
display.fillRect(SCREEN_WIDTH - CELL_SIZE + 4, 4, CELL_SIZE - 8, CELL_SIZE - 8, SH110X_WHITE);
}
float s = sin(playerAngle);
float c = cos(playerAngle);
if (flashlightOn) {
float angleLeft = playerAngle - (PI / 6.0);
float angleRight = playerAngle + (PI / 6.0);
int leftX = playerX + cos(angleLeft) * currentBattery;
int leftY = playerY + sin(angleLeft) * currentBattery;
int rightX = playerX + cos(angleRight) * currentBattery;
int rightY = playerY + sin(angleRight) * currentBattery;
display.drawLine((int)playerX, (int)playerY, leftX, leftY, SH110X_WHITE);
display.drawLine((int)playerX, (int)playerY, rightX, rightY, SH110X_WHITE);
}
if (enemyActive) {
bool enemyVisible = isPointInCone(enemyX, enemyY);
if ((millis() % 3000) < 500) enemyVisible = true;
if (enemyVisible) display.fillCircle((int)enemyX, (int)enemyY, 4, SH110X_WHITE);
}
for (int i = 0; i < 10; i++) {
float px = iSprite[i][0];
float py = iSprite[i][1];
float rotatedX = (px * c) - (py * s);
float rotatedY = (px * s) + (py * c);
display.drawPixel((int)(playerX + rotatedX), (int)(playerY + rotatedY), SH110X_WHITE);
}
display.display();
}