board.cpp 63.3 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 76 77 78 79
    , m_xTiles(0)
    , m_yTiles(0)
    , m_delay(0)
    , m_level(0)
    , m_shuffle(0)
    , m_gameState(GameState::Normal)
    , m_cheat(false)
    , m_gravityFlag(true)
    , m_solvableFlag(false)
    , m_chineseStyleFlag(false)
    , m_tilesCanSlideFlag(false)
    , m_highlightedTile(-1)
    , m_paintConnection(false)
    , m_paintPossibleMoves(false)
    , m_paintInProgress(false)
Frederik Schwarzer's avatar
Frederik Schwarzer committed
80 81
    , m_tileRemove1()
    , m_tileRemove2()
82 83
    , m_soundPick(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-touch.ogg")))
    , m_soundFall(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("sounds/kshisen/tile-fall-tile.ogg")))
Benjamin Meyer's avatar
Benjamin Meyer committed
84
{
85
    m_tileRemove1.setX(-1);
86

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
251 252
void Board::unmarkTile()
{
253 254 255 256 257 258 259 260
    // 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.
261
    TilePos const oldTilePos(m_markX, m_markY);
262 263
    m_markX = -1;
    m_markY = -1;
264
    updateField(oldTilePos);
265 266
}

Frederik Schwarzer's avatar
Frederik Schwarzer committed
267
void Board::mousePressEvent(QMouseEvent * e)
268
{
269 270 271 272 273 274 275 276
    // 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;
    }
277
    switch (m_gameState) {
278
        case GameState::Normal:
279
            break;
280
        case GameState::Over:
281 282
            newGame();
            return;
283
        case GameState::Paused:
284 285
            setPauseEnabled(false);
            return;
286
        case GameState::Stuck:
287
            return;
288
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
289
    // Calculate field position
Frederik Schwarzer's avatar
Frederik Schwarzer committed
290 291
    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
292

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

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

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

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

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

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

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

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

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

372
    m_field.resize(x * y);
373

374 375
    m_xTiles = x;
    m_yTiles = y;
376

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

Frederik Schwarzer's avatar
Frederik Schwarzer committed
379
    // set the minimum size of the scalable window
Frederik Schwarzer's avatar
Frederik Schwarzer committed
380 381 382
    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();
383

Frederik Schwarzer's avatar
Frederik Schwarzer committed
384
    setMinimumSize(w, h);
385

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

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

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

414

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

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

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

    // distribute all tiles on board
430 431
    int curTile = 1;
    int tileCount = 0;
432

Frederik Schwarzer's avatar
Frederik Schwarzer committed
433 434 435 436 437 438 439 440
    /*
     * 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
     */
441 442
    for (int y = 0; y < yTiles(); ++y) {
        for (int x = 0; x < xTiles(); ++x) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
443
            // do not duplicate flowers or seasons
444
            if (!m_chineseStyleFlag || !((curTile >= SEASONS_START && curTile <= (SEASONS_START + 3)) || (curTile >= FLOWERS_START && curTile <= (FLOWERS_START + 3)))) {
445
                setField(TilePos(x, y), curTile);
446 447 448
                if (++tileCount >= 4) {
                    tileCount = 0;
                    ++curTile;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
449
                }
450
            } else {
451
                tileCount = 0;
452
                setField(TilePos(x, y), curTile++);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
453
            }
454 455
            if (curTile > Board::nTiles) {
                curTile = 1;
456
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
457 458 459
        }
    }

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

    // shuffle the field
Frederik Schwarzer's avatar
Frederik Schwarzer committed
469 470
    int const tx = xTiles();
    int const ty = yTiles();
471
    for (int i = 0; i < tx * ty * m_shuffle; ++i) {
472 473
        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
474
        // keep and use t, because the next setField() call changes what field() will return
475 476
        // so there would a significant impact on shuffling with the field() call put into the
        // place where 't' is used
477 478 479
        int const t = field(tilePos1);
        setField(tilePos1, field(tilePos2));
        setField(tilePos2, t);
Frederik Schwarzer's avatar
Frederik Schwarzer committed
480 481
    }

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


492 493 494
    std::vector<int> oldfield = m_field;
    std::vector<int> tiles;
    std::vector<int> pos;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
495
    //jwickers: in case the game cannot made solvable we do not want to run an infinite loop
496
    int maxAttempts = 200;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
497

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

        // restore field
510
        m_field = oldfield;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
511 512

        // redistribute unsolved tiles
513
        while (numberOfTiles > 0) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
514
            // get a random tile
Frederik Schwarzer's avatar
Frederik Schwarzer committed
515 516 517 518
            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
519 520

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

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

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


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

    update();
    resetTimer();
    emit changed();
545 546
}

547 548
bool Board::tilesMatch(int tile1, int tile2) const
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
549
    // identical tiles always match
550
    if (tile1 == tile2) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
551
        return true;
552
    }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
553 554
    // when chinese style is set, there are special rules
    // for flowers and seasons
555
    if (m_chineseStyleFlag) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
556
        // if both tiles are seasons
557
        if (tile1 >= SEASONS_START && tile1 <= SEASONS_START + 3
558
            && tile2 >= SEASONS_START && tile2 <= SEASONS_START + 3) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
559
            return true;
560
        }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
561
        // if both tiles are flowers
562
        if (tile1 >= FLOWERS_START && tile1 <= FLOWERS_START + 3
563
            && tile2 >= FLOWERS_START && tile2 <= FLOWERS_START + 3) {
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
}

595
void Board::updateField(TilePos const & tilePos)
596
{
597 598
    QRect const r(xOffset() + tilePos.x() * m_tiles.qWidth() * 2,
                  yOffset() + tilePos.y() * m_tiles.qHeight() * 2,
Frederik Schwarzer's avatar
Frederik Schwarzer committed
599 600
                  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
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
607 608 609
    int const boxWidth = width() * 0.6;
    int const boxHeight = height() * 0.6;
    QRect const contentsRect = QRect((width() - boxWidth) / 2, (height() - boxHeight) / 2, boxWidth, boxHeight);
610
    QFont font;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
611
    int const fontsize = 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
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
623 624 625 626
    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;
627 628
    for (int i = 0; i < xTiles(); ++i) {
        for (int j = 0; j < yTiles(); ++j) {
629
            int const tile = field(TilePos(i, j));
630 631 632
            if (tile == EMPTY) {
                continue;
            }
Frederik Schwarzer's avatar
Frederik Schwarzer committed
633

Frederik Schwarzer's avatar
Frederik Schwarzer committed
634 635 636
            int const xpos = xOffset() + i * fw;
            int const ypos = yOffset() + j * fh;
            QRect const r(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
{
Frederik Schwarzer's avatar
Frederik Schwarzer committed
653
    QRect 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, int slideX1, int slideY1, int slideX2, int slideY2)
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
Frederik Schwarzer's avatar
Frederik Schwarzer committed
708 709
    int const dx = slideX1 - slideX2;
    int const dy = slideY1 - slideY2;
Frederik Schwarzer's avatar
Frederik Schwarzer committed
710
    int current_tile;
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 715
        if (tilePos.y() < slideY2) {
            for (int i = tilePos.y() + 1; i <= slideY2; ++i) {
                current_tile = field(TilePos(tilePos.x(), i));
716
                if (current_tile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
717
                    continue;
718
                }
719 720 721 722
                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
723
            }
724
        } else {
725 726
            for (int i = tilePos.y() - 1; i >= slideY2; --i) {
                current_tile = field(TilePos(tilePos.x(), i));
727
                if (current_tile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
728
                    continue;
729
                }
730 731 732 733
                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
734 735
            }
        }
736
    } else if (dy == 0) {
737 738 739
        if (tilePos.x() < slideX2) {
            for (int i = tilePos.x() + 1; i <= slideX2; ++i) {
                current_tile = field(TilePos(i, tilePos.y()));
740
                if (current_tile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
741
                    continue;
742
                }
743 744 745 746
                setField(TilePos(i, tilePos.y()), EMPTY);
                setField(TilePos(i + dx, tilePos.y()), current_tile);
                updateField(TilePos(i, tilePos.y()));
                updateField(TilePos(i + dx, tilePos.y()));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
747
            }
748
        } else {
749 750
            for (int i = tilePos.x() - 1; i >= slideX2; --i) {
                current_tile = field(TilePos(i, tilePos.y()));
751
                if (current_tile == EMPTY) {
Frederik Schwarzer's avatar
Frederik Schwarzer committed
752
                    continue;
753
                }
754 755 756 757
                setField(TilePos(i, tilePos.y()), EMPTY);
                setField(TilePos(i + dx, tilePos.y()), current_tile);
                updateField(TilePos(i, tilePos.y()));
                updateField(TilePos(i + dx, tilePos.y()));
Frederik Schwarzer's avatar
Frederik Schwarzer committed
758 759 760
            }
        }
    }
761 762
}

763
void Board::performSlide(TilePos const & tilePos, Path const & slide)
764
{