#include <SPI.h>
// --- Pin Definitions ---
const int BUTTON_A_PIN = 2;
const int BUTTON_B_PIN = 3;
const int JOYSTICK_SW_PIN = 4;
const int BUZZER_PIN = 5;
const int JOYSTICK_X_PIN = A0;
const int JOYSTICK_Y_PIN = A1;
// Nokia Screen Control Pins
const int LCD_RST_PIN = 7;
const int LCD_DC_PIN = 8;
const int LCD_CS_PIN = 9;
// --- System & State Variables ---
enum SystemState {
STATE_MENU,
STATE_SNAKE,
STATE_TETRIS,
STATE_INVADERS
};
SystemState currentState = STATE_MENU;
byte displayBuffer[504];
int menuSelection = 0;
// --- Snake Variables ---
const int MAX_SNAKE_LENGTH = 100;
int8_t snakeX[MAX_SNAKE_LENGTH];
int8_t snakeY[MAX_SNAKE_LENGTH];
int snakeLen;
int dir;
int8_t appleX;
int8_t appleY;
bool snakeGameOver;
int snakeScore;
int snakeSpeed = 200;
unsigned long snakeLastUpdate = 0;
bool snakeInputLocked = false;
// --- Space Invaders Variables ---
const int INV_COLS = 5;
const int INV_ROWS = 3;
bool iAliens[INV_ROWS][INV_COLS];
int iFleetX, iFleetY, iFleetDir;
unsigned long iLastMoveTime = 0;
int iAlienSpeed = 600;
int iPlayerX;
unsigned long pLastMoveTime = 0;
unsigned long pLastFireTime = 0;
int iBulletX, iBulletY;
bool iBulletActive = false;
int aBulletX, aBulletY;
bool aBulletActive = false;
unsigned long aLastFireTime = 0;
unsigned long aBulletMoveTime = 0;
unsigned long iBulletMoveTime = 0;
int bulletDelay = 40;
bool iGameOver = false;
int iScore = 0;
int iAliveCount = 0;
// --- Tetris Variables ---
const int TETRIS_COLS = 10;
const int TETRIS_ROWS = 20;
bool tBoard[TETRIS_ROWS][TETRIS_COLS];
int tPieceX, tPieceY;
int tPieceType, tPieceRot;
int tNextPieceType;
int tHoldPieceType = -1; // -1 = hold slot is currently empty
bool tCanHold = true;
unsigned long tLastFallTime = 0;
int tFallSpeed = 400;
bool tGameOver = false;
int tScore = 0;
unsigned long tLastMoveTime = 0;
// Tetris Shapes (16-bit hex)
const uint16_t tShapes[7][4] = {
{0x0F00, 0x2222, 0x00F0, 0x4444}, // I
{0x04E0, 0x0464, 0x00E4, 0x04C4}, // T
{0x0660, 0x0660, 0x0660, 0x0660}, // O
{0x0C60, 0x0264, 0x0C60, 0x0264}, // S
{0x06C0, 0x0462, 0x06C0, 0x0462}, // Z
{0x02E0, 0x0446, 0x00E8, 0x0C44}, // J
{0x08E0, 0x0644, 0x00E2, 0x044C} // L
};
// Micro Font for the Scoreboard (3x5 pixels)
const uint8_t tFont[10][3] = {
{0x1F, 0x11, 0x1F}, {0x00, 0x1F, 0x00}, {0x1D, 0x15, 0x17},
{0x15, 0x15, 0x1F}, {0x07, 0x04, 0x1F}, {0x17, 0x15, 0x1D},
{0x1F, 0x15, 0x1D}, {0x01, 0x01, 0x1F}, {0x1F, 0x15, 0x1F}, {0x17, 0x15, 0x1F}
};
void setup() {
Serial.begin(9600);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(BUTTON_A_PIN, INPUT_PULLUP);
pinMode(BUTTON_B_PIN, INPUT_PULLUP);
pinMode(JOYSTICK_SW_PIN, INPUT_PULLUP);
pinMode(LCD_CS_PIN, OUTPUT);
pinMode(LCD_DC_PIN, OUTPUT);
pinMode(LCD_RST_PIN, OUTPUT);
// --- Initialize Screen ---
SPI.begin();
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
initLCD();
randomSeed(analogRead(A5));
// Start the console in the Menu
currentState = STATE_MENU;
Serial.println("Console Booted. Entering Main Menu.");
}
void loop() {
if (currentState == STATE_MENU) runMenu();
else if (currentState == STATE_SNAKE) runSnake();
else if (currentState == STATE_TETRIS) runTetris();
else if (currentState == STATE_INVADERS) runInvaders();
}
void runMenu() {
int joyX = analogRead(JOYSTICK_X_PIN);
int joyY = analogRead(JOYSTICK_Y_PIN);
static unsigned long menuJoyTime = 0;
// --- 2D Grid Joystick Navigation ---
if (millis() - menuJoyTime > 200) {
bool moved = false;
if (joyX < 300 && (menuSelection == 1 || menuSelection == 3)) { menuSelection--; moved = true; } // Move Left
else if (joyX > 700 && (menuSelection == 0 || menuSelection == 2)) { menuSelection++; moved = true; } // Move Right
else if (joyY < 300 && (menuSelection == 2 || menuSelection == 3)) { menuSelection -= 2; moved = true; } // Move Up
else if (joyY > 700 && (menuSelection == 0 || menuSelection == 1)) { menuSelection += 2; moved = true; } // Move Down
if (moved) { tone(BUZZER_PIN, 400, 20); menuJoyTime = millis(); }
}
// --- Selection Logic ---
if (digitalRead(BUTTON_A_PIN) == LOW) {
if (menuSelection == 3) {
// Slot 4 is empty! Play an error sound and do nothing.
tone(BUZZER_PIN, 150, 100);
while(digitalRead(BUTTON_A_PIN) == LOW) delay(10);
} else {
tone(BUZZER_PIN, 880, 100); delay(100); tone(BUZZER_PIN, 1760, 200);
if (menuSelection == 0) { initSnake(); currentState = STATE_SNAKE; }
else if (menuSelection == 1) { initTetris(); currentState = STATE_TETRIS; }
else if (menuSelection == 2) { initInvaders(); currentState = STATE_INVADERS; }
while(digitalRead(BUTTON_A_PIN) == LOW) delay(10);
}
return;
}
// --- Draw Menu Graphics ---
for(int i = 0; i < 504; i++) displayBuffer[i] = 0x00;
for (int x = 0; x < 84; x++) { setPixel(x, 0, true); setPixel(x, 47, true); }
for (int y = 0; y < 48; y++) { setPixel(0, y, true); setPixel(83, y, true); }
// Draw Quadrant Icons
drawSnakeIcon(11, 4); // Top Left (Snake)
drawMenuTetris(57, 5); // Top Right (Tetris)
drawMenuInvader(16, 31); // Bottom Left (Space Invaders)
// Draw the 2-Pixel Thick Selection Box dynamically based on position
if (menuSelection == 0) drawSelectionBox(2, 2, 38, 22);
else if (menuSelection == 1) drawSelectionBox(44, 2, 38, 22);
else if (menuSelection == 2) drawSelectionBox(2, 24, 38, 22);
else if (menuSelection == 3) drawSelectionBox(44, 24, 38, 22);
renderScreen();
}
// ==========================================
// --- SNAKE GAME ENGINE ---
// ==========================================
void initSnake() {
snakeLen = 3;
dir = 1;
snakeScore = 0;
snakeSpeed = 200; // Reset speed to 200 when game starts
snakeGameOver = false;
snakeInputLocked = false;
snakeLastUpdate = millis();
for(int i = 0; i < snakeLen; i++) {
snakeX[i] = 10 - i;
snakeY[i] = 5;
}
spawnApple();
}
void runSnake() {
if (snakeGameOver) {
fillScreen(0x55); delay(250);
fillScreen(0xAA); delay(250);
if (digitalRead(BUTTON_A_PIN) == LOW) {
tone(BUZZER_PIN, 440, 100);
Serial.println("Returning to Main Menu.");
currentState = STATE_MENU;
while(digitalRead(BUTTON_A_PIN) == LOW) delay(10);
}
} else {
readSnakeInput();
if (millis() - snakeLastUpdate >= snakeSpeed) {
updateSnake();
drawSnakeGame();
snakeLastUpdate = millis();
}
}
}
void spawnApple() {
bool valid = false;
while (!valid) {
appleX = random(0, 20);
appleY = random(0, 11);
valid = true;
for (int i = 0; i < snakeLen; i++) {
if (appleX == snakeX[i] && appleY == snakeY[i]) { valid = false; break; }
}
}
}
void readSnakeInput() {
if (snakeInputLocked) return;
int joyX = analogRead(JOYSTICK_X_PIN);
int joyY = analogRead(JOYSTICK_Y_PIN);
if (joyY < 300 && dir != 2) { dir = 0; snakeInputLocked = true; }
else if (joyX > 700 && dir != 3) { dir = 1; snakeInputLocked = true; }
else if (joyY > 700 && dir != 0) { dir = 2; snakeInputLocked = true; }
else if (joyX < 300 && dir != 1) { dir = 3; snakeInputLocked = true; }
}
void updateSnake() {
snakeInputLocked = false;
for (int i = snakeLen - 1; i > 0; i--) {
snakeX[i] = snakeX[i-1]; snakeY[i] = snakeY[i-1];
}
if (dir == 0) snakeY[0]--;
else if (dir == 1) snakeX[0]++;
else if (dir == 2) snakeY[0]++;
else if (dir == 3) snakeX[0]--;
if (snakeX[0] < 0 || snakeX[0] >= 20 || snakeY[0] < 0 || snakeY[0] >= 11) { dieSnake(); return; }
for (int i = 1; i < snakeLen; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) { dieSnake(); return; }
}
if (snakeX[0] == appleX && snakeY[0] == appleY) {
tone(BUZZER_PIN, 1200, 50);
snakeScore++;
if (snakeLen < MAX_SNAKE_LENGTH) snakeLen++;
// The game speeds up a tiny bit when you eat, but won't go faster than 60ms
if (snakeSpeed > 60) snakeSpeed -= 5;
spawnApple();
}
}
void dieSnake() {
tone(BUZZER_PIN, 150, 500);
snakeGameOver = true;
Serial.print("Snake Died. Final Score: ");
Serial.println(snakeScore);
}
void drawSnakeGame() {
for(int i = 0; i < 504; i++) displayBuffer[i] = 0x00;
for (int x = 0; x < 84; x++) { setPixel(x, 0, true); setPixel(x, 47, true); }
for (int y = 0; y < 48; y++) { setPixel(0, y, true); setPixel(83, y, true); }
drawBlock(appleX, appleY, true);
for (int i = 0; i < snakeLen; i++) drawBlock(snakeX[i], snakeY[i], true);
renderScreen();
}
// ==========================================
// --- SPACE INVADERS ENGINE ---
// ==========================================
void initInvaders() {
for (int r = 0; r < INV_ROWS; r++) {
for (int c = 0; c < INV_COLS; c++) iAliens[r][c] = true;
}
iFleetX = 10; iFleetY = 5; iFleetDir = 1;
iAlienSpeed = 600;
iPlayerX = 40;
iBulletActive = false;
aBulletActive = false; // Reset alien bullet
iGameOver = false;
iScore = 0;
iAliveCount = INV_ROWS * INV_COLS;
// Reset all timers
pLastMoveTime = millis();
pLastFireTime = millis();
aLastFireTime = millis();
aBulletMoveTime = millis();
iBulletMoveTime = millis();
}
void runInvaders() {
if (iGameOver) {
fillScreen(0x55); delay(250); fillScreen(0xAA); delay(250);
if (digitalRead(BUTTON_A_PIN) == LOW) {
tone(BUZZER_PIN, 440, 100);
currentState = STATE_MENU;
while(digitalRead(BUTTON_A_PIN) == LOW) delay(10);
}
return;
}
// --- Input Handling ---
if (millis() - pLastMoveTime > 40) { // 40ms delay = smooth, controllable speed
int joyX = analogRead(JOYSTICK_X_PIN);
if (joyX < 300 && iPlayerX > 2) { iPlayerX -= 2; pLastMoveTime = millis(); }
if (joyX > 700 && iPlayerX < 78) { iPlayerX += 2; pLastMoveTime = millis(); }
}
// Player Shooting (600ms delay stops button spamming)
if (digitalRead(BUTTON_A_PIN) == LOW && !iBulletActive && (millis() - pLastFireTime > 600)) {
iBulletX = iPlayerX + 2;
iBulletY = 40;
iBulletActive = true;
pLastFireTime = millis();
tone(BUZZER_PIN, 1500, 30);
}
// --- Alien Firing AI ---
// Aliens try to shoot every 1.2 seconds, getting faster as they die
int currentAlienFireRate = 1200 - ((15 - iAliveCount) * 50);
if (!aBulletActive && millis() - aLastFireTime > currentAlienFireRate) {
int randomCol = random(0, INV_COLS); // Pick a random column
// Find the bottom-most alien in this column to shoot from
int bottomRow = -1;
for (int r = INV_ROWS - 1; r >= 0; r--) {
if (iAliens[r][randomCol]) { bottomRow = r; break; }
}
if (bottomRow != -1) { // Only shoot if an alien actually exists here!
aBulletX = iFleetX + (randomCol * 8) + 2;
aBulletY = iFleetY + (bottomRow * 6) + 4;
aBulletActive = true;
aLastFireTime = millis();
tone(BUZZER_PIN, 400, 40); // Deep alien laser sound
} else {
aLastFireTime = millis(); // Missed opportunity, reset timer
}
}
// --- Alien Bullet Physics ---
if (aBulletActive) {
// Only move the bullet every 40 milliseconds
if (millis() - aBulletMoveTime > bulletDelay) {
aBulletY += 1; // Flies DOWN towards player
aBulletMoveTime = millis();
}
if (aBulletY >= 48) aBulletActive = false; // Missed off screen
// Player Hitbox Collision Check (X: Ship width, Y: Ship height)
if (aBulletX >= iPlayerX && aBulletX <= iPlayerX + 4 && aBulletY >= 43 && aBulletY <= 46) {
tone(BUZZER_PIN, 100, 1000); // Death explosion
iGameOver = true;
}
}
// --- Player Bullet Physics ---
if (iBulletActive) {
// Only move the bullet every 40 milliseconds
if (millis() - iBulletMoveTime > bulletDelay) {
iBulletY -= 1; // Flies UP
iBulletMoveTime = millis();
}
if (iBulletY <= 0) iBulletActive = false;
for (int r = 0; r < INV_ROWS; r++) {
for (int c = 0; c < INV_COLS; c++) {
if (iAliens[r][c]) {
int ax = iFleetX + (c * 8); int ay = iFleetY + (r * 6);
if (iBulletX >= ax && iBulletX <= ax + 4 && iBulletY >= ay && iBulletY <= ay + 4) {
iAliens[r][c] = false; iBulletActive = false;
iScore += 10; iAliveCount--; tone(BUZZER_PIN, 300, 50);
if (iAlienSpeed > 50) iAlienSpeed -= 30;
if (iAliveCount == 0) initInvaders();
}
}
}
}
}
// --- Fleet Physics ---
if (millis() - iLastMoveTime > iAlienSpeed) {
int leftCol = INV_COLS, rightCol = -1;
int bottomRow = -1; // Track the lowest surviving row
for (int r = 0; r < INV_ROWS; r++) {
for (int c = 0; c < INV_COLS; c++) {
if (iAliens[r][c]) {
if (c < leftCol) leftCol = c;
if (c > rightCol) rightCol = c;
if (r > bottomRow) bottomRow = r; // Find lowest row
}
}
}
int fleetTrueX_Left = iFleetX + (leftCol * 8);
int fleetTrueX_Right = iFleetX + (rightCol * 8) + 4;
// Calculate the exact pixel Y-coordinate of the lowest surviving alien
// (r * 6 is the row spacing, + 4 is the height of the alien sprite itself)
int fleetTrueY_Bottom = iFleetY + (bottomRow * 6) + 4;
// Wall Bouncing Logic
if (iFleetDir == 1 && fleetTrueX_Right >= 82) { iFleetDir = -1; iFleetY += 4; }
else if (iFleetDir == -1 && fleetTrueX_Left <= 2) { iFleetDir = 1; iFleetY += 4; }
else { iFleetX += (iFleetDir * 3); }
// Use the true bottom boundary for the Game Over check!
// (We also check bottomRow != -1 just to be safe in case you killed them all at the exact same millisecond)
if (bottomRow != -1 && fleetTrueY_Bottom >= 42) {
tone(BUZZER_PIN, 100, 1000);
iGameOver = true;
}
iLastMoveTime = millis();
tone(BUZZER_PIN, 70, 20);
}
// --- Graphics ---
for(int i = 0; i < 504; i++) displayBuffer[i] = 0x00;
for (int x = 0; x < 84; x++) { setPixel(x, 0, true); setPixel(x, 47, true); }
for (int y = 0; y < 48; y++) { setPixel(0, y, true); setPixel(83, y, true); }
// Draw Player
setPixel(iPlayerX + 2, 43, true);
setPixel(iPlayerX + 2, 44, true);
setPixel(iPlayerX + 1, 45, true); setPixel(iPlayerX + 2, 45, true); setPixel(iPlayerX + 3, 45, true);
setPixel(iPlayerX, 46, true); setPixel(iPlayerX + 1, 46, true); setPixel(iPlayerX + 2, 46, true); setPixel(iPlayerX + 3, 46, true); setPixel(iPlayerX + 4, 46, true);
// Draw Bullets
if (iBulletActive) { setPixel(iBulletX, iBulletY, true); setPixel(iBulletX, iBulletY + 1, true); }
if (aBulletActive) { setPixel(aBulletX, aBulletY, true); setPixel(aBulletX, aBulletY + 1, true); } // NEW: Alien Bullet
// Draw Aliens
for (int r = 0; r < INV_ROWS; r++) {
for (int c = 0; c < INV_COLS; c++) {
if (iAliens[r][c]) {
int ax = iFleetX + (c * 8); int ay = iFleetY + (r * 6);
setPixel(ax+1, ay, true); setPixel(ax+3, ay, true);
setPixel(ax, ay+1, true); setPixel(ax+1, ay+1, true); setPixel(ax+2, ay+1, true); setPixel(ax+3, ay+1, true); setPixel(ax+4, ay+1, true);
setPixel(ax, ay+2, true); setPixel(ax+2, ay+2, true); setPixel(ax+4, ay+2, true);
setPixel(ax+1, ay+3, true); setPixel(ax+3, ay+3, true);
}
}
}
drawTinyNumber(iScore, 2, 2);
renderScreen();
}
// ==========================================
// --- TETRIS GAME ENGINE ---
// ==========================================
void initTetris() {
for (int r = 0; r < TETRIS_ROWS; r++) {
for (int c = 0; c < TETRIS_COLS; c++) tBoard[r][c] = false;
}
tScore = 0;
tGameOver = false;
tFallSpeed = 400;
// Prime the pump with the very first next piece
tNextPieceType = random(0, 7);
tHoldPieceType = -1;
spawnTetrisPiece();
}
void spawnTetrisPiece() {
tPieceType = tNextPieceType; // Take the piece from the predictor
tNextPieceType = random(0, 7); // Roll a new piece for the predictor
tPieceRot = 0;
tPieceX = 3;
tPieceY = 0;
tCanHold = true; // You are allowed to use hold again on a new piece
if (checkTetrisCollision(tPieceX, tPieceY, tPieceRot)) {
tone(BUZZER_PIN, 150, 500);
tGameOver = true;
}
}
bool checkTetrisCollision(int testX, int testY, int testRot) {
uint16_t shape = tShapes[tPieceType][testRot];
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
if (shape & (1 << (15 - (r * 4 + c)))) {
int boardX = testX + c;
int boardY = testY + r;
if (boardX < 0 || boardX >= TETRIS_COLS || boardY >= TETRIS_ROWS) return true;
if (boardY >= 0 && tBoard[boardY][boardX]) return true;
}
}
}
return false;
}
void lockTetrisPiece() {
uint16_t shape = tShapes[tPieceType][tPieceRot];
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
if (shape & (1 << (15 - (r * 4 + c)))) {
int boardY = tPieceY + r;
if (boardY >= 0) tBoard[boardY][tPieceX + c] = true;
}
}
}
tone(BUZZER_PIN, 200, 30);
checkTetrisLines();
spawnTetrisPiece();
}
void checkTetrisLines() {
int linesCleared = 0;
for (int r = TETRIS_ROWS - 1; r >= 0; r--) {
bool full = true;
for (int c = 0; c < TETRIS_COLS; c++) {
if (!tBoard[r][c]) { full = false; break; }
}
if (full) {
linesCleared++;
for (int moveR = r; moveR > 0; moveR--) {
for (int c = 0; c < TETRIS_COLS; c++) tBoard[moveR][c] = tBoard[moveR - 1][c];
}
for (int c = 0; c < TETRIS_COLS; c++) tBoard[0][c] = false;
r++;
}
}
if (linesCleared > 0) {
tone(BUZZER_PIN, 1200, 100);
// Standard Tetris scoring multiplier
if (linesCleared == 1) tScore += 100;
else if (linesCleared == 2) tScore += 300;
else if (linesCleared == 3) tScore += 500;
else if (linesCleared == 4) tScore += 800; // TETRIS
if (tFallSpeed > 100) tFallSpeed -= (linesCleared * 15);
}
}
void runTetris() {
if (tGameOver) {
fillScreen(0x55); delay(250); fillScreen(0xAA); delay(250);
if (digitalRead(BUTTON_A_PIN) == LOW) {
tone(BUZZER_PIN, 440, 100);
currentState = STATE_MENU;
while(digitalRead(BUTTON_A_PIN) == LOW) delay(10);
}
return;
}
// --- Input Handling ---
if (millis() - tLastMoveTime > 120) {
int joyX = analogRead(JOYSTICK_X_PIN);
int joyY = analogRead(JOYSTICK_Y_PIN);
if (joyX < 300) { if (!checkTetrisCollision(tPieceX - 1, tPieceY, tPieceRot)) tPieceX--; tLastMoveTime = millis(); }
else if (joyX > 700) { if (!checkTetrisCollision(tPieceX + 1, tPieceY, tPieceRot)) tPieceX++; tLastMoveTime = millis(); }
else if (joyY > 700) { if (!checkTetrisCollision(tPieceX, tPieceY + 1, tPieceRot)) tPieceY++; tLastMoveTime = millis(); }
}
// Hard Drop (BUTTON_A_PIN)
static bool swLast = HIGH;
bool swNow = digitalRead(BUTTON_A_PIN);
if (swNow == LOW && swLast == HIGH) {
while (!checkTetrisCollision(tPieceX, tPieceY + 1, tPieceRot)) {
tPieceY++;
}
lockTetrisPiece();
tLastFallTime = millis();
tone(BUZZER_PIN, 800, 50); // Slam sound
}
swLast = swNow;
// Rotate Piece (Button B)
static bool btnALast = HIGH;
bool btnANow = digitalRead(BUTTON_B_PIN);
if (btnANow == LOW && btnALast == HIGH) {
int nextRot = (tPieceRot + 1) % 4;
if (!checkTetrisCollision(tPieceX, tPieceY, nextRot)) { tPieceRot = nextRot; tone(BUZZER_PIN, 600, 20); }
}
btnALast = btnANow;
// Hold Piece (JOYSTICK_SW_PIN)
static bool btnBLast = HIGH;
bool btnBNow = digitalRead(JOYSTICK_SW_PIN);
if (btnBNow == LOW && btnBLast == HIGH && tCanHold) {
if (tHoldPieceType == -1) {
tHoldPieceType = tPieceType;
spawnTetrisPiece(); // Grab the next one immediately
} else {
int temp = tPieceType;
tPieceType = tHoldPieceType;
tHoldPieceType = temp;
tPieceX = 3; tPieceY = 0; tPieceRot = 0;
}
tCanHold = false; // Lock holding until the piece lands
tone(BUZZER_PIN, 1000, 30);
}
btnBLast = btnBNow;
// --- Gravity ---
if (millis() - tLastFallTime > tFallSpeed) {
if (!checkTetrisCollision(tPieceX, tPieceY + 1, tPieceRot)) tPieceY++;
else lockTetrisPiece();
tLastFallTime = millis();
}
// --- Draw Graphics ---
for(int i = 0; i < 504; i++) displayBuffer[i] = 0x00;
int offsetX = 32; // Center the board
int offsetY = 4;
// Draw U-shaped border around the pit
for(int y=0; y<=40; y++) { setPixel(offsetX - 2, offsetY + y, true); setPixel(offsetX + 21, offsetY + y, true); }
for(int x=-2; x<=21; x++) setPixel(offsetX + x, offsetY + 41, true);
// Draw settled blocks
for (int r = 0; r < TETRIS_ROWS; r++) {
for (int c = 0; c < TETRIS_COLS; c++) {
if (tBoard[r][c]) drawMiniBlock(offsetX + (c*2), offsetY + (r*2));
}
}
// Draw current falling piece
drawMiniShape(tPieceType, tPieceRot, offsetX + (tPieceX*2), offsetY + (tPieceY*2));
// Draw Next Piece Predictor (Right side)
drawMiniShape(tNextPieceType, 0, 60, 6);
// Draw Hold Piece (Left side)
if (tHoldPieceType != -1) drawMiniShape(tHoldPieceType, 0, 6, 20);
// Draw the Live Score (Top Left)
drawTinyNumber(tScore, 2, 4);
renderScreen();
}
// --- Micro UI Helpers for Tetris ---
void drawMiniShape(int type, int rot, int px, int py) {
uint16_t shape = tShapes[type][rot];
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
if (shape & (1 << (15 - (r * 4 + c)))) drawMiniBlock(px + (c*2), py + (r*2));
}
}
}
void drawTinyNumber(int num, int x, int y) {
if (num == 0) { drawTinyDigit(0, x, y); return; }
int temp = num; int digits = 0;
while (temp > 0) { digits++; temp /= 10; }
int currX = x + (digits - 1) * 4; // 3 pixels wide + 1 pixel gap
temp = num;
while (temp > 0) {
drawTinyDigit(temp % 10, currX, y);
currX -= 4;
temp /= 10;
}
}
void drawTinyDigit(int digit, int x, int y) {
for(int c=0; c<3; c++) {
for(int r=0; r<5; r++) {
if (tFont[digit][c] & (1 << r)) setPixel(x+c, y+r, true);
}
}
}
// ==========================================
// --- LOW LEVEL GRAPHICS & HARDWARE ---
// ==========================================
void drawHollowBox(int startGridX, int startGridY, int endGridX, int endGridY) {
for (int x = startGridX; x <= endGridX; x++) {
drawBlock(x, startGridY, true);
drawBlock(x, endGridY, true);
}
for (int y = startGridY; y <= endGridY; y++) {
drawBlock(startGridX, y, true);
drawBlock(endGridX, y, true);
}
}
void drawMiniBlock(int px, int py) {
// Tetris requires 2x2 pixel blocks to fit on the screen!
setPixel(px, py, true);
setPixel(px + 1, py, true);
setPixel(px, py + 1, true);
setPixel(px + 1, py + 1, true);
}
void drawBlock(int gridX, int gridY, bool color) {
int startX = gridX * 4 + 2;
int startY = gridY * 4 + 2;
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
setPixel(startX + i, startY + j, color);
}
}
}
void setPixel(int x, int y, bool color) {
if (x < 0 || x >= 84 || y < 0 || y >= 48) return;
int byteIndex = (y / 8) * 84 + x;
int bitMask = 1 << (y % 8);
if (color) displayBuffer[byteIndex] |= bitMask;
else displayBuffer[byteIndex] &= ~bitMask;
}
void lcdWrite(byte data, bool isCommand) {
digitalWrite(LCD_DC_PIN, isCommand ? LOW : HIGH);
digitalWrite(LCD_CS_PIN, LOW);
SPI.transfer(data);
digitalWrite(LCD_CS_PIN, HIGH);
}
void initLCD() {
digitalWrite(LCD_RST_PIN, LOW); delay(10); digitalWrite(LCD_RST_PIN, HIGH);
lcdWrite(0x21, true); lcdWrite(0xC5, true); lcdWrite(0x04, true);
lcdWrite(0x14, true); lcdWrite(0x20, true); lcdWrite(0x0C, true);
}
void renderScreen() {
lcdWrite(0x80, true); lcdWrite(0x40, true);
for (int i = 0; i < 504; i++) lcdWrite(displayBuffer[i], false);
}
void fillScreen(byte pattern) {
lcdWrite(0x80, true); lcdWrite(0x40, true);
for (int i = 0; i < 504; i++) lcdWrite(pattern, false);
}
// --- Menu UI Helpers ---
void drawSelectionBox(int x, int y, int w, int h) {
// Draws a 2-pixel thick hollow box using direct pixel manipulation
for(int i = 0; i < w; i++) {
setPixel(x + i, y, true);
setPixel(x + i, y + 1, true);
setPixel(x + i, y + h - 1, true);
setPixel(x + i, y + h - 2, true);
}
for(int i = 2; i < h - 2; i++) {
setPixel(x, y + i, true);
setPixel(x + 1, y + i, true);
setPixel(x + w - 1, y + i, true);
setPixel(x + w - 2, y + i, true);
}
}
void drawMenuTetris(int ox, int oy) {
// Draws a clean 4x4 scale Tetris T-Block
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+i, oy+j, true);
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+4+i, oy+j, true);
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+8+i, oy+j, true);
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+4+i, oy+4+j, true);
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+i, oy+12+j, true);
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+4+i, oy+12+j, true);
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+8+i, oy+12+j, true);
for(int i=0; i<4; i++) for(int j=0; j<4; j++) setPixel(ox+8+i, oy+8+j, true);
}
void drawMenuInvader(int ox, int oy) {
// Draws a classic space invader using our 2x2 mini-blocks!
drawMiniBlock(ox+2, oy); drawMiniBlock(ox+6, oy);
drawMiniBlock(ox, oy+2); drawMiniBlock(ox+2, oy+2); drawMiniBlock(ox+4, oy+2); drawMiniBlock(ox+6, oy+2); drawMiniBlock(ox+8, oy+2);
drawMiniBlock(ox, oy+4); drawMiniBlock(ox+4, oy+4); drawMiniBlock(ox+8, oy+4);
drawMiniBlock(ox+2, oy+6); drawMiniBlock(ox+6, oy+6);
}
void drawSnakeIcon(int offsetX, int offsetY) {
const uint8_t snakePixels[] = {
3,0, 4,0, 5,0, 6,0, 7,0, 8,0, 9,0, 10,0,
2,1, 11,1,
1,2, 12,2,
1,3, 4,3, 8,3, 12,3,
1,4, 4,4, 8,4, 12,4,
1,5, 12,5,
2,6, 10,6, 12,6,
3,7, 4,7, 5,7, 6,7, 7,7, 8,7, 9,7, 12,7,
7,8, 12,8, 16,8, 17,8,
7,9, 11,9, 14,9, 15,9, 18,9,
6,10, 11,10, 13,10, 19,10,
6,11, 11,11, 12,11, 17,11, 18,11,
5,12, 11,12, 16,12,
4,13, 11,13, 15,13,
4,14, 15,14,
4,15, 15,15,
5,16, 14,16,
6,17, 7,17, 8,17, 9,17, 10,17, 11,17, 12,17, 13,17,
};
// Calculate how many coordinate pairs are in the array
int numPixels = sizeof(snakePixels) / 2;
// Loop through the array and draw the icon!
for (int i = 0; i < numPixels; i++) {
int px = snakePixels[i * 2]; // Get X coordinate
int py = snakePixels[i * 2 + 1]; // Get Y coordinate
setPixel(offsetX + px, offsetY + py, true);
}
}Loading
nokia-5110
nokia-5110