Իմ նպատակն է արդեն դասական դարձած Խաղ 15֊ի (Game 15, Puzzle 15) ընդհանրացված տարբերակի օրինակով ներկայացնել Qt գրադարանի հիմնական հասկացությունները։
Խաղ 15֊ն իրենից ներկայացնում է մի քառակուսի շրջանակ (կամ արկղիկ), որի մեջ 4×4
կարգով դասավորված են 15 խաղաքարեր և մեկ դիրք էլ ազատ է։
+----+----+----+----+
| 1 | 2 | 3 | 4 |
+----+----+----+----+
| 5 | 6 | 7 | 8 |
+----+----+----+----+
| 9 | 10 | 11 | 12 |
+----+----+----+----+
| 13 | 14 | 15 | |
+----+----+----+----+
Խաղի սկզբում խաղաքարերը խառնված են և խաղացողի նպատակն է, ազատ դիրքն օգտագործելով ու խաղաքարերը տեղաշարժելով, թվերը դասավորել աճման կարգով։ Բնականաբար, լավագույնն է համարվում քայլերի նվազագույն քանակով լուծումը։
Խաղ 15-ի ընդհանրացում կարող է լինել Խաղ N×M֊ը, որտեղ N×M-1
հատ խաղաքարերը դասավորված N
տողերով և M
սյուներով խաղադաշտի վրա։ Խաղի կանոնները նույնն են։
Խաղը սկսվում է թվերի պատահական դասավորությամբ, որտեղ զրո թիվը գտնվում է ներքևի աջ անկյունում։ Այդ դասավորությունը պետք է լուծելի լինի։ Այն է, պետք է հնարավոր լինի վերջավոր քանակի տեղափոխություններով (տրանսպոզիցիաներով) թվերը դասավորել աճման կարգով։ Այդ հնարավորությունը ապահովում է թվերի վեկտորի ինվերսիաների զույգ լինելը։ (լրացնել մաթեմատիկական հիմնավորմամբ)
Խաղի տրամաբանությունը մոդելավորող GameNxM
դասը նախատեսված է նոր խաղ ստեղծելու, խաղի մեկ քայլ կատարելու և խաղի ավարտը որոշելու համար։
Այս դասի rows
և columns
անդամները համապատասխանաբար ցույց են տալիս խաղի մատրիցի տողերն ու սյուները, իսկ matrix
֊ը խաղի մատրիցն է։
class GameNxM {
public:
// Կոնստրուկտոր
GameNxM( int rw, int cl );
// Խաղի նախապատրաստում
void reset();
// Մեկ քայլի կատարում
void step( int rw, int cl );
// Խաղի ավարտված լինելը
bool gameOver() const;
// Մատրիցի տրված բջջի արժեքը
int valueAt( int ro, int cl ) const;
private:
int rows = 0; // տողերի քանակը
int columns = 0; // սյուների քանակը
QVector<QVector<int>> matrix; // թվերի մատրիցը
int steps = 0; // քայլերի հաշվիչ
};
Կոնստրուկտորը ստանում է թվերի մատրիցի տողերի և սյուների քանակը, և դրանք վերագրում է համապատասխանաբար դասի rows
և columns
անդամներին, ապա կանչում է նոր խաղ ստեղծող reset
մեթոդը։
GameNxM::GameNxM( int rw, int cl )
: rows{rw}, columns{cl}
{
reset();
}
reset
մեթոդը rows
տողերի և columns
սյուների փոխարեն ստեղծում է rows+2
տողերով և columns+2
սյուներով մատրից։ Այս լրացուցիչ տողերն անհարժեշտ են, որպեսզի խաղի մեկ քայլը կատերելիս մատրիցի բոլոր բջիջների համար կատարվեն նույնանման ստուգումներ։
matrix.clear();
for( uint r = 0; r < rows + 2; ++r )
matrix.push_back( QVector<int>(columns + 2, -1) );
Օրինակ, 4×4
չափի խաղի համար դեևս չարժեքավարված մատրիցը կունենա հետևյալ տեսքը, որտեղ լրացուցիչ վանդակերնը պարունակում են -1
արժեքը։
+----+----+----+----+----+----+
| -1 | -1 | -1 | -1 | -1 | -1 |
+----+----+----+----+----+----+
| -1 | | | | | -1 |
+----+----+----+----+----+----+
| -1 | | | | | -1 |
+----+----+----+----+----+----+
| -1 | | | | | -1 |
+----+----+----+----+----+----+
| -1 | | | | | -1 |
+----+----+----+----+----+----+
| -1 | -1 | -1 | -1 | -1 | -1 |
+----+----+----+----+----+----+
Այնուհետև գեներացվում են [1;N×M-1]
միջակայքի հաջորդական թվերը․
int count = rows * columns - 1;
QVector<int> rnums(count);
std::iota(rnums.begin(), rnums.end(), 1);
C++ լեզվի շաբլոնների ստանդարտ գրադարանի iota
ալգորիթմը rnums
կոնտեյները լրացնում է 1
֊ից սկսող հաջորդական թվերով։
Ստանադարդ գրադարանի մեկ այլ ալգորիթմ՝ shuffle
, պատահական եղանակով խառնում է տրված կոնտեյների տարրերը՝ օգտագործելով պատահական թվերի մի որևէ գեներատոր։ Տվյալ պքում այդ գեներատորը default_random_engine
է․
auto re = std::default_random_engine{};
std::shuffle(rnums.begin(), rnums.end(), re);
Խմբերի տեսությունից հայտնի է, որ Խաղ 15֊ը լուծելի է միայն այն դեպքում, երբ խառնելուց հետո առաջացած ինվերսիաների քանակը զույգ է։ Դա ապահովելու համար նախ հաշվվում է ինվերսիաների քանակը․
int inv = 0;
for( int i = 0; i < count - 1; ++i )
for( int j = i + 1; j < count; ++ j )
if( rnums[i] > rnums[j] ) ++inv;
Հետո, եթե այդ թիվը կենտ է, ապա տեղերով փոխվում են առաջին երկու տարրերը՝ կարարվում է ևս մի տրանսպոզիցիա․
if( inv % 2 == 1 ) qSwap(rnums[0], rnums[1]);
Մնում է այս թվերով արժեքավորել խաղի մատրիցը, բայց մինչ այդ պետք է ավելացնել նառ վերջին 0
֊ն։
rnums.push_back(0);
int nx = 0;
for( int r = 1; r <= rows; ++r )
for( int c = 1; c <= columns; ++c )
matrix[r][c] = rnums[nx++];
Խաղացողը կարող է տեղաշարժել միայն այն խաղաքարերը, որոնց հարևանությամբ գտնվում է դատատրկ վանդակը։ Խաղի մոդելի տեսակետից դատարկ է համարվում մատրիցի 0
թիվը պարունակող բջիջը։ step
մեթոդն իր արգումենտում ստանում է տեղի և սյան ինդեքսներ։ Եթե այդ ինդեքսներով որոշվող բջջի չորս հարևաններից որևէ մեկը պարունակում է 0
արժեքը (դատարկ է), ապա նշված բջջի և զրոն պարունակող բջջի արժեքները փոխատեղվում են։ Ամեն մի փոփոխությունից հետո մեկով ավելացվում է քայլերի հաշվիչը։
void GameNxM::oneStep( int rw , int cl )
{
if( matrix[rw-1][cl] == 0 ) {
qSwap(matrix[rw][cl], matrix[rw-1][cl]);
++steps;
}
else if( matrix[rw+1][cl] == 0 ) {
qSwap(matrix[rw][cl], matrix[rw+1][cl]);
++steps;
}
else if( matrix[rw][cl-1] == 0 ) {
qSwap(matrix[rw][cl], matrix[rw][cl-1]);
++steps;
}
else if( matrix[rw][cl+1] == 0 ) {
qSwap(matrix[rw][cl], matrix[rw][cl+1]);
++steps;
}
}
Այս մեթոդի պարզությունը ստացվել է այն բանի շնորհիվ, որ մատրիցի պարագծով գրված են -1
արժեքները։ Հակառակ դեպքում step
մեթոդի ստուգումներն ավելի շատ ու ավելի բարդ կլինեին։
Խաղը համարվում է ավարտված, եթե [1;N×M-1]
թվերը դասավորված են ճիշտ հաջորդականությամբ (աճման կարգով)։ gameOver
մեթոդի for
ցիկլն անցնում է այդ թվերով և ստուգում է, որ դրանք գրանցված լինեն ճիշտ ինդեքսներով։
bool GameNxM::gameOver() const
{
for( int i = 1; i < rows * columns; ++i ) {
auto r = i / rows + 1;
auto c = i % rows;
if( matrix[r][c] != i ) return false;
}
return true;
}
Ինձ անհրաժեշտ է, որ խաղաքարն ունենա որոշակի ֆիքսված հատկություններ․ չափ, եզրագիծ, տառատեսակ և այլն։ Բացի այդ, ես ուզում եմ, որ մկնիկի click
ազդանշանին (signal) խաղաքարը արձագանքի իր տողի և սյան համարներով։ Qt գրադարանի QLabel
օբյեկտն ամենահարման էր այնպիսի կարգավորումների համար։ Ես ընդլայնել եմ QLabel
դասը որպես Tile
(խաղաքար) դաս, նրանում ավելացնելով clicked
ազդանշանը։
class Tile : public QLabel {
Q_OBJECT
public:
Tile( int, int, QWidget* = nullptr );
private:
int row;
int column;
signals:
void clicked( int, int );
protected:
void mousePressEvent( QMouseEvent* event ) override;
};
Tile
դասի row
անդամը ցույց է տալիս, թե խաղաքարը խաղադաշտի որ տողի մեջ է, իսկ column
անդամը՝ թե որ սյան մեջ է։ Այս անդամների արժեքները տրվում են կոնստրուկտրի առաջին երկու պարամետրերով (դրանք սկսվում են 1
-ից)։ Կոնստրուկտորի մեջ են որոշվում նաև խաղաքարի հիմնական հատկությունները։
Խաղադաշտը մոդելավորելու համար ես QWidget
դասն ընդլայնել եմ որպես Borad
(խաղադաշտ) դաս։ rows
անդամը տողերի քանակն է, columns
անդամը՝ սյուների, իսկ tiles
ցուցակը պարունակում է խաղաքարերի հասցեները։ Կոնստրուկտորով տրվում են խաղադաշտի չափերը։
class Board : public QWidget {
Q_OBJECT
public:
Board( int, int, QWidget* = nullptr );
void setModel( GameNxM* );
private:
void updateLabels();
private:
int rows = 0;
int columns = 0;
QVector<Tile*> tiles;
GameNxM* model = nullptr;
private slots:
void clickedOnTile( int, int );
};
Խաղադաշտի վրա խաղաքարերը դասավորվում են QGridLayout
-ի օգնությամբ։ Եվ բոլոր խաղաքարերի clicked
սիգնալը կապվում է Board
դասի clickedOnTile
սլոտին։ clickedOnTile
սլոտը կատարում է խաղի մեկ քայլ՝ կանչելով մոդելի step
մեթոդը, և թարմացնում է խաղաքարերի թվերը Board
դասի updateLabels
մեթոդով։
void Board::clickedOnTile( int r, int c )
{
model->oneStep(r, c);
updateLabels();
}
```
## Ծրագրի գլխավոր պատուհանը
`Window` դասը Qt գրադարանի `QMainWindow` դասի ընդլայնումն է։ `board` անդամը խաղատախտակի ցուցիչն է, իսկ `model` անդամը խաղի մոդելի ցուցիչն է։
````c++
class Window : public QMainWindow {
Q_OBJECT
public:
explicit Window( QWidget* parent = nullptr );
private:
Board* board = nullptr;
GameNxM* engine = nullptr;
};
Կոնստրուկտորը ստեղծում է խաղատախտակն ու խաղի մոդելը և դրանք կապում համապատասխան ցուցիչներին։
Window::Window( QWidget* parent )
: QMainWindow(parent)
{
setWindowTitle( "Game NxM");
board = new Board(4, 4, this);
engine = new GameNxM(4, 4);
board->setModel(engine);
setCentralWidget(board);
}
Առայժմ ես ուզում եմ գլխավոր պատուհանի մենյուների տողում ունենալ երկու մենյու․ «Game» և «Help»։ Առաջինում լինելու է երկու գործողություն․ «New», որը սկսում է նոր խաղ, և «Exit», որն ավարտում է ծրագրի աշխատանքը։ «Help» մենյուն ունենալու է միյան մեկ կետ՝ «About», որը տեղեկություն է տալու ծրագրի մասին։ Առայժմ մենյուների տեքստերը թող լինեն անգլերեն, իսկ հետո ցույց կտամ, թե ինչպես ծրագրում ավելացնել այլ լեզուներ։
Մինչև մենյուները կառուցելը Window
դասում ավելացնեմ երկու սլոտ․ newGame
, որի օգնությամբ սկսվելու է նոր խաղ.
void Window::newGame()
{
if( engine != nullptr ) {
engine->reset();
board->setModel(engine);
}
}
Եվ aboutGame
սլոտը, որը ցույց է տալու տեղեկությունների պատուհանը․
void Window::aboutGame()
{
QString text = "<b>Game N×M</b> - 2015";
QMessageBox::about(this, "Game N×M", text);
}
Հիմա մենյուների մասին։ Ծրագրի մենյուների տողը QMenuBar
տիպի օբյեկտ է, իսկ «Game» և «Help» մենյուները՝ QMenu
տիպի։ Window
դասում հայտարարեմ համապատասխան ցուցիչները․
QMenuBar* mainMenu = nullptr;
QMenu* mnuGame = nullptr;
QMenu* mnuHelp = nullptr;
Որպեսզի newGame
, close
և aboutGame
սլոտները կապեմ մենյուների կետերին, պետք են երեք QAction
օբյեկտներ։ Դրանց համար նույնպես հայտարարեմ ցուցիչներ․
QAction* actNew = nullptr; // նոր խաղ
QAction* actEnd = nullptr; // ելք ծրագրից
QAction* actAbout = nullptr; // ծարագրի մասին
Window
դասում ավելացնեմ createActions
և createMenus
մեթոդները (սրանք private են), որոնցից առաջինը կառուցում է գործողությունները, իսկ երկրորդը՝ մենյունները։ Հետո այս մեթոդները կանչվելու են կոնստրուկտորից։ (Կարելի է, իհարկե, այս երկու մեթոդների կոդը գրել միանգամից կոնստրուկտորի մեջ։)
createActions
մեթոդում ամեն մի QAction
ցուցիչին կապվում է նոր ստեղծված QAction
օբյեկտ, որի առաջին արգումենտը նրա անունն է (այս անունը դառնալու է մենյուի տեքստ), իսկ երկրորդը հիմնական պատուհանի ցուցյիչը։ Հետո, connect
ֆունկցիայի միջոցով, այդ գործողության triggered
ազդանշանին կապվում է Window
դասի համապատասխան սլոտը։ Օրինակ, նոր խաղ սկսող «New» գործողության համար․
actNew = new QAction("New", this);
connect(actNew, SIGNAL(triggered()), this, SLOT(newGame()));
Մենյուները կառուցելու համար պետք է նախ ստեղծել մենյուների տողը․
mainMenu = new QMenuBar(this);
Գետո պետք է ստեղծել առանձին մենյուները, դրանցում ավելացնել գործողությունները, իսկ մենյուն էլ ավելացնել մենյուների տողում։ Օրինակ, «Game» մենյուի համար․
mnuGame = new QMenu("Game", mainMenu);
mainMenu->addAction(mnuGame->menuAction());
mnuGame->addAction(actNew);
mnuGame->addSeparator();
mnuGame->addAction(actEnd);
Եվ վերջում, Window
դասի setMenuBar
մեթոդով mainMenu
նշել որպես հիմնական մենյու։
setMenuBar(mainMenu);