board.cpp 63.8 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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
48
static std::array<int, 5> const s_delay = {1000, 750, 500, 250, 125};
49
static std::array<int, 6> const s_sizeX = {14, 16, 18, 24, 26, 30};
50
static std::array<int, 6> const 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
    , 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)
Frederik Schwarzer's avatar
Frederik Schwarzer committed
76
    , m_gravCols()
77 78 79 80
    , m_highlightedTile(-1)
    , m_paintConnection(false)
    , m_paintPossibleMoves(false)
    , m_paintInProgress(false)
Frederik Schwarzer's avatar
Frederik Schwarzer committed
81 82
    , m_tileRemove1()
    , m_tileRemove2()
83 84
    , 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
85
{
86
    m_tileRemove1.setX(-1);
87

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

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

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

98 99
void Board::loadSettings()
{
100
    if (!loadTileset(Prefs::tileSet())) {
101
        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
102 103 104
    }

    // Load background
105
    if (!loadBackground(Prefs::background())) {
106
        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
107 108
    }

109
    // There are tile sets, that have only one tile for e.g. the flowers group.
Frederik Schwarzer's avatar
Frederik Schwarzer committed
110
    // If these tile sets are played in non-chineseStyle, this one tile face
111 112 113 114
    // 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.
115
    if (Prefs::tileSet().endsWith(QLatin1String("egypt.desktop")) || Prefs::tileSet().endsWith(QLatin1String("alphabet.desktop"))) {
116 117 118 119
        setChineseStyleFlag(true);
    } else {
        setChineseStyleFlag(Prefs::chineseStyle());
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
120
    setTilesCanSlideFlag(Prefs::tilesCanSlide());
Frederik Schwarzer's avatar
Frederik Schwarzer committed
121 122
    // 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
123
    setSolvableFlag(Prefs::solvable());
124
    m_shuffle = Prefs::level() * 4 + 1;
125
    setSize(s_sizeX.at(Prefs::size()), s_sizeY.at(Prefs::size()));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
126
    setGravityFlag(Prefs::gravity());
127
    setDelay(s_delay.at(Prefs::speed()));
128
    setSoundsEnabled(Prefs::sounds());
129

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

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

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

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

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

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

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

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

205
// TODO: Why is this called every second?
206
int Board::field(TilePos const & tilePos) const
207 208
{
#ifdef DEBUGGING
209
    if (!isValidPosWithOutline(tilePos)) {
210 211 212 213
        qCCritical(KSHISEN_General) << "Attempted read from invalid field position:"
                                    << tilePos.x()
                                    << ","
                                    << tilePos.y();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
214
    }
215 216
#endif

Frederik Schwarzer's avatar
Frederik Schwarzer committed
217
    if (!isValidPos(tilePos)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
218
        return EMPTY;
219
    }
220

221
    return m_field.at(tilePos.y() * xTiles() + tilePos.x());
222 223
}

224
void Board::gravity(bool update)
225
{
226
    m_gravCols.clear();
227
    if (!m_gravityFlag) {
228 229
        return;
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
230
    bool fallingTiles = false;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
231
    for (int i = 0; i < xTiles(); ++i) {
232
        if (gravity(i, update)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
233
            fallingTiles = true;
234
            m_gravCols.push_back(i);
235 236
        }
    }
237
    if (Prefs::sounds() && fallingTiles) {
238
        m_soundFall.start();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
239
    }
240 241
}

242
bool Board::gravity(int column, bool update)
243
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
244
    bool isAffected = false;
245
    if (m_gravityFlag) {
246 247
        int rptr = yTiles() - 1;
        int wptr = yTiles() - 1;
248
        while (rptr >= 0) {
249
            if (field(TilePos(column, wptr)) != EMPTY) {
250 251
                --rptr;
                --wptr;
252
            } else {
253 254 255
                if (field(TilePos(column, rptr)) != EMPTY) {
                    setField(TilePos(column, wptr), field(TilePos(column, rptr)));
                    setField(TilePos(column, rptr), EMPTY);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
256
                    isAffected = true;
257
                    if (update) {
258 259
                        updateField(TilePos(column, rptr));
                        updateField(TilePos(column, wptr));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
260
                    }
261 262
                    --wptr;
                    --rptr;
263
                } else {
264
                    --rptr;
265
                }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
266 267 268
            }
        }
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
269
    return isAffected;
270 271
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
272 273
void Board::unmarkTile()
{
274 275 276 277 278 279 280 281
    // 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.
282
    TilePos const oldTilePos(m_markX, m_markY);
283 284
    m_markX = -1;
    m_markY = -1;
285
    updateField(oldTilePos);
286 287
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
288
void Board::mousePressEvent(QMouseEvent * e)
289
{
290 291 292 293 294 295 296 297
    // 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;
    }
298
    switch (m_gameState) {
299
        case GameState::Normal:
300
            break;
301
        case GameState::Over:
302 303
            newGame();
            return;
304
        case GameState::Paused:
305 306
            setPauseEnabled(false);
            return;
307
        case GameState::Stuck:
308
            return;
309
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
310
    // Calculate field position
Frederik Schwarzer's avatar
Frederik Schwarzer committed
311 312
    int posX = (e->pos().x() - xOffset()) / (m_tiles.qWidth() * 2);
    int posY = (e->pos().y() - yOffset()) / (m_tiles.qHeight() * 2);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
313

314
    if (e->pos().x() < xOffset() || e->pos().y() < yOffset() || posX >= xTiles() || posY >= yTiles()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
315 316
        posX = -1;
        posY = -1;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
317 318 319
    }

    // Mark tile
320
    if (e->button() == Qt::LeftButton) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
321 322
        clearHighlight();

Frederik Schwarzer's avatar
Frederik Schwarzer committed
323
        if (posX != -1) {
324
            marked(TilePos(posX, posY));
325
        } else {
Frederik Schwarzer's avatar
typo  
Frederik Schwarzer committed
326
            // unmark when clicking outside the board
327
            unmarkTile();
328
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
329 330 331
    }

    // Assist by highlighting all tiles of same type
332
    if (e->button() == Qt::RightButton) {
333
        int const clickedTile = field(TilePos(posX, posY));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
334 335

        // Clear marked tile
336
        if (m_markX != -1 && field(TilePos(m_markX, m_markY)) != clickedTile) {
337
            unmarkTile();
338
        } else {
339 340
            m_markX = -1;
            m_markY = -1;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
341 342 343
        }

        // Perform highlighting
Frederik Schwarzer's avatar
Frederik Schwarzer committed
344
        if (clickedTile != m_highlightedTile) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
345
            int const oldHighlighted = m_highlightedTile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
346
            m_highlightedTile = clickedTile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
347 348
            for (int i = 0; i < xTiles(); ++i) {
                for (int j = 0; j < yTiles(); ++j) {
349
                    int const fieldTile = field(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
350 351
                    if (fieldTile != EMPTY) {
                        if (fieldTile == oldHighlighted) {
352
                            updateField(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
353
                        } else if (fieldTile == clickedTile) {
354
                            updateField(TilePos(i, j));
355
                        } else if (m_chineseStyleFlag) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
356
                            if (clickedTile >= SEASONS_START && clickedTile <= (SEASONS_START + 3) && fieldTile >= SEASONS_START && fieldTile <= (SEASONS_START + 3)) {
357
                                updateField(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
358
                            } else if (clickedTile >= FLOWERS_START && clickedTile <= (FLOWERS_START + 3) && fieldTile >= FLOWERS_START && fieldTile <= (FLOWERS_START + 3)) {
359
                                updateField(TilePos(i, j));
360
                            }
361
                            // oldHighlighted
Frederik Schwarzer's avatar
Frederik Schwarzer committed
362
                            if (oldHighlighted >= SEASONS_START && oldHighlighted <= (SEASONS_START + 3) && fieldTile >= SEASONS_START && fieldTile <= (SEASONS_START + 3)) {
363
                                updateField(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
364
                            } else if (oldHighlighted >= FLOWERS_START && oldHighlighted <= (FLOWERS_START + 3) && fieldTile >= FLOWERS_START && fieldTile <= (FLOWERS_START + 3)) {
365
                                updateField(TilePos(i, j));
366
                            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
367 368 369 370 371 372
                        }
                    }
                }
            }
        }
    }
373 374
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
375
int Board::xOffset() const
376
{
377
    int tw = m_tiles.qWidth() * 2;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
378
    return (width() - (tw * xTiles())) / 2;
379 380
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
381
int Board::yOffset() const
382
{
383
    int th = m_tiles.qHeight() * 2;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
384
    return (height() - (th * yTiles())) / 2;
385 386 387 388
}

void Board::setSize(int x, int y)
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
389
    if (x == xTiles() && y == yTiles()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
390
        return;
391
    }
392

393
    m_field.resize(x * y);
394

395 396
    m_xTiles = x;
    m_yTiles = y;
397

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
400
    // set the minimum size of the scalable window
Frederik Schwarzer's avatar
Frederik Schwarzer committed
401 402 403
    double const MINIMUM_SCALE = 0.2;
    int const w = qRound(m_tiles.qWidth() * 2.0 * MINIMUM_SCALE) * xTiles() + m_tiles.width();
    int const h = qRound(m_tiles.qHeight() * 2.0 * MINIMUM_SCALE) * yTiles() + m_tiles.height();
404

Frederik Schwarzer's avatar
Frederik Schwarzer committed
405
    setMinimumSize(w, h);
406

Frederik Schwarzer's avatar
Frederik Schwarzer committed
407 408 409
    resizeBoard();
    newGame();
    emit changed();
410 411
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
412
void Board::resizeEvent(QResizeEvent * e)
413
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
414
    qCDebug(KSHISEN_General) << "[resizeEvent]";
415
    if (e->spontaneous()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
416
        qCDebug(KSHISEN_General) << "[resizeEvent] spontaneous";
417
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
418 419
    resizeBoard();
    emit resized();
420 421 422 423
}

void Board::resizeBoard()
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
424
    // calculate tile size required to fit all tiles in the window
Frederik Schwarzer's avatar
Frederik Schwarzer committed
425
    QSize const newsize = m_tiles.preferredTileSize(QSize(width(), height()), xTiles(), yTiles());
426
    m_tiles.reloadTileset(newsize);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
427
    //recalculate bg, if needed
428
    m_background.sizeChanged(width(), height());
Frederik Schwarzer's avatar
Frederik Schwarzer committed
429 430
    //reload our bg brush, using the cache in libkmahjongg if possible
    QPalette palette;
431 432
    palette.setBrush(backgroundRole(), m_background.getBackground());
    setPalette(palette);
433 434
}

435

436 437
void Board::newGame()
{
438
    m_gameState = GameState::Normal;
439
    setCheatModeEnabled(false);
440

441 442 443
    m_markX = -1;
    m_markY = -1;
    m_highlightedTile = -1; // will clear previous highlight
Frederik Schwarzer's avatar
Frederik Schwarzer committed
444 445 446

    resetUndo();
    resetRedo();
447 448
    m_connection.clear();
    m_possibleMoves.clear();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
449 450

    // distribute all tiles on board
451 452
    int curTile = 1;
    int tileCount = 0;
453

Frederik Schwarzer's avatar
Frederik Schwarzer committed
454 455 456 457 458 459 460 461
    /*
     * 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
     */
462 463
    for (int y = 0; y < yTiles(); ++y) {
        for (int x = 0; x < xTiles(); ++x) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
464
            // do not duplicate flowers or seasons
465
            if (!m_chineseStyleFlag || !((curTile >= SEASONS_START && curTile <= (SEASONS_START + 3)) || (curTile >= FLOWERS_START && curTile <= (FLOWERS_START + 3)))) {
466
                setField(TilePos(x, y), curTile);
467 468 469
                if (++tileCount >= 4) {
                    tileCount = 0;
                    ++curTile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
470
                }
471
            } else {
472
                tileCount = 0;
473
                setField(TilePos(x, y), curTile++);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
474
            }
475 476
            if (curTile > Board::nTiles) {
                curTile = 1;
477
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
478 479 480
        }
    }

481
    if (m_shuffle == 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
482 483
        update();
        resetTimer();
484
        emit newGameStarted();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
485 486 487 488 489
        emit changed();
        return;
    }

    // shuffle the field
Frederik Schwarzer's avatar
Frederik Schwarzer committed
490 491
    int const tx = xTiles();
    int const ty = yTiles();
492
    for (int i = 0; i < tx * ty * m_shuffle; ++i) {
493 494
        TilePos const tilePos1(m_random.getLong(tx), m_random.getLong(ty));
        TilePos const tilePos2(m_random.getLong(tx), m_random.getLong(ty));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
495
        // keep and use t, because the next setField() call changes what field() will return
496 497
        // so there would a significant impact on shuffling with the field() call put into the
        // place where 't' is used
498 499 500
        int const t = field(tilePos1);
        setField(tilePos1, field(tilePos2));
        setField(tilePos2, t);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
501 502
    }

503
    // if m_solvableFlag is false, the game does not need to be solvable; we can drop out here
504
    if (!m_solvableFlag) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
505 506
        update();
        resetTimer();
507
        emit newGameStarted();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
508 509 510 511 512
        emit changed();
        return;
    }


513 514 515
    std::vector<int> oldfield = m_field;
    std::vector<int> tiles;
    std::vector<int> pos;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
516
    //jwickers: in case the game cannot made solvable we do not want to run an infinite loop
517
    int maxAttempts = 200;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
518

519
    while (!isSolvable(false) && maxAttempts > 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
520
        // generate a list of free tiles and positions
521
        int numberOfTiles = 0;
522
        for (int i = 0; i < xTiles() * yTiles(); ++i) {
523 524 525
            if (m_field.at(i) != EMPTY) {
                pos.at(numberOfTiles) = i;
                tiles.at(numberOfTiles) = m_field.at(i);
526
                ++numberOfTiles;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
527
            }
528
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
529 530

        // restore field
531
        m_field = oldfield;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
532 533

        // redistribute unsolved tiles
534
        while (numberOfTiles > 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
535
            // get a random tile
Frederik Schwarzer's avatar
Frederik Schwarzer committed
536 537 538 539
            int const r1 = m_random.getLong(numberOfTiles);
            int const r2 = m_random.getLong(numberOfTiles);
            int const tile = tiles.at(r1);
            int const apos = pos.at(r2);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
540 541

            // truncate list
542 543
            tiles.at(r1) = tiles.at(numberOfTiles - 1);
            pos.at(r2) = pos.at(numberOfTiles - 1);
544
            --numberOfTiles;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
545 546

            // put this tile on the new position
547
            m_field.at(apos) = tile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
548 549 550
        }

        // remember field
551
        oldfield = m_field;
552
        --maxAttempts;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
553 554
    }
    // debug, tell if make solvable failed
555
    if (maxAttempts == 0) {
556
        qCCritical(KSHISEN_General) << "NewGame make solvable failed";
Frederik Schwarzer's avatar
Frederik Schwarzer committed
557 558 559 560
    }


    // restore field
561
    m_field = oldfield;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
562 563 564 565

    update();
    resetTimer();
    emit changed();
566 567
}

568 569
bool Board::tilesMatch(int tile1, int tile2) const
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
570
    // identical tiles always match
571
    if (tile1 == tile2) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
572
        return true;
573
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
574 575
    // when chinese style is set, there are special rules
    // for flowers and seasons
576
    if (m_chineseStyleFlag) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
577
        // if both tiles are seasons
578
        if (tile1 >= SEASONS_START && tile1 <= SEASONS_START + 3
579
            && tile2 >= SEASONS_START && tile2 <= SEASONS_START + 3) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
580
            return true;
581
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
582
        // if both tiles are flowers
583
        if (tile1 >= FLOWERS_START && tile1 <= FLOWERS_START + 3
584
            && tile2 >= FLOWERS_START && tile2 <= FLOWERS_START + 3) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
585
            return true;
586
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
587 588
    }
    return false;
589 590
}

591
bool Board::isTileHighlighted(TilePos const & tilePos) const
592
{
593
    if (tilePos.x() == m_markX && tilePos.y() == m_markY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
594
        return true;
595
    }
596

597
    if (tilesMatch(m_highlightedTile, field(tilePos))) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
598
        return true;
599
    }
600

601
    // m_tileRemove1.first != -1 is used because the repaint of the first if
602
    // on undrawConnection highlighted the tiles that fell because of gravity
603 604
    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
605
            return true;
606
        }
607

608
        if (tilePos.x() == m_connection.back().x() && tilePos.y() == m_connection.back().y()) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
609
            return true;
610
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
611
    }
612

Frederik Schwarzer's avatar
Frederik Schwarzer committed
613
    return false;
614 615
}

616
void Board::updateField(TilePos const & tilePos)
617
{
618 619
    QRect const r(xOffset() + tilePos.x() * m_tiles.qWidth() * 2,
                  yOffset() + tilePos.y() * m_tiles.qHeight() * 2,
Frederik Schwarzer's avatar
Frederik Schwarzer committed
620 621
                  m_tiles.width(),
                  m_tiles.height());
622

Frederik Schwarzer's avatar
Frederik Schwarzer committed
623
    update(r);
624 625
}

626
void Board::showInfoRect(QPainter & p, QString const & message)
627
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
628 629 630
    int const boxWidth = width() * 0.6;
    int const boxHeight = height() * 0.6;
    QRect const contentsRect = QRect((width() - boxWidth) / 2, (height() - boxHeight) / 2, boxWidth, boxHeight);
631
    QFont font;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
632
    int const fontsize = boxHeight / 13;
633 634 635 636 637
    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
638

639 640
    p.drawText(contentsRect, Qt::AlignCenter | Qt::TextWordWrap, message);
}
Frederik Schwarzer's avatar
Frederik Schwarzer committed
641

Frederik Schwarzer's avatar
Frederik Schwarzer committed
642
void Board::drawTiles(QPainter & p, QPaintEvent * e)
643
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
644 645 646 647
    int const w = m_tiles.width();
    int const h = m_tiles.height();
    int const fw = m_tiles.qWidth() * 2;
    int const fh = m_tiles.qHeight() * 2;
648 649
    for (int i = 0; i < xTiles(); ++i) {
        for (int j = 0; j < yTiles(); ++j) {
650
            int const tile = field(TilePos(i, j));
651 652 653
            if (tile == EMPTY) {
                continue;
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
654

Frederik Schwarzer's avatar
Frederik Schwarzer committed
655 656 657
            int const xpos = xOffset() + i * fw;
            int const ypos = yOffset() + j * fh;
            QRect const r(xpos, ypos, w, h);
658
            if (e->rect().intersects(r)) {
659
                if (isTileHighlighted(TilePos(i, j))) {
660 661 662
                    p.drawPixmap(xpos, ypos, m_tiles.selectedTile(1));
                } else {
                    p.drawPixmap(xpos, ypos, m_tiles.unselectedTile(1));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
663
                }
664 665 666

                //draw face
                p.drawPixmap(xpos, ypos, m_tiles.tileface(tile - 1));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
667 668 669
            }
        }
    }
670 671
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
672
void Board::paintEvent(QPaintEvent * e)
673
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
674
    QRect const ur = e->rect(); // rectangle to update
675 676 677 678
    QPainter p(this);
    p.fillRect(ur, m_background.getBackground());

    switch (m_gameState) {
679
        case GameState::Normal:
680 681
            drawTiles(p, e);
            break;
682
        case GameState::Paused:
683 684
            showInfoRect(p, i18n("Game Paused\nClick to resume game."));
            break;
685
        case GameState::Stuck:
686 687 688
            drawTiles(p, e);
            showInfoRect(p, i18n("Game Stuck\nNo more moves possible."));
            break;
689
        case GameState::Over:
690 691 692
            showInfoRect(p, i18n("Game Over\nClick to start a new game."));
            break;
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
693

694
    if (m_paintConnection) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
695 696
        p.setPen(QPen(QColor("red"), lineWidth()));

697
        auto pt1 = m_connection.cbegin();
698
        auto pt2 = pt1 + 1;
699
        while (pt2 != m_connection.cend()) {
700
            p.drawLine(midCoord(*pt1), midCoord(*pt2));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
701 702 703
            ++pt1;
            ++pt2;
        }
704
        QTimer::singleShot(delay(), this, &Board::undrawConnection);
705
        m_paintConnection = false;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
706
    }
707
    if (m_paintPossibleMoves) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
708 709
        p.setPen(QPen(QColor("blue"), lineWidth()));
        // paint all possible moves
710
        foreach (auto const move, m_possibleMoves) {
711
            auto pt1 = move.path().cbegin();
712
            auto pt2 = pt1 + 1;
713
            while (pt2 != move.path().cend()) {
714
                p.drawLine(midCoord(*pt1), midCoord(*pt2));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
715 716 717 718
                ++pt1;
                ++pt2;
            }
        }
719
        m_paintConnection = false;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
720
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
721
    p.end();
722 723
}

724
void Board::reverseSlide(TilePos const & tilePos, int slideX1, int slideY1, int slideX2, int slideY2)
725
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
726 727
    // 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
728
    // calculate the offset for the tiles to slide
Frederik Schwarzer's avatar
Frederik Schwarzer committed
729 730
    int const dx = slideX1 - slideX2;
    int const dy = slideY1 - slideY2;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
731
    int current_tile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
732
    // move all tiles between slideX2, slideY2 and x, y to slide with that offset
733
    if (dx == 0) {
734 735 736
        if (tilePos.y() < slideY2) {
            for (int i = tilePos.y() + 1; i <= slideY2; ++i) {
                current_tile = field(TilePos(tilePos.x(), i));
737
                if (current_tile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
738
                    continue;
739
                }
740 741 742 743
                setField(TilePos(tilePos.x(), i), EMPTY);
                setField(TilePos(tilePos.x(), i + dy), current_tile);
                updateField(TilePos(tilePos.x(), i));
                updateField(TilePos(tilePos.x(), i + dy));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
744
            }
745
        } else {
746 747
            for (int i = tilePos.y() - 1; i >= slideY2; --i) {
                current_tile = field(TilePos(tilePos.x(), i));
748
                if (current_tile == EMPTY)