#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_WALL 1
#define CELL_SNAKE 2
#define CELL_FOOD 3
#define CELL_BRICK 4
#define CELL_SNAKEBRICK 5
#define AXIS_X 0
#define AXIS_Y 1
#define FIELD(x,y) _field[(x) + ((y) * _width)]
struct Cell
{
int type;
QBrush color;
Cell(int t, QColor c) : type(t), color(QBrush(c)) {}
bool isFood() const { return type == CELL_FOOD; }
bool willKill() const { return type == CELL_WALL || type == CELL_SNAKE || type == CELL_SNAKEBRICK; }
bool isFree() const { return type == CELL_FREE; }
bool isBrick() const { return type == CELL_BRICK || type == CELL_SNAKEBRICK; }
bool canSpawnFood() const { return type == CELL_SNAKE || type == CELL_FREE || type == CELL_BRICK; }
bool ballWillDestroy() const { return isFood() || isBrick(); }
};
struct Ball
{
int x, y;
int dx, dy;
const Cell *cell;
QPoint gridPos;
Ball(int startX, int startY) : x(startX), y(startY), dx(qrand() % 4 + 1), dy(qrand() % 4 + 1),
cell(new Cell(CELL_WALL, QColor::fromHsv(qrand() % 360, 32 + qrand() % 64, 32 + qrand() % 64))) {}
virtual ~Ball() { delete cell; }
};
struct Paddle
{
int x, y;
int axis;
int size;
const Cell *cell;
Paddle(int startX, int startY, GameCore *field, int ax) : x(startX), y(startY), axis(ax), size(5),
cell(new Cell(CELL_WALL, QColor::fromHsv(qrand() % 360, 192 + qrand() % 64, 192 + qrand() % 64))) {
for (int i = 0; i < size; ++i) {
if (axis == AXIS_X) {
field->setField(x + i, y, cell);
} else {
field->setField(x, y + i, cell);
}
}
}
};
const Cell empty(CELL_FREE, QColor());
const Cell wall(CELL_WALL, QColor::fromRgb(80, 80, 90));
const Cell snakebrick(CELL_SNAKEBRICK, QColor::fromRgb(200, 200, 200));
const Cell brick(CELL_BRICK, QColor::fromRgb(255, 255, 255));
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)),
};
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) {
_field[x + _width * y] = val;
_widget->update(x * SCALING, y * SCALING, SCALING, SCALING);
}
GameCore::GameCore(QWidget *widget)
: _widget(widget),
_deaths(0),
_lastMeal(QDateTime::currentMSecsSinceEpoch()),
_field(nullptr)
{
qsrand((uint)_lastMeal);
_width = widget->width() / SCALING;
_height = widget->height() / SCALING;
if (_width <= 0 || _height <= 0)
return;
initField();
// 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));
for (int y = 15; y < _height - 16; y += 3) {
for (int x = 15; x < _width - 16; x += 3) {
FIELD(x, y) = &brick;
FIELD(x + 1, y) = &brick;
FIELD(x, y + 1) = &brick;
FIELD(x + 1, y + 1) = &brick;
}
}
qDebug() << "Field:" << _width << _height;
addFood();
QTimer *t = new QTimer(widget);
t->start(15);
// GAME
QTimer::connect(t, &QTimer::timeout, [this]() {
//
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);
}
}
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 (QDateTime::currentMSecsSinceEpoch() - _lastMeal > 60000) {
_lastMeal = QDateTime::currentMSecsSinceEpoch();
addFood();
}
}
//
// 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
const QPoint noo(nx / SCALING, ny / SCALING);
if (old != noo && !FIELD(noo.x(), noo.y())->isFree()) {
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;
}
// Destroy cell?
if (FIELD(noo.x(), noo.y())->ballWillDestroy()) {
if (FIELD(noo.x(), noo.y())->isFood()) {
// TODO: Make paddle that last touched the ball bigger
addFood();
}
if (FIELD(noo.x(), noo.y())->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 {
// Free movevemnt
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->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)->isFree()) {
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)->isFree()) {
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)->isFree()) {
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)->isFree()) {
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
}
});
}
GameCore::~GameCore()
{
free(_field);
}
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 = &wall;
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 (qrand() % 2 == 0) {
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::initField()
{
if (_field == nullptr) {
_field = (const Cell**)calloc(_width * _height, sizeof(*_field));
}
for (int i = 0; i < _width; ++i) {
FIELD(i, 0) = &wall;
FIELD(i, _height - 1) = &wall;
}
for (int i = 0; i < _height; ++i) {
FIELD(0, i) = &wall;
FIELD(_width - 1, i) = &wall;
}
for (int y = 1; y < _height - 1; ++y) {
for (int x = 1; x < _width - 1; ++x) {
FIELD(x, y) = ∅
}
}
_widget->update();
}
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->isFree())
continue;
p.setBrush(c->color);
p.drawRect(x * SCALING, y * SCALING, SCALING-1, SCALING-1);
}
}
}