4
\$\begingroup\$

I'm trying to learn how to use Qt (5.11.1) for GUI applications so I've done a simple memory game where 12 tiles are displayed and every time the user clicks on a tile it will show an image, so they have to match them into 6 pairs of images.

There's a countdown of 1 minute. Game ends if the time is up before all 6 pairs have been matched, or if all 6 pairs are matched, only that it will show different messages to the user. There is no next level, saving score or anything, so it's very simple.

I know there's room for adding many more features, but would like to know what can be improved from what I've done so far.

My mainwindow.h file:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTimer>
#include <QTime>
#include <QString>
#include <QVector>
#include <QHash>
#include <QRandomGenerator>
#include <QPushButton>
#include <QMessageBox>

namespace Ui {
    class MainWindow;
}

class MainWindow : public QMainWindow{
    Q_OBJECT


public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    QTimer *timer=new QTimer();
    QTime time;
    QVector<QString> tiles{"tile01", "tile02", "tile03", "tile04",
                              "tile05", "tile06", "tile07", "tile08",
                              "tile09", "tile10", "tile11", "tile12"};
    QHash<QString, QString> tile_image;
    int score=0;
    bool isTurnStarted;
    QPushButton* previousTile;
    QPushButton* currentTile;
    int matchesLeft;
    QMessageBox msgBox;


private slots:
    void updateCountdown();
    void tileCliked();
    void randomize(QVector<QString> &tiles);
    void bindTileImage(QVector<QString> &tiles, QHash<QString, QString> &tile_image);
    void findTurnResult();
    void restartTiles();
    void showImage();
    void findFinalResult();
    void updateState();
    void initalizeGame();


private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

My mainwindow.cpp file:

#include "mainwindow.h"
#include "ui_mainwindow.h"

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

    //Connect timer to the slot that will handle the timer
    connect(timer, SIGNAL(timeout()), this, SLOT(updateState()));

    //Connect each button to the same slot, which will figure out which button was pressed and show its associated image file accordingly
    connect(ui->tiles01, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles02, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles03, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles04, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles05, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles06, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles07, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles08, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles09, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles10, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles11, SIGNAL(clicked()), this, SLOT(tileCliked()));
    connect(ui->tiles12, SIGNAL(clicked()), this, SLOT(tileCliked()));

    initalizeGame();
}


void MainWindow::tileCliked(){
    //get the tile that was clicked
    currentTile=qobject_cast<QPushButton*>(sender());

    //get the image linked to that tile in the map and set tile background to it
    showImage();

    //disable current tile so it can't be clicked again (unless there is no match, in which case it will be re-enabled)
    currentTile->setEnabled(false);

    //do something depending on whether the revealed tile is the first or the second tile in the turn
    if (!isTurnStarted){
        previousTile=currentTile;
        isTurnStarted=true;
    }
    else{
        //change score and display it
        findTurnResult();
        ui->lblScore->setText(QString::number(score));

        //reset turn
        isTurnStarted=false;
    }
}


void MainWindow::showImage(){
    QString tile_name=currentTile->objectName();
    QString img=tile_image[tile_name];
    currentTile->setStyleSheet("#" + tile_name + "{ background-image: url(://" + img + ") }");
}


void MainWindow::restartTiles(){
    //return tiles from current turn to the default state (remove backgrounds)
    previousTile->setStyleSheet("#" + previousTile->objectName() + "{ }");
    currentTile->setStyleSheet("#" + currentTile->objectName() + "{ }");

    //re-enable both tiles so they can be used on another turn
    currentTile->setEnabled(true);
    previousTile->setEnabled(true);

    //re-enable the whole tile section
    ui->frame->setEnabled(true);
}


void MainWindow::findFinalResult(){
    msgBox.setWindowTitle("Game has ended");
    msgBox.setIcon(QMessageBox::Information);
    msgBox.setStandardButtons(QMessageBox::Yes);
    msgBox.addButton(QMessageBox::No);
    msgBox.setDefaultButton(QMessageBox::Yes);
    msgBox.setEscapeButton(QMessageBox::No);

    if (matchesLeft==0){
        timer->stop();
        msgBox.setText("Good job! Final score: " + QString::number(score) + "\nPlay again?");
        if (QMessageBox::Yes == msgBox.exec()){
            initalizeGame();
        }
        else{
            QCoreApplication::quit();
        }
    }
    else{
        if (time.toString()=="00:00:00"){
            timer->stop();
            ui->frame->setEnabled(false);
            msgBox.setText("Game over.\nPlay again?");
            if (QMessageBox::Yes == msgBox.exec()){
                initalizeGame();
            }
            else{
                QCoreApplication::quit();
            }
        }
    }
}


void MainWindow::findTurnResult(){
    //check if there is a match (the current tile matches the previous tile in the turn)
    if (tile_image[currentTile->objectName()]==tile_image[previousTile->objectName()]){
        score+=15;
        matchesLeft--;

        //if there is a match, find out if all tiles have been matched.
        findFinalResult();
    }
    else{
        score-=5;

        //disable the whole tile section so no tiles can be turned during the 1-second "memorizing period"
        ui->frame->setEnabled(false);

        //if there is no match, let user memorize tiles and after 1 second hide tiles from current turn so they can be used on another turn
        QTimer::singleShot(1000, this, SLOT(restartTiles()));   
    }
}

void MainWindow::initalizeGame(){
    //start turn
    isTurnStarted=false;

    //Set score
    score=0;
    ui->lblScore->setText(QString::number(score));;

    //Set matches counter
    matchesLeft=6;

    //Set clock for countdown
    time.setHMS(0,1,0);

    //Initialize countdown
    ui->countdown->setText(time.toString("m:ss"));

    // Start timer with a value of 1000 milliseconds, indicating that it will time out every second.
    timer->start(1000);

    //Randomly sort tiles in container
    randomize(tiles);

    //Grab pairs of tiles and bind the name of an image file to each pair
    bindTileImage(tiles, tile_image);

    //enable tiles frame
    ui->frame->setEnabled(true);

    //enable every tile and reset its image
    QList<QPushButton *> btns =  ui->centralWidget->findChildren<QPushButton*>();
    foreach (QPushButton* b, btns) {
        b->setEnabled(true);
        b->setStyleSheet("#" + b->objectName() + "{ }");
    }
}


void MainWindow::updateCountdown(){
    time=time.addSecs(-1);
    ui->countdown->setText(time.toString("m:ss"));
}


void MainWindow::updateState(){
    updateCountdown();
    findFinalResult();
}


void MainWindow::randomize(QVector<QString> &tiles){
    int a,b,min,max;
    min = 0;
    max = tiles.size()-1;
    for(int i=0; i<tiles.size(); i++){
        a=QRandomGenerator::global()->generate() % ((max + 1) - min) + min;
        b=QRandomGenerator::global()->generate() % ((max + 1) - min) + min;
        std::swap(tiles[a],tiles[b]);
    }
}


void MainWindow::bindTileImage(QVector<QString> &tiles, QHash<QString, QString> &tile_image){
    auto iter=tiles.begin();
    for (int i=1; i<=6; i++){
        QString file_name="0"+QString::number(i)+".png";
        tile_image[(*iter)]=file_name;
        iter++;
        tile_image[(*iter)]=file_name;
        iter++;
    }
}


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

And the mainwindow.ui file:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Memory game</string>
  </property>
  <property name="styleSheet">
   <string notr="true">#centralWidget {
background-image: url(://background.png);
}

#howToPlay {
color: white;
}

#countdown {
color: white;
}

#scoring {
color: white;
}

#lblScore {
qproperty-alignment: AlignCenter;
color: white;
background: teal;
border: 3px solid silver;
border-radius: 7px;
}</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <widget class="QLabel" name="howToPlay">
    <property name="geometry">
     <rect>
      <x>160</x>
      <y>40</y>
      <width>471</width>
      <height>31</height>
     </rect>
    </property>
    <property name="font">
     <font>
      <pointsize>14</pointsize>
      <weight>75</weight>
      <bold>true</bold>
     </font>
    </property>
    <property name="text">
     <string>Click on two tiles and try to match the images</string>
    </property>
   </widget>
   <widget class="QLabel" name="countdown">
    <property name="geometry">
     <rect>
      <x>690</x>
      <y>20</y>
      <width>81</width>
      <height>20</height>
     </rect>
    </property>
    <property name="font">
     <font>
      <pointsize>10</pointsize>
      <weight>75</weight>
      <bold>true</bold>
     </font>
    </property>
    <property name="text">
     <string>cronómetro</string>
    </property>
   </widget>
   <widget class="QLabel" name="scoring">
    <property name="geometry">
     <rect>
      <x>330</x>
      <y>520</y>
      <width>71</width>
      <height>21</height>
     </rect>
    </property>
    <property name="font">
     <font>
      <pointsize>12</pointsize>
      <weight>75</weight>
      <bold>true</bold>
     </font>
    </property>
    <property name="text">
     <string>Puntos:</string>
    </property>
   </widget>
   <widget class="QLabel" name="lblScore">
    <property name="geometry">
     <rect>
      <x>410</x>
      <y>510</y>
      <width>41</width>
      <height>31</height>
     </rect>
    </property>
    <property name="font">
     <font>
      <pointsize>14</pointsize>
      <weight>75</weight>
      <bold>true</bold>
     </font>
    </property>
    <property name="text">
     <string>0</string>
    </property>
   </widget>
   <widget class="QFrame" name="frame">
    <property name="geometry">
     <rect>
      <x>70</x>
      <y>80</y>
      <width>661</width>
      <height>431</height>
     </rect>
    </property>
    <property name="frameShape">
     <enum>QFrame::StyledPanel</enum>
    </property>
    <property name="frameShadow">
     <enum>QFrame::Raised</enum>
    </property>
    <widget class="QPushButton" name="tile10">
     <property name="geometry">
      <rect>
       <x>180</x>
       <y>300</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile05">
     <property name="geometry">
      <rect>
       <x>20</x>
       <y>160</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile06">
     <property name="geometry">
      <rect>
       <x>180</x>
       <y>160</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile09">
     <property name="geometry">
      <rect>
       <x>20</x>
       <y>300</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile07">
     <property name="geometry">
      <rect>
       <x>340</x>
       <y>160</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile03">
     <property name="geometry">
      <rect>
       <x>340</x>
       <y>20</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile11">
     <property name="geometry">
      <rect>
       <x>340</x>
       <y>300</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile01">
     <property name="geometry">
      <rect>
       <x>20</x>
       <y>20</y>
       <width>130</width>
       <height>110</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile04">
     <property name="geometry">
      <rect>
       <x>500</x>
       <y>20</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile12">
     <property name="geometry">
      <rect>
       <x>500</x>
       <y>300</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile02">
     <property name="geometry">
      <rect>
       <x>180</x>
       <y>20</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
    <widget class="QPushButton" name="tile08">
     <property name="geometry">
      <rect>
       <x>500</x>
       <y>160</y>
       <width>131</width>
       <height>111</height>
      </rect>
     </property>
     <property name="text">
      <string/>
     </property>
    </widget>
   </widget>
   <zorder>frame</zorder>
   <zorder>howToPlay</zorder>
   <zorder>countdown</zorder>
   <zorder>score</zorder>
   <zorder>lblScore</zorder>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

Please note I translated variable and slot names into English for better understanding. I could have missed something.

Thanks!!

\$\endgroup\$
4
\$\begingroup\$

When dealing with Qt 5+ prefer the new connect syntax:

QObject::connect(ui->tiles01, &QPushButton::clicked, this, &MainWindow::tileCliked);

Instead of using sender() in the slot you can use a functor to pass the sender along:

QObject::connect(ui->tiles01, &QPushButton::clicked, this, [=](){tileCliked(ui->tiles01)});

Then tileCliked becomes

void MainWindow::tileCliked(QPushButton* sender){
    //...
}

You shuffle isn't a proper shuffle. Instead you want to do a fisher-yates shuffle:

void MainWindow::randomize(QVector<QString> &tiles){
    int a,b,min,max;
    max = tiles.size()-1;
    for(int i=0; i<tiles.size(); i++){
        min = i;
        a = i;
        b = QRandomGenerator::global()->generate() % ((max + 1) - min) + min;
        if(b != a)
            std::swap(tiles[i],tiles[b]);
    }
}
\$\endgroup\$
  • 4
    \$\begingroup\$ Any reason not to simply use std::shuffle(), and avoid re-implementing it entirely? \$\endgroup\$ – Toby Speight Jan 10 at 16:07
  • \$\begingroup\$ Not sure why I didn't use std::shuffle()... Would this be a better approach for my randomize() slot?: unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); shuffle (tiles.begin(), tiles.end(), std::default_random_engine(seed)); \$\endgroup\$ – Floella Jan 10 at 19:43

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.