board.cpp 63.5 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() << " and the default tileset have been tried.";
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() << " and the default background have been tried.";
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
            resizeBoard();
142
            return true;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
143 144 145
        }
    }
    //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 (isTileSeason(clickedTile) && isTileSeason(fieldTile)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
338
                                repaintTile(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
339
                            } else if (isTileFlower(clickedTile) && isTileFlower(fieldTile)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
340
                                repaintTile(TilePos(i, j));
341
                            }
342
                            // oldHighlighted
Frederik Schwarzer's avatar
Frederik Schwarzer committed
343
                            if (isTileSeason(oldHighlighted) && isTileSeason(fieldTile)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
344
                                repaintTile(TilePos(i, j));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
345
                            } else if (isTileFlower(oldHighlighted) && isTileFlower(fieldTile)) {
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
Frederik Schwarzer's avatar
Frederik Schwarzer committed
446
            if (!m_chineseStyleFlag || !(isTileSeason(curTile) || isTileFlower(curTile))) {
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
Frederik Schwarzer's avatar
Frederik Schwarzer committed
559
        if (isTileSeason(tile1) && isTileSeason(tile2)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
560
            return true;
561
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
562
        // if both tiles are flowers
Frederik Schwarzer's avatar
Frederik Schwarzer committed
563
        if (isTileFlower(tile1) && isTileFlower(tile2)) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
564
            return true;
565
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
566 567
    }
    return false;
568 569
}

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

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

580
    // m_tileRemove1.first != -1 is used because the repaint of the first if
581
    // on undrawConnection highlighted the tiles that fell because of gravity
582 583
    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
584
            return true;
585
        }
586

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
592
    return false;
593 594
}

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
602
    update(r);
603 604
}

605
void Board::showInfoRect(QPainter & p, QString const & message)
606
{
607 608 609
    auto const boxWidth = width() * 0.6;
    auto const boxHeight = height() * 0.6;
    auto const contentsRect = QRect((width() - boxWidth) / 2, (height() - boxHeight) / 2, boxWidth, boxHeight);
610
    QFont font;
611
    auto const fontsize = static_cast<int>(boxHeight / 13);
612 613 614 615 616
    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
617

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
621
void Board::drawTiles(QPainter & p, QPaintEvent * e)
622
{
623 624 625 626 627 628 629
    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));
630 631 632
            if (tile == EMPTY) {
                continue;
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
633

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

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

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

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

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

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

703
void Board::reverseSlide(TilePos const & tilePos, Slide const & slide)
704
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
705 706
    // 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
707
    // calculate the offset for the tiles to slide
708 709
    auto const dx = slide.front().x() - slide.back().x();
    auto const dy = slide.front().y() - slide.back().y();
710
    auto currentTile = 0;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
711
    // move all tiles between slideX2, slideY2 and x, y to slide with that offset
712
    if (dx == 0) {
713 714
        if (tilePos.y() < slide.back().y()) {
            for (auto i = tilePos.y() + 1; i <= slide.back().y(); ++i) {
715 716
                currentTile = field(TilePos(tilePos.x(), i));
                if (currentTile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
717
                    continue;
718
                }
719
                setField(TilePos(tilePos.x(), i), EMPTY);
720
                setField(TilePos(tilePos.x(), i + dy), currentTile);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
721 722
                repaintTile(TilePos(tilePos.x(), i));
                repaintTile(TilePos(tilePos.x(), i + dy));