#include "snake.h" #include #include #include #include #include #include #include #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 AXIS_X 0 #define AXIS_Y 1 #define FIELD(x,y) _field[(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; } bool isPaddleFree() const { return type == CELL_FREE || type == CELL_SNAKEBRICK || type == CELL_BREAKOUT_BRICK || type == CELL_FOOD; } 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; } bool ballWillDestroy() const { return isFood() || isBrick(); } }; static const Cell empty(CELL_FREE, QColor()); static const Cell snakebrick(CELL_SNAKEBRICK, QColor::fromRgb(200, 200, 200)); 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 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()), _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)); 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 } }); } 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->isFree()) continue; p.setBrush(c->color); p.drawRect(x * SCALING, y * SCALING, SCALING-1, SCALING-1); } } }