#include <LedControl.h>
#include <avr/pgmspace.h>
// ---------------- SETTINGS ----------------
const int SCREEN_WIDTH = 16;
const int SCREEN_HEIGHT = 8;
const int PADDLE_SIZE = 3;
const int MAX_SCORE = 5;
const int PIN_P1 = A0;
const int PIN_P2 = A1;
LedControl lc = LedControl(12, 11, 10, 2);
// ---------------- STATE MANAGEMENT ----------------
enum State { MENU, STARTING, PLAYING, GAMEOVER };
State currentState = MENU;
int gameMode = 1;
int lastDisplayedMode = -1; // To prevent constant menu refreshing
int winner = 0;
unsigned long stateTimer = 0;
unsigned long lastFrame = 0;
// ---------------- GAME VARIABLES ----------------
int ballX, ballY, dirX, dirY;
int p1PaddleY, p2PaddleY;
int p1Score = 0, p2Score = 0;
unsigned long lastBallMove = 0;
int ballInterval = 180;
const int ORIGINAL_BALL_INTERVAL = 180;
// ---------------- GRAPHICS ----------------
const byte icon1P[8] PROGMEM = {B00010000,B00110000,B00010000,B00010000,B00010000,B00010000,B00111000,B00000000};
const byte icon2P[8] PROGMEM = {B00111000,B01000100,B00000100,B00111000,B01000000,B01000000,B01111100,B00000000};
void setup() {
for (int i = 0; i < 2; i++) {
lc.shutdown(i, false);
lc.setIntensity(i, 8);
lc.clearDisplay(i);
}
randomSeed(analogRead(A5));
}
void loop() {
unsigned long now = millis();
int p1Raw = analogRead(PIN_P1);
p1PaddleY = map(p1Raw, 0, 1023, 0, SCREEN_HEIGHT - PADDLE_SIZE);
switch (currentState) {
case MENU:
handleMenu(p1Raw);
break;
case STARTING:
handleStarting(now);
break;
case PLAYING:
handleGame(now);
break;
case GAMEOVER:
handleGameOver(p1Raw, now);
break;
}
}
// ---------------- FIXED STATE HANDLERS ----------------
void handleMenu(int p1Raw) {
gameMode = (p1Raw < 512) ? 1 : 2;
// ONLY clear and redraw if the mode changed (Stops the blinking!)
if (gameMode != lastDisplayedMode) {
lc.clearDisplay(0);
lc.clearDisplay(1);
for (int i = 0; i < 8; i++) {
lc.setRow(0, i, pgm_read_byte(&(gameMode == 1 ? icon1P[i] : icon2P[i])));
}
lastDisplayedMode = gameMode;
}
// Start logic
if (p1Raw > 460 && p1Raw < 560) {
if (stateTimer == 0) stateTimer = millis();
lc.setLed(1, 4, 4, (millis() % 500 < 250)); // Blinking dot shows it's "loading"
if (millis() - stateTimer > 1500) {
resetGame();
currentState = STARTING;
stateTimer = millis();
lastDisplayedMode = -1;
}
} else {
stateTimer = 0;
lc.setLed(1, 4, 4, false);
}
}
void handleStarting(unsigned long now) {
// Clear once and wait
if (now - stateTimer < 1500) {
if ((now / 300) % 2 == 0) {
setPixel(7, 3, true); setPixel(8, 3, true);
} else {
lc.clearDisplay(0); lc.clearDisplay(1);
}
} else {
currentState = PLAYING;
}
}
void handleGame(unsigned long now) {
if (gameMode == 2) {
p2PaddleY = map(analogRead(PIN_P2), 0, 1023, 0, SCREEN_HEIGHT - PADDLE_SIZE);
} else {
updateAI(now);
}
if (now - lastBallMove > (unsigned long)ballInterval) {
ballX += dirX;
ballY += dirY;
checkCollisions();
lastBallMove = now;
}
// Draw at a fixed 30 FPS to ensure smooth movement without flicker
if (now - lastFrame > 33) {
drawGame();
lastFrame = now;
}
}
void handleGameOver(int p1Raw, unsigned long now) {
int side = (winner == 1) ? 0 : 1;
// Blink the winner's side without using delay()
if ((now / 400) % 2 == 0) {
for(int r=0; r<8; r++) lc.setRow(side, r, B11111111);
} else {
lc.clearDisplay(side);
}
if (p1Raw > 460 && p1Raw < 560) {
if (stateTimer == 0) stateTimer = millis();
if (millis() - stateTimer > 2000) {
currentState = MENU;
stateTimer = 0;
lc.clearDisplay(0); lc.clearDisplay(1);
}
} else {
stateTimer = 0;
}
}
// ---------------- HELPERS ----------------
void updateAI(unsigned long now) {
static unsigned long lastAI = 0;
if (now - lastAI > 80) {
int center = p2PaddleY + 1;
if (center < ballY) p2PaddleY++;
else if (center > ballY) p2PaddleY--;
p2PaddleY = constrain(p2PaddleY, 0, SCREEN_HEIGHT - PADDLE_SIZE);
lastAI = now;
}
}
void checkCollisions() {
if (ballY <= 0 || ballY >= SCREEN_HEIGHT - 1) dirY = -dirY;
if (ballX == 1 && ballY >= p1PaddleY && ballY < p1PaddleY + PADDLE_SIZE) {
dirX = 1; ballInterval = max(60, ballInterval - 10);
}
if (ballX == 14 && ballY >= p2PaddleY && ballY < p2PaddleY + PADDLE_SIZE) {
dirX = -1; ballInterval = max(60, ballInterval - 10);
}
if (ballX < 0) { p2Score++; startServe(); }
if (ballX > 15) { p1Score++; startServe(); }
if (p1Score >= MAX_SCORE) { winner = 1; currentState = GAMEOVER; }
if (p2Score >= MAX_SCORE) { winner = 2; currentState = GAMEOVER; }
}
void startServe() {
ballX = 7; ballY = random(2, 6);
dirX = random(0, 2) ? 1 : -1;
dirY = random(0, 2) ? 1 : -1;
ballInterval = ORIGINAL_BALL_INTERVAL;
lc.clearDisplay(0); lc.clearDisplay(1);
}
void resetGame() {
p1Score = 0; p2Score = 0;
startServe();
}
void setPixel(int x, int y, bool state) {
if (x < 0 || x > 15 || y < 0 || y > 7) return;
int dev = (x > 7) ? 1 : 0;
int col = x % 8;
lc.setLed(dev, y, col, state);
}
void drawGame() {
lc.clearDisplay(0);
lc.clearDisplay(1);
for (int i = 0; i < PADDLE_SIZE; i++) {
setPixel(0, p1PaddleY + i, true);
setPixel(15, p2PaddleY + i, true);
}
setPixel(ballX, ballY, true);
}
/*How to start game
Select Mode: Turn your potentiometer (P1).
Turn it Left/Up to see the "1" icon.
Turn it Right/Down to see the "2" icon.
Confirm/Start: Move the paddle to the center. You will see a single pixel light up
on the rightmatrix. Keep it there for 1.5 seconds, and the game will start.
Game Over: The screen of the winner will flash. To go back to the menu and
play again, hold the P1 paddle in the center for 2 seconds.*/