#include <FastLED.h>
/* ===== הגדרות מטריצה ===== */
#define LED_PIN 5
#define WIDTH 8
#define HEIGHT 8
#define NUM_LEDS (WIDTH * HEIGHT)
#define CHIPSET WS2812B
#define COLOR_ORDER GRB
#define BRIGHTNESS 128
CRGB leds[NUM_LEDS];
/* ===== ג'ויסטיק ===== */
#define JOY_X_PIN A0
#define JOY_Y_PIN A1
#define DEAD_LOW 450
#define DEAD_HIGH 550
/* ===== כפתור Pause (INPUT_PULLUP) ===== */
#define BTN_PIN A2
#define DEBOUNCE_MS 40
bool paused = false;
int btnStable = HIGH;
int btnLastReading = HIGH;
unsigned long btnLastChange = 0;
/* ===== באזר אקטיבי ===== */
#define BUZZER_PIN 11 // Active buzzer (HIGH=ON)
/* ===== תזמון תנועה רציפה ===== */
#define MOVE_INTERVAL_MS 500
unsigned long lastStepMs = 0;
/* ===== כללי גבול ===== */
#define BOUNDARY_MODE 2 // 0=פסילה, 1=Bounce, 2=Wrap
/* ===== פתיחת שלב קלה ===== */
#define SAFE_OPEN_STEPS 4
#define CENTER_MIN_X 2
#define CENTER_MAX_X (WIDTH - 3)
#define CENTER_MIN_Y 2
#define CENTER_MAX_Y (HEIGHT - 3)
/* ===== מצב משחק ===== */
int px = WIDTH / 2;
int py = HEIGHT / 2;
int dirX = 1;
int dirY = 0;
unsigned long score = 0;
bool gameOver = false;
/* ===== שלבים מרובי-מטרות ===== */
#define MAX_STAGE_TARGETS 16
int stageNum = 1; // מתחילים בשלב 1 (1 ירוקה + 1 אדומה)
int remainingGreens = 0;
// מערכים למיקומי המטרות
int greensX[MAX_STAGE_TARGETS];
int greensY[MAX_STAGE_TARGETS];
int redsX[MAX_STAGE_TARGETS];
int redsY[MAX_STAGE_TARGETS];
/* ===== מצב "מסך ניקוד" לא-חוסם ===== */
bool scoreOverlayActive = false;
unsigned long scoreOverlayEndAt = 0;
/* ===== ניהול צליל לא-חוסם ===== */
bool buzzerActive = false;
unsigned long buzzerOffAt = 0;
/* ===== תבנית כישלון לא-חוסמת (הבזקי אדום + ביפים) ===== */
bool failPatternActive = false;
int failStage = 0;
unsigned long failNextAt = 0;
/* ========= הצהרות ========= */
uint16_t xyToIndex(uint8_t x, uint8_t y);
void readJoystickAndSetDirection();
void stepPlayer();
void drawFrame();
void drawScoreOverlay();
void resetGame(bool keepScore = false);
void spawnStage(int n);
/* ===== פתיחת שלב קלה ===== */
bool pathClearNoRedNoWall(int x, int y, int dx, int dy, int steps);
bool chooseSafeStart(int minSteps);
/* ===== אודיו/אפקטים לא-חוסמים ===== */
void buzzFor(unsigned long ms);
void playGoodChirp();
void startFailJingle(); // התחלת דפוס הכישלון
void updateNonBlockingEffects(); // להריץ בכל loop
/* ===== כפתור Pause ===== */
void updatePauseButton();
/* ===== ציור ===== */
void showBlack();
/* ========= SETUP ========= */
void setup() {
Serial.begin(115200);
delay(200);
FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
showBlack();
pinMode(JOY_X_PIN, INPUT);
pinMode(JOY_Y_PIN, INPUT);
pinMode(BTN_PIN, INPUT_PULLUP); // כפתור Pause
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// seed לרנדום
randomSeed(analogRead(A3)); // אם אין A3 אצלך, החלף לפין אנלוגי פנוי
resetGame(false);
}
/* ========= LOOP ========= */
void loop() {
// כפתור Pause קודם—שיהיה מגיב תמיד
updatePauseButton();
// אפקטים/צלילים לא-חוסמים
updateNonBlockingEffects();
// מצב פסילה: להציג דפוס, לא לאתחל עד שיסתיים, ואז לאתחל רק אחרי הזזה בג'ויסטיק
if (gameOver) {
if (!failPatternActive) {
readJoystickAndSetDirection();
if (dirX != 0 || dirY != 0) {
resetGame(false);
}
}
return; // לא לצייר פריים רגיל בזמן פסילה
}
// Pause: עוצר צעדים; משאיר ציור מצב נוכחי
if (paused) {
// אפשר עדיין לעדכן כיוון לפי ג'ויסטיק כדי שימשיך מיד בכיוון החדש כשנשחרר Pause
readJoystickAndSetDirection();
drawFrame(); // השאר מצב דומם על המסך
return;
}
// רגיל
readJoystickAndSetDirection();
unsigned long now = millis();
if (now - lastStepMs >= getMoveInterval(stageNum)) {
stepPlayer();
lastStepMs = now;
}
// ציור: אם יש מסך ניקוד — מציגים אותו; אחרת פריים רגיל
if (scoreOverlayActive) {
drawScoreOverlay();
} else if (!failPatternActive) {
drawFrame();
}
}
// מחזירה את משך הזמן בין צעדים לפי שלב
unsigned long getMoveInterval(int stage) {
unsigned long base = 500; // מהירות בסיס (שלב 1–2)
unsigned long step = 50; // כמה להוריד בכל שלב מעל 2
if (stage <= 2) return base;
unsigned long interval = base - (stage - 2) * step;
// לא לרדת מתחת ל־150ms (כדי לא להגיע למהירות קיצונית)
if (interval < 150) interval = 150;
return interval;
}
/* ===== קלט ג'ויסטיק וקביעת כיוון (תנועה רציפה) ===== */
void readJoystickAndSetDirection() {
int xVal = analogRead(JOY_X_PIN);
int yVal = analogRead(JOY_Y_PIN);
int dx = 0, dy = 0;
if (xVal < DEAD_LOW) dx = -1;
else if (xVal > DEAD_HIGH) dx = +1;
// ציר Y הפוך (למטה כאשר הערך קטן)
if (yVal < DEAD_LOW) dy = +1;
else if (yVal > DEAD_HIGH) dy = -1;
if (dx == 0 && dy == 0) return;
// עדיפות לציר עם סטייה גדולה יותר – מונע דיאגונל
int devX = abs(xVal - 512);
int devY = abs(yVal - 512);
if (devX >= devY) {
dy = 0;
} else {
dx = 0;
}
dirX = dx;
dirY = dy;
}
/* ===== צעד תנועה + לוגיקת שלבים ===== */
void stepPlayer() {
int nx = px + dirX;
int ny = py + dirY;
// טיפול גבול לפי מצב
if (nx < 0 || nx >= WIDTH || ny < 0 || ny >= HEIGHT) {
#if BOUNDARY_MODE == 0
gameOver = true;
Serial.println(F("GAME OVER (border)"));
startFailJingle();
return;
#elif BOUNDARY_MODE == 1
// Bounce: היפוך כיוון והצמדה לגבול
if (nx < 0) {
nx = 0;
dirX = +1;
} else if (nx >= WIDTH) {
nx = WIDTH - 1;
dirX = -1;
}
if (ny < 0) {
ny = 0;
dirY = +1;
} else if (ny >= HEIGHT) {
ny = HEIGHT - 1;
dirY = -1;
}
#elif BOUNDARY_MODE == 2
// Wrap: עטיפה לצד השני
if (nx < 0) nx = WIDTH - 1;
else if (nx >= WIDTH) nx = 0;
if (ny < 0) ny = HEIGHT - 1;
else if (ny >= HEIGHT) ny = 0;
#endif
}
// עדכון מיקום
px = nx;
py = ny;
// פגיעה באדומה = הפסד
for (int i = 0; i < stageNum && i < MAX_STAGE_TARGETS; i++) {
if (px == redsX[i] && py == redsY[i]) {
gameOver = true;
Serial.println(F("GAME OVER (red)"));
startFailJingle();
return;
}
}
// בדיקת ירוקה — אוכלים אחת לכל היותר בצעד
bool ateGreen = false;
for (int i = 0; i < stageNum && i < MAX_STAGE_TARGETS; i++) {
if (greensX[i] < 0) continue; // כבר נאכלה
if (px == greensX[i] && py == greensY[i]) {
// אכלנו ירוקה
greensX[i] = -1;
greensY[i] = -1; // סימון כ"נאכל"
score++;
remainingGreens--;
Serial.print(F("Score: "));
Serial.println(score);
playGoodChirp();
// אין מרכוז אחרי אכילת ירוקה — ממשיכים מהנקודה והכיוון הנוכחיים
ateGreen = true;
break;
}
}
// סיום שלב: כל הירוקות נאכלו
if (ateGreen && remainingGreens <= 0) {
// מסך ניקוד לא-חוסם למשך ~1 שנייה
scoreOverlayActive = true;
scoreOverlayEndAt = millis() + 1000UL;
stageNum++;
if (stageNum > MAX_STAGE_TARGETS) stageNum = MAX_STAGE_TARGETS;
spawnStage(stageNum);
}
}
/* ===== ציור פריים רגיל ===== */
void drawFrame() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
// אדומות
for (int i = 0; i < stageNum && i < MAX_STAGE_TARGETS; i++) {
leds[xyToIndex(redsX[i], redsY[i])] = CRGB::Red;
}
// ירוקות שטרם נאכלו
for (int i = 0; i < stageNum && i < MAX_STAGE_TARGETS; i++) {
if (greensX[i] >= 0)
leds[xyToIndex(greensX[i], greensY[i])] = CRGB::Green;
}
// שחקן
leds[xyToIndex(px, py)] = CRGB::Blue;
FastLED.show();
}
/* ===== ציור מסך ניקוד (לא-חוסם) ===== */
void drawScoreOverlay() {
if (millis() >= scoreOverlayEndAt) {
scoreOverlayActive = false;
return;
}
fill_solid(leds, NUM_LEDS, CRGB::Black);
unsigned long n = score;
if (n > NUM_LEDS) n = NUM_LEDS;
unsigned long placed = 0;
for (int y = 0; y < HEIGHT && placed < n; y++) {
for (int x = 0; x < WIDTH && placed < n; x++) {
leds[xyToIndex(x, y)] = CRGB::Green;
placed++;
}
}
FastLED.show();
}
/* ===== איפוס משחק ===== */
void resetGame(bool keepScore) {
if (!keepScore) score = 0;
px = WIDTH / 2;
py = HEIGHT / 2;
dirX = 1;
dirY = 0;
stageNum = 1;
spawnStage(stageNum);
gameOver = false;
// ניקוי אפקטים/חיווי
scoreOverlayActive = false;
failPatternActive = false;
buzzerActive = false;
paused = false;
digitalWrite(BUZZER_PIN, LOW);
lastStepMs = millis();
drawFrame();
Serial.println(F("--- NEW GAME ---"));
}
/* ===== יצירת שלב: n ירוקות ו-n אדומות בלי חפיפות ===== */
void spawnStage(int n) {
if (n > MAX_STAGE_TARGETS) n = MAX_STAGE_TARGETS;
// איפוס מערכים
for (int i = 0; i < n; i++) {
greensX[i] = greensY[i] = -1;
redsX[i] = redsY[i] = -1;
}
remainingGreens = n;
// מניחים ירוקות
int placed = 0;
while (placed < n) {
int x = random(0, WIDTH);
int y = random(0, HEIGHT);
bool clash = (x == px && y == py);
for (int j = 0; j < placed && !clash; j++)
if (greensX[j] == x && greensY[j] == y) clash = true;
for (int j = 0; j < placed && !clash; j++)
if (redsX[j] == x && redsY[j] == y) clash = true;
if (!clash) {
greensX[placed] = x;
greensY[placed] = y;
placed++;
}
}
// מניחים אדומות
placed = 0;
while (placed < n) {
int x = random(0, WIDTH);
int y = random(0, HEIGHT);
bool clash = (x == px && y == py);
for (int j = 0; j < n && !clash; j++)
if (greensX[j] == x && greensY[j] == y) clash = true;
for (int j = 0; j < placed && !clash; j++)
if (redsX[j] == x && redsY[j] == y) clash = true;
if (!clash) {
redsX[placed] = x;
redsY[placed] = y;
placed++;
}
}
// פתיחת שלב קלה: מיקום מרכזי + כיוון עם SAFE_OPEN_STEPS צעדים בטוחים (בלי קיר/אדומה)
if (!chooseSafeStart(SAFE_OPEN_STEPS)) {
px = WIDTH / 2;
py = HEIGHT / 2;
dirX = 1;
dirY = 0;
}
}
/* ===== מיפוי זיג-זג ===== */
uint16_t xyToIndex(uint8_t x, uint8_t y) {
if (y % 2 == 0) return y * WIDTH + x;
return y * WIDTH + (WIDTH - 1 - x);
}
/* ====================== פתיחת שלב קלה ====================== */
bool pathClearNoRedNoWall(int x, int y, int dx, int dy, int steps) {
// לא לעמוד על אדומה בתחילת הדרך
for (int i = 0; i < stageNum && i < MAX_STAGE_TARGETS; i++) {
if (x == redsX[i] && y == redsY[i]) return false;
}
// קדימה steps צעדים: לא לצאת מהמסך ולא לדרוך על אדומה
int cx = x, cy = y;
for (int s = 0; s < steps; s++) {
cx += dx;
cy += dy;
if (cx < 0 || cx >= WIDTH || cy < 0 || cy >= HEIGHT) return false;
for (int i = 0; i < stageNum && i < MAX_STAGE_TARGETS; i++) {
if (cx == redsX[i] && cy == redsY[i]) return false;
}
}
return true;
}
bool chooseSafeStart(int minSteps) {
const int dirs[4][2] = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }; // ימינה, שמאלה, למטה, למעלה
for (int attempt = 0; attempt < 60; attempt++) {
int sx = random(CENTER_MIN_X, CENTER_MAX_X + 1);
int sy = random(CENTER_MIN_Y, CENTER_MAX_Y + 1);
// אל תעמוד על אדומה
bool bad = false;
for (int i = 0; i < stageNum && i < MAX_STAGE_TARGETS; i++) {
if (sx == redsX[i] && sy == redsY[i]) {
bad = true;
break;
}
}
if (bad) continue;
// סדר כיוונים אקראי
int order[4] = { 0, 1, 2, 3 };
for (int i = 0; i < 4; i++) {
int j = random(i, 4);
int tmp = order[i];
order[i] = order[j];
order[j] = tmp;
}
for (int k = 0; k < 4; k++) {
int dx = dirs[order[k]][0];
int dy = dirs[order[k]][1];
if (pathClearNoRedNoWall(sx, sy, dx, dy, minSteps)) {
px = sx;
py = sy;
dirX = dx;
dirY = dy;
lastStepMs = millis();
return true;
}
}
}
return false; // לא נמצא – fallback ב-spawnStage
}
/* ====================== כפתור Pause ====================== */
void updatePauseButton() {
int reading = digitalRead(BTN_PIN);
if (reading != btnLastReading) {
btnLastChange = millis();
btnLastReading = reading;
}
if ((millis() - btnLastChange) > DEBOUNCE_MS) {
if (reading != btnStable) {
btnStable = reading;
// לחיצה אמיתית: INPUT_PULLUP -> LOW
if (btnStable == LOW && !gameOver) {
paused = !paused;
// אופציונלי: כבה באזר אם היה פעיל בזמן מעבר לפאוז
if (paused) {
buzzerActive = false;
digitalWrite(BUZZER_PIN, LOW);
}
}
}
}
}
/* ====================== אודיו/אפקטים לא-חוסמים ====================== */
void buzzFor(unsigned long ms) {
digitalWrite(BUZZER_PIN, HIGH);
buzzerActive = true;
buzzerOffAt = millis() + ms;
}
void playGoodChirp() {
buzzFor(120); // “טוב קרה”
}
void startFailJingle() {
failPatternActive = true;
failStage = 0;
failNextAt = 0; // להריץ מיד את השלב הראשון
}
void updateNonBlockingEffects() {
unsigned long now = millis();
// כיבוי באזר כשהגיע הזמן
if (buzzerActive && now >= buzzerOffAt) {
buzzerActive = false;
digitalWrite(BUZZER_PIN, LOW);
}
// דפוס כישלון: קצר-קצר-ארוך עם הבהובי אדום
if (failPatternActive && now >= failNextAt) {
switch (failStage) {
case 0: // אדום + ביפ קצר
fill_solid(leds, NUM_LEDS, CRGB::Red);
FastLED.show();
buzzFor(120);
failNextAt = now + 120;
failStage = 1;
break;
case 1: // שחור (הפסקה קצרה)
showBlack();
failNextAt = now + 120;
failStage = 2;
break;
case 2: // אדום + ביפ קצר שני
fill_solid(leds, NUM_LEDS, CRGB::Red);
FastLED.show();
buzzFor(120);
failNextAt = now + 120;
failStage = 3;
break;
case 3: // שחור (הפסקה קצרה)
showBlack();
failNextAt = now + 120;
failStage = 4;
break;
case 4: // אדום + ביפ ארוך
fill_solid(leds, NUM_LEDS, CRGB::Red);
FastLED.show();
buzzFor(400);
failNextAt = now + 400;
failStage = 5;
break;
case 5: // סיום
showBlack();
failPatternActive = false;
break;
}
}
// מסך ניקוד: נסגר אוטומטית ב-drawScoreOverlay() כשמגיע הזמן
}
/* ====================== ציור ועזרים ====================== */
void showBlack() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();
}