8
\$\begingroup\$

I made a QT version of "Snake console game in C++" using Qt Creator 4.9.0 on Windows 7:

Picture of the running game

I would like to know what can be improved, focused on the QT stuff since im new to it.
I seperated the program as followed:

QT-Classes

  • SnakeWindow: Contains the GUI Elements like the Buttons, LCD Displays and The SnakeBoard to display the game. It also sends the button signals to the SnakeBoard and receives the score and delay to be displayed in the LCD Displays.
  • SnakeBoard: This is basically an extended QFrame which runs all the events like moving the snake and reacting to button events. It uses the non QT class Board which was already used in the console snakeGame

Non-QT-Classes

  • Board: Describes the field were the snake is moved arround. Also generates food at a random position, let the snake grow and check for the game over conditions wall or snake hit. It contains a Snake to handle snake related stuff.
  • Snake: Represents the Snake. Places the snake in the middle of the board. Enables to move the snake in all directions. Also the snake can be grown.

main.cpp

#include "SnakeWindow.h"

#include <QApplication>
#include <QDebug>

int main(int argc, char *argv[])
    try{
    QApplication app(argc, argv);
    SnakeWindow window;
    //window.show();
    window.showFullScreen();
    return app.exec();
}
catch(...) {
    qDebug() << "Unknown Error\n";
}

SnakeWindow.h

#ifndef SNAKEWINDOW_H
#define SNAKEWINDOW_H

#include <QWidget>

namespace Ui {
class SnakeWindow;
}

class SnakeWindow : public QWidget
{
    Q_OBJECT

public:
    explicit SnakeWindow(QWidget *parent = nullptr);
    ~SnakeWindow();

private:
    Ui::SnakeWindow *ui;
};



#endif // SNAKEWINDOW_H

SnakeWindow.cpp

#include "SnakeWindow.h"
#include "ui_SnakeWindow.h"

SnakeWindow::SnakeWindow(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::SnakeWindow)
{
    ui->setupUi(this);

    connect(ui->startButton, &QPushButton::clicked,
            ui->snakeBoard, &SnakeBoard::start);
    connect(ui->quitButton, &QPushButton::clicked,
            qApp, &QApplication::quit);
    connect(ui->pauseButton, &QPushButton::clicked,
            ui->snakeBoard, &SnakeBoard::pause);
    connect(ui->snakeBoard, &SnakeBoard::scoreChanged,
            [=](int score) {ui->scoreLcd->display(score);});
    connect(ui->snakeBoard, &SnakeBoard::delayChanged,
            [=](int level) {ui->delayLcd->display(level);});
}

SnakeWindow::~SnakeWindow()
{
    delete ui;
}

SnakeBoard.h

#ifndef SNAKEBOARD_H
#define SNAKEBOARD_H

#include <QFrame>
#include <QBasicTimer>

#include "Board.h"

class SnakeBoard : public QFrame {
    Q_OBJECT
public:
    SnakeBoard(QWidget* parent = nullptr);

    QSize sizeHint() const override;
    QSize minimumSizeHint() const override;

public slots:
    void start();
    void pause();
    void gameOver();

signals:
    void scoreChanged(int score);
    void delayChanged(int delay);

protected:
    void paintEvent(QPaintEvent *event) override;
    void keyPressEvent(QKeyEvent *event) override;
    void timerEvent(QTimerEvent *event) override;

private:
    int squareWidth();
    int squareHeight();

    void drawField(
        QPainter& painter, int x, int y, snakeGame::FieldType fieldType);

    void drawWall(QPainter& painter, int x, int y);
    void drawFood(QPainter& painter, int x, int y);

    void drawSnakeHead(QPainter& painter, int x, int y);
    void drawSnakeSegment(QPainter& painter, int x, int y);

    static constexpr auto boardWidth{40};
    static constexpr auto boardHeight{20};

    static constexpr auto initDelay{300};
    static constexpr auto initPoints{100};

    QBasicTimer timer;

    snakeGame::Board board{boardWidth, boardHeight};
    snakeGame::SnakeDirection snakeDirection{snakeGame::SnakeDirection::right};

    bool isStarted{false};
    bool isPaused{false};
    bool isGameOver{false};
    bool snakeWasMoved{true};
    int score{0};
    int delay{initDelay};
    int points{initPoints};
};

#endif // SNAKEBOARD_H

SnakeBoard.cpp

#include "SnakeBoard.h"

#include <QKeyEvent>
#include <QColor>
#include <QPainter>
#include <QRgb>
#include <QTimerEvent>

#include <QDebug>

using namespace snakeGame;

SnakeBoard::SnakeBoard(QWidget* parent)
    : QFrame{parent},
    board{boardWidth, boardHeight},
    snakeDirection{SnakeDirection::right}
{
    setFrameStyle(QFrame::Panel | QFrame::Sunken);
    setFocusPolicy(Qt::StrongFocus);

    board.updateSnakePosition();
    board.placeFood();
}

QSize SnakeBoard::sizeHint() const
{
    return QSize(boardWidth * 15 + frameWidth() * 2,
                 boardHeight * 15 + frameWidth() * 2);
}

QSize SnakeBoard::minimumSizeHint() const
{
    return QSize(boardWidth * 5 + frameWidth() * 2,
                 boardHeight * 5 + frameWidth() * 2);
}

void SnakeBoard::start()
{
    if (isGameOver) {
        isGameOver = false;
        board.reset();
        board.updateSnakePosition();
        board.placeFood();

        score = 0;
        points = initPoints;
        delay = initDelay;
    }

    if (isPaused)
        return;

    isStarted = true;

    emit scoreChanged(score);
    emit delayChanged(delay);

    timer.start(delay, this);
}

void SnakeBoard::pause()
{
    if (!isStarted)
        return;

    isPaused = !isPaused;
    if (isPaused) {
        timer.stop();
    }
    else {
        timer.start(delay, this);
    }
    update();
}

void SnakeBoard::gameOver()
{
    timer.stop();
    isGameOver = true;
    isStarted = false;
}

void SnakeBoard::paintEvent(QPaintEvent *event)
{
    QFrame::paintEvent(event);

    QPainter painter(this);
    QRect rect = contentsRect();


    if(isGameOver) {
        QFont font;
        font.setPixelSize(20);
        painter.setFont(font);
        painter.drawText(rect, Qt::AlignCenter, tr("Game Over"));
        return;
    }

    if(!isStarted) {
        QFont font;
        font.setPixelSize(20);
        painter.setFont(font);
        painter.drawText(rect, Qt::AlignCenter, tr(
            "Press start\n Use arrow keys to control the Snake"));
        return;
    }

    if (isPaused) {
        QFont font;
        font.setPixelSize(20);
        painter.setFont(font);
        painter.drawText(rect, Qt::AlignCenter, tr("Pause"));
        return;
    }

    auto boardTop = rect.bottom() - boardHeight * squareHeight();

    for (int i = 0; i < boardHeight; ++i) {
        for (int j = 0; j < boardWidth; ++j) {
            auto fieldType =
                board.fieldTypeAt(
                    static_cast<std::size_t>(j),
                    static_cast<std::size_t>(i));
            drawField(painter, rect.left() + j * squareWidth(),
                      boardTop + i * squareHeight(), fieldType);
        }
    }
}


void SnakeBoard::keyPressEvent(QKeyEvent *event)
{
    auto key = event->key();

    if (key == Qt::Key_P) {
        emit pause();
    }
    if (key == Qt::Key_Space) {
        emit start();
    }
    else if (!isStarted || isGameOver || !snakeWasMoved) {
        QFrame::keyPressEvent(event);
        return;
    }

    switch (key) {
    case Qt::Key_Left:       
        if (snakeDirection != SnakeDirection::right) {
            snakeDirection = SnakeDirection::left;
            snakeWasMoved = false;
        }
        break;
    case Qt::Key_Right:
        if (snakeDirection != SnakeDirection::left) {
            snakeDirection = SnakeDirection::right;
            snakeWasMoved = false;
        }
        break;
    case Qt::Key_Down:
        if (snakeDirection != SnakeDirection::up) {
            snakeDirection = SnakeDirection::down;
            snakeWasMoved = false;
        }
        break;
    case Qt::Key_Up:
        if (snakeDirection != SnakeDirection::down) {
            snakeDirection = SnakeDirection::up;
            snakeWasMoved = false;
        }
        break;
    default:
        QFrame::keyPressEvent(event);
    }
}

void SnakeBoard::timerEvent(QTimerEvent *event)
{
    if (isGameOver){
        QFrame::timerEvent(event);
        return;
    }

    if (event->timerId() == timer.timerId()) {

        board.moveSnake(snakeDirection);
        snakeWasMoved = true;

        if (board.snakeHitFood()) {
            board.eatFood();
            board.growSnake();
            board.updateSnakePosition();
            board.placeFood();
            score += points;
            points += static_cast<double>(initDelay / delay) * initPoints;
            delay -= 4;

            emit scoreChanged(score);
            emit delayChanged(delay);
        }
        else if (board.snakeHitWall() || board.snakeHitSnake()) {
            emit gameOver();
        }
        else {
            board.updateSnakePosition();
        }

        update();

        timer.start(delay, this);
    }
    else {
        QFrame::timerEvent(event);
    }
}

int SnakeBoard::squareWidth()
{
    return contentsRect().width() / boardWidth;
}

int SnakeBoard::squareHeight()
{
    return contentsRect().height() / boardHeight;
}

void SnakeBoard::drawField(
    QPainter& painter, int x, int y, snakeGame::FieldType fieldType)
{
    switch(fieldType){
    case FieldType::empty:
        break;
    case FieldType::wall:
        drawWall(painter, x, y);
        break;
    case FieldType::food:
        drawFood(painter, x, y);
        break;
    case FieldType::snakeHead:
        drawSnakeHead(painter, x, y);
        break;
    case FieldType::snakeSegment:
        drawSnakeSegment(painter, x, y);
        break;
    }
}

void SnakeBoard::drawWall(QPainter& painter, int x, int y)
{
    constexpr auto colorBrown = 0xbf8040;

    QColor color = QRgb{colorBrown};
    painter.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2,
                     color);

    painter.setPen(color.light());
    painter.drawLine(x, y + squareHeight() - 1, x, y);
    painter.drawLine(x, y, x + squareWidth() - 1, y);

    painter.setPen(color.dark());
    painter.drawLine(x + 1, y + squareHeight() - 1,
                     x + squareWidth() - 1, y + squareHeight() - 1);
    painter.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
                     x + squareWidth() - 1, y + 1);
}

void SnakeBoard::drawFood(QPainter& painter, int x, int y)
{
    constexpr auto colorRed = 0xff0000;
    QColor color = QRgb{colorRed};
    painter.setPen(color.light());
    painter.setBrush(QBrush{color});
    painter.drawEllipse(x +1,y +1,squareWidth() -3, squareHeight() -3);
}

void SnakeBoard::drawSnakeHead(QPainter& painter, int x, int y)
{
    constexpr auto colorDarkerLimeGreen = 0x00b300;

    QColor color = QRgb{colorDarkerLimeGreen};
    painter.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2,
                     color);

    painter.setPen(color.light());
    painter.drawLine(x, y + squareHeight() - 1, x, y);
    painter.drawLine(x, y, x + squareWidth() - 1, y);

    painter.setPen(color.dark());
    painter.drawLine(x + 1, y + squareHeight() - 1,
                     x + squareWidth() - 1, y + squareHeight() - 1);
    painter.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
                     x + squareWidth() - 1, y + 1);
}

void SnakeBoard::drawSnakeSegment(QPainter& painter, int x, int y)
{
    constexpr auto colorLimeGreen = 0x00e600;

    QColor color = QRgb{colorLimeGreen};
    painter.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2,
                     color);

    painter.setPen(color.light());
    painter.drawLine(x, y + squareHeight() - 1, x, y);
    painter.drawLine(x, y, x + squareWidth() - 1, y);

    painter.setPen(color.dark());
    painter.drawLine(x + 1, y + squareHeight() - 1,
                     x + squareWidth() - 1, y + squareHeight() - 1);
    painter.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
                     x + squareWidth() - 1, y + 1);
}

Board.h

#ifndef BOARD_H
#define BOARD_H

#include "Snake.h"

#include <vector>
#include <random>
#include <iosfwd>

namespace snakeGame {

    enum class SnakeDirection;

    enum class FieldType {
        empty,
        snakeSegment,
        snakeHead,
        wall,
        food
    };

    enum class SnakeDirection {
        up, right, down, left
    };

    class Board
    {
    public:
        Board(std::size_t width, std::size_t height);

        void reset();

        void placeFood();
        void updateSnakePosition();
        bool snakeHitFood() const;
        void eatFood();
        void growSnake();
        bool snakeHitWall() const;
        bool snakeHitSnake() const;
        void moveSnake(SnakeDirection snakeDirection);

        void debugPrintSnakeCoordinates();

        FieldType fieldTypeAt(std::size_t x, std::size_t y);
    private:
        std::vector<std::vector<FieldType>> initFieldWithWalls(
            std::size_t width, std::size_t height);
        void removeOldSnakePosition(const std::vector<SnakeSegment>& body);
        void addNewSnakePosition(const std::vector<SnakeSegment>& body);

        const std::size_t mWidth;
        const std::size_t mHeight;

        Snake mSnake;
        std::vector<std::vector<FieldType>> mFields;

        std::random_device mRandomDevice;
        std::default_random_engine mGenerator;
        std::uniform_int_distribution<std::size_t> mWidthDistribution;
        std::uniform_int_distribution<std::size_t> mHeightDistribution;

        friend std::wostream& operator<<(std::wostream& os, const Board& obj);
    };

    std::wostream& operator<<(std::wostream& os, const Board& obj);

}
#endif

Board.cpp

#include "Board.h"

#include <algorithm>
#include <iostream>

namespace snakeGame {

    Board::Board(std::size_t width, std::size_t height)
        : mWidth{width},
        mHeight{height},
        mSnake{ width, height },
        mFields{ initFieldWithWalls(width, height) },
        mRandomDevice{},
        mGenerator{ mRandomDevice() },
        mWidthDistribution{ 2, width - 3 },
        mHeightDistribution{ 2, height - 3 }
    {
    }

    void Board::reset()
    {
        mFields = initFieldWithWalls(mWidth, mHeight);
        mSnake = Snake{mWidth,mHeight};
    }

    void Board::placeFood()
    {
        for (;;) {
            auto x = mWidthDistribution(mGenerator);
            auto y = mHeightDistribution(mGenerator);

            if(mFields.at(y).at(x) == FieldType::empty){
                mFields.at(y).at(x) = FieldType::food;
                return;
            }
        }
    }

    void Board::updateSnakePosition()
    {
        auto snakeBody = mSnake.getBody();

        removeOldSnakePosition(snakeBody);
        addNewSnakePosition(snakeBody);
    }

    bool Board::snakeHitFood() const
    {
        auto pos = mSnake.getBody().at(0).pos;
        return mFields.at(pos.y).at(pos.x) == FieldType::food;
    }

    void Board::eatFood()
    {
        auto pos = mSnake.getBody()[0].pos;
        mFields.at(pos.y).at(pos.x) = FieldType::empty;
    }

    void Board::growSnake()
    {
        mSnake.grow();
    }

    bool Board::snakeHitWall() const
    {
        auto pos = mSnake.getBody()[0].pos;
        return mFields.at(pos.y).at(pos.x) == FieldType::wall;
    }

    bool Board::snakeHitSnake() const
    {
        auto pos = mSnake.getBody()[0].pos;
        return mFields.at(pos.y).at(pos.x) == FieldType::snakeSegment;
    }

    void Board::moveSnake(SnakeDirection snakeDirection)
    {
        switch (snakeDirection) {
        case SnakeDirection::right:
            mSnake.moveRight();
            break;

        case SnakeDirection::down:
            mSnake.moveDown();
            break;

        case SnakeDirection::left:
            mSnake.moveLeft();
            break;

        case SnakeDirection::up:
            mSnake.moveUp();
            break;
        }
    }

    void Board::debugPrintSnakeCoordinates()
    {
        auto body = mSnake.getBody();

        for (std::size_t i = 0; i < body.size(); ++i) {
            auto pos = body.at(i).pos;

            std::wcout << "nr:" << i << "x:" << pos.x
                       << "\t" << "y:" << pos.y << "\t";

            auto field = mFields.at(pos.y).at(pos.x);

            switch(field)
            {
            case FieldType::snakeHead:
                std::wcout << L"Head\t";
                [[fallthrough]];
            case FieldType::snakeSegment:
                std::wcout << L"Body\n";
                [[fallthrough]];
            default:
                std::wcout << L"    \n";
            }
        }
    }

    FieldType Board::fieldTypeAt(std::size_t x, std::size_t y)
    {
        return mFields.at(y).at(x);
    }

    std::vector<std::vector<FieldType>> Board::initFieldWithWalls(
        std::size_t width, std::size_t height)
    {
        std::vector<FieldType> row(width, FieldType::empty);
        std::vector<std::vector<FieldType>> field(height, row);

        std::fill(field.at(0).begin(), field.at(0).end(), FieldType::wall);
        std::fill(field.at(field.size() - 1).begin(),
                  field.at(field.size() - 1).end(), FieldType::wall);

        for (auto it_row = field.begin() + 1;
             it_row < field.end() - 1; ++it_row) {
            (*it_row).at(0) = FieldType::wall;
            (*it_row).at(it_row->size() - 1) = FieldType::wall;
        }
        return field;
    }

    void Board::removeOldSnakePosition(const std::vector<SnakeSegment>& body)
    {
        for (const auto& snakeSegment : body) {
            auto prev = snakeSegment.prev;
            mFields.at(prev.y).at(prev.x) = FieldType::empty;
        }
    }

    void Board::addNewSnakePosition(const std::vector<SnakeSegment>& body)
    {
        auto first{ true };
        for (const auto& snakeSegment : body) {
            auto pos = snakeSegment.pos;

            if (first) {
                mFields.at(pos.y).at(pos.x) = FieldType::snakeHead;
                first = false;
            }
            else {
                mFields.at(pos.y).at(pos.x) = FieldType::snakeSegment;
            }
        }
    }


    std::wostream& operator<<(std::wostream& os, const Board& obj)
    {
        for (const auto& row : obj.mFields) {
            for (const auto& element : row) {

                switch(element){
                case FieldType::empty:
                    os << L' ';
                    break;
                case FieldType::wall:
                    os << L'#';
                    break;
                case FieldType::food:
                    os << L'*';
                    break;
                case FieldType::snakeHead:
                    os << L'@';
                    break;
                case FieldType::snakeSegment:
                    os << L'o';
                    break;
                }
            }
            os << '\n';
        }
        return os;
    }

}

Snake.h

#ifndef SNAKE_H
#define SNAKE_H

#include <vector>
#include <cstddef>

namespace snakeGame {

    struct Point {
        std::size_t x;
        std::size_t y;
    };

    struct SnakeSegment
    {
        Point pos{ 0 , 0 };
        Point prev{ pos };
    };

    class Snake
    {
    public:
        Snake(std::size_t boardWidth, std::size_t boardHeight);

        std::vector<SnakeSegment> getBody() const;

        void moveRight();
        void moveDown();
        void moveLeft();
        void moveUp();
        void grow();

    private:
        void safeCurrentPosToLastOfFirstElement();
        void moveRemainingElements();

        std::vector<SnakeSegment> mBody;
    };

    std::vector<SnakeSegment> initSnake(
        std::size_t fieldWidth, std::size_t fieldHeight);

}
#endif

Snake.cpp

#include "Snake.h"

namespace snakeGame {

    Snake::Snake(std::size_t fieldWidth, std::size_t fieldHeight)
        :mBody{ initSnake(fieldWidth, fieldHeight) }
    {
    }

    std::vector<SnakeSegment> Snake::getBody() const
    {
        return mBody;
    }


    void Snake::moveRight()
    {
        safeCurrentPosToLastOfFirstElement();
        ++mBody.at(0).pos.x;
        moveRemainingElements();
    }

    void Snake::moveDown()
    {
        safeCurrentPosToLastOfFirstElement();
        ++mBody.at(0).pos.y;
        moveRemainingElements();
    }

    void Snake::moveLeft()
    {
        safeCurrentPosToLastOfFirstElement();
        --mBody.at(0).pos.x;
        moveRemainingElements();
    }

    void Snake::moveUp()
    {
        safeCurrentPosToLastOfFirstElement();
        --mBody.at(0).pos.y;
        moveRemainingElements();
    }

    void Snake::grow()
    {
        mBody.push_back(SnakeSegment{
            {mBody.at(mBody.size() - 1).prev.x,
            mBody.at(mBody.size() - 1).prev.y}
        });
    }

    void Snake::safeCurrentPosToLastOfFirstElement()
    {
        mBody.at(0).prev.x = mBody.at(0).pos.x;
        mBody.at(0).prev.y = mBody.at(0).pos.y;
    }

    void Snake::moveRemainingElements()
    {
        for (std::size_t i = 1; i < mBody.size(); ++i) {
            mBody.at(i).prev.x = mBody.at(i).pos.x;
            mBody.at(i).prev.y = mBody.at(i).pos.y;
            mBody.at(i).pos.x = mBody.at(i - 1).prev.x;
            mBody.at(i).pos.y = mBody.at(i - 1).prev.y;
        }
    }


    std::vector<SnakeSegment> initSnake(
        std::size_t boardWidth, std::size_t boardHeight)
    {
        auto x = boardWidth / 2;
        auto y = boardHeight / 2;
        std::vector<SnakeSegment> body{
            SnakeSegment{ {x, y} },
            SnakeSegment{ {x - 1, y} },
        };
        return body;
    }

}

Snake_qt.pro

QT += widgets
CONFIG += c++17

SOURCES += \
    Board.cpp \
    Snake.cpp \
    SnakeBoard.cpp \
    SnakeWindow.cpp \
    main.cpp

HEADERS += \
    Board.h \
    Snake.h \
    SnakeBoard.h \
    SnakeWindow.h

FORMS += \
    SnakeWindow.ui

SnakeWindow.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SnakeWindow</class>
 <widget class="QWidget" name="SnakeWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>771</width>
    <height>618</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Snake</string>
  </property>
  <property name="styleSheet">
   <string notr="true"/>
  </property>
  <layout class="QGridLayout" name="gridLayout">
   <item row="1" column="2" colspan="2">
    <widget class="QLCDNumber" name="delayLcd">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
       <horstretch>0</horstretch>
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="font">
      <font>
       <pointsize>20</pointsize>
      </font>
     </property>
     <property name="styleSheet">
      <string notr="true">background-color: rgb(255, 255, 127);</string>
     </property>
     <property name="smallDecimalPoint">
      <bool>false</bool>
     </property>
     <property name="digitCount">
      <number>5</number>
     </property>
     <property name="mode">
      <enum>QLCDNumber::Dec</enum>
     </property>
     <property name="segmentStyle">
      <enum>QLCDNumber::Flat</enum>
     </property>
    </widget>
   </item>
   <item row="2" column="0" colspan="4">
    <widget class="SnakeBoard" name="snakeBoard">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
       <horstretch>0</horstretch>
       <verstretch>8</verstretch>
      </sizepolicy>
     </property>
     <property name="styleSheet">
      <string notr="true">background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(255, 178, 102, 255), stop:0.55 rgba(235, 148, 61, 255), stop:0.98 rgba(0, 0, 0, 255), stop:1 rgba(0, 0, 0, 0));</string>
     </property>
     <property name="frameShape">
      <enum>QFrame::StyledPanel</enum>
     </property>
     <property name="frameShadow">
      <enum>QFrame::Raised</enum>
     </property>
    </widget>
   </item>
   <item row="3" column="3">
    <widget class="QPushButton" name="quitButton">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
       <horstretch>0</horstretch>
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="font">
      <font>
       <pointsize>20</pointsize>
      </font>
     </property>
     <property name="focusPolicy">
      <enum>Qt::NoFocus</enum>
     </property>
     <property name="styleSheet">
      <string notr="true">
background-color: rgb(255, 170, 255);</string>
     </property>
     <property name="text">
      <string>Quit</string>
     </property>
    </widget>
   </item>
   <item row="0" column="0">
    <widget class="QLabel" name="scoreLabel">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="font">
      <font>
       <pointsize>20</pointsize>
       <weight>50</weight>
       <bold>false</bold>
      </font>
     </property>
     <property name="text">
      <string>Score</string>
     </property>
     <property name="alignment">
      <set>Qt::AlignCenter</set>
     </property>
    </widget>
   </item>
   <item row="1" column="0" colspan="2">
    <widget class="QLCDNumber" name="scoreLcd">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
       <horstretch>0</horstretch>
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="font">
      <font>
       <pointsize>20</pointsize>
      </font>
     </property>
     <property name="styleSheet">
      <string notr="true">background-color: rgb(255, 255, 127);</string>
     </property>
     <property name="smallDecimalPoint">
      <bool>false</bool>
     </property>
     <property name="digitCount">
      <number>10</number>
     </property>
     <property name="segmentStyle">
      <enum>QLCDNumber::Flat</enum>
     </property>
    </widget>
   </item>
   <item row="0" column="2" colspan="2">
    <widget class="QLabel" name="delayLabel">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
     </property>
     <property name="font">
      <font>
       <pointsize>20</pointsize>
       <weight>50</weight>
       <bold>false</bold>
      </font>
     </property>
     <property name="text">
      <string>Delay in ms</string>
     </property>
     <property name="alignment">
      <set>Qt::AlignCenter</set>
     </property>
    </widget>
   </item>
   <item row="3" column="0">
    <widget class="QPushButton" name="startButton">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
       <horstretch>0</horstretch>
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="font">
      <font>
       <pointsize>20</pointsize>
      </font>
     </property>
     <property name="focusPolicy">
      <enum>Qt::NoFocus</enum>
     </property>
     <property name="styleSheet">
      <string notr="true">background-color: rgb(85, 255, 0);</string>
     </property>
     <property name="text">
      <string>Start</string>
     </property>
    </widget>
   </item>
   <item row="3" column="1">
    <widget class="QPushButton" name="pauseButton">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
       <horstretch>0</horstretch>
       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="font">
      <font>
       <pointsize>20</pointsize>
      </font>
     </property>
     <property name="focusPolicy">
      <enum>Qt::NoFocus</enum>
     </property>
     <property name="styleSheet">
      <string notr="true">background-color: rgb(170, 255, 0);</string>
     </property>
     <property name="text">
      <string>Pause</string>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <customwidgets>
  <customwidget>
   <class>SnakeBoard</class>
   <extends>QFrame</extends>
   <header location="global">SnakeBoard.h</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>
\$\endgroup\$
4
+50
\$\begingroup\$

Here are some things that may help you improve your program.

Use consistent formatting

It appears that your code uses tabs in some places and spaces in others, making the code appear badly formatted unless the settings in the reader's editor are set exactly the same as in yours. To prevent this, I recommend using spaces everywhere. It's a habit that may even earn you more money!

Fix the bug

There is a subtle bug in the code. If the user crashes into the left wall, it's no longer possible to restart the game.

Don't store data unnecessary data

In the Board class, there is not any need to have the mRandomDevice stored as a member. Instead, initialize mGenerator like this:

mGenerator{ std::random_device{}() },

This creates, uses, and discards a std::random_device instance.

Carefully consider the use of data structures

In the Board class, the internal representation is a vector of vectors. However, since these are fixed size, it seems that perhaps std::array of std::array would be more appropriate. Alternatively, a single std::array could be used with helper routines to convert from, say, your Point class to the appropriate location in the data structure.

Avoid convoluted control flows

The placeFood() routine is currently like this:

void Board::placeFood()
{
    for (;;) {
        auto x = mWidthDistribution(mGenerator);
        auto y = mHeightDistribution(mGenerator);

        if(mFields.at(y).at(x) == FieldType::empty){
            mFields.at(y).at(x) = FieldType::food;
            return;
        }
    }
}

I think it could be made easier to read and understand if it's written like this:

void Board::placeFood()
{
    auto [x, y] = randomEmptyLocation(); 
    mFields.at(y).at(x) = FieldType::food;
}

Note that this is using the C++17 structured binding declaration for convenience. This also uses two helper functions:

std::tuple<std::size_t, std::size_t> Board::randomLocation() {
    return { mWidthDistribution(mGenerator), 
             mHeightDistribution(mGenerator)};
}

std::tuple<std::size_t, std::size_t> Board::randomEmptyLocation() {
    auto [x, y] = randomLocation(); 
    while (fieldTypeAt(x, y) != FieldType::empty) {
        std::tie(x, y) = randomLocation();
    }
    return {x, y};
}

Also, of course, #include <tuple> is required to use this. Alternatively, instead of using individual x and y, the interface could be redesigned to more fully use the existing Point struct.

Avoid wasteful copies

The Snake::getBody() function duplicates and returns an entire vector. This is not really necessary since most places that call this function are only doing so to get the position of the head. For that reason, a better approach would be to provide headloc() function that would return the location of the head as either a Point or as a std::tuple as shown above.

Think about more efficient algorithms

The updateSnakePosition() code is not very efficient. As mentioned above, it makes a duplicate of the entire snake, but then it erases the entire snake from the board and then adds it back again in the new position. This is wholly unnecessary because the only updates required are the head, the segment just behind the head and the tail. It's not critical here, but it's useful to get into the habit of writing efficient code.

Think carefully about the user interface

Is there a purpose to the gradient on the field? It seems to me to be solely a distraction because it doesn't convey anything to the user and is, in my humble opinion, not aesthetically pleasing.

Use const where practical

Several of the functions could be const but are not, such as SnakeBoard::squareHeight and Board::fieldTypeAt. It would be better to declare them const.

\$\endgroup\$
  • \$\begingroup\$ thanks. many good suggestions. i give you sppecial credit for the space vs tab survey. \$\endgroup\$ – Sandro4912 May 21 at 7:17
  • \$\begingroup\$ i have one concern. If i switch the datastructure in class Board to std::array for mFields i need to know its size during compile time and cannot pass the size with the constructor argumenets Board(std::size_t width, std::size_t height); \$\endgroup\$ – Sandro4912 May 21 at 16:57
  • \$\begingroup\$ @Sandro4912: that's true, but on the other hand, the width and height in the existing code are constexpr, so that's not much of a loss. If you want to allow a more dynamic sizing at runtime, then, yes, it would make sense to use a dynamically sized data structure. \$\endgroup\$ – Edward May 21 at 17:03
  • \$\begingroup\$ I guess you are right. After all we only want one board with the right size in all the game, not several different sized ones. Over engineering in this case i guess. \$\endgroup\$ – Sandro4912 May 21 at 17:05
  • \$\begingroup\$ As a practical matter, efficiency probably doesn't really matter much in this instance on modern computers. So if you think you might want to make it dynamic in the future, it may be better just to leave it as it is. Most of the time in the current program is taken up by deliberate delays between screen updates anyway, and unless the player is very good or the hardware very bad, I doubt anyone will notice much difference either way. \$\endgroup\$ – Edward May 21 at 17:17
1
\$\begingroup\$

Like hinted in the other answer there is a quite nasty bug which prevents the Game to restart after the Snake runs into the left wall.

I hunted the bug und fixed it. Maybe its interesing whats the cause:

After the crash with the left wall GameOver is still emitted in the timerEvent when the start Button is pressed. The Reason is the function "snakeHitSnake()" gives a false true here:

void SnakeBoard::timerEvent(QTimerEvent *event)
{
        ...
        else if (board.snakeHitWall() || board.snakeHitSnake()) {
            emit gameOver();
        }
        ...
}

To fix this issue we must realize that the start Button is faulty:

void SnakeBoard::start()
{
    if (isGameOver) {
        isGameOver = false;
        board.reset();
        board.updateSnakePosition();
        board.placeFood();
...
}

In the if statement board.reset() is called:

void Board::reset()
{
    mFields = initFieldWithWalls(mWidth, mHeight);
    mSnake = Snake{mWidth,mHeight};
}

That already reinits all the field and makes a fresh new snake. So board.updateSnakePosition(); after is not needed and causing the bug.

Simply omiting it fixxes it:

void SnakeBoard::start()
{
    if (isGameOver) {
        isGameOver = false;
        board.reset();
        board.placeFood();
...
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.