summaryrefslogblamecommitdiffstats
path: root/src/snake.cpp
blob: e855250c1c9aeecaf91c8549715875b0b9768353 (plain) (tree)
1
2
3
4
5
6
7
8
9
                     






                      
                    







                    
                   
                             

                    
                             
                         
                     




                                               
 

              



                 

                                                                         
                                                     
                                                                                                                                        
                                                     


                                                                                                                                          


                                                                  











                                                                                     





                     
                       
                                                                                                  

                                                                                                                  







                                    

               
                                                                                                      

                                                                                                            







                                                




























                                                      

  
           

       



                        
               
                                                                             
                                                                                                               
                                     
  
 





                                                                
                     
                
                                                    
                                                      
                    
 
                            

                                         

                                    




                                                              





                                                               




                                                                                                        
     

                        
                                              
              

                            
           
                                                               



















                                                                                                                                   

                                 
                 



                                                                              
                 









                                           
                 




                                                                                                

                                                          










                                                                                                                    
                                             


                                                             








                                                                    
                                                     


                                                                    
                         









                                                             

                                        

                              
                 



                                                                   
                                                                                                
                                                                
                          
             





                                                                            
         


                                                                                      
                                   

















                                                                   
                              
                                                         


                                                                 




















                                                                                                 











                                                                                     
                                




                                                          

                                  
                                             











                                                                                 
                                              

























                                                                               

                                      


















                                                                                                                        
                                                                              






                                                                                             
                                                                                         


























                                                                                                                         
                                                                              






                                                                                             
                                                                                         








                                                                                        



         
                     
 


                               
                 



                        

 
                                                                                  
 

                     





                                                            
                               

                   
                                                               





                               
                         
 
                                   


                                    
                                                    





                                                                  




                  
                        
 











                                                                                                  
     







                                                   
     


                                                  
     


















                                                           




                      





















                                                                                   








                               
                        



                                  

                                                                            




                  
                                        









                                                                                                           

                                        
                         
                                 



                                                                       
#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 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<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()),
     _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] = &empty;
    }
    // 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);
        }
    }
}