#include "snake.h"
#include <QWidget>
#include <QPaintEvent>
#include <QPainter>
#include <QRectF>
#include <QTimer>
#include <QDebug>
#include <QDateTime>
#define SCALING (16)
#define DIR_UP 0
#define DIR_DOWN 1
#define DIR_LEFT 2
#define DIR_RIGHT 3
#define CELL_FREE 0
#define CELL_PADDLE_BACKING 1
#define CELL_SNAKE 2
#define CELL_FOOD 3
#define CELL_BREAKOUT_BRICK 4
#define CELL_SNAKEBRICK 5
#define CELL_PADDLE 6
#define CELL_GOL 7
#define AXIS_X 0
#define AXIS_Y 1
#define FIELD(x,y) _field[(x) + ((y) * _width)]
#define FIELD2(x,y) _field2[(x) + ((y) * _width)]
struct Paddle;
struct Cell
{
int type;
QBrush color;
Paddle *player;
Cell(int t, QColor c) : type(t), color(QBrush(c)), player(nullptr) {}
bool isFood() const { return type == CELL_FOOD; }
bool willKill() const { return type == CELL_PADDLE_BACKING | type == CELL_PADDLE || type == CELL_SNAKE || type == CELL_SNAKEBRICK; }
bool isFree() const { return type == CELL_FREE || type == CELL_GOL; }
bool isPaddleFree() const { return type == CELL_FREE || type == CELL_SNAKEBRICK || type == CELL_BREAKOUT_BRICK || type == CELL_FOOD || type == CELL_GOL; }
bool isBrick() const { return type == CELL_BREAKOUT_BRICK || type == CELL_SNAKEBRICK; }
bool canSpawnFood() const { return type == CELL_SNAKE || type == CELL_FREE || type == CELL_BREAKOUT_BRICK || type == CELL_GOL; }
bool ballWillDestroy() const { return isFood() || isBrick() || type == CELL_GOL; }
bool isAlive() const { return type == CELL_FOOD || type == CELL_BREAKOUT_BRICK || type == CELL_GOL; }
};
static const Cell empty(CELL_FREE, QColor());
static const Cell snakebrick(CELL_SNAKEBRICK, QColor::fromRgb(200, 200, 200));
static const Cell golBrick(CELL_GOL, QColor::fromRgb(180, 200, 220));
static Cell breakoutCenterBrick(CELL_BREAKOUT_BRICK, QColor::fromRgb(255, 255, 255));
static const Cell food[] = {
Cell(CELL_FOOD, QColor::fromRgb(0, 255, 0)),
Cell(CELL_FOOD, QColor::fromRgb(0, 200, 0)),
Cell(CELL_FOOD, QColor::fromRgb(0, 160, 0)),
Cell(CELL_FOOD, QColor::fromRgb(0, 120, 0)),
};
struct Paddle;
struct Ball
{
int x, y;
int dx, dy;
const Cell *cell;
QPoint gridPos;
Paddle *lastPlayer;
Ball(int startX, int startY) : x(startX), y(startY), dx(qrand() % 4 + 1), dy(qrand() % 4 + 1),
cell(new Cell(CELL_PADDLE_BACKING, QColor::fromHsv(qrand() % 360, 32 + qrand() % 64, 32 + qrand() % 64))),
lastPlayer(nullptr) {}
virtual ~Ball() { delete cell; }
};
struct Paddle
{
int x, y;
int axis;
int size;
Cell *cell;
Cell *dell;
Paddle(int startX, int startY, GameCore *field, int ax) : x(startX), y(startY), axis(ax), size(5),
cell(new Cell(CELL_PADDLE, QColor::fromHsv(qrand() % 360, 192 + qrand() % 64, 192 + qrand() % 64))),
dell(nullptr) {
for (int i = 0; i < size; ++i) {
if (axis == AXIS_X) {
field->setField(x + i, y, cell);
} else {
field->setField(x, y + i, cell);
}
}
}
bool shrink(GameCore *field) {
if (size <= 0)
return false;
size--;
if (axis == AXIS_X) {
field->setField(x + size, y, &empty);
} else {
field->setField(x, y + size, &empty);
}
return true;
}
bool grow(GameCore *field) {
if (axis == AXIS_X) {
if (x < 0 || x + size >= field->width())
return false;
if (field->field(x + size, y)->isFree()) {
field->setField(x + size, y, cell);
}
} else {
if (y < 0 || y + size >= field->height())
return false;
if (field->field(x, y + size)->isFree()) {
field->setField(x, y + size, cell);
}
}
size++;
return true;
}
virtual ~Paddle() { delete cell; delete dell; }
};
class Snake
{
public:
int x, y;
int direction;
int len;
QList<QPoint> parts;
Cell *cell;
Snake(int sx, int sy) : x(sx), y(sy), direction(DIR_UP), len(5), parts(),
cell(new Cell(CELL_SNAKE, QColor::fromHsv(qrand() % 360, 128 + qrand() % 128, 128 + qrand() % 128))) {}
virtual ~Snake() { delete cell; }
};
void GameCore::setField(int x, int y, const Cell *val) {
if (_field[x + _width * y] == val)
return;
_field[x + _width * y] = val;
_widget->update(x * SCALING, y * SCALING, SCALING, SCALING);
}
GameCore::GameCore(QWidget *widget)
: _widget(widget),
_deaths(0),
_lastMeal(QDateTime::currentMSecsSinceEpoch()),
_lastPaddle(QDateTime::currentMSecsSinceEpoch()),
_field(nullptr)
{
qsrand((uint)_lastMeal);
_width = widget->width() / SCALING;
_height = widget->height() / SCALING;
if (_width <= 0 || _height <= 0)
return;
int cellCount = _width * _height;
_field = (const Cell**)calloc(cellCount, sizeof(*_field));
_field2 = (const Cell**)calloc(cellCount, sizeof(*_field));
for (int i = 0; i < cellCount; ++i) {
_field[i] = ∅
}
// Should be either 4 player pong or two player + tetris
// Bricks in the center also only make sense without tetris
_paddles.append(new Paddle(1, _height - 2, this, AXIS_X));
_paddles.append(new Paddle(1, 1, this, AXIS_X));
_paddles.append(new Paddle(_width - 2, 1, this, AXIS_Y));
_paddles.append(new Paddle(1, 1, this, AXIS_Y));
// Assign players to cells
for (int i = 0; i < _paddles.size(); ++i) {
_paddles[i]->dell = new Cell(CELL_PADDLE_BACKING, _paddles[i]->cell->color.color().darker(250));
_paddles[i]->cell->player = _paddles[i];
_paddles[i]->dell->player = _paddles[i];
}
drawPaddleBorders();
addBreakoutBlocks();
qDebug() << "Field:" << _width << _height;
addFood();
_t = new QTimer(widget);
_t->start(15);
// GAME
QTimer::connect(_t, &QTimer::timeout, [this, cellCount]() {
//
static int tick = 0;
++tick;
// SNAKE
if (tick % 6 == 0) {
for (Snake *snake : _snakes) {
// AI
const Cell* what[4];
int dist[4];
int best = 0;
scanDir(snake, 0, -1, what[DIR_UP], dist[DIR_UP]);
scanDir(snake, 0, 1, what[DIR_DOWN], dist[DIR_DOWN]);
scanDir(snake, -1, 0, what[DIR_LEFT], dist[DIR_LEFT]);
scanDir(snake, 1, 0, what[DIR_RIGHT], dist[DIR_RIGHT]);
for (int i = 1; i < 4; ++i) {
if (what[i]->isFood()) {
if (!what[best]->isFood() || dist[best] > dist[i]) {
best = i;
}
} else if (!what[best]->isFood() && (dist[i] > dist[best] || (dist[i] > 10 && qrand() % (4 * (4 - i)) == 0))) {
best = i;
}
}
if (what[best]->isFood()) {
snake->direction = best;
} else if (dist[snake->direction] <= 1 || qrand() % 20 == 0) {
snake->direction = best;
}
// Move
switch (snake->direction) {
case DIR_UP:
snake->y--; break;
case DIR_DOWN:
snake->y++; break;
case DIR_LEFT:
snake->x--; break;
case DIR_RIGHT:
snake->x++; break;
}
if (snake->x < 0 || snake->x >= _width || snake->y < 0 || snake->y >= _height ||
FIELD(snake->x, snake->y)->willKill()) {
// Snek ded
_snakes.removeAll(snake);
for (const QPoint &p : snake->parts) {
if (FIELD(p.x(), p.y())->isFood())
continue;
setField(p.x(), p.y(), &snakebrick);
}
delete snake;
if (_snakes.isEmpty()) {
addSnake();
}
break; // Skip rest of list for this tick since we removed a snake from the list while iterating
}
// Remove tail from field if snake reached desired length
if (snake->parts.size() >= snake->len) {
const QPoint &p = snake->parts.front();
snake->parts.pop_front();
if (FIELD(p.x(), p.y()) == snake->cell) {
setField(p.x(), p.y(), &empty);
}
}
// Eat
if (FIELD(snake->x, snake->y)->isFood()) {
_lastMeal = QDateTime::currentMSecsSinceEpoch();
if (qrand() % snake->len > 40) {
// Snake break!
snake->len = 10;
while (snake->parts.size() > snake->len) {
const QPoint &p = snake->parts.front();
snake->parts.pop_front();
if (!FIELD(p.x(), p.y())->isFood()) {
setField(p.x(), p.y(), &snakebrick);
}
}
int cnt = 0;
for (int i = 0; i < cellCount; ++i) {
if (_field[i]->isFood()) {
if (++cnt > 5)
break;
}
}
if (cnt <= 5) {
addFood();
}
} else {
snake->len += 5;
}
addFood();
}
setField(snake->x, snake->y, snake->cell);
snake->parts.append(QPoint(snake->x, snake->y));
}
// If no food was picked up within a minute, spawn more
if (!_snakes.isEmpty() && QDateTime::currentMSecsSinceEpoch() - _lastMeal > 60000) {
_lastMeal = QDateTime::currentMSecsSinceEpoch();
addFood();
}
// Spawn more balls?
if (_lastPaddle + 60000 < QDateTime::currentMSecsSinceEpoch()) {
qDebug() << "No ball action!";
_lastPaddle = QDateTime::currentMSecsSinceEpoch();
addBall();
}
}
//
// Balls
Ball *bbottom = nullptr, *btop = nullptr, *bleft = nullptr, *bright = nullptr;
for (Ball* ball : _balls) {
const QPoint old(ball->x / SCALING, ball->y / SCALING);
int nx = ball->x + ball->dx;
int ny = ball->y + ball->dy;
// Safety
if (nx < 0) {
nx = 0;
ball->dx = qAbs(ball->dx);
} else if (nx >= _width * SCALING) {
nx = _width * SCALING - 1;
ball->dx = -qAbs(ball->dx);
}
if (ny < 0) {
ny = 0;
ball->dy = qAbs(ball->dy);
} else if (ny >= _height * SCALING) {
ny = _height * SCALING - 1;
ball->dy = -qAbs(ball->dy);
}
// Collision check
const QPoint noo(nx / SCALING, ny / SCALING);
const Cell *newCell = FIELD(noo.x(), noo.y());
if (old != noo && !newCell->isFree()) {
// Yes, new cell occupied, see what should happen
bool one = false;
if (FIELD(old.x(), noo.y()) == ball->cell || FIELD(old.x(), noo.y())->isFree()) {
one = true;
if (old.x() > noo.x()) {
ball->dx = qAbs(ball->dx);
} else if (old.x() < noo.x()) {
ball->dx = -qAbs(ball->dx);
}
}
if (FIELD(noo.x(), old.y()) == ball->cell || FIELD(noo.x(), old.y())->isFree()) {
one = true;
if (old.y() > noo.y()) {
ball->dy = qAbs(ball->dy);
} else if (old.y() < noo.y()) {
ball->dy = -qAbs(ball->dy);
}
}
if (!one) {
ball->dx = -ball->dx;
ball->dy = -ball->dy;
}
auto *p = newCell->player;
if (p != nullptr) {
// Collision with player-cell: either paddle, or its backing wall
if (newCell->type == CELL_PADDLE_BACKING) {
p->shrink(this);
ball->lastPlayer = nullptr;
checkPongGameOver();
} else {
_lastPaddle = QDateTime::currentMSecsSinceEpoch();
ball->lastPlayer = p;
}
}
// Destroy cell?
if (newCell->ballWillDestroy()) {
if (newCell->isFood()) {
if (ball->lastPlayer != nullptr) {
ball->lastPlayer->grow(this);
}
addFood();
}
if (newCell->isBrick()) {
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
if (FIELD(noo.x() + x, noo.y() + y)->isBrick()) {
setField(noo.x() + x, noo.y() + y, &empty);
}
}
}
} else {
setField(noo.x(), noo.y(), &empty);
}
}
} else {
// No collision, free movement
ball->gridPos = noo;
ball->x = nx;
ball->y = ny;
if (old != noo) {
if (FIELD(old.x(), old.y()) == ball->cell) {
setField(old.x(), old.y(), &empty);
}
setField(noo.x(), noo.y(), ball->cell);
}
}
if (bbottom == nullptr || (ball->dy > 0 && bbottom->y < ball->y)) {
bbottom = ball;
}
if (btop == nullptr || (ball->dy < 0 && btop->y > ball->y)) {
btop = ball;
}
if (bright == nullptr || (ball->dx > 0 && bright->x < ball->x)) {
bright = ball;
}
if (bleft == nullptr || (ball->dx < 0 && bleft->x > ball->x)) {
bleft = ball;
}
}
// Paddles
if (bbottom != nullptr) {
for (Paddle *paddle : _paddles) {
if (paddle->size <= 0)
continue;
if (paddle->axis == AXIS_X) {
bool left = false, right = false;
Ball *check;
// Paddle moves along the X axis
if (paddle->y < 10) {
// Paddle is at top
check = btop;
} else {
// Paddle at bottom
check = bbottom;
}
if (paddle->x > 0 && check->gridPos.x() < paddle->x + paddle->size / 2) {
// Move left
left = true;
} else if (paddle->x + paddle->size < _width && check->gridPos.x() > paddle->x + paddle->size / 2) {
// Move right
right = true;
}
if (left) {
if (FIELD(paddle->x - 1, paddle->y)->isPaddleFree()) {
paddle->x--;
setField(paddle->x, paddle->y, paddle->cell);
if (FIELD(paddle->x + paddle->size, paddle->y) == paddle->cell) {
setField(paddle->x + paddle->size, paddle->y, &empty);
}
}
} else if (right) {
if (FIELD(paddle->x + paddle->size, paddle->y)->isPaddleFree()) {
setField(paddle->x + paddle->size, paddle->y, paddle->cell);
if (FIELD(paddle->x, paddle->y) == paddle->cell) {
setField(paddle->x, paddle->y, &empty);
}
paddle->x++;
}
}
} else {
// Paddle moves along the Y axis
bool top = false, bottom = false;
Ball *check;
// Paddle moves along the y ayis
if (paddle->x < 10) {
// Paddle is at top
check = bleft;
} else {
// Paddle at bottom
check = bright;
}
if (paddle->y > 0 && check->gridPos.y() < paddle->y + paddle->size / 2) {
// Move top
top = true;
} else if (paddle->y + paddle->size < _height && check->gridPos.y() > paddle->y + paddle->size / 2) {
// Move bottom
bottom = true;
}
if (top) {
if (FIELD(paddle->x, paddle->y - 1)->isPaddleFree()) {
paddle->y--;
setField(paddle->x, paddle->y, paddle->cell);
if (FIELD(paddle->x, paddle->y + paddle->size) == paddle->cell) {
setField(paddle->x, paddle->y + paddle->size, &empty);
}
}
} else if (bottom) {
if (FIELD(paddle->x, paddle->y + paddle->size)->isPaddleFree()) {
setField(paddle->x, paddle->y + paddle->size, paddle->cell);
if (FIELD(paddle->x, paddle->y) == paddle->cell) {
setField(paddle->x, paddle->y, &empty);
}
paddle->y++;
}
}
} // Done with movement logic
} // End loop over paddles
} // End paddles
// Game of life
if (tick % 13 == 0) {
auto **tmp = _field;
_field = _field2;
_field2 = tmp;
memcpy(_field, _field2, _width * _height * sizeof(void*));
bool killall = ((tick / 4000) % 2);
int killed = 0;
for (int y = 0; y < _height; ++y) {
for (int x = 0; x < _width; ++x) {
if (FIELD2(x,y) != &golBrick && FIELD2(x,y) != &empty) {
FIELD(x,y) = FIELD2(x,y);
continue;
}
if (killall) {
if (FIELD2(x,y) == &golBrick) {
setField(x,y, &empty);
if (++killed > 5) {
goto endgol;
}
}
continue;
}
int alive = 0;
// Direct
if (x > 0 && FIELD2(x-1,y)->isAlive()) alive++;
if (y > 0 && FIELD2(x,y-1)->isAlive()) alive++;
if (x+1 < _width && FIELD2(x+1,y)->isAlive()) alive++;
if (y+1 < _height && FIELD2(x,y+1)->isAlive()) alive++;
// Diagonal
if (x > 0 && y > 0 && FIELD2(x-1,y-1)->isAlive()) alive++;
if (x > 0 && y+1 < _height && FIELD2(x-1,y+1)->isAlive()) alive++;
if (y > 0 && x+1 < _width && FIELD2(x+1,y-1)->isAlive()) alive++;
if (x+1 < _width && y+1 < _height && FIELD2(x+1,y+1)->isAlive()) alive++;
// Check
if (alive < 2 || alive > 3) {
setField(x,y, &empty);
} else if (alive == 3) {
setField(x,y, &golBrick);
} else {
FIELD(x,y) = FIELD2(x,y);
}
}
}
endgol:;
} // End GOL
});
}
GameCore::~GameCore()
{
for (Paddle *p: _paddles) {
delete p;
}
free(_field);
if(_t->isActive()) {
_t->stop();
}
_widget->update();
}
void GameCore::scanDir(Snake *snake, int dx, int dy, const Cell* &what, int &dist)
{
int x = snake->x;
int y = snake->y;
dist = 0;
for (;;) {
x += dx;
y += dy;
dist++;
if (x < 0 || x >= _width || y < 0 || y >= _height) {
what = &snakebrick;
return;
}
if (FIELD(x, y)->willKill() || FIELD(x, y)->isFood()) {
what = FIELD(x, y);
return;
}
}
}
void GameCore::addSnake()
{
for (int i = 0; i < 100; ++i) {
int x = qrand() % _width;
int y = qrand() % _height;
if (FIELD(x, y)->isFree()) {
if (_snakes.count() <= _balls.count()) {
qDebug() << "Adding Snake at" << x << y;
_snakes.append(new Snake(x, y));
} else {
qDebug() << "Adding Ball at" << x << y;
_balls.append(new Ball(x * SCALING, y * SCALING));
}
break;
}
}
}
void GameCore::addBall()
{
for (int i = 0; i < 100; ++i) {
int x = qrand() % _width;
int y = qrand() % _height;
if (FIELD(x, y)->isFree()) {
qDebug() << "Adding Ball at" << x << y;
Ball *b = new Ball(x * SCALING, y * SCALING);
if (b == nullptr) {
qDebug() << "NULLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL";
}
_balls.append(b);
break;
}
}
}
void GameCore::drawPaddleBorders()
{
// 0.. = bottom, top, right, left
for (int i = 1; i < _width - 1; ++i) {
FIELD(i, 0) = _paddles[1]->dell;
FIELD(i, _height - 1) = _paddles[0]->dell;
}
for (int i = 1; i < _height - 1; ++i) {
FIELD(0, i) = _paddles[3]->dell;
FIELD(_width - 1, i) = _paddles[2]->dell;
}
_widget->update();
}
void GameCore::addBreakoutBlocks()
{
for (int y = 15; y < _height - 16; y += 3) {
for (int x = 15; x < _width - 16; x += 3) {
if (FIELD(x, y)->isPaddleFree()) {
FIELD(x, y) = &breakoutCenterBrick;
}
if (FIELD(x + 1, y)->isPaddleFree()) {
FIELD(x + 1, y) = &breakoutCenterBrick;
}
if (FIELD(x, y + 1)->isPaddleFree()) {
FIELD(x, y + 1) = &breakoutCenterBrick;
}
if (FIELD(x + 1, y + 1)->isPaddleFree()) {
FIELD(x + 1, y + 1) = &breakoutCenterBrick;
}
}
}
_widget->update();
}
void GameCore::checkPongGameOver()
{
int cnt = 0;
Paddle *last = nullptr;
for (Paddle *p : _paddles) {
if (p->size > 0) {
cnt++;
last = p;
}
}
if (cnt == 1) {
// Winrar
breakoutCenterBrick.color = QBrush(last->cell->color.color().lighter(250));
for (Paddle *p : _paddles) {
while (p->size < 5 && p->grow(this)) {
// Nothing
}
}
addBreakoutBlocks();
}
}
void GameCore::pauseAndResume()
{
if(_t->isActive()) {
_t->stop();
} else {
_t->start();
}
}
void GameCore::addFood()
{
for (int i = 0; i < 10; ++i) {
int x = qrand() % _width;
int y = qrand() % _height;
if (FIELD(x, y)->canSpawnFood()) {
setField(x, y, &food[qrand() % (sizeof(food) / sizeof(*food))]);
break;
}
}
}
void GameCore::paint(QPaintEvent *event)
{
QPainter p(_widget);
const int width = qMin(_width, (event->rect().x() + event->rect().width() + SCALING - 1) / SCALING);
const int height = qMin(_height, (event->rect().y() + event->rect().height() + SCALING - 1) / SCALING);
for (int y = event->rect().y() / SCALING; y < height; ++y) {
if (y < 0)
continue;
for (int x = event->rect().x() / SCALING; x < width; ++x) {
if (x < 0)
continue;
const Cell *c = FIELD(x, y);
if (c == &empty)
continue;
p.setBrush(c->color);
p.drawRect(x * SCALING, y * SCALING, SCALING-1, SCALING-1);
}
}
}