/*Arduino BREAKOUT Game on 8x8 Matrix WS2812b
by mircemk, June 2025
*/
#include <FastLED.h>
#define LED_PIN 6
#define NUM_LEDS 64
#define MATRIX_WIDTH 8
#define MATRIX_HEIGHT 8
#define LEFT_BUTTON_PIN 9
#define RIGHT_BUTTON_PIN 10
#define BUZZER_PIN 2
// Game constants
#define MIN_X_SPEED 0.2
#define MAX_X_SPEED 0.35
#define MIN_Y_SPEED 0.25
#define MAX_Y_SPEED 0.4
#define SPEED_DECAY 0.98
// Sound settings
#define TONE_HIT 1200
#define TONE_PADDLE 800
#define TONE_WALL 1000
#define TONE_GAMEOVER 400
#define TONE_DURATION 15
#define TONE_COOLDOWN 50
CRGB leds[NUM_LEDS];
// Game elements
int paddlePos = 3;
int paddleWidth = 3;
float ballX = 4;
float ballY = 6;
float ballSpeedX = 0.2;
float ballSpeedY = -0.25;
bool bricks[3][8];
CRGB brickColors[3][8];
int score = 0;
int level = 1;
bool gameOver = false;
bool levelCompleted = false;
bool newGame = false;
bool displayScoreDone = false;
// Timing control
unsigned long lastFrameTime = 0;
unsigned long lastSoundTime = 0;
const unsigned long FRAME_TIME = 50;
const byte SMILEY[8] = {
B00111100,
B01000010,
B10100101,
B10000001,
B10100101,
B10011001,
B01000010,
B00111100
};
// Font definition for letters and numbers (5x7 font)
const byte font[37][5] = {
{0x7C, 0x22, 0x22, 0x22, 0x7C}, // A
{0x7E, 0x4A, 0x4A, 0x4A, 0x34}, // B
{0x3C, 0x42, 0x42, 0x42, 0x24}, // C
{0x7E, 0x42, 0x42, 0x42, 0x3C}, // D
{0x7E, 0x4A, 0x4A, 0x4A, 0x42}, // E
{0x7E, 0x0A, 0x0A, 0x0A, 0x02}, // F
{0x3C, 0x42, 0x52, 0x52, 0x34}, // G
{0x7E, 0x08, 0x08, 0x08, 0x7E}, // H
{0x00, 0x42, 0x7E, 0x42, 0x00}, // I
{0x20, 0x40, 0x42, 0x3E, 0x02}, // J
{0x7E, 0x08, 0x14, 0x22, 0x42}, // K
{0x7E, 0x40, 0x40, 0x40, 0x40}, // L
{0x7E, 0x04, 0x18, 0x04, 0x7E}, // M
{0x7E, 0x04, 0x08, 0x10, 0x7E}, // N
{0x3C, 0x42, 0x42, 0x42, 0x3C}, // O
{0x7E, 0x12, 0x12, 0x12, 0x0C}, // P
{0x3C, 0x42, 0x52, 0x22, 0x5C}, // Q
{0x7E, 0x12, 0x32, 0x52, 0x0C}, // R
{0x24, 0x4A, 0x4A, 0x4A, 0x30}, // S
{0x02, 0x02, 0x7E, 0x02, 0x02}, // T
{0x3E, 0x40, 0x40, 0x40, 0x3E}, // U
{0x1E, 0x20, 0x40, 0x20, 0x1E}, // V
{0x3E, 0x40, 0x30, 0x40, 0x3E}, // W
{0x66, 0x18, 0x18, 0x18, 0x66}, // X
{0x06, 0x08, 0x70, 0x08, 0x06}, // Y
{0x62, 0x52, 0x4A, 0x46, 0x42}, // Z
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // 0
{0x00, 0x42, 0x7F, 0x40, 0x00}, // 1
{0x72, 0x49, 0x49, 0x49, 0x46}, // 2
{0x21, 0x41, 0x45, 0x4B, 0x31}, // 3
{0x18, 0x14, 0x12, 0x7F, 0x10}, // 4
{0x27, 0x45, 0x45, 0x45, 0x39}, // 5
{0x3E, 0x49, 0x49, 0x49, 0x32}, // 6
{0x01, 0x71, 0x09, 0x05, 0x03}, // 7
{0x36, 0x49, 0x49, 0x49, 0x36}, // 8
{0x26, 0x49, 0x49, 0x49, 0x3E}, // 9
{0x00, 0x00, 0x00, 0x00, 0x00} // Space
};
// Function to get the index of a character in the font array
int getCharIndex(char c) {
if (c >= 'A' && c <= 'Z') {
return c - 'A';
} else if (c >= '0' && c <= '9') {
return c - '0' + 26;
} else {
return 36; // Space
}
}
// Function to scroll text on the LED matrix
void scrollText(const char* text, int delayTime) {
int length = strlen(text);
for (int offset = 0; offset < length * 6 + MATRIX_WIDTH; offset++) {
fill_solid(leds, NUM_LEDS, CRGB::Black);
for (int i = 0; i < length; i++) {
int charIndex = getCharIndex(text[i]);
for (int col = 0; col < 5; col++) {
if (i * 6 + col - offset >= 0 && i * 6 + col - offset < MATRIX_WIDTH) {
byte column = font[charIndex][col];
for (int row = 0; row < 7; row++) {
if (column & (1 << row)) {
leds[getPixelIndex(i * 6 + col - offset, row)] = CRGB::White;
}
}
}
}
}
FastLED.show();
delay(delayTime);
}
}
void setup() {
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(255);
pinMode(LEFT_BUTTON_PIN, INPUT_PULLUP);
pinMode(RIGHT_BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
delay (1000);
scrollText(" MINI BREAKOUT", 80);
initializeGame();
}
void playSound(int frequency, int duration) {
noTone(BUZZER_PIN); // Ensure the buzzer is off before playing a new sound
tone(BUZZER_PIN, frequency, duration);
delay(duration);
noTone(BUZZER_PIN); // Turn off the buzzer after the duration
}
void initializeGame() {
FastLED.setBrightness(50); // Ensure consistent brightness
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 8; col++) {
bricks[row][col] = true;
brickColors[row][col] = CHSV(random(0, 255), 255, 255); // Random color
}
}
paddlePos = 3;
ballX = 4.0;
ballY = 6.0;
ballSpeedX = 0.2;
ballSpeedY = -0.25;
score = 0;
level = 1;
paddleWidth = 3;
gameOver = false;
levelCompleted = false;
newGame = false;
displayScoreDone = false;
lastFrameTime = millis();
lastSoundTime = 0;
}
void initializeLevel() {
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 8; col++) {
bricks[row][col] = true;
brickColors[row][col] = CHSV(random(0, 255), 255, 255); // Random color
}
}
paddlePos = 3;
ballX = 4.0;
ballY = 6.0;
ballSpeedX = 0.2;
ballSpeedY = -0.25;
levelCompleted = false;
lastFrameTime = millis();
}
void loop() {
unsigned long currentTime = millis();
if (!gameOver && !newGame) {
if (currentTime - lastFrameTime >= FRAME_TIME) {
handleInput();
updateGame();
lastFrameTime = currentTime;
}
drawGame();
FastLED.show();
} else if (gameOver && !displayScoreDone) {
char scoreText[20];
sprintf(scoreText, "SCORE: %d", score);
scrollText(scoreText, 100);
displayScoreDone = true;
displaySmiley();
} else if (gameOver && displayScoreDone) {
if (digitalRead(LEFT_BUTTON_PIN) == LOW || digitalRead(RIGHT_BUTTON_PIN) == LOW) {
delay(300);
initializeGame();
newGame = false; // Reset newGame for the next game cycle
displayScoreDone = false;
}
}
}
void handleInput() {
if (digitalRead(LEFT_BUTTON_PIN) == LOW && paddlePos > 0) {
paddlePos--;
}
if (digitalRead(RIGHT_BUTTON_PIN) == LOW && paddlePos < MATRIX_WIDTH - paddleWidth) {
paddlePos++;
}
}
void updateGame() {
ballX += ballSpeedX;
ballY += ballSpeedY;
// Brick collisions
bool allBricksDestroyed = true;
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 8; col++) {
if (bricks[row][col]) {
allBricksDestroyed = false;
if (ballY >= row - 0.1 && ballY < row + 1.1 &&
ballX >= col - 0.1 && ballX < col + 1.1) {
bricks[row][col] = false;
float dx = ballX - (col + 0.5);
float dy = ballY - (row + 0.5);
if (abs(dx) > abs(dy)) {
ballSpeedX = -ballSpeedX;
} else {
ballSpeedY = -ballSpeedY;
}
ballSpeedX += (random(-15, 16) / 100.0);
ballSpeedX = constrain(ballSpeedX, -MAX_X_SPEED, MAX_X_SPEED);
ballSpeedY = constrain(ballSpeedY, -MAX_Y_SPEED, MAX_Y_SPEED);
score += 1;
playSound(TONE_HIT, TONE_DURATION);
break;
}
}
}
}
// Check for level completion
if (allBricksDestroyed) {
levelCompleted = true;
level++;
if (level > 3) {
gameOver = true;
playSound(TONE_GAMEOVER, TONE_DURATION * 5);
} else {
if (level == 2) {
paddleWidth = 2;
} else if (level == 3) {
paddleWidth = 1;
}
initializeLevel();
}
}
// Wall collisions
if (ballX <= 0 || ballX >= MATRIX_WIDTH - 1) {
ballSpeedX = -ballSpeedX;
ballX = (ballX <= 0) ? 0.1 : (MATRIX_WIDTH - 1.1);
if (abs(ballSpeedY) < MIN_Y_SPEED) {
ballSpeedY += (ballSpeedY > 0 ? MIN_Y_SPEED : -MIN_Y_SPEED) * 0.5;
}
playSound(TONE_WALL, TONE_DURATION);
}
if (ballY <= 0) {
ballSpeedY = abs(ballSpeedY);
ballY = 0.1;
playSound(TONE_WALL, TONE_DURATION);
}
// Paddle collision
if (ballY >= MATRIX_HEIGHT - 2 && ballY < MATRIX_HEIGHT - 1) {
if (ballX >= paddlePos && ballX < paddlePos + paddleWidth) {
ballY = MATRIX_HEIGHT - 2;
float hitPos = (ballX - paddlePos) / paddleWidth;
float angle = (hitPos - 0.5) * PI * 0.7;
float speed = sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
ballSpeedX = sin(angle) * speed;
ballSpeedY = -abs(cos(angle) * speed);
if (abs(ballSpeedY) < MIN_Y_SPEED) {
ballSpeedY = -MIN_Y_SPEED;
}
playSound(TONE_PADDLE, TONE_DURATION);
}
}
// Check if the ball collapses next to paddle on the left or right side
if (ballY >= MATRIX_HEIGHT - 1 && (ballX < paddlePos || ballX >= paddlePos + paddleWidth)) {
gameOver = true;
playSound(TONE_GAMEOVER, TONE_DURATION * 5);
}
ballSpeedX *= SPEED_DECAY;
ballSpeedY *= SPEED_DECAY;
if (abs(ballSpeedX) < MIN_X_SPEED) {
ballSpeedX = (ballSpeedX < 0) ? -MIN_X_SPEED : MIN_X_SPEED;
}
if (abs(ballSpeedY) < MIN_Y_SPEED) {
ballSpeedY = (ballSpeedY < 0) ? -MIN_Y_SPEED : MIN_Y_SPEED;
}
ballSpeedX = constrain(ballSpeedX, -MAX_X_SPEED, MAX_X_SPEED);
ballSpeedY = constrain(ballSpeedY, -MAX_Y_SPEED, MAX_Y_SPEED);
if (ballY >= MATRIX_HEIGHT) {
gameOver = true;
playSound(TONE_GAMEOVER, TONE_DURATION * 5);
}
}
void drawGame() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
// Draw bricks
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 8; col++) {
if (bricks[row][col]) {
leds[getPixelIndex(col, row)] = brickColors[row][col];
}
}
}
// Draw paddle
for (int i = 0; i < paddleWidth; i++) {
leds[getPixelIndex(paddlePos + i, MATRIX_HEIGHT - 1)] = CRGB::Blue;
}
// Draw ball
leds[getPixelIndex(int(ballX), int(ballY))] = CRGB::White;
}
void displaySmiley() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
if (SMILEY[row] & (1 << (7 - col))) {
leds[getPixelIndex(col, row)] = CRGB::Yellow;
}
}
}
FastLED.show();
}
int getPixelIndex(int x, int y) {
return y * MATRIX_WIDTH + x;
}