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!!

Toby Speight
88k14 gold badges104 silver badges325 bronze badges
asked Jan 10, 2019 at 15:44
\$\endgroup\$
0

1 Answer 1

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]);
 }
}
answered Jan 10, 2019 at 16:02
\$\endgroup\$
2
  • 4
    \$\begingroup\$ Any reason not to simply use std::shuffle(), and avoid re-implementing it entirely? \$\endgroup\$ Commented Jan 10, 2019 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\$ Commented Jan 10, 2019 at 19:43

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.