summaryrefslogblamecommitdiffstats
path: root/src/snake.cpp
blob: 5246a3c1c5ccf6f278e2084ddb0a8b76a557e61a (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_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) = &empty;
        }
    }
    _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);
        }
    }
}