board.cpp 63.7 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
    if (!isValidPosWithOutline(tilePos)) {
209 210 211 212
        qCCritical(KSHISEN_General) << "Attempted read from invalid field position:"
                                    << tilePos.x()
                                    << ","
                                    << tilePos.y();
Frederik Schwarzer's avatar
Frederik Schwarzer committed
213
    }
214

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

219
    return m_field.at(tilePos.y() * xTiles() + tilePos.x());
220 221
}

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

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

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
286
void Board::mousePressEvent(QMouseEvent * e)
287
{
288 289 290 291 292 293 294 295
    // 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;
    }
296
    switch (m_gameState) {
297
        case GameState::Normal:
298
            break;
299
        case GameState::Over:
300 301
            newGame();
            return;
302
        case GameState::Paused:
303 304
            setPauseEnabled(false);
            return;
305
        case GameState::Stuck:
306
            return;
307
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
308
    // Calculate field position
Frederik Schwarzer's avatar
Frederik Schwarzer committed
309 310
    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
311

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

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

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

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

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

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

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

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

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

391
    m_field.resize(x * y);
392

393 394
    m_xTiles = x;
    m_yTiles = y;
395

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
398
    // set the minimum size of the scalable window
Frederik Schwarzer's avatar
Frederik Schwarzer committed
399 400 401
    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();
402

Frederik Schwarzer's avatar
Frederik Schwarzer committed
403
    setMinimumSize(w, h);
404

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

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

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

433

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

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

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

    // distribute all tiles on board
449 450
    int curTile = 1;
    int tileCount = 0;
451

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

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

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

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


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

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

        // restore field
529
        m_field = oldfield;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
530 531

        // redistribute unsolved tiles
532
        while (numberOfTiles > 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
533
            // get a random tile
Frederik Schwarzer's avatar
Frederik Schwarzer committed
534 535 536 537
            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
538 539

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

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

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


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

    update();
    resetTimer();
    emit changed();
564 565
}

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

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

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

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

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
611
    return false;
612 613
}

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
621
    update(r);
622 623
}

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

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
640
void Board::drawTiles(QPainter & p, QPaintEvent * e)
641
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
642 643 644 645
    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;
646 647
    for (int i = 0; i < xTiles(); ++i) {
        for (int j = 0; j < yTiles(); ++j) {
648
            int const tile = field(TilePos(i, j));
649 650 651
            if (tile == EMPTY) {
                continue;
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
652

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

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

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

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

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

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

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