//Copyright (C) 18.06.2025, Kirill ZHivotkov
/*
Игра "Color Lines".
*/
#include <Adafruit_NeoPixel.h>
#include <Vector.h>
#include <limits.h>
#define PIN 4
#define RGB 7
#define SIZE 9
#define START_BALLS 50
#define NEW_BALLS 3
#define MOVE 512
#define SLEEP 0
#define SPEED 200
#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
#define YJ1 A1
#define SW1 A2
Adafruit_NeoPixel pixels(SIZE * SIZE, PIN);
struct Color {
int r;
int g;
int b;
};
struct Coord {
int x;
int y;
Color c;
};
int p_x = 0;
int p_y = 0;
static int blinkCounter = 0;
static int lastBallBlinkingCheck = 0;
bool error = false;
bool isSelected = false;
bool isBlinking = false;
bool isOverTheBall = false;
bool ballPathExists = false;
bool allBallsAreSet = false;
bool lineIsRemoved = false;
char maze[SIZE][SIZE] = { //Empty field
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'},
{'.', '.', '.', '.', '.', '.', '.', '.', '.'}
};
Coord ball;
Coord start = {-1, -1, {0, 0, 0}};
Coord finish = {-1, -1, {0, 0, 0}};
Coord storage[START_BALLS] = {};
Vector<Coord> v(storage, START_BALLS);
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*/}};
void setup() {
Serial.println("\nSetup call (possible crash)");
Serial.begin(9600);
pixels.begin();
pixels.setBrightness(UCHAR_MAX);
//Link joystick
pinMode(XJ1, INPUT);
pinMode(YJ1, INPUT);
pinMode(SW1, INPUT);
//Link joystick
//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)
//Generate random ball set with the number of balls specified
GenerateRandomBallSet(START_BALLS);
CreateBallMaze();
//Generate random ball set with the number of balls specified
}
//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
//Remove ball line
void RemoveBallLine() {
int x, y;
int k;
lineIsRemoved = false;
for (int i = 0; i < SIZE; i++) {
k = 0;
for (int j = 0; j < SIZE - 1; j++) {
if (pixels.getPixelColor((XY(i, j))) == pixels.getPixelColor((XY(i, j + 1))) && pixels.getPixelColor((XY(i, j))) != 0 && pixels.getPixelColor((XY(i, j + 1))) != 0) {
k++;
//Serial.print("COLOR > ");
//Serial.print(pixels.getPixelColor((XY(i, j))));
//Serial.print(" > ");
//Serial.println(k);
} else {
x = i;
y = j;
if (k >= 2) {
for (int p = y - k; p <= y; p++) {
v.remove(GetBallIndex(x, p));
maze[x][p] = '.';
Serial.print(x);
Serial.print(" ");
Serial.print(p);
Serial.println();
pixels.setPixelColor(XY(x, p), pixels.Color(0, 0, 0));
pixels.show();
}
lineIsRemoved = true;
}
k = 0;
}
if (lineIsRemoved == true) {
break;
}
}
if (k >= 2) {
for (int p = SIZE - 1 - k; p <= SIZE - 1; p++) {
v.remove(GetBallIndex(x, p));
maze[x][p] = '.';
Serial.print(x);
Serial.print(" ");
Serial.print(p);
Serial.println();
pixels.setPixelColor(XY(x, p), pixels.Color(0, 0, 0));
pixels.show();
}
lineIsRemoved = true;
}
if (lineIsRemoved == true) {
break;
}
}
//pixels.setPixelColor(XY(finish.x, finish.y), pixels.Color(UCHAR_MAX, UCHAR_MAX, UCHAR_MAX));
//pixels.show();
}
//Remove ball line
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) {
Serial.println("Path exists!");
ballPathExists = true;
RemoveBallLine();
//Clear the grid for the next move
ClearBallPath();
//Clear the grid for the next move
} else {
Serial.println("No path found!");
ballPathExists = false;
//1
pixels.setPixelColor(XY(finish.x, finish.y), pixels.Color(0, 0, 0));
pixels.show();
v.pop_back();
//1
//2
Coord tmp;
tmp.x = start.x;
tmp.y = start.y;
tmp.c = start.c;
pixels.setPixelColor(XY(start.x, start.y), pixels.Color(start.c.r, start.c.g, start.c.b));
pixels.show();
v.push_back(tmp);
//Clear the grid for the next move
ClearBallPath();
//Clear the grid for the next move
}
}
void ClearBallPath() {
maze[start.x][start.y] = ballPathExists == true ? '.' : '#';
maze[finish.x][finish.y] = lineIsRemoved == true ? '.' : ballPathExists == true ? '#' : '.';
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();
}
}
//Check if a ball may be set (path in a maze)
//Create a ball maze
void CreateBallMaze() {
for (int i = 0; i < v.size(); i++) {
if (v[i].x != start.x && v[i].x != finish.x && v[i].y != start.y && v[i].y != finish.y) {
//if (pixels.getPixelColor(XY(v[i].x, v[i].y)) != 0 && pixels.getPixelColor(v[i].x, v[i].y)) != BLACK) {
maze[v[i].x][v[i].y] = '#';
//}
}
}
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
Serial.print(maze[i][j]);
Serial.print(" ");
}
Serial.println();
}
}
//Create a ball maze
//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)
//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 coord
Coord GetBlinkingBallCoord() {
for (int j = 0; j < v.size(); j++) {
if (v[j].x == p_x && v[j].y == p_y) {
return v[j];
}
}
return {};
}
//Get blinking ball coord
//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 0;
}
//Get blinking ball index
//Draw all balls at once
void DrawAllBallsAtOnce() {
for (int i = 0; i < v.size(); i++) {
pixels.setPixelColor(XY(v[i].x, v[i].y), pixels.Color(v[i].c.r, v[i].c.g, v[i].c.b));
pixels.show();
}
}
//Draw all balls at once
//Check if one ball does not overlap with another
void ResetBallAfterOverlap(int x, int y, int color) {
//if (allBallsAreSet == false) {
for (int j = 0; j < v.size(); j++) {
if ((v[j].x == x) && (v[j].y == y)) {
Serial.println("OVERLAPPED!");
delay(SLEEP);
error = true;
}
}
//allBallsAreSet = true;
//}
}
//Check if one ball does not overlap with another
//Set a random ball RGB color
void SetBallRGBColor(int x, int y, int index) {
ball.x = x;
ball.y = y;
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) {
error = false;
ResetBallAfterOverlap(x, y, color);
if (error == false) {
SetBallRGBColor(x, y, color);
delay(SLEEP);
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) {
Serial.println("OVER");
isOverTheBall = true;
pixels.setPixelColor(XY(p_x, p_y), pixels.Color(UCHAR_MAX, UCHAR_MAX, UCHAR_MAX));
pixels.show();
} else {
DrawAllBallsAtOnce();
Serial.println("FREE");
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;
Serial.println("Start!");
isBlinking = true;
blinkCounter = 0;
start = GetBlinkingBallCoord();
Serial.print(start.x);
Serial.print(" ");
Serial.println(start.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;
Serial.println("Finish!");
isBlinking = false;
blinkCounter = 0;
v.remove(GetBallIndex(start.x, start.y));
pixels.setPixelColor(XY(start.x, start.y), pixels.Color(0, 0, 0));
pixels.show();
finish.x = p_x;
finish.y = p_y;
finish.c = start.c;
v.push_back(finish);
pixels.setPixelColor(XY(finish.x, finish.y), pixels.Color(finish.c.r, finish.c.g, finish.c.b));
pixels.show();
CreateBallMaze();
CheckIfPathExists(maze, start.x, start.y, finish.x, finish.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();
p_y--;
SetBallStartPisition();
}
delay(SPEED);
} else if (x == 0 && y == MOVE) { //RIGHT
if (p_y < SIZE - 1) {
isSelected = false;
SetPlayerFreeMove();
p_y++;
SetBallStartPisition();
}
delay(SPEED);
} else if (y == 2 * MOVE - 1 && x == MOVE) { //DOWN
if (p_x < SIZE - 1) {
isSelected = false;
SetPlayerFreeMove();
p_x++;
SetBallStartPisition();
}
delay(SPEED);
} else if (y == 0 && x == MOVE) { //UP
if (p_x >= 1) {
isSelected = false;
SetPlayerFreeMove();
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) {
//if (allBallsAreSet == false) {
ClearGameField();
v.clear();
for (int i = 0; i < qty; i++) {
SetRandomBallPosition(random(0, SIZE), random(0, SIZE), random(0, RGB));
if (error == true) {
i--;
}
}
allBallsAreSet = true;
Serial.println("ALL BALLS SET!");
//}
}
//Generate a random multi-colored field with ships (according to the Naval Clash game rules
//Program loop
void loop() {
randomSeed(micros());
PlayerJoystickControl();
//Separate thread for the blinking ball
lastBallBlinkingCheck = 0;
if (millis() - lastBallBlinkingCheck > 500) {
lastBallBlinkingCheck = millis();
if (isBlinking == true) {
if (blinkCounter % 2 == 0) {
pixels.setPixelColor(XY(start.x, start.y), pixels.Color(0, 0, 0));
pixels.show();
blinkCounter++;
} else {
pixels.setPixelColor(XY(start.x, start.y), pixels.Color(start.c.r, start.c.g, start.c.b));
pixels.show();
blinkCounter++;
}
}
}
//Separate thread for the blinking ball
/*
pixels.setPixelColor(0 + 0 * SIZE, pixels.Color(0, UCHAR_MAX, UCHAR_MAX));
Serial.println(pixels.getPixelColor(0 + 0 * SIZE));
pixels.setPixelColor(1 + 0 * SIZE, pixels.Color(0, UCHAR_MAX, 0));
Serial.println(pixels.getPixelColor(1 + 0 * SIZE));
pixels.setPixelColor(2 + 0 * SIZE, pixels.Color(UCHAR_MAX, 0, 0));
Serial.println(pixels.getPixelColor(2 + 0 * SIZE));
pixels.setPixelColor(3 + 0 * SIZE, pixels.Color(UCHAR_MAX, 0, UCHAR_MAX));
Serial.println(pixels.getPixelColor(3 + 0 * SIZE));
pixels.setPixelColor(4 + 0 * SIZE, pixels.Color(0, 0, UCHAR_MAX));
Serial.println(pixels.getPixelColor(4 + 0 * SIZE));
pixels.setPixelColor(5 + 0 * SIZE, pixels.Color(UCHAR_MAX, UCHAR_MAX, 0));
Serial.println(pixels.getPixelColor(5 + 0 * SIZE));
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
int XY(int x, int y) {
return y + SIZE * x;
}