Skip to content
Խաղ 15֊ի (Game 15, Puzzle 15) ծրագրավորումը Qt գրադարանի օգտագործմամբ։
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
icons
.gitignore Initial commit Mar 3, 2015
LICENSE
README.md Լրացումներ 5 Mar 14, 2015
board.cpp
board.h Լրացումներ 5 Mar 14, 2015
gamenxm.cpp Լրացումներ 5 Mar 14, 2015
gamenxm.h Լրացումներ 5 Mar 14, 2015
gamenxm.pro Լրացումներ 5 Mar 14, 2015
gamenxm.qrc Լրացումներ 5 Mar 14, 2015
gamenxm_hy.ts Լրացումներ 6 Mar 14, 2015
main.cpp Լրացումներ 5 Mar 14, 2015
tile.cpp
tile.h Լրացումներ 3 Mar 8, 2015
window.cpp Լրացումներ 5 Mar 14, 2015
window.h

README.md

Խաղ N×M

Իմ նպատակն է արդեն դասական դարձած Խաղ 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);
You can’t perform that action at this time.