board.cpp 63.9 KB
Newer Older
Frederik Schwarzer's avatar
Frederik Schwarzer committed
1
2
/***************************************************************************
 *   KShisen - A japanese game similar to mahjongg                         *
3
4
5
 *   Copyright 1997   Mario Weilguni <mweilguni@sime.com>                  *
 *   Copyright 2002-2004  Dave Corrie <kde@davecorrie.com>                 *
 *   Copyright 2007  Mauricio Piacentini <mauricio@tabuleiro.com>          *
6
 *   Copyright 2009-2016  Frederik Schwarzer <schwarzer@kde.org>           *
Frederik Schwarzer's avatar
Frederik Schwarzer committed
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/
21

Frederik Schwarzer's avatar
Frederik Schwarzer committed
22
// own
23
#include "board.h"
Laurent Montel's avatar
Laurent Montel committed
24

Frederik Schwarzer's avatar
Frederik Schwarzer committed
25
26
27
// STL
#include <algorithm>
#include <array>
Frederik Schwarzer's avatar
Frederik Schwarzer committed
28

Frederik Schwarzer's avatar
Frederik Schwarzer committed
29
// Qt
Frederik Schwarzer's avatar
Frederik Schwarzer committed
30
31
#include <QMouseEvent>
#include <QPainter>
Frederik Schwarzer's avatar
Frederik Schwarzer committed
32
#include <QStandardPaths>
Frederik Schwarzer's avatar
Frederik Schwarzer committed
33
34
#include <QTimer>

Frederik Schwarzer's avatar
Frederik Schwarzer committed
35
36
37
38
// KDE
#include <KLocalizedString>

// KShisen
Frederik Schwarzer's avatar
Frederik Schwarzer committed
39
#include "debug.h"
Frederik Schwarzer's avatar
Frederik Schwarzer committed
40
#include "prefs.h"
Frederik Schwarzer's avatar
Frederik Schwarzer committed
41

42
43
namespace KShisen
{
44
45
46
#define EMPTY 0
#define SEASONS_START 28
#define FLOWERS_START 39
47

48
49
50
static std::array<int, 5> constexpr s_delay {{1000, 750, 500, 250, 125}};
static std::array<int, 6> constexpr s_sizeX {{14, 16, 18, 24, 26, 30}};
static std::array<int, 6> constexpr s_sizeY {{6, 9, 8, 12, 14, 16}};
51

Frederik Schwarzer's avatar
Frederik Schwarzer committed
52
Board::Board(QWidget * parent)
53
    : QWidget(parent)
Frederik Schwarzer's avatar
Frederik Schwarzer committed
54
55
56
57
58
59
    , m_gameClock()
    , m_tiles()
    , m_background()
    , m_random()
    , m_undo()
    , m_redo()
60
61
    , m_markX(0)
    , m_markY(0)
Frederik Schwarzer's avatar
Frederik Schwarzer committed
62
63
64
    , m_connection()
    , m_possibleMoves()
    , m_field()
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    , m_xTiles(0)
    , m_yTiles(0)
    , m_delay(0)
    , m_level(0)
    , m_shuffle(0)
    , m_gameState(GameState::Normal)
    , m_cheat(false)
    , m_gravityFlag(true)
    , m_solvableFlag(false)
    , m_chineseStyleFlag(false)
    , m_tilesCanSlideFlag(false)
    , m_highlightedTile(-1)
    , m_paintConnection(false)
    , m_paintPossibleMoves(false)
    , m_paintInProgress(false)
Frederik Schwarzer's avatar
Frederik Schwarzer committed
80
81
    , m_tileRemove1()
    , m_tileRemove2()
82
83
    , m_soundPick(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-touch.ogg")))
    , m_soundFall(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-fall-tile.ogg")))
Benjamin Meyer's avatar
Benjamin Meyer committed
84
{
85
    m_tileRemove1.setX(-1);
86

87
    m_random.setSeed(0);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
88
    resetTimer();
89

Frederik Schwarzer's avatar
Frederik Schwarzer committed
90
    QPalette palette;
91
92
    palette.setBrush(backgroundRole(), m_background.getBackground());
    setPalette(palette);
93

Frederik Schwarzer's avatar
Frederik Schwarzer committed
94
    loadSettings();
95
}
96

97
98
void Board::loadSettings()
{
99
    if (!loadTileset(Prefs::tileSet())) {
100
        qCWarning(KSHISEN_General) << "An error occurred when loading the tileset" << Prefs::tileSet() << "KShisen will continue with the default tileset.";
Frederik Schwarzer's avatar
Frederik Schwarzer committed
101
102
103
    }

    // Load background
104
    if (!loadBackground(Prefs::background())) {
105
        qCWarning(KSHISEN_General) << "An error occurred when loading the background" << Prefs::background() << "KShisen will continue with the default background.";
Frederik Schwarzer's avatar
Frederik Schwarzer committed
106
107
    }

108
    // There are tile sets, that have only one tile for e.g. the flowers group.
Frederik Schwarzer's avatar
Frederik Schwarzer committed
109
    // If these tile sets are played in non-chineseStyle, this one tile face
110
111
112
113
    // appears too often and not every tile matches another one with the same
    // face because they are technically different (e.g different flowers).
    // The solution is to enforce chineseStyle gameplay for tile sets that are
    // known to be reduced. Those are Egypt and Alphabet for now.
114
    if (Prefs::tileSet().endsWith(QLatin1String("egypt.desktop")) || Prefs::tileSet().endsWith(QLatin1String("alphabet.desktop"))) {
115
116
117
118
        setChineseStyleFlag(true);
    } else {
        setChineseStyleFlag(Prefs::chineseStyle());
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
119
    setTilesCanSlideFlag(Prefs::tilesCanSlide());
Frederik Schwarzer's avatar
Frederik Schwarzer committed
120
121
    // Need to load solvable before size because setSize calls newGame which
    // uses the solvable flag. Same with shuffle.
Frederik Schwarzer's avatar
Frederik Schwarzer committed
122
    setSolvableFlag(Prefs::solvable());
123
    m_shuffle = Prefs::level() * 4 + 1;
124
    setSize(s_sizeX.at(Prefs::size()), s_sizeY.at(Prefs::size()));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
125
    setGravityFlag(Prefs::gravity());
126
    setDelay(s_delay.at(Prefs::speed()));
127
    setSoundsEnabled(Prefs::sounds());
128

Frederik Schwarzer's avatar
Frederik Schwarzer committed
129
    if (m_level != Prefs::level()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
130
        newGame();
131
132
    }
    m_level = Prefs::level();
Benjamin Meyer's avatar
Benjamin Meyer committed
133
134
}

135
bool Board::loadTileset(QString const & pathToTileset)
136
{
137
    if (m_tiles.loadTileset(pathToTileset)) {
138
        if (m_tiles.loadGraphics()) {
139
            Prefs::setTileSet(pathToTileset);
Laurent Montel's avatar
Laurent Montel committed
140
            Prefs::self()->save();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
141
142
143
144
145
            resizeBoard();
        }
        return true;
    }
    //Try default
146
147
    if (m_tiles.loadDefault()) {
        if (m_tiles.loadGraphics()) {
148
            Prefs::setTileSet(m_tiles.path());
Laurent Montel's avatar
Laurent Montel committed
149
            Prefs::self()->save();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
150
151
152
153
            resizeBoard();
        }
    }
    return false;
154
155
}

156
bool Board::loadBackground(QString const & pathToBackground)
157
{
158
    if (m_background.load(pathToBackground, width(), height())) {
159
        if (m_background.loadGraphics()) {
160
            Prefs::setBackground(pathToBackground);
Laurent Montel's avatar
Laurent Montel committed
161
            Prefs::self()->save();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
162
163
164
165
166
            resizeBoard();
            return true;
        }
    }
    //Try default
167
168
    if (m_background.loadDefault()) {
        if (m_background.loadGraphics()) {
169
            Prefs::setBackground(m_background.path());
Laurent Montel's avatar
Laurent Montel committed
170
            Prefs::self()->save();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
171
172
173
174
            resizeBoard();
        }
    }
    return false;
175
176
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
177
int Board::xTiles() const
178
{
179
    return m_xTiles;
180
181
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
182
int Board::yTiles() const
183
{
184
    return m_yTiles;
185
186
}

187
188
189
190
191
int Board::tiles() const
{
    return m_field.size();
}

192
void Board::setField(TilePos const & tilePos, int value)
193
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
194
    if (!isValidPos(tilePos)) {
195
196
197
198
        qCCritical(KSHISEN_General) << "Attempted write to invalid field position:"
                                    << tilePos.x()
                                    << ","
                                    << tilePos.y();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
199
    }
200

201
    m_field.at(tilePos.y() * xTiles() + tilePos.x()) = value;
202
203
}

204
int Board::field(TilePos const & tilePos) const
205
{
206
    if (!isValidPosWithOutline(tilePos)) {
207
208
209
210
        qCCritical(KSHISEN_General) << "Attempted read from invalid field position:"
                                    << tilePos.x()
                                    << ","
                                    << tilePos.y();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
211
    }
212

Frederik Schwarzer's avatar
Frederik Schwarzer committed
213
    if (!isValidPos(tilePos)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
214
        return EMPTY;
215
    }
216

217
    return m_field.at(tilePos.y() * xTiles() + tilePos.x());
218
219
}

220
void Board::applyGravity()
221
{
222
    if (!m_gravityFlag) {
223
224
        return;
    }
225
    for (decltype(xTiles()) column = 0; column < xTiles(); ++column) {
226
227
        auto rptr = yTiles() - 1;
        auto wptr = yTiles() - 1;
228
        while (rptr >= 0) {
229
            auto wptrPos = TilePos(column, wptr);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
230
            if (field(wptrPos) != EMPTY) {
231
                --rptr;
232
                --wptr;
233
            } else {
234
                auto rptrPos = TilePos(column, rptr);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
235
236
237
                if (field(rptrPos) != EMPTY) {
                    setField(wptrPos, field(rptrPos));
                    setField(rptrPos, EMPTY);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
238
239
                    repaintTile(rptrPos);
                    repaintTile(wptrPos);
240
241
242
243
244
245
246
247
                    --wptr;
                    --rptr;
                    if (Prefs::sounds()) {
                        m_soundFall.start();
                    }
                } else {
                    --rptr;
                }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
248
249
250
            }
        }
    }
251
252
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
253
254
void Board::unmarkTile()
{
255
256
257
258
259
260
261
262
    // if nothing is marked, nothing to do
    if (m_markX == -1 || m_markY == -1) {
        return;
    }
    drawPossibleMoves(false);
    m_possibleMoves.clear();
    // We need to set m_markX and m_markY to -1 before calling
    // updateField() to ensure the tile is redrawn as unmarked.
263
    auto const oldTilePos = TilePos(m_markX, m_markY);
264
265
    m_markX = -1;
    m_markY = -1;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
266
    repaintTile(oldTilePos);
267
268
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
269
void Board::mousePressEvent(QMouseEvent * e)
270
{
271
272
273
274
275
276
277
278
    // Do not process mouse events while the connection is drawn.
    // Clicking on one of the already connected tiles would have selected
    // it before removing it. This is more a workaround than a proper fix
    // but I have to understand the usage of m_paintConnection first in
    // order to consider its reusage here. (schwarzer)
    if (m_paintInProgress) {
        return;
    }
279
    switch (m_gameState) {
280
        case GameState::Normal:
281
            break;
282
        case GameState::Over:
283
284
            newGame();
            return;
285
        case GameState::Paused:
286
287
            setPauseEnabled(false);
            return;
288
        case GameState::Stuck:
289
            return;
290
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
291
    // Calculate field position
292
293
    auto posX = (e->pos().x() - xOffset()) / (m_tiles.qWidth() * 2);
    auto posY = (e->pos().y() - yOffset()) / (m_tiles.qHeight() * 2);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
294

295
    if (e->pos().x() < xOffset() || e->pos().y() < yOffset() || posX >= xTiles() || posY >= yTiles()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
296
297
        posX = -1;
        posY = -1;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
298
299
300
    }

    // Mark tile
301
    if (e->button() == Qt::LeftButton) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
302
303
        clearHighlight();

Frederik Schwarzer's avatar
Frederik Schwarzer committed
304
        if (posX != -1) {
305
            marked(TilePos(posX, posY));
306
        } else {
Frederik Schwarzer's avatar
typo    
Frederik Schwarzer committed
307
            // unmark when clicking outside the board
308
            unmarkTile();
309
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
310
311
312
    }

    // Assist by highlighting all tiles of same type
313
    if (e->button() == Qt::RightButton) {
314
        auto const clickedTile = field(TilePos(posX, posY));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
315
316

        // Clear marked tile
317
        if (m_markX != -1 && field(TilePos(m_markX, m_markY)) != clickedTile) {
318
            unmarkTile();
319
        } else {
320
321
            m_markX = -1;
            m_markY = -1;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
322
323
324
        }

        // Perform highlighting
Frederik Schwarzer's avatar
Frederik Schwarzer committed
325
        if (clickedTile != m_highlightedTile) {
326
            auto const oldHighlighted = m_highlightedTile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
327
            m_highlightedTile = clickedTile;
328
329
330
            for (decltype(xTiles()) i = 0; i < xTiles(); ++i) {
                for (decltype(yTiles()) j = 0; j < yTiles(); ++j) {
                    auto const fieldTile = field(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
331
332
                    if (fieldTile != EMPTY) {
                        if (fieldTile == oldHighlighted) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
333
                            repaintTile(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
334
                        } else if (fieldTile == clickedTile) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
335
                            repaintTile(TilePos(i, j));
336
                        } else if (m_chineseStyleFlag) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
337
                            if (clickedTile >= SEASONS_START && clickedTile <= (SEASONS_START + 3) && fieldTile >= SEASONS_START && fieldTile <= (SEASONS_START + 3)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
338
                                repaintTile(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
339
                            } else if (clickedTile >= FLOWERS_START && clickedTile <= (FLOWERS_START + 3) && fieldTile >= FLOWERS_START && fieldTile <= (FLOWERS_START + 3)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
340
                                repaintTile(TilePos(i, j));
341
                            }
342
                            // oldHighlighted
Frederik Schwarzer's avatar
Frederik Schwarzer committed
343
                            if (oldHighlighted >= SEASONS_START && oldHighlighted <= (SEASONS_START + 3) && fieldTile >= SEASONS_START && fieldTile <= (SEASONS_START + 3)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
344
                                repaintTile(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
345
                            } else if (oldHighlighted >= FLOWERS_START && oldHighlighted <= (FLOWERS_START + 3) && fieldTile >= FLOWERS_START && fieldTile <= (FLOWERS_START + 3)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
346
                                repaintTile(TilePos(i, j));
347
                            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
348
349
350
351
352
353
                        }
                    }
                }
            }
        }
    }
354
355
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
356
int Board::xOffset() const
357
{
358
    auto tw = m_tiles.qWidth() * 2;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
359
    return (width() - (tw * xTiles())) / 2;
360
361
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
362
int Board::yOffset() const
363
{
364
    auto th = m_tiles.qHeight() * 2;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
365
    return (height() - (th * yTiles())) / 2;
366
367
368
369
}

void Board::setSize(int x, int y)
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
370
    if (x == xTiles() && y == yTiles()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
371
        return;
372
    }
373

374
    m_field.resize(x * y);
375

376
377
    m_xTiles = x;
    m_yTiles = y;
378

Frederik Schwarzer's avatar
Frederik Schwarzer committed
379
    std::fill(m_field.begin(), m_field.end(), EMPTY);
380

Frederik Schwarzer's avatar
Frederik Schwarzer committed
381
    // set the minimum size of the scalable window
382
    auto constexpr minScale = 0.2;
383
384
    auto const w = qRound(m_tiles.qWidth() * 2.0 * minScale) * xTiles() + m_tiles.width();
    auto const h = qRound(m_tiles.qHeight() * 2.0 * minScale) * yTiles() + m_tiles.height();
385

Frederik Schwarzer's avatar
Frederik Schwarzer committed
386
    setMinimumSize(w, h);
387

Frederik Schwarzer's avatar
Frederik Schwarzer committed
388
389
390
    resizeBoard();
    newGame();
    emit changed();
391
392
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
393
void Board::resizeEvent(QResizeEvent * e)
394
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
395
    qCDebug(KSHISEN_General) << "[resizeEvent]";
396
    if (e->spontaneous()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
397
        qCDebug(KSHISEN_General) << "[resizeEvent] spontaneous";
398
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
399
400
    resizeBoard();
    emit resized();
401
402
403
404
}

void Board::resizeBoard()
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
405
    // calculate tile size required to fit all tiles in the window
406
    auto const newsize = m_tiles.preferredTileSize(QSize(width(), height()), xTiles(), yTiles());
407
    m_tiles.reloadTileset(newsize);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
408
    //recalculate bg, if needed
409
    m_background.sizeChanged(width(), height());
Frederik Schwarzer's avatar
Frederik Schwarzer committed
410
411
    //reload our bg brush, using the cache in libkmahjongg if possible
    QPalette palette;
412
413
    palette.setBrush(backgroundRole(), m_background.getBackground());
    setPalette(palette);
414
415
}

416

417
418
void Board::newGame()
{
419
    m_gameState = GameState::Normal;
420
    setCheatModeEnabled(false);
421

422
423
424
    m_markX = -1;
    m_markY = -1;
    m_highlightedTile = -1; // will clear previous highlight
Frederik Schwarzer's avatar
Frederik Schwarzer committed
425
426
427

    resetUndo();
    resetRedo();
428
429
    m_connection.clear();
    m_possibleMoves.clear();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
430
431

    // distribute all tiles on board
432
433
    auto curTile = 1;
    auto tileCount = 0;
434

Frederik Schwarzer's avatar
Frederik Schwarzer committed
435
436
437
438
439
440
441
442
    /*
     * Note by jwickers: i changed the way to distribute tiles
     *  in chinese mahjongg there are 4 tiles of each
     *  except flowers and seasons (4 flowers and 4 seasons,
     *  but one unique tile of each, that is why they are
     *  the only ones numbered)
     * That uses the chineseStyle flag
     */
443
444
    for (decltype(yTiles()) y = 0; y < yTiles(); ++y) {
        for (decltype(xTiles()) x = 0; x < xTiles(); ++x) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
445
            // do not duplicate flowers or seasons
446
            if (!m_chineseStyleFlag || !((curTile >= SEASONS_START && curTile <= (SEASONS_START + 3)) || (curTile >= FLOWERS_START && curTile <= (FLOWERS_START + 3)))) {
447
                setField(TilePos(x, y), curTile);
448
449
450
                if (++tileCount >= 4) {
                    tileCount = 0;
                    ++curTile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
451
                }
452
            } else {
453
                tileCount = 0;
454
                setField(TilePos(x, y), curTile++);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
455
            }
456
457
            if (curTile > Board::nTiles) {
                curTile = 1;
458
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
459
460
461
        }
    }

462
    if (m_shuffle == 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
463
464
        update();
        resetTimer();
465
        emit newGameStarted();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
466
467
468
469
470
        emit changed();
        return;
    }

    // shuffle the field
471
472
473
474
475
    auto const tx = xTiles();
    auto const ty = yTiles();
    for (decltype(tx * ty * m_shuffle) i = 0; i < tx * ty * m_shuffle; ++i) {
        auto const tilePos1 = TilePos(m_random.getLong(tx), m_random.getLong(ty));
        auto const tilePos2 = TilePos(m_random.getLong(tx), m_random.getLong(ty));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
476
        // keep and use t, because the next setField() call changes what field() will return
477
478
        // so there would a significant impact on shuffling with the field() call put into the
        // place where 't' is used
479
        auto const t = field(tilePos1);
480
481
        setField(tilePos1, field(tilePos2));
        setField(tilePos2, t);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
482
483
    }

484
    // if m_solvableFlag is false, the game does not need to be solvable; we can drop out here
485
    if (!m_solvableFlag) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
486
487
        update();
        resetTimer();
488
        emit newGameStarted();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
489
490
491
492
493
        emit changed();
        return;
    }


494
    auto oldfield = m_field;
495
496
    decltype(m_field) tiles(m_field.size());
    decltype(m_field) pos(m_field.size());
Frederik Schwarzer's avatar
Frederik Schwarzer committed
497
    //jwickers: in case the game cannot made solvable we do not want to run an infinite loop
498
    auto maxAttempts = 200;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
499

500
    while (!isSolvable(false) && maxAttempts > 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
501
        // generate a list of free tiles and positions
502
503
        auto numberOfTiles = 0;
        for (decltype(xTiles() * yTiles()) i = 0; i < xTiles() * yTiles(); ++i) {
504
505
506
            if (m_field.at(i) != EMPTY) {
                pos.at(numberOfTiles) = i;
                tiles.at(numberOfTiles) = m_field.at(i);
507
                ++numberOfTiles;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
508
            }
509
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
510
511

        // restore field
512
        m_field = oldfield;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
513
514

        // redistribute unsolved tiles
515
        while (numberOfTiles > 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
516
            // get a random tile
517
518
519
520
            auto const r1 = m_random.getLong(numberOfTiles);
            auto const r2 = m_random.getLong(numberOfTiles);
            auto const tile = tiles.at(r1);
            auto const apos = pos.at(r2);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
521
522

            // truncate list
523
524
            tiles.at(r1) = tiles.at(numberOfTiles - 1);
            pos.at(r2) = pos.at(numberOfTiles - 1);
525
            --numberOfTiles;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
526
527

            // put this tile on the new position
528
            m_field.at(apos) = tile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
529
530
531
        }

        // remember field
532
        oldfield = m_field;
533
        --maxAttempts;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
534
535
    }
    // debug, tell if make solvable failed
536
    if (maxAttempts == 0) {
537
        qCCritical(KSHISEN_General) << "NewGame make solvable failed";
Frederik Schwarzer's avatar
Frederik Schwarzer committed
538
539
540
541
    }


    // restore field
542
    m_field = oldfield;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
543
544
545
546

    update();
    resetTimer();
    emit changed();
547
548
}

549
550
bool Board::tilesMatch(int tile1, int tile2) const
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
551
    // identical tiles always match
552
    if (tile1 == tile2) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
553
        return true;
554
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
555
556
    // when chinese style is set, there are special rules
    // for flowers and seasons
557
    if (m_chineseStyleFlag) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
558
        // if both tiles are seasons
559
        if (tile1 >= SEASONS_START && tile1 <= SEASONS_START + 3
560
            && tile2 >= SEASONS_START && tile2 <= SEASONS_START + 3) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
561
            return true;
562
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
563
        // if both tiles are flowers
564
        if (tile1 >= FLOWERS_START && tile1 <= FLOWERS_START + 3
565
            && tile2 >= FLOWERS_START && tile2 <= FLOWERS_START + 3) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
566
            return true;
567
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
568
569
    }
    return false;
570
571
}

572
bool Board::isTileHighlighted(TilePos const & tilePos) const
573
{
574
    if (tilePos.x() == m_markX && tilePos.y() == m_markY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
575
        return true;
576
    }
577

578
    if (tilesMatch(m_highlightedTile, field(tilePos))) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
579
        return true;
580
    }
581

582
    // m_tileRemove1.first != -1 is used because the repaint of the first if
583
    // on undrawConnection highlighted the tiles that fell because of gravity
584
585
    if (!m_connection.empty() && m_tileRemove1.x() != -1) {
        if (tilePos.x() == m_connection.front().x() && tilePos.y() == m_connection.front().y()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
586
            return true;
587
        }
588

589
        if (tilePos.x() == m_connection.back().x() && tilePos.y() == m_connection.back().y()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
590
            return true;
591
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
592
    }
593

Frederik Schwarzer's avatar
Frederik Schwarzer committed
594
    return false;
595
596
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
597
void Board::repaintTile(TilePos const & tilePos)
598
{
599
    auto const r = QRect(xOffset() + tilePos.x() * m_tiles.qWidth() * 2,
Frederik Schwarzer's avatar
Format.    
Frederik Schwarzer committed
600
601
602
                         yOffset() + tilePos.y() * m_tiles.qHeight() * 2,
                         m_tiles.width(),
                         m_tiles.height());
603

Frederik Schwarzer's avatar
Frederik Schwarzer committed
604
    update(r);
605
606
}

607
void Board::showInfoRect(QPainter & p, QString const & message)
608
{
609
610
611
    auto const boxWidth = width() * 0.6;
    auto const boxHeight = height() * 0.6;
    auto const contentsRect = QRect((width() - boxWidth) / 2, (height() - boxHeight) / 2, boxWidth, boxHeight);
612
    QFont font;
613
    auto const fontsize = static_cast<int>(boxHeight / 13);
614
615
616
617
618
    font.setPointSize(fontsize);
    p.setFont(font);
    p.setBrush(QBrush(QColor(100, 100, 100, 150)));
    p.setRenderHint(QPainter::Antialiasing);
    p.drawRoundedRect(contentsRect, 10, 10);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
619

620
621
    p.drawText(contentsRect, Qt::AlignCenter | Qt::TextWordWrap, message);
}
Frederik Schwarzer's avatar
Frederik Schwarzer committed
622

Frederik Schwarzer's avatar
Frederik Schwarzer committed
623
void Board::drawTiles(QPainter & p, QPaintEvent * e)
624
{
625
626
627
628
629
630
631
    auto const w = m_tiles.width();
    auto const h = m_tiles.height();
    auto const fw = m_tiles.qWidth() * 2;
    auto const fh = m_tiles.qHeight() * 2;
    for (decltype(xTiles()) i = 0; i < xTiles(); ++i) {
        for (decltype(yTiles()) j = 0; j < yTiles(); ++j) {
            auto const tile = field(TilePos(i, j));
632
633
634
            if (tile == EMPTY) {
                continue;
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
635

636
637
638
            auto const xpos = xOffset() + i * fw;
            auto const ypos = yOffset() + j * fh;
            auto const r = QRect(xpos, ypos, w, h);
639
            if (e->rect().intersects(r)) {
640
                if (isTileHighlighted(TilePos(i, j))) {
641
642
643
                    p.drawPixmap(xpos, ypos, m_tiles.selectedTile(1));
                } else {
                    p.drawPixmap(xpos, ypos, m_tiles.unselectedTile(1));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
644
                }
645
646
647

                //draw face
                p.drawPixmap(xpos, ypos, m_tiles.tileface(tile - 1));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
648
649
650
            }
        }
    }
651
652
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
653
void Board::paintEvent(QPaintEvent * e)
654
{
655
    auto const ur = e->rect(); // rectangle to update
656
657
658
659
    QPainter p(this);
    p.fillRect(ur, m_background.getBackground());

    switch (m_gameState) {
660
        case GameState::Normal:
661
662
            drawTiles(p, e);
            break;
663
        case GameState::Paused:
664
665
            showInfoRect(p, i18n("Game Paused\nClick to resume game."));
            break;
666
        case GameState::Stuck:
667
668
669
            drawTiles(p, e);
            showInfoRect(p, i18n("Game Stuck\nNo more moves possible."));
            break;
670
        case GameState::Over:
671
672
673
            showInfoRect(p, i18n("Game Over\nClick to start a new game."));
            break;
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
674

675
    if (m_paintConnection) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
676
677
        p.setPen(QPen(QColor("red"), lineWidth()));

678
        auto pt1 = m_connection.cbegin();
679
        auto pt2 = pt1 + 1;
680
        while (pt2 != m_connection.cend()) {
681
            p.drawLine(midCoord(*pt1), midCoord(*pt2));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
682
683
684
            ++pt1;
            ++pt2;
        }
685
        QTimer::singleShot(delay(), this, &Board::undrawConnection);
686
        m_paintConnection = false;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
687
    }
688
    if (m_paintPossibleMoves) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
689
690
        p.setPen(QPen(QColor("blue"), lineWidth()));
        // paint all possible moves
691
        foreach (auto const move, m_possibleMoves) {
692
            auto pt1 = move.path().cbegin();
693
            auto pt2 = pt1 + 1;
694
            while (pt2 != move.path().cend()) {
695
                p.drawLine(midCoord(*pt1), midCoord(*pt2));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
696
697
698
699
                ++pt1;
                ++pt2;
            }
        }
700
        m_paintConnection = false;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
701
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
702
    p.end();
703
704
}

705
void Board::reverseSlide(TilePos const & tilePos, Slide const & slide)
706
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
707
708
    // slide[XY]2 is the current location of the last tile to slide
    // slide[XY]1 is its destination
Frederik Schwarzer's avatar
Frederik Schwarzer committed
709
    // calculate the offset for the tiles to slide
710
711
    auto const dx = slide.front().x() - slide.back().x();
    auto const dy = slide.front().y() - slide.back().y();
712
    auto currentTile = 0;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
713
    // move all tiles between slideX2, slideY2 and x, y to slide with that offset
714
    if (dx == 0) {
715
716
        if (tilePos.y() < slide.back().y()) {
            for (auto i = tilePos.y() + 1; i <= slide.back().y(); ++i) {
717
718
                currentTile = field(TilePos(tilePos.x(), i));
                if (currentTile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
719
                    continue;
720
                }
721
                setField(TilePos(tilePos.x(), i), EMPTY);
722
                setField(TilePos(tilePos.x(), i + dy), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
723
724
                repaintTile(TilePos(tilePos.x(), i));
                repaintTile(TilePos(tilePos.x(), i + dy));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
725
            }
726
        } else {
727
            for (auto i = tilePos.y() - 1; i >= slide.back().y(); --i) {
728
729
                currentTile = field(TilePos(tilePos.x(), i));
                if (currentTile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
730
                    continue;
731
                }
732
                setField(TilePos(tilePos.x(), i), EMPTY);
733
                setField(TilePos(tilePos.x(), i + dy), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
734
735
                repaintTile(TilePos(tilePos.x(), i));
                repaintTile(TilePos(tilePos.x(), i + dy));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
736
737
            }
        }
738
    } else if (dy == 0) {
739
740
        if (tilePos.x() < slide.back().x()) {
            for (auto i = tilePos.x() + 1; i <= slide.back().x(); ++i) {
741
742
                currentTile = field(TilePos(i, tilePos.y()));
                if (currentTile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
743
                    continue;
744
                }
745
                setField(TilePos(i, tilePos.y()), EMPTY);
746
                setField(TilePos(i + dx, tilePos.y()), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
747
748
                repaintTile(TilePos(i, tilePos.y()));
                repaintTile(TilePos(i + dx, tilePos.y()));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
749
            }
750
        } else {
751
            for (auto i = tilePos.x() - 1; i >= slide.back().x(); --i) {
752
753
                currentTile = field(TilePos(i, tilePos.y()));
                if (currentTile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
754
                    continue;
755
                }
756
                setField(TilePos(i, tilePos.y()), EMPTY);
757
                setField(TilePos(i + dx, tilePos.y()), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
758
759
                repaintTile(TilePos(i, tilePos.y()));
                repaintTile(TilePos(i + dx, tilePos.y()));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
760
761
762
            }
        }
    }
763
764
}

765
void Board::performSlide(TilePos const & tilePos, Slide const & slide)
766
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
767
    // check if there is something to slide
768
    if (slide.empty()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
769
        return;
770
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
771
772
773
774

    // slide.first is the current location of the last tile to slide
    // slide.last is its destination
    // calculate the offset for the tiles to slide
775
776
    auto const dx = slide.back().x() - slide.front().x();
    auto const dy = slide.back().y() - slide.front().y();
777
    auto currentTile = 0;
778
    // move all tiles between m_markX, m_markY and the last tile to slide with that offset
779
    if (dx == 0) {
780
        if (tilePos.y() < slide.front().y()) {
781
            for (auto i = slide.front().y(); i > tilePos.y(); --i) {
782
                currentTile = field(TilePos(tilePos.x(), i));
783
                setField(TilePos(tilePos.x(), i), EMPTY);
784
                setField(TilePos(tilePos.x(), i + dy), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
785
786
                repaintTile(TilePos(tilePos.x(), i));
                repaintTile(TilePos(tilePos.x(), i + dy));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
787
            }
788
        } else {
789
            for (auto i = slide.front().y(); i < tilePos.y(); ++i) {
790
                currentTile = field(TilePos(tilePos.x(), i));
791
                setField(TilePos(tilePos.x(), i), EMPTY);
792
                setField(TilePos(tilePos.x(), i + dy), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
793
794
                repaintTile(TilePos(tilePos.x(), i));
                repaintTile(TilePos(tilePos.x(), i + dy));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
795
796
            }
        }
797
    } else if (dy == 0) {
798
        if (tilePos.x() < slide.front().x()) {
799
            for (auto i = slide.front().x(); i > tilePos.x(); --i) {
800
                currentTile = field(TilePos(i, tilePos.y()));
801
                setField(TilePos(i, tilePos.y()), EMPTY);
802
                setField(TilePos(i + dx, tilePos.y()), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
803
804
                repaintTile(TilePos(i, tilePos.y()));
                repaintTile(TilePos(i + dx, tilePos.y()));