7
\$\begingroup\$

I reworked the Game I did in FLTK some while ago: Hunt the Wumpus GUI (FLTK)

This time I used C++ with the Qt Framework. The result looks like this:

If you want to try out the game. The full code can be found in my Git Hub: https://github.com/sandro4912/hunt-the-wumpus

One more hint not find in the Instructions. If the main game is started you can type "show" to show all the hazards in the neighbour rooms. Or "hide" to hide them. I have a basic keylogger looking for these words to be typed to debug/cheat.

Since the codebase is to big to post it here, I want to focus on some important parts of the code.

  • Room: represents a single Room in the Dungeon.
  • DungeonView: The view which displays the dungeon. It manages dragging the player to annother room and shooting the arrows.
  • Dungeon: Manages the Scene and the DungeonView and the rooms
  • roomutility: helper functions to create the dungeon / connect the rooms.

Let me know what you think of the code. Is it easy to read / understand? Are there any bad practices?

Feel free to also check the other classes in the repo. If there's anything strange, let me know.

room.h

#ifndef ROOM_H
#define ROOM_H

#include <QGraphicsWidget>

#include <array>

class QAction;

class Room : public QGraphicsWidget
{
    Q_OBJECT
public:
    explicit Room(QGraphicsItem *parent = nullptr);

    void emitNeigbourHazards();

    [[nodiscard]] bool isEmpty() const;

    void setWumpus(bool value);
    [[nodiscard]] bool hasWumpus() const;

    void setPit(bool value);
    [[nodiscard]] bool hasPit() const;

    void setBat(bool value);
    [[nodiscard]] bool hasBat() const;

    void setPlayer(bool value);
    [[nodiscard]] bool hasPlayer() const;

    void setPlayerWasHere(bool value);
    [[nodiscard]] bool playerWasHere() const;

    void setTarget(bool value);
    [[nodiscard]] bool isTarget() const;

    void addNeighbour(Room *neighbour);
    [[nodiscard]] QVector<Room *> neighbours() const;

    void showContent(bool value);

    void clear();

    [[nodiscard]] QRectF boundingRect() const override;

    void paint(QPainter *painter,
               const QStyleOptionGraphicsItem *option,
               QWidget *widget) override;

    enum { Type = UserType + 1 };

    [[nodiscard]] int type() const override;
signals:
    void wumpusNear();
    void batNear();
    void pitNear();

    void playerDiedFromWumpus();
    void playerDiedFromPit();
    void playerDraggedByBat();

    void entered();

public slots:
    void enter();

protected:
    void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;

    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

    void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override;
    void dragLeaveEvent(QGraphicsSceneDragDropEvent *event) override;
    void dropEvent(QGraphicsSceneDragDropEvent *event) override;

private slots:
    void toggleGuessWumpus();
    void toggleGuessPit();
    void toggleGuessBat();

private:
    bool isNeighbour(Room *room) const;

    bool minDistanceReached(QGraphicsSceneMouseEvent *event,
                            Qt::MouseButton button);
    void executePlayerDrag(QGraphicsSceneMouseEvent *event);

    bool isPlayerDrag(QGraphicsSceneDragDropEvent *event);

    void drawRoom(QPainter *painter, const QImage &roomImage);
    void drawWumpus(QPainter *painter, const QImage &wumpusImage);
    void drawBat(QPainter *painter, const QImage &batImage);
    void drawPit(QPainter *painter, const QImage &pitImage);
    void drawPlayer(QPainter *painter);

    [[nodiscard]] QImage roomImage() const;
    [[nodiscard]] QImage roomVisitedImage() const;
    [[nodiscard]] QImage roomTargetImage() const;
    [[nodiscard]] QImage batImage() const;
    [[nodiscard]] QImage batConfirmedImage() const;
    [[nodiscard]] QImage pitImage() const;
    [[nodiscard]] QImage pitConfirmedImage() const;
    [[nodiscard]] QImage wumpusImage() const;
    [[nodiscard]] QImage wumpusConfirmedImage() const;
    [[nodiscard]] QImage playerImage() const;
    [[nodiscard]] QImage playerDraggedImage() const;

    bool mBatConfirmed{ false };

    bool mHasWumpus{ false };
    bool mHasPit{ false };
    bool mHasBat{ false };
    bool mHasPlayer{ false };
    bool mPlayerWasHere{ false };

    bool mGuessWumpus{ false };
    bool mGuessBat{ false };
    bool mGuessPit{ false };

    bool mShowContent{ false };

    bool mIsTarget{ false };

    QAction *mGuessWumpusAction;
    QAction *mGuessBatAction;
    QAction *mGuessPitAction;

    QVector<Room *> mNeighbours;
};

#endif // ROOM_H

room.cpp

#include "room.h"

#include "dragplayermimedata.h"

#include <QApplication>
#include <QAction>
#include <QCursor>
#include <QDrag>
#include <QMimeData>
#include <QGraphicsWidget>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>
#include <QMenuBar>
#include <QMouseEvent>
#include <QPainter>

Room::Room(QGraphicsItem *parent)
    : QGraphicsWidget(parent)
{     
    Q_ASSERT(roomImage().size() == roomVisitedImage().size());
    Q_ASSERT(roomVisitedImage().size() == roomTargetImage().size());
    Q_ASSERT(roomImage().height() ==
             pitImage().height() + batImage().height());
    Q_ASSERT(roomImage().height() ==
             pitImage().height() + playerImage().height());   
    Q_ASSERT(roomImage().height() ==
             pitImage().height() + wumpusImage().height());
    Q_ASSERT(roomImage().width() ==
             batImage().width() + playerImage().width() +
             wumpusImage().width());
    Q_ASSERT(batImage().size() == batConfirmedImage().size());
    Q_ASSERT(pitImage().size() == pitConfirmedImage().size());
    Q_ASSERT(wumpusImage().size() == wumpusConfirmedImage().size());

    setAcceptDrops(true);

    mGuessWumpusAction = new QAction{tr("Has &Wumpus")};
    mGuessWumpusAction->setCheckable(true);
    connect(mGuessWumpusAction, &QAction::triggered, 
            this, &Room::toggleGuessWumpus);

    mGuessBatAction = new QAction{tr("Has &Bat")};
    mGuessBatAction->setCheckable(true);
    connect(mGuessBatAction, &QAction::triggered, this, &Room::toggleGuessBat);

    mGuessPitAction = new QAction{tr("Has &Pit")};
    mGuessPitAction->setCheckable(true);
    connect(mGuessPitAction, &QAction::triggered, this, &Room::toggleGuessPit);

    addAction(mGuessWumpusAction);
    addAction(mGuessBatAction);
    addAction(mGuessPitAction);

    resize(boundingRect().size());

    setVisible(false);
}

void Room::emitNeigbourHazards()
{
    auto batEmitted = false;
    auto wumpusEmitted = false;
    auto pitEmitted = false;
    for(const auto& neigbour : mNeighbours) {
        if(neigbour == nullptr) {
            continue;
        }

        if (!batEmitted && neigbour->hasBat()) {
            emit batNear();
            batEmitted = true;
        }
        if (!wumpusEmitted && neigbour->hasWumpus()) {
            emit wumpusNear();
            wumpusEmitted = true;
        }
        if (!pitEmitted && neigbour->hasPit()) {
            emit pitNear();
            pitEmitted = true;
        }
    }
}

bool Room::isEmpty() const
{
    return !hasPlayer() && !hasBat()&& !hasPit() && !hasWumpus();
}

void Room::setWumpus(bool value)
{
    if(value && hasPlayer()) {
        emit playerDiedFromWumpus();
    }

    mHasWumpus = value;
    update();
}

bool Room::hasWumpus() const
{
    return mHasWumpus;
}

void Room::setPit(bool value)
{
    mHasPit = value;
    update();
}

bool Room::hasPit() const
{
    return mHasPit;
}

void Room::setBat(bool value)
{
    mHasBat = value;
    update();
}

bool Room::hasBat() const
{
    return mHasBat;
}

void Room::setPlayer(bool value)
{
    mHasPlayer = value;

    if (value) {
        mPlayerWasHere = true;
    }
    update();
}

bool Room::hasPlayer() const
{
    return mHasPlayer;
}

void Room::setPlayerWasHere(bool value)
{
    mPlayerWasHere = value;
    update();
}

bool Room::playerWasHere() const
{
    return mPlayerWasHere;
}

void Room::setTarget(bool value)
{
    mIsTarget = value;
    update();
}

bool Room::isTarget() const
{
    return mIsTarget;
}

void Room::addNeighbour(Room *neighbour)
{  
    mNeighbours.push_back(neighbour);
}

QVector<Room *> Room::neighbours() const
{
    return mNeighbours;
}

void Room::showContent(bool value)
{
    mShowContent = value;
    update();
}

void Room::clear()
{
    mBatConfirmed = false;

    mHasWumpus = false;
    mHasPit = false;
    mHasBat = false;
    mHasPlayer = false;
    mPlayerWasHere = false;

    mGuessWumpus = false;
    mGuessBat = false;
    mGuessPit = false;

    mShowContent = false;

    mIsTarget = false;
}

QRectF Room::boundingRect() const
{
    return QRectF{ 0, 0,
        static_cast<qreal>(roomImage().width()),
        static_cast<qreal>(roomImage().height())};
}

void Room::paint(
        QPainter *painter,
        const QStyleOptionGraphicsItem *option,
        QWidget *widget)
{
    Q_UNUSED(option)
    Q_UNUSED(widget)


    if (isTarget()) {
        drawRoom(painter, roomTargetImage());
    }
    else if (playerWasHere()) {
        drawRoom(painter, roomVisitedImage());
    }
    else {
        drawRoom(painter, roomImage());
    }

    if (hasPlayer()) {
        drawPlayer(painter);
    }

    if (mShowContent) {
        if (hasWumpus()) {
            drawWumpus(painter, wumpusConfirmedImage());
        }
        if (hasBat()) {
            drawBat(painter, batConfirmedImage());
        }
        if (hasPit()) {
            drawPit(painter, pitConfirmedImage());
        }
    }
    else {
        if (mGuessWumpus) {
            drawWumpus(painter, wumpusImage());
        }
        if (mBatConfirmed) {
            drawBat(painter, batConfirmedImage());
        }
        else if (mGuessBat) {
            drawBat(painter, batImage());
        }
        if (mGuessPit) {
            drawPit(painter, pitImage());
        }
    }
}

int Room::type() const
{
    return Type;
}

void Room::enter()
{   
    if (mHasWumpus) {
        emit playerDiedFromWumpus();
        return;
    }
    if (mHasPit) {
        emit playerDiedFromPit();
        return;
    }
    if (mHasBat) {
        emit playerDraggedByBat();
        mBatConfirmed = true;
        mGuessBatAction->setChecked(true);
        mGuessBatAction->setEnabled(false);
        return;
    }

    setVisible(true);
    for(auto &neighbour : mNeighbours) {
        neighbour->setVisible(true);
        neighbour->update();
    }

    emitNeigbourHazards();
    emit entered();
    setPlayer(true);
}

void Room::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
    if (hasPlayer()) {
        return;
    }

    QMenu menu;
    menu.addAction(mGuessWumpusAction);
    menu.addAction(mGuessBatAction);
    menu.addAction(mGuessPitAction);
    menu.exec(event->screenPos());
}

void Room::mousePressEvent(QGraphicsSceneMouseEvent *event)
// needs to be overriden to toggle mouseMoveEvent
{
    Q_UNUSED(event)
}

void Room::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if(!hasPlayer()) {
        return;
    }

    if (minDistanceReached(event, Qt::LeftButton)) {
        executePlayerDrag(event);
    }
}

void Room::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
// needs to be overriden to toggle mouseMoveEvent
{
    Q_UNUSED(event)
}

void Room::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
{
    const auto dragPlayerData =
            qobject_cast<const DragPlayerMimeData *>(event->mimeData());

    if(dragPlayerData && isNeighbour(dragPlayerData->room()) &&
            dragPlayerData->imageData() == playerDraggedImage()) {
        event->setAccepted(true);
    }
    else {
        event->setAccepted(false);
    }
}

void Room::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
{
    if (isPlayerDrag(event)) {
        setPlayer(false);
        update();
    }
}

void Room::dropEvent(QGraphicsSceneDragDropEvent *event)
{
    const auto roomData =
            qobject_cast<const DragPlayerMimeData *>(event->mimeData());

    if(roomData && isNeighbour(roomData->room()) &&
            roomData->imageData() == playerDraggedImage()) {
        enter();
        update();
        event->setAccepted(true);
    }
    else {
        event->setAccepted(false);
    }
}

void Room::toggleGuessWumpus()
{
    if(mGuessWumpus) {
        mGuessWumpus = false;
        mGuessWumpusAction->setChecked(false);
    }
    else {
        mGuessWumpus = true;
        mGuessWumpusAction->setChecked(true);
    }
    update();
}

void Room::toggleGuessPit()
{
    if(mGuessPit) {
        mGuessPit = false;
        mGuessPitAction->setChecked(false);
    }
    else {
        mGuessPit = true;
        mGuessPitAction->setChecked(true);
    }
    update();
}

void Room::toggleGuessBat()
{
    if(mGuessBat) {
        mGuessBat = false;
        mGuessBatAction->setChecked(false);
    }
    else {
        mGuessBat = true;
        mGuessBatAction->setChecked(true);
    }
    update();
}

bool Room::isNeighbour(Room *room) const
{
    if (room == nullptr) {
        return false;
    }

    for(const auto &neighbour : mNeighbours) {
        if (room == neighbour) {
            return true;
        }
    }
    return false;
}

bool Room::minDistanceReached(QGraphicsSceneMouseEvent *event,
                              Qt::MouseButton button)
{
    return QLineF(event->screenPos(),
                  event->buttonDownScreenPos(button))
           .length() >= QApplication::startDragDistance();
}

void Room::executePlayerDrag(QGraphicsSceneMouseEvent *event)
{
    setPlayer(false);

    auto drag = new QDrag(event->widget());
    auto mime = new DragPlayerMimeData;
    mime->setRoom(this);
    drag->setMimeData(mime);

    mime->setImageData(playerDraggedImage());

    drag->setPixmap(QPixmap::fromImage(playerDraggedImage()));
    drag->setHotSpot(QPoint(0, 0));

    auto result = drag->exec();

    if (result == Qt::IgnoreAction) {
        setPlayer(true);
    }
}

bool Room::isPlayerDrag(QGraphicsSceneDragDropEvent *event)
{
    return qobject_cast<const DragPlayerMimeData *>(event->mimeData()) &&
            event->mimeData()->imageData() == playerDraggedImage();
}

void Room::drawRoom(QPainter *painter, const QImage &roomImage)
{
    painter->drawImage(boundingRect(), roomImage);
}

void Room::drawWumpus(QPainter *painter, const QImage &wumpusImage)
{
    auto xOffset = roomImage().width() - wumpusImage.width();
    painter->drawImage(
                QPointF{boundingRect().x() + xOffset,  boundingRect().y()},
                wumpusImage);
}

void Room::drawBat(QPainter *painter, const QImage &batImage)
{
    painter->drawImage(
                QPointF{boundingRect().x(),  boundingRect().y()}, batImage);
}


void Room::drawPit(QPainter *painter, const QImage &pitImage)
{
    auto yOffset = roomImage().height() - pitImage.height();
    painter->drawImage(
                QPointF{boundingRect().x(),  boundingRect().y() + yOffset},
                pitImage);
}

void Room::drawPlayer(QPainter *painter)
{
    auto xOffset = roomImage().width() - wumpusImage().width() -
            playerImage().width();
    painter->drawImage(
                QPointF{boundingRect().x() + xOffset,  boundingRect().y()},
                playerImage());
}

QImage Room::roomImage() const
{
    return QImage{":/ressources/room.png"};
}

QImage Room::roomVisitedImage() const
{
    return QImage{":/ressources/room_visited.png"};
}

QImage Room::roomTargetImage() const
{
    return QImage{":/ressources/room_target.png"};
}

QImage Room::batImage() const
{
    return QImage{":/ressources/bat.png"};
}

QImage Room::batConfirmedImage() const
{
    return QImage{":/ressources/bat_confirmed.png"};
}

QImage Room::pitImage() const
{
    return QImage{":/ressources/pit.png"};
}

QImage Room::pitConfirmedImage() const
{
    return QImage{":/ressources/pit_confirmed.png"};
}

QImage Room::wumpusImage() const
{
    return QImage{":/ressources/wumpus.png"};
}

QImage Room::wumpusConfirmedImage() const
{
    return QImage{":/ressources/wumpus_confirmed.png"};
}

QImage Room::playerImage() const
{
    return QImage{":/ressources/player.png"};
}

QImage Room::playerDraggedImage() const
{
    return QImage{":/ressources/player_dragged.png"};
}

dungeonview.h

#ifndef DUNGEONVIEW_H
#define DUNGEONVIEW_H

#include <QGraphicsView>

class Room;

class DungeonView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit DungeonView(QWidget *parent = nullptr);

signals:
    void shootHitWumpus();
    void shootMissedWumpus();

protected:
    void mousePressEvent(QMouseEvent *event) override;

    void mouseReleaseEvent(QMouseEvent *event) override;

    void mouseMoveEvent(QMouseEvent *event) override;

private:
    void startShootArrowMode(Room *room);
    void stopShootArrowMode(QMouseEvent *event);

    void shootArrow(QMouseEvent *event);

    void startDragPlayerMode(QMouseEvent *event);
    void stopDragPlayerMode(QMouseEvent *event);

    [[nodiscard]] bool maxArrowRangeReached() const;
    bool isMarked(Room *room) const;
    bool isNeigbourOfLastMarkedRoom(Room *room) const;

    bool leftRoom(Room *current, Room* last);
    bool enteredRoom(Room *current, Room* last);

    static constexpr auto mArrowRoomRange{ 3 };


    bool mShootArrowSelectOn{ false };

    Room *mlastRoom{ nullptr };

    QVector<Room *> mMarkedRooms;
};

#endif // DUNGEONVIEW_H

dungeonview.cpp

#include "dungeonview.h"

#include "room.h"

#include <QDebug>
#include <QMouseEvent>

#include <algorithm>

DungeonView::DungeonView(QWidget *parent)
    :QGraphicsView{ parent }
{
    setMouseTracking(true);
}

void DungeonView::mousePressEvent(QMouseEvent *event)
{
    if (auto room = qgraphicsitem_cast<Room *>(itemAt(event->pos()));
            room && room->hasPlayer()) {

        if (event->button() == Qt::RightButton) {           
            startShootArrowMode(room);
        }
        else if (event->button() == Qt::LeftButton) {
            startDragPlayerMode(event);
        }
    }
}

void DungeonView::mouseReleaseEvent(QMouseEvent *event)
{
    if (mShootArrowSelectOn && event->button() == Qt::RightButton) {
        stopShootArrowMode(event);
    }
    else if (auto room = qgraphicsitem_cast<Room *>(itemAt(event->pos())) ;
             room && room->hasPlayer() &&
             event->button() == Qt::LeftButton) {       
        stopDragPlayerMode(event);
    }
}

void DungeonView::mouseMoveEvent(QMouseEvent *event)
{
    auto room = qgraphicsitem_cast<Room *>(itemAt(event->pos()));
    if(room && !mShootArrowSelectOn) {
        QGraphicsView::mouseMoveEvent(event);
    }

    if(room == mlastRoom) {
        return;
    }
    if(!mShootArrowSelectOn && leftRoom(room, mlastRoom)) {
        setCursor(Qt::ArrowCursor);
    }
    else if (mShootArrowSelectOn && !maxArrowRangeReached() &&
             !isMarked(room) && isNeigbourOfLastMarkedRoom(room)) {        
        room->setTarget(true);
        mMarkedRooms.push_back(room);
    }
    else if (enteredRoom(room, mlastRoom) && room->hasPlayer()) {
        setCursor(Qt::OpenHandCursor);
    }
    mlastRoom = room;
}

void DungeonView::startShootArrowMode(Room *room)
{
    mShootArrowSelectOn = true;

    room->setTarget(true);
    mlastRoom = room;
    mMarkedRooms.push_back(room);

    setCursor(Qt::CrossCursor);
}

void DungeonView::stopShootArrowMode(QMouseEvent *event)
{
    for (auto &room : mMarkedRooms) {
        room->setTarget(false);
    }

    if (mMarkedRooms.size() > 1) {
        shootArrow(event);
    }

    mMarkedRooms.clear();
    mlastRoom = nullptr;
    mShootArrowSelectOn = false;

    setCursor(Qt::ArrowCursor);
}

void DungeonView::shootArrow(QMouseEvent *event)
{
    auto room = qgraphicsitem_cast<Room *>(itemAt(event->pos()));

    if(room && room == mMarkedRooms.back()) {
        auto hitWumpus = false;

        for (auto i = 1; i < mMarkedRooms.size(); ++i) {
            if(room->hasWumpus()) {
                emit shootHitWumpus();
                hitWumpus = true;
                break;
            }
        }
        if (!hitWumpus) {
            emit shootMissedWumpus();
        }
    }
}

void DungeonView::startDragPlayerMode(QMouseEvent *event)
{
    setCursor(Qt::ClosedHandCursor);
    QGraphicsView::mousePressEvent(event);
}

void DungeonView::stopDragPlayerMode(QMouseEvent *event)
{
    setCursor(Qt::OpenHandCursor);
    QGraphicsView::mousePressEvent(event);
}

bool DungeonView::maxArrowRangeReached() const
{
    return mMarkedRooms.size() > mArrowRoomRange;
}

bool DungeonView::isMarked(Room *room) const
{
    if(mMarkedRooms.empty()) {
        return false;
    }

    return std::find(mMarkedRooms.begin(),
                     mMarkedRooms.end(), room) != mMarkedRooms.end();
}

bool DungeonView::isNeigbourOfLastMarkedRoom(Room *room) const
{
    if(mMarkedRooms.empty()) {
        return false;
    }

    auto lastRoomNeigbours = mMarkedRooms.back()->neighbours();

    return std::find(lastRoomNeigbours.begin(),
                     lastRoomNeigbours.end(), room) != lastRoomNeigbours.end();
}

bool DungeonView::leftRoom(Room *current, Room *last)
{
    return (current == nullptr && last != nullptr);
}

bool DungeonView::enteredRoom(Room *current, Room *last)
{
    return (current != nullptr && last == nullptr);
}

dungeon.h

#ifndef DUNGEON_H
#define DUNGEON_H

#include <QWidget>

class Room;

class QGraphicsScene;
class QGraphicsLineItem;
class DungeonView;

class Dungeon : public QWidget
{
    Q_OBJECT
public:
    explicit Dungeon(QWidget *parent = nullptr);

    void showHazards(bool value);

    void enter();
    void reset();

    [[nodiscard]] int remainingArrows() const;

signals:
    void wumpusNear();
    void batNear();
    void pitNear();

    void playerDraggedByBat();

    void playerDiedFromWumpus();
    void playerDiedFromPit();
    void playerIsOutOfArrows();

    void playerKilledWumpus();

    void enteredRoom();
    void arrowShot();

private slots:
    void moveWumpusIfWakeUp();
    void showLinesToNeigboursOfRoom();
    void decreaseArrows();

private:
    void scaleViewToSize();

    void emptyRooms();
    void hideDungeon();
    void createRooms();
    void connectToRooms();
    void addRoomsToScene();

    void addLinesToScene();


    void addLineToNeigbours(const Room *room);
    bool lineExistsInScene(const QLineF &line);
    QGraphicsLineItem *findLineInScene(const QLineF &line);

    static constexpr auto mCountOfRooms = 20;
    static constexpr auto mCountOfPits = 2;
    static constexpr auto mCountOfBats = 2;
    static constexpr auto mCountOfArrows = 5;

    int mRemainingArrows{ mCountOfArrows };

    QGraphicsScene *mGraphicsScene;
    DungeonView *mDungeonView;
    QVector<Room *> mRooms;
};


#endif // DUNGEON_H

dungeon.cpp

#include "dungeon.h"

#include "dungeonview.h"

#include "room.h"
#include "roomutility.h"

#include <QGuiApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QRandomGenerator>
#include <QScreen>
#include <QVBoxLayout>

#include <QDebug>

#include <algorithm>

Dungeon::Dungeon(QWidget *parent)
    : QWidget(parent),
      mGraphicsScene{ new QGraphicsScene },
    mDungeonView{ new DungeonView }
{
    createRooms();
    connectRoomsAsDodekaeder(mRooms);
    setPositionOfRooms(mRooms);
    addRoomsToScene();
    addLinesToScene();
    connectToRooms();

    populateRoomsRandom(mRooms, mCountOfPits, mCountOfBats);

    mDungeonView->setScene(mGraphicsScene);
    mDungeonView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    mDungeonView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    connect(mDungeonView, &DungeonView::shootHitWumpus,
            this, &Dungeon::playerKilledWumpus);
    connect(mDungeonView, &DungeonView::shootMissedWumpus,
            this, &Dungeon::moveWumpusIfWakeUp);
    connect(mDungeonView, &DungeonView::shootHitWumpus,
            this, &Dungeon::decreaseArrows);
    connect(mDungeonView, &DungeonView::shootMissedWumpus,
            this, &Dungeon::decreaseArrows);
    connect(mDungeonView, &DungeonView::shootHitWumpus,
            this, &Dungeon::arrowShot);
    connect(mDungeonView, &DungeonView::shootMissedWumpus,
            this, &Dungeon::arrowShot);


    auto layout = new QVBoxLayout;
    layout->addWidget(mDungeonView);
    layout->setSpacing(0);
    layout->setContentsMargins(0,0,0,0);
    setLayout(layout);

    scaleViewToSize();
    setFixedSize(size());
}

void Dungeon::showHazards(bool value)
{
    for(auto &room : mRooms) {
        room->showContent(value);
    }
}

void Dungeon::enter()
{
    for (;;) {
        auto idx = QRandomGenerator::global()->bounded(0, mRooms.size() - 1);

        if(mRooms[idx]->isEmpty()) {
            mRooms[idx]->enter();
            break;
        }
    }
}

void Dungeon::reset()
{
    emptyRooms();
    hideDungeon();
    mRemainingArrows = mCountOfArrows;

    populateRoomsRandom(mRooms, mCountOfPits, mCountOfBats);
}

int Dungeon::remainingArrows() const
{
    return mRemainingArrows;
}

void Dungeon::moveWumpusIfWakeUp()
{
    auto direction = QRandomGenerator::global()->bounded(0, 3);

    if (direction == 3) {   // 25% chance that wumpus won't move
        return;
    }

    auto wumpusRoomIt{ std::find_if(mRooms.begin(), mRooms.end(),
                        [](Room *room) { return room->hasWumpus(); }) };

    (*wumpusRoomIt)->setWumpus(false);
    auto newRoom = (*wumpusRoomIt)->neighbours()[direction];
    newRoom->setWumpus(true);

    auto playerRoomIt{ std::find_if(mRooms.begin(), mRooms.end(),
                        [](Room *room) { return room->hasPlayer(); }) };

    (*playerRoomIt)->emitNeigbourHazards();
}

void Dungeon::showLinesToNeigboursOfRoom()
{
    auto room = qobject_cast<Room *>(sender());
    auto startPoint = room->mapToScene(room->rect().center());
    auto neighbours = room->neighbours();

    for (const auto &neighbour : neighbours) {
        auto endPoint = neighbour->mapToScene(neighbour->rect().center());
        QLineF line{ startPoint, endPoint };

        auto lineItem = findLineInScene(line);
        if (lineItem) {
            lineItem->show();
        }
    }
}

void Dungeon::decreaseArrows()
{
    --mRemainingArrows;
    if (mRemainingArrows == 0) {
        emit playerIsOutOfArrows();
    }
}

void Dungeon::scaleViewToSize()
{
    mDungeonView->fitInView(mDungeonView->scene()->sceneRect(),
                             Qt::KeepAspectRatio);
}

void Dungeon::emptyRooms()
{
    for(auto room : mRooms) {
        room->clear();
    }
}

void Dungeon::hideDungeon()
{
    auto items = mGraphicsScene->items();

    for (auto &item : items) {
        item->hide();
    }
}

void Dungeon::createRooms()
{
    mRooms.reserve(mCountOfRooms);

    for(int i = 0; i< mCountOfRooms; ++i) {
        mRooms.push_back(new Room);
    }
}

void Dungeon::connectToRooms()
{
    for(const auto &room : mRooms) {
        connect(room, &Room::entered,
                this, &Dungeon::showLinesToNeigboursOfRoom);
        connect(room, &Room::entered,
                this, &Dungeon::enteredRoom);

        connect(room, &Room::wumpusNear,
                this, &Dungeon::wumpusNear);
        connect(room, &Room::batNear,
                this, &Dungeon::batNear);
        connect(room, &Room::pitNear,
                this, &Dungeon::pitNear);

        connect(room, &Room::playerDiedFromWumpus,
                this, &Dungeon::playerDiedFromWumpus);
        connect(room, &Room::playerDiedFromPit,
                this, &Dungeon::playerDiedFromPit);
        connect(room, &Room::playerDraggedByBat,
                this, &Dungeon::playerDraggedByBat);
        connect(room, &Room::playerDraggedByBat,
                this, &Dungeon::enter);
    }
}

void Dungeon::addRoomsToScene()
{
    for(const auto &room : mRooms) {
        mGraphicsScene->addItem(room);
    }
}

void Dungeon::addLinesToScene()
{
    for (const auto& room : mRooms) {
        addLineToNeigbours(room);
    }
}

void Dungeon::addLineToNeigbours(const Room *room)
{
    auto startPoint = room->mapToScene(room->rect().center());
    constexpr auto lineWidth = 10;
    QPen pen;
    pen.setBrush(Qt::black);
    pen.setWidth(lineWidth);

    auto neighbours = room->neighbours();

    for (const auto &neighbour : neighbours) {

        auto endPoint = neighbour->mapToScene(neighbour->rect().center());
        QLineF line{ startPoint, endPoint };

        if (lineExistsInScene(line)) {
            continue;
        }

        auto lineItem = mGraphicsScene->addLine(line, pen);
        lineItem->setZValue(-1);
        lineItem->hide();
    }

    mGraphicsScene->update();
}

bool Dungeon::lineExistsInScene(const QLineF &line)
{
    return findLineInScene(line) != nullptr;
}

QGraphicsLineItem *Dungeon::findLineInScene(const QLineF &line)
{
    auto items = mGraphicsScene->items();

    for(const auto &item : items) {
        auto existingLineItem = qgraphicsitem_cast<QGraphicsLineItem *>(item);

        if(existingLineItem) {
            auto p1 = existingLineItem->line().p1();
            auto p2 = existingLineItem->line().p2();

            auto newP1 = line.p1();
            auto newP2 = line.p2();

            if((p1 == newP1 && p2 == newP2) || (p1 == newP2 && p2 == newP1)) {
                return existingLineItem;
            }
        }
    }
    return nullptr;
}

roomutility.h

#ifndef ROOMUTILITY_H
#define ROOMUTILITY_H

#include <QVector>

class Room;

void connectRoomsAsDodekaeder(QVector<Room *> &rooms);

void setNeigbours(QVector<Room *> &rooms,
                  int roomNo,
                  int roomNoNeigbourOne,
                  int roomNoNeigbourTwo,
                  int roomNoNeigbourThree);

void populateRoomsRandom(
        QVector<Room *> &rooms, int countOfPits, int countOfBats);

void setPositionOfRooms(QVector<Room *> &rooms);

Room *randomRoom(const QVector<Room *> &rooms);

#endif // ROOMUTILITY_H

roomutility.cpp

#include "roomutility.h"

#include "room.h"

#include <QRandomGenerator>

static constexpr auto countOfRooms = 20;

void connectRoomsAsDodekaeder(QVector<Room *> &rooms)
{
    Q_ASSERT(rooms.size() == countOfRooms);

    setNeigbours(rooms, 0, 1, 4, 19);
    setNeigbours(rooms, 1, 0 , 2, 17);
    setNeigbours(rooms, 2, 1 , 3, 15);
    setNeigbours(rooms, 3, 2 , 4, 13);
    setNeigbours(rooms, 4, 0 , 3, 5);
    setNeigbours(rooms, 5, 4 , 6, 12);
    setNeigbours(rooms, 6, 5 , 7, 19);
    setNeigbours(rooms, 7, 6 , 8, 11);
    setNeigbours(rooms, 8, 7 , 9, 18);
    setNeigbours(rooms, 9, 8 , 10, 16);
    setNeigbours(rooms, 10, 9 , 11, 14);
    setNeigbours(rooms, 11, 7 , 10, 12);
    setNeigbours(rooms, 12, 5 , 11, 13);
    setNeigbours(rooms, 13, 3 , 12, 14);
    setNeigbours(rooms, 14, 10 , 13, 15);
    setNeigbours(rooms, 15, 2 , 14, 16);
    setNeigbours(rooms, 16, 9 , 15, 17);
    setNeigbours(rooms, 17, 1 , 16, 18);
    setNeigbours(rooms, 18, 8 , 17, 19);
    setNeigbours(rooms, 19, 0 , 6, 18);
}

void setNeigbours(QVector<Room *> &rooms,
                  int roomNo,
                  int roomNoNeigbourOne,
                  int roomNoNeigbourTwo,
                  int roomNoNeigbourThree)
{
    Q_ASSERT(roomNo >= 0 && roomNo < rooms.size());
    Q_ASSERT(roomNoNeigbourOne >= 0 && roomNoNeigbourOne < rooms.size());
    Q_ASSERT(roomNoNeigbourTwo >= 0 && roomNoNeigbourTwo < rooms.size());
    Q_ASSERT(roomNoNeigbourThree >= 0 && roomNoNeigbourThree < rooms.size());
    Q_ASSERT(roomNo != roomNoNeigbourOne);
    Q_ASSERT(roomNoNeigbourOne != roomNoNeigbourTwo);
    Q_ASSERT(roomNoNeigbourTwo != roomNoNeigbourThree);
    Q_ASSERT(roomNoNeigbourThree != roomNo);

    rooms[roomNo]->addNeighbour(rooms[roomNoNeigbourOne]);
    rooms[roomNo]->addNeighbour(rooms[roomNoNeigbourTwo]);
    rooms[roomNo]->addNeighbour(rooms[roomNoNeigbourThree]);
}

void populateRoomsRandom(
        QVector<Room *> &rooms, int countOfPits, int countOfBats)
{
    constexpr auto countOfWumpus = 1;

    Q_ASSERT(countOfPits >= 0);
    Q_ASSERT(countOfBats >= 0);
    Q_ASSERT(countOfPits + countOfBats + countOfWumpus < rooms.size());

    auto room = randomRoom(rooms);
    room->setWumpus(true);

    for (auto i = 0; i < countOfPits; ++i) {
        for (;;) {
            auto room = randomRoom(rooms);
            if (room->isEmpty()) {
                room->setPit(true);
                break;
            }
        }
    }

    for (auto i = 0; i < countOfBats; ++i) {
        for (;;) {
            auto room = randomRoom(rooms);
            if (room->isEmpty()) {
                room->setBat(true);
                break;
            }
        }
    }
}

void setPositionOfRooms(QVector<Room *> &rooms)
{
    const auto roomWidth = static_cast<int>(rooms[0]->size().width());
    const auto roomHeight = static_cast<int>(rooms[0]->size().height());

    QVector<QPoint> points
    {
        QPoint{ 400 - roomWidth / 2, 450 - roomHeight / 2},
        QPoint{ 600 - roomWidth / 2, 450 - roomHeight / 2},
        QPoint{ 600 - roomWidth / 2, 550 - roomHeight / 2},
        QPoint{ 500 - roomWidth / 2, 600 - roomHeight / 2},
        QPoint{ 400 - roomWidth / 2, 550 - roomHeight / 2},
        QPoint{ 250 - roomWidth / 2, 600 - roomHeight / 2},
        QPoint{ 250 - roomWidth / 2, 450 - roomHeight / 2},
        QPoint{ 100 - roomWidth / 2, 300 - roomHeight / 2},
        QPoint{ 500 - roomWidth / 2, 75 - roomHeight / 2},
        QPoint{ 900 - roomWidth / 2, 300 - roomHeight / 2},
        QPoint{ 900 - roomWidth / 2, 900 - roomHeight / 2},
        QPoint{ 100 - roomWidth / 2, 900 - roomHeight / 2},
        QPoint{ 300 - roomWidth / 2, 750 - roomHeight / 2},
        QPoint{ 500 - roomWidth / 2, 750 - roomHeight / 2},
        QPoint{ 750 - roomWidth / 2, 750 - roomHeight / 2},
        QPoint{ 750 - roomWidth / 2, 600 - roomHeight / 2},
        QPoint{ 800 - roomWidth / 2, 450 - roomHeight / 2},
        QPoint{ 700 - roomWidth / 2, 300 - roomHeight / 2},
        QPoint{ 500 - roomWidth / 2, 200 - roomHeight / 2},
        QPoint{ 350 - roomWidth / 2, 300 - roomHeight / 2},
    };

    Q_ASSERT(points.size() == rooms.size());

    for(auto i = 0; i<rooms.size(); ++i) {
        rooms[i]->setPos(points[i]);
    }
}

Room *randomRoom(const QVector<Room *> &rooms)
{
    auto idx = QRandomGenerator::global()->bounded(0, rooms.size() - 1);
    return rooms[idx];
}
\$\endgroup\$
  • \$\begingroup\$ Interesting question -- does drag and drop work for you? It's failing here on Qt version 5.12 on a 64-bit Fedora platform but I don't see a problem in the code. \$\endgroup\$ – Edward Dec 5 '19 at 18:58
  • \$\begingroup\$ yes drag and drop works just fine on my system. I only tryed it on KDE Neon 64Bit. I can try it out on windows 10 aswell. What means it fails you can even drag the player to ann other room? \$\endgroup\$ – Sandro4912 Dec 5 '19 at 19:04
  • \$\begingroup\$ Which version of Qt are you using? \$\endgroup\$ – Edward Dec 5 '19 at 19:05
  • 1
    \$\begingroup\$ Maybe the issue is even a bug in the fedora. One time i found a bug in Qt with a project which only happened in neon. \$\endgroup\$ – Sandro4912 Dec 5 '19 at 19:33
  • 2
    \$\begingroup\$ nice that you could find it. so my programm helped to make the linux better. \$\endgroup\$ – Sandro4912 Dec 6 '19 at 20:08
4
\$\begingroup\$

In all this is a good effort and helped me discover and report a bug in mutter, so I learned some things, too. There are still some things that I think can help you improve your program.

Fix the bug

It's a minor bug, but if the player decides to play again, the Room::clear() method doesn't completely reset each room. Specifically, if the user has made a guess about the presence of a hazard, those guesses remain in the subsequent game. Also, if the player actually encountered a bat, that box remains disabled. One way to fix that is to add these lines to Room::clear():

// also send signals
mGuessPitAction->setChecked(false);
mGuessWumpusAction->setChecked(false);
mGuessBatAction->setChecked(false);
mGuessBatAction->setEnabled(true);

However I think I would instead create and use functions as shown in the next two suggestions.

Prefer using boolean values directly over if

The Room code currently has this code:

void Room::toggleGuessBat()
{
    if(mGuessBat) {
        mGuessBat = false;
        mGuessBatAction->setChecked(false);
    }
    else {
        mGuessBat = true;
        mGuessBatAction->setChecked(true);
    }
    update();
}

That seems a bit verbose to me. We could shorten it by using the boolean value directly:

void Room::toggleGuessBat()
{
    mGuessBat = !mGuessBat;
    mGuessBatAction->setChecked(mGuessBat);
    update();
}

However, we could also get rid of the variable entirely as in the next suggestion.

Eliminate redundant variables

The bug I mentioned above was caused by the fact that a stored boolean value and the display value were out of synchronization. One certain way to eliminate all such bugs is to not have two separate things. That is, the variable could be eliminated and only the value of the QAction used instead. This makes it very simple to keep synchronization but also means that we no longer have a separate variable and so, of course, the program is more tightly coupled to the interface. This may or may not be a good idea, depending on your tastes and goals, but I believe that in this case it has more advantages than disadvantages. For instance, we can replace the three slots with a single very simple one:

void Room::selfUpdate()
{
    update();
}

And now all three connect calls can use the same slot:

connect(mGuessWumpusAction, &QAction::triggered, this, &Room::selfUpdate);

The QAction will take care of the appearance and the boolean variable and all that is left is to change from checking the eliminated boolean variables to the states of the checkboxes like this:

if (mGuessWumpusAction->isChecked()) {
    drawWumpus(painter, wumpusImage());
}

Simplify your code

The existing Room::boundingRect() is this:

QRectF Room::boundingRect() const
{
    return QRectF{ 0, 0,
        static_cast<qreal>(roomImage().width()),
        static_cast<qreal>(roomImage().height())};
}

I would use an alternative constructor that takes a QRect to simplify the code and eliminate casts:

QRectF Room::boundingRect() const
{
    return roomImage().rect();
}

Use a std::array for fixed-size collections

The mRooms is currently a QArray, but it could just as easily be a fixed-size std::array. The advantage is that many of the checks that are currently in the code are no longer needed. For example, we could change the Dungeon class so that in now includes an array of actual Room objects rather than uninitialized pointers.

std::array<Room, mCountOfRooms> mRooms;

That now eliminates the need for createRooms. Then we could change the helper function connectRoomsAsDodekaeder() to a private member function of Dungeon and use a lambda instead of a separate additional helper function.

void Dungeon::connectRoomsAsDodekaeder()
{
    auto makeNeighbours = [this](std::size_t src, std::array<std::size_t, 3>n){
        for (const auto i: n) {
            this->mRooms[src].addNeighbour(&(this->mRooms[i]));
        }
    };

    makeNeighbours(0, {1, 4, 19});
    makeNeighbours(1, {0 , 2, 17});
    makeNeighbours(2, {1 , 3, 15});
    makeNeighbours(3, {2 , 4, 13});
    makeNeighbours(4, {0 , 3, 5});
    makeNeighbours(5, {4 , 6, 12});
    makeNeighbours(6, {5 , 7, 19});
    makeNeighbours(7, {6 , 8, 11});
    makeNeighbours(8, {7 , 9, 18});
    makeNeighbours(9, {8 , 10, 16});
    makeNeighbours(10, {9 , 11, 14});
    makeNeighbours(11, {7 , 10, 12});
    makeNeighbours(12, {5 , 11, 13});
    makeNeighbours(13, {3 , 12, 14});
    makeNeighbours(14, {10 , 13, 15});
    makeNeighbours(15, {2 , 14, 16});
    makeNeighbours(16, {9 , 15, 17});
    makeNeighbours(17, {1 , 16, 18});
    makeNeighbours(18, {8 , 17, 19});
    makeNeighbours(19, {0 , 6, 18});
}

Consider simplifying randomization

Right now the code to randomize the locations of hazards selects random numbers until it finds an empty room and then places each hazard. There is a simpler way to do this:

void Dungeon::populateRooms()
{
    // create a temporary array of room pointers
    std::array<Room *, mCountOfRooms> mixer;
    std::size_t i{0};
    for (auto &r : mRooms) {
        mixer[i++] = &r;
    }
    // shuffle the pointers to simplify initialization
    std::random_shuffle(mixer.begin(), mixer.end());
    auto it{mixer.begin()};
    // place the wumpus
    (*it++)->setWumpus(true);
    // now the bats
    for (int bats{mCountOfBats}; bats; --bats) {
        (*it++)->setBat(true);
    }
    // now the pits
    for (int pits{mCountOfPits}; pits; --pits) {
        (*it++)->setPit(true);
    }
}

Using random_shuffle, we essentially do the randomization just once and then simply sequentially add the hazards.

Don't leak memory

Most of the memory is automatically freed by Qt because the objects are all linked together. However, the three QAction items allocated in the Room constructor are not freed. Adding the appropriate destructor to Room could fix that memory leak.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Can it really work with std::array<Room, mCountOfRooms> mRooms; ? If I do that I cannot do auto room = qobject_cast<Room *>(sender()); in showLinesToNeigboursOfRoom. I modified it to use std::array<Room *, mCountOfRooms> mRooms It looks like it works but in the constructor of the array I have to type 20 times new Room \$\endgroup\$ – Sandro4912 Dec 9 '19 at 9:04
  • \$\begingroup\$ for the memory leak. I think it would be more QT like to assign the QActions Room as a parent in the constructor. like mGuessWumpusAction = new QAction{tr("Has &Wumpus"), this}; \$\endgroup\$ – Sandro4912 Dec 9 '19 at 9:19
  • \$\begingroup\$ and good idea with the temporary array. I thought i could not use random_shuffle because the rooms are already connected. I updated the repo with the suggested changes. \$\endgroup\$ – Sandro4912 Dec 9 '19 at 9:23
  • 1
    \$\begingroup\$ I've sent you a pull request to show how to turn Room * into Room as per my suggestion above. As for the memory leak, I prefer your solution. \$\endgroup\$ – Edward Dec 10 '19 at 11:46
  • \$\begingroup\$ I merged the commit. thanks for many good advice's. \$\endgroup\$ – Sandro4912 Dec 10 '19 at 16:51
2
\$\begingroup\$

Piggy-backing off of Edward's answer around leaking memory, I would advocate the use of smart pointers here (if you're using C++11 or higher). You then wouldn't have to write the deconstructor and memory would be deallocated when the object loses scope.

For instance, your code

QAction* mGuessWumpusAction = new QAction{tr("Has &Wumpus")};

would become

#include <memory>

std::shared_ptr<QAction> mGuessWumpusAction = std::shared_ptr<QAction>(
    new QAction{tr("Has &Wumpus")}
);

and shared_ptr is super nice because it overloads -> so that mGuessWumpusAction->doStuff() still "dereferences" in the way you'd think.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Since it's a QAction is better to assign the parent pointer instead of using a smart pointer \$\endgroup\$ – Moia Dec 9 '19 at 7:27
  • 1
    \$\begingroup\$ In plain c++ I would agree it would be good to use smart pointers instead of plain pointers. However the QT way of manage memory is in most cases to assign the Object a parent which then automatically deletes the child. See doc.qt.io/qt-5/objecttrees.html. \$\endgroup\$ – Sandro4912 Dec 9 '19 at 8:45
  • \$\begingroup\$ Okay, very interesting! I actually didn't know that. I'm guessing that comes from pre-C++11 times when people forgot to delete pointers all the time and so QT added some built-in functionality to support that? \$\endgroup\$ – Chip Bell Dec 10 '19 at 5:28
  • \$\begingroup\$ yes QT is alot older than C++11 so they stayed with there way of manage memory. \$\endgroup\$ – Sandro4912 Dec 11 '19 at 10:40

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.