//Copyright (C) 18.06.2025, Kirill ZHivotkov
/*
Игра "Color Lines".
*/
#include <Adafruit_NeoPixel.h>
#include <LiquidCrystal_I2C.h>
#include <Vector.h>
#include <limits.h>
#define R1 13 //PIN
#define G1 12 //PIN
#define B1 11 //PIN
#define R2 10 //PIN
#define G2 9 //PIN
#define B2 8 //PIN
#define R3 7 //PIN
#define G3 6 //PIN
#define B3 5 //PIN
#define PIN 4 //PIN
#define BUZZER 3 //PIN
#define RGB 7 //VALUE
#define SIZE 10 //VALUE
#define START 10 //VALUE
#define NEW 3 //VALUE
#define MIN 5 //VALUE
#define MOVE 512 //VALUE
#define SLEEP 0 //VALUE
#define SPEED 200 //VALUE
#define BLACK 16777215
#define CYAN 65535
#define RED 65280
#define GREEN 16711680
#define MAGENTA 16711935
#define BLUE 255
#define YELLOW 16776960
#define ORANGE 16744192
#define XJ1 A0 //PIN
#define YJ1 A1 //PIN
#define SW1 A2 //PIN
//Declare matrix class variable
Adafruit_NeoPixel pixels(SIZE * SIZE, PIN);
//Declare matrix class variable
//Declare display class variable
LiquidCrystal_I2C lcd(39, 16, 2); //Address = 39
//Declare display class variable
struct Color {
int r, g, b;
};
struct Coord {
int x, y;
Color c;
};
int p_x = 0;
int p_y = 0;
static int gameScorePoints = 0;
bool isError = false;
bool isSelected = false;
bool isBlinking = false;
bool isOverTheBall = false;
bool isRemoved = false;
char maze[SIZE][SIZE] = { //Empty field
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.'}
};
Coord ball;
Coord start = {-1, -1, {0, 0, 0}};
Coord prev = {-1, -1, {0, 0, 0}};
Coord storage[SIZE * SIZE] = {};
Vector<Coord> v(storage, SIZE * SIZE);
Color colors[RGB] = {{0, UCHAR_MAX, UCHAR_MAX /*Cyan*/},
{0, UCHAR_MAX, 0 /*Green*/},
{UCHAR_MAX, 0, 0 /*Red*/},
{UCHAR_MAX, 0, UCHAR_MAX /*Magenta*/},
{0, 0, UCHAR_MAX /*Blue*/},
{UCHAR_MAX, UCHAR_MAX, 0 /*Yellow*/},
{UCHAR_MAX, UCHAR_MAX / 2, 0 /*Orange*/}};
Color c[NEW];
void setup() {
Serial.println("\nSetup call (possible crash)");
Serial.begin(9600);
Serial1.begin(9600);
//Link RGB LEDS
pinMode(R1, OUTPUT);
pinMode(G1, OUTPUT);
pinMode(B1, OUTPUT);
pinMode(R2, OUTPUT);
pinMode(G2, OUTPUT);
pinMode(B2, OUTPUT);
pinMode(R3, OUTPUT);
pinMode(G3, OUTPUT);
pinMode(B3, OUTPUT);
//Link RGB LEDS
pixels.begin();
pixels.setBrightness(UCHAR_MAX);
//Clear vectors initialized with zeroes
v.clear();
//Clear vectors initialized with zeroes
//Shuffle decks and colors (primarily for the random set)
randomSeed(micros());
ShuffleColors(colors, RGB);
//Shuffle decks and colors (primarily for the random set)
//Set the initial led position (black one in the left top corner)
pixels.setPixelColor(0, pixels.Color(UCHAR_MAX, UCHAR_MAX, UCHAR_MAX));
pixels.show();
//Set the initial led position (black one in the left top corner)
//Clear vectors initialized with zeroes
v.clear();
//Clear vectors initialized with zeroes
//Generate random ball set with the number of balls specified
GenerateRandomBallSet(START);
ShowBallsOnBFSGrid();
//Generate random ball set with the number of balls specified
//Switch on next balls color hints
SwitchOnNextBallsColorHints();
//Switch on next balls color hints
//Link display
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("SCORE: ");
lcd.setCursor(0, 1);
lcd.print("BALLS: ");
//Link display
DisplayGameScore(0);
DisplayNumberOfBalls();
}
//BFS
int dRow[] = {-1, 1, 0, 0};
int dCol[] = {0, 0, -1, 1};
typedef struct {
int x, y;
int dist;
} QueueNode;
bool isValid(int row, int col, bool visited[SIZE][SIZE], char grid[SIZE][SIZE]) {
return (row >= 0 && row < SIZE && col >= 0 && col < SIZE && grid[row][col] == '.' && !visited[row][col]);
}
int bfs(char grid[SIZE][SIZE], int xStart, int yStart, int xEnd, int yEnd) {
bool visited[SIZE][SIZE] = {false};
QueueNode queue[SIZE * SIZE];
int front = 0, rear = 0;
queue[rear++] = (QueueNode){xStart, yStart, 0};
visited[xStart][yStart] = true;
while (front < rear) {
QueueNode current = queue[front++];
if (current.x == xEnd && current.y == yEnd) {
return current.dist;
}
for (int i = 0; i < 4; i++) {
int newRow = current.x + dRow[i];
int newCol = current.y + dCol[i];
if (isValid(newRow, newCol, visited, grid)) {
visited[newRow][newCol] = true;
queue[rear++] = (QueueNode){newRow, newCol, current.dist + 1};
}
}
}
return -1;
}
//BFS
//Set RGB LED color (next 3 random balls distribution)
void RGBLedColor(int r, int g, int b, int r_value, int g_value, int b_value) {
analogWrite(r, r_value);
analogWrite(g, g_value);
analogWrite(b, b_value);
}
//Set RGB LED color (next 3 random balls distribution)
//Move ball to another position
void BallMoveSuccess() {
maze[start.x][start.y] = '.';
maze[p_x][p_y] = '#';
isBlinking = false;
v.remove(GetBallIndex(start.x, start.y));
pixels.setPixelColor(XY(start.x, start.y), pixels.Color(0, 0, 0));
pixels.show();
v.push_back({p_x, p_y, start.c});
pixels.setPixelColor(XY(p_x, p_y), pixels.Color(start.c.r, start.c.g, start.c.b));
pixels.show();
}
//Move ball to another position
//Set game score
void DisplayGameScore(int score) {
lcd.setCursor(7, 0);
lcd.print(score);
}
//Set game score
//Set number of balls
void DisplayNumberOfBalls() {
if (v.size() < SIZE * SIZE) {
lcd.setCursor(7, 1);
lcd.print(v.size());
}
}
//Set number of balls
//Set game over message
void GameOver() {
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(3, 1);
lcd.print("GAME OVER!");
tone(BUZZER, 50, 200);
}
//Set game over message
//Check if a path from start to finish exists
void CheckIfPathExists(char maze[SIZE][SIZE], int x1, int y1, int x2, int y2) {
int result = bfs(maze, x1, y1, x2, y2);
if (result != -1) { //If the path to a new position exists (not blocked by the group of other balls)
lcd.setCursor(0, 2);
lcd.print("PATH IS FOUND!");
//Success of changing the ball position
BallMoveSuccess();
//Success of changing the ball position
//Call a ball line remove method (with the number of balls specified just in case is may be removed)
RemoveBallLine(MIN);
//Call a ball line remove method (with the number of balls specified just in case is may be removed)
//Create more random balls of different colors after successful move
GenerateRandomBallSet(NEW);
//Create more random balls of different colors after successful move
//Switch on next balls color hints
SwitchOnNextBallsColorHints();
//Switch on next balls color hints
} else {
lcd.setCursor(0, 2);
lcd.print(" ");
lcd.setCursor(0, 2);
lcd.print("NO PATH!");
tone(BUZZER, 100, 200);
maze[start.x][start.y] = '#';
maze[p_x][p_y] = '.';
isSelected = false;
}
DisplayNumberOfBalls();
ShowBallsOnBFSGrid();
}
//Check if a path from start to finish exists
//Clear a maze from the path trace
void ShowBallsOnBFSGrid() {
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
if (maze[i][j] == 'V') {
maze[i][j] = '.';
}
Serial.print(maze[i][j]);
Serial.print(" ");
}
Serial.println();
}
}
//Clear a maze from the path trace
//Shuffle decks and their colors (template function)
template<class T>
void ShuffleColors(T arr[], int limit) {
for (int i = 0; i < limit; i++) {
int n = random(0, limit);
auto temp = arr[n];
arr[n] = arr[i];
arr[i] = temp;
}
}
//Shuffle decks and their colors (template function)
//Remove ball line
void RemoveBallSound() {
delay(100);
tone(BUZZER, 150, 50);
}
//Remove ball line
void RemoveBallLine(int qty) {
int x1, y1, x2, y2;
int bonusCounter = 0;
int lineScore = 0;
isRemoved = false;
//HOR
for (int left = p_x; left < SIZE; left++) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(left, p_y))) {
x1 = --left;
y1 = p_y;
break;
}
if (left == SIZE - 1) {
x1 = left;
y1 = p_y;
break;
}
}
for (int right = p_x; right >= 0; right--) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(right, p_y))) {
x2 = ++right;
y2 = p_y;
break;
}
if (right == 0) {
x2 = right;
y2 = p_y;
break;
}
}
if (abs(x2 - x1) + 1 >= MIN) {
for (int p = min(x1, x2); p < min(x1, x2) + abs(x2 - x1) + 1; p++) {
v.remove(GetBallIndex(p, p_y));
maze[p][p_y] = '.';
if (p != p_x) {
pixels.setPixelColor(XY(p, p_y), pixels.Color(0, 0, 0));
pixels.show();
RemoveBallSound();
lineScore += 10;
}
}
if (abs(x2 - x1) + 1 > MIN) {
lineScore += (abs(x2 - x1) + 1) * 20;
}
bonusCounter++;
isRemoved = true;
}
//VER
for (int up = p_y; up >= 0; up--) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(p_x, up))) {
x1 = p_x;
y1 = ++up;
break;
}
if (up == 0) {
x1 = p_x;
y1 = up;
break;
}
}
for (int down = p_y; down < SIZE; down++) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(p_x, down))) {
x2 = p_x;
y2 = --down;
break;
}
if (down == SIZE - 1) {
x2 = p_x;
y2 = down;
break;
}
}
if (abs(y2 - y1) + 1 >= MIN) {
for (int p = min(y1, y2); p < min(y1, y2) + abs(y2 - y1) + 1; p++) {
v.remove(GetBallIndex(p_x, p));
maze[p_x][p] = '.';
if (p != p_y) {
pixels.setPixelColor(XY(p_x, p), pixels.Color(0, 0, 0));
pixels.show();
RemoveBallSound();
lineScore += 10;
}
}
if (abs(y2 - y1) + 1 > MIN) {
lineScore += (abs(y2 - y1) + 1) * 20;
}
bonusCounter++;
isRemoved = true;
}
//RIGHT DOWN
for (int right = p_x, down = p_y; right >= 0 || down < SIZE; right--, down++) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(right, down))) {
x1 = ++right;
y1 = --down;
break;
}
if ((down != SIZE - 1 && right == 0) || (down == SIZE - 1 && right != 0) || (down == SIZE - 1 && right == 0)) {
x1 = right;
y1 = down;
break;
}
}
//RIGHT DOWN
//LEFT UP
for (int left = p_x, up = p_y; up >= 0 || left < SIZE; left++, up--) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(left, up))) {
x2 = --left;
y2 = ++up;
break;
}
if ((left != SIZE - 1 && up == 0) || (left == SIZE - 1 && up != 0) || (left == SIZE - 1 && up == 0)) {
x2 = left;
y2 = up;
break;
}
}
//LEFT UP
if ((abs(x2 - x1) + 1) >= MIN) {
for (int p = x1, t = y1; p < x1 + abs(x2 - x1) + 1; p++, t--) {
v.remove(GetBallIndex(p, t));
maze[p][t] = '.';
if (p != p_x && t != p_y) {
pixels.setPixelColor(XY(p, t), pixels.Color(0, 0, 0));
pixels.show();
RemoveBallSound();
lineScore += 10;
}
}
if ((abs(x2 - x1) + 1) > MIN) {
lineScore += (abs(x2 - x1) + 1) * 20;
}
bonusCounter++;
isRemoved = true;
}
//RIGHT UP
for (int right = p_x, up = p_y; right >= 0 || up >= 0; right--, up--) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(right, up))) {
x1 = ++right;
y1 = ++up;
break;
}
if ((right != 0 && up == 0) || (right == 0 && up != 0) || (right == 0 && up == 0)) {
x1 = right;
y1 = up;
break;
}
}
//RIGHT UP
//LEFT DOWN
for (int left = p_x, down = p_y; down < SIZE || left < SIZE; left++, down++) {
if (pixels.getPixelColor(XY(p_x, p_y)) != pixels.getPixelColor(XY(left, down))) {
x2 = --left;
y2 = --down;
break;
}
if ((left == SIZE - 1 && down != SIZE - 1) || (left != SIZE - 1 && down == SIZE) || (left == SIZE - 1 && down == SIZE - 1)) {
x2 = left;
y2 = down;
break;
}
}
//LEFT DOWN
if (abs(x2 - x1) + 1 >= MIN) {
for (int p = x1, t = y1; p < x1 + abs(x2 - x1) + 1; p++, t++) {
v.remove(GetBallIndex(p, t));
maze[p][t] = '.';
if (p != p_x && t != p_y) {
pixels.setPixelColor(XY(p, t), pixels.Color(0, 0, 0));
pixels.show();
RemoveBallSound();
lineScore += 10;
}
}
if (abs(x2 - x1) + 1 > MIN) {
lineScore += (abs(x2 - x1) + 1) * 20;
}
bonusCounter++;
isRemoved = true;
}
if (isRemoved == true) {
pixels.setPixelColor(XY(p_x, p_y), pixels.Color(UCHAR_MAX, UCHAR_MAX, UCHAR_MAX));
pixels.show();
RemoveBallSound();
lineScore += 10;
//Set game score
if (bonusCounter >= 1) {
gameScorePoints += bonusCounter * lineScore;
DisplayGameScore(gameScorePoints);
}
//Set game score
}
}
//Set a random ball RGB color
void SwitchOnNextBallsColorHints() {
c[0] = colors[random(0, RGB)];
c[1] = colors[random(0, RGB)];
c[2] = colors[random(0, RGB)];
RGBLedColor(R1, G1, B1, c[0].r, c[0].g, c[0].b);
RGBLedColor(R2, G2, B2, c[1].r, c[1].g, c[1].b);
RGBLedColor(R3, G3, B3, c[2].r, c[2].g, c[2].b);
}
//Set a random ball RGB color
//Clear game field
void ClearGameField() {
for (int i = 0; i < v.size(); i++) {
pixels.setPixelColor(XY(v[i].x, v[i].y), pixels.Color(0, 0, 0));
pixels.show();
}
}
//Clear game field
//Get blinking ball index
int GetBallIndex(int x, int y) {
for (int j = 0; j < v.size(); j++) {
if (v[j].x == x && v[j].y == y) {
return j;
}
}
return -1;
}
//Get blinking ball index
//Check if one ball does not overlap with another
void ResetBallAfterOverlap(int x, int y, int color) {
for (int j = 0; j < v.size(); j++) {
if ((v[j].x == x) && (v[j].y == y)) {
Serial.println("OVERLAPPED!");
delay(SLEEP);
isError = true;
}
}
}
//Check if one ball does not overlap with another
//Set a random ball RGB color
void SetBallRGBColor(int x, int y, int index, int qty, int i) {
ball.x = x;
ball.y = y;
if (qty == NEW) {
ball.c = c[i];
} else {
ball.c = colors[index];
}
v.push_back(ball);
pixels.setPixelColor(XY(x, y), pixels.Color(ball.c.r, ball.c.g, ball.c.b));
pixels.show();
}
//Set a random ball RGB color
//Set a random ball position
void SetRandomBallPosition(int x, int y, int color, int qty, int i) {
isError = false;
ResetBallAfterOverlap(x, y, color);
if (isError == false) {
SetBallRGBColor(x, y, color, qty, i);
maze[x][y] = '#';
delay(5);
Serial.println("OK!");
}
}
//Set a random ball position
//Set player free move
void SetPlayerFreeMove() {
if (pixels.getPixelColor(XY(p_x, p_y)) != CYAN &&
pixels.getPixelColor(XY(p_x, p_y)) != GREEN &&
pixels.getPixelColor(XY(p_x, p_y)) != RED &&
pixels.getPixelColor(XY(p_x, p_y)) != MAGENTA &&
pixels.getPixelColor(XY(p_x, p_y)) != BLUE &&
pixels.getPixelColor(XY(p_x, p_y)) != YELLOW &&
pixels.getPixelColor(XY(p_x, p_y)) != ORANGE) {
pixels.setPixelColor(XY(p_x, p_y), pixels.Color(0, 0, 0));
pixels.show();
}
}
//Set player free move
//Set ball start position
void SetBallStartPisition() {
if (pixels.getPixelColor(XY(p_x, p_y)) != 0) {
pixels.setPixelColor(XY(prev.x, prev.y), pixels.Color(prev.c.r, prev.c.g, prev.c.b));
pixels.show();
isOverTheBall = true;
pixels.setPixelColor(XY(p_x, p_y), pixels.Color(UCHAR_MAX, UCHAR_MAX, UCHAR_MAX));
pixels.show();
} else {
pixels.setPixelColor(XY(prev.x, prev.y), pixels.Color(prev.c.r, prev.c.g, prev.c.b));
pixels.show();
isOverTheBall = false;
pixels.setPixelColor(XY(p_x, p_y), pixels.Color(UCHAR_MAX, UCHAR_MAX, UCHAR_MAX));
pixels.show();
}
}
//Set ball start position
//Set a player joystick control
void PlayerJoystickControl() {
//Confirm start of finish
if ((analogRead(SW1) == 0) && (isSelected == false) && (isOverTheBall == true)) {
isSelected = true;
isBlinking = true;
start = v[GetBallIndex(p_x, p_y)];
pixels.setPixelColor(XY(p_x, p_y), pixels.Color(start.c.r, start.c.g, start.c.b));
pixels.show();
} else if ((analogRead(SW1) == 0) && (isSelected == false) && (isOverTheBall == false) && (isBlinking == true)) {
isSelected = true;
CheckIfPathExists(maze, start.x, start.y, p_x, p_y);
}
//Confirm start of finish
//Joystick control
int x = analogRead(A1);
int y = analogRead(A0);
if (x == 2 * MOVE - 1 && y == MOVE) { //LEFT
if (p_y >= 1) {
isSelected = false;
SetPlayerFreeMove();
prev.x = p_x;
prev.y = p_y;
prev.c = v[GetBallIndex(p_x, p_y)].c;
p_y--;
SetBallStartPisition();
}
delay(SPEED);
} else if (x == 0 && y == MOVE) { //RIGHT
if (p_y < SIZE - 1) {
isSelected = false;
SetPlayerFreeMove();
prev.x = p_x;
prev.y = p_y;
prev.c = v[GetBallIndex(p_x, p_y)].c;
p_y++;
SetBallStartPisition();
}
delay(SPEED);
} else if (y == 2 * MOVE - 1 && x == MOVE) { //DOWN
if (p_x < SIZE - 1) {
isSelected = false;
SetPlayerFreeMove();
prev.x = p_x;
prev.y = p_y;
prev.c = v[GetBallIndex(p_x, p_y)].c;
p_x++;
SetBallStartPisition();
}
delay(SPEED);
} else if (y == 0 && x == MOVE) { //UP
if (p_x >= 1) {
isSelected = false;
SetPlayerFreeMove();
prev.x = p_x;
prev.y = p_y;
prev.c = v[GetBallIndex(p_x, p_y)].c;
p_x--;
SetBallStartPisition();
}
delay(SPEED);
}
//Joystick control
}
//Set a player joystick control
//Generate a random multi-colored field with ships (according to the Naval Clash game rules)
void GenerateRandomBallSet(int qty) {
for (int i = 0; i < qty; i++) {
SetRandomBallPosition(random(0, SIZE), random(0, SIZE), random(0, RGB), qty, i);
if (isError == true) {
i--;
}
if (v.size() == SIZE * SIZE) {
GameOver();
break;
}
}
Serial.println("ALL BALLS SET!");
}
//Generate a random multi-colored field with ships (according to the Naval Clash game rules
//Program loop
void loop() {
PlayerJoystickControl();
//Separate thread for the blinking ball
if (isBlinking == true) {
pixels.setPixelColor(XY(start.x, start.y), pixels.Color(0, 0, 0));
pixels.show();
delay(30);
pixels.setPixelColor(XY(start.x, start.y), pixels.Color(start.c.r, start.c.g, start.c.b));
pixels.show();
delay(30);
}
//Separate thread for the blinking ball
/*
Taking color codes (in the real project they may be different)
//Cyan
pixels.setPixelColor(0 + 0 * SIZE, pixels.Color(0, UCHAR_MAX, UCHAR_MAX));
Serial.println(pixels.getPixelColor(0 + 0 * SIZE));
//Green
pixels.setPixelColor(1 + 0 * SIZE, pixels.Color(0, UCHAR_MAX, 0));
Serial.println(pixels.getPixelColor(1 + 0 * SIZE));
//Red
pixels.setPixelColor(2 + 0 * SIZE, pixels.Color(UCHAR_MAX, 0, 0));
Serial.println(pixels.getPixelColor(2 + 0 * SIZE));
//Magenta
pixels.setPixelColor(3 + 0 * SIZE, pixels.Color(UCHAR_MAX, 0, UCHAR_MAX));
Serial.println(pixels.getPixelColor(3 + 0 * SIZE));
//Blue
pixels.setPixelColor(4 + 0 * SIZE, pixels.Color(0, 0, UCHAR_MAX));
Serial.println(pixels.getPixelColor(4 + 0 * SIZE));
//Yellow
pixels.setPixelColor(5 + 0 * SIZE, pixels.Color(UCHAR_MAX, UCHAR_MAX, 0));
Serial.println(pixels.getPixelColor(5 + 0 * SIZE));
//Orange
pixels.setPixelColor(6 + 0 * SIZE, pixels.Color(UCHAR_MAX, UCHAR_MAX / 2, 0));
Serial.println(pixels.getPixelColor(6 + 0 * SIZE));
delay(60000 * 5);
*/
}
//Program loop
//LED matrix coordinate transform (different from the real one)
int XY(int x, int y) {
return y + SIZE * x;
}
//LED matrix coordinate transform (different from the real one)
//LED matrix coordinate transform (use it for the real project)
/*
int XY(int x, int y) {
if (y % 2 == 0) {
return y * SIZE + x;
} else {
return ((y + 1) * SIZE - 1) - x;
}
}
*/
//LED matrix coordinate transform (use it for the real project)