File indexing completed on 2024-04-28 04:03:12

0001 /***************************************************************************
0002     File                 : gamemanager.cpp
0003     Project              : Knights
0004     Description          : Game manager
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2016-2018 Alexander Semke (alexander.semke@web.de)
0007     SPDX-FileCopyrightText: 2009-2011 Miha Čančula (miha@noughmad.eu)
0008 
0009  ***************************************************************************/
0010 
0011 /***************************************************************************
0012  *                                                                         *
0013  *  SPDX-License-Identifier: GPL-2.0-or-later
0014  *                                                                         *
0015  *  This program is distributed in the hope that it will be useful,        *
0016  *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
0017  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
0018  *  GNU General Public License for more details.                           *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program; if not, write to the Free Software           *
0022  *   Foundation, Inc., 51 Franklin Street, Fifth Floor,                    *
0023  *   Boston, MA  02110-1301  USA                                           *
0024  *                                                                         *
0025  ***************************************************************************/
0026 
0027 #include "gamemanager.h"
0028 #include "proto/protocol.h"
0029 #include "rules/chessrules.h"
0030 #include "externalcontrol.h"
0031 #include "settings.h"
0032 #include "difficultydialog.h"
0033 #include "knightsdebug.h"
0034 
0035 #include <KLocalizedString>
0036 #include <KGameDifficulty>
0037 #include <KGameSound>
0038 
0039 #ifdef HAVE_SPEECH
0040 #include <QtTextToSpeech>
0041 #endif
0042 
0043 #include <QDir>
0044 #include <QTimer>
0045 #include <QStandardPaths>
0046 #include <QRandomGenerator>
0047 #include <QRegExp>
0048 
0049 #include <memory>
0050 
0051 using namespace Knights;
0052 
0053 const int TimerInterval = 100;
0054 const int LineLimit = 80; // Maximum characters per line for PGN format
0055 
0056 void Offer::accept() const {
0057     Manager::self()->setOfferResult(id, AcceptOffer);
0058 }
0059 
0060 void Offer::decline() const {
0061     Manager::self()->setOfferResult(id, DeclineOffer);
0062 }
0063 
0064 class Knights::GameManagerPrivate {
0065 public:
0066     GameManagerPrivate();
0067         ~GameManagerPrivate();
0068 
0069     Color activePlayer;
0070     bool running;
0071     bool gameStarted;
0072     bool gameOverInProcess;
0073     int timer;
0074 
0075     TimeControl whiteTimeControl;
0076     TimeControl blackTimeControl;
0077 
0078     QStack<Move> moveHistory;
0079     QStack<Move> moveUndoStack;
0080 
0081     Rules* rules;
0082     QMap<int, Offer> offers;
0083     QSet<int> usedOfferIds;
0084 
0085     std::unique_ptr<KGameSound> captureWhiteSound;
0086     std::unique_ptr<KGameSound> captureBlackSound;
0087     std::unique_ptr<KGameSound> moveWhiteSound;
0088     std::unique_ptr<KGameSound> moveBlackSound;
0089 
0090 #ifdef HAVE_SPEECH
0091     QTextToSpeech* speech;
0092 #endif
0093     ExternalControl* extControl;
0094 
0095     QString filename;
0096     Color winner;
0097     bool initComplete;
0098     bool loading;
0099 
0100     int nextOfferId();
0101 };
0102 
0103 GameManagerPrivate::GameManagerPrivate()
0104     : activePlayer(NoColor),
0105       running(false),
0106       gameStarted(false),
0107       gameOverInProcess(false),
0108       timer(0),
0109       rules(nullptr),
0110 #ifdef HAVE_SPEECH
0111       speech(nullptr),
0112 #endif
0113       extControl(nullptr),
0114       winner(NoColor),
0115       initComplete(false),
0116       loading(false) {
0117 
0118     const QDir dir = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("sounds"), QStandardPaths::LocateDirectory);
0119     captureWhiteSound = std::unique_ptr<KGameSound>( new KGameSound(dir.filePath(QStringLiteral("capture_white.ogg"))) );
0120     captureBlackSound = std::unique_ptr<KGameSound>( new KGameSound(dir.filePath(QStringLiteral("capture_black.ogg"))) );
0121     moveWhiteSound = std::unique_ptr<KGameSound>( new KGameSound(dir.filePath(QStringLiteral("move_white.ogg"))) ) ;
0122         moveBlackSound = std::unique_ptr<KGameSound>( new KGameSound(dir.filePath(QStringLiteral("move_black.ogg"))) );
0123 }
0124 
0125 GameManagerPrivate::~GameManagerPrivate()
0126 {
0127         delete rules;
0128 }
0129 
0130 int GameManagerPrivate::nextOfferId() {
0131     int i = usedOfferIds.size() + 1;
0132     while (usedOfferIds.contains(i))
0133         ++i;
0134     return i;
0135 }
0136 
0137 Q_GLOBAL_STATIC(Manager, instance)
0138 
0139 Manager* Manager::self() {
0140     return instance;
0141 }
0142 
0143 Manager::Manager(QObject* parent) : QObject(parent),
0144     d_ptr(new GameManagerPrivate) {
0145 }
0146 
0147 Manager::~Manager() {
0148     delete d_ptr;
0149 }
0150 
0151 void Manager::startTime() {
0152     Q_D(GameManager);
0153     if ( !d->running ) {
0154         d->timer = startTimer ( TimerInterval );
0155         d->running = true;
0156     }
0157 }
0158 
0159 void Manager::stopTime() {
0160     Q_D(GameManager);
0161     if ( d->running ) {
0162         killTimer(d->timer);
0163         d->running = false;
0164     }
0165 }
0166 
0167 void Manager::setTimeRunning(bool running) {
0168     if ( running )
0169         startTime();
0170     else
0171         stopTime();
0172 }
0173 
0174 void Manager::setCurrentTime(Color color, const QTime& time) {
0175     Q_D(GameManager);
0176     switch ( color ) {
0177     case White:
0178         d->whiteTimeControl.currentTime = time;
0179         break;
0180     case Black:
0181         d->blackTimeControl.currentTime = time;
0182         break;
0183     default:
0184         return;
0185     }
0186     Q_EMIT timeChanged ( color, time );
0187 }
0188 
0189 void Manager::timerEvent(QTimerEvent* ) {
0190     Q_D(GameManager);
0191     QTime time;
0192     switch ( d->activePlayer ) {
0193     case White:
0194         if ( d->whiteTimeControl.currentTime == QTime(0, 0, 0, TimerInterval) )
0195             gameOver(Black);
0196         d->whiteTimeControl.currentTime = d->whiteTimeControl.currentTime.addMSecs ( -TimerInterval );
0197         time = d->whiteTimeControl.currentTime;
0198         break;
0199     case Black:
0200         if ( d->blackTimeControl.currentTime == QTime(0, 0, 0, TimerInterval) )
0201             gameOver(White);
0202         d->blackTimeControl.currentTime = d->blackTimeControl.currentTime.addMSecs ( -TimerInterval );
0203         time = d->blackTimeControl.currentTime;
0204         break;
0205     default:
0206         time = QTime();
0207         break;
0208     }
0209     Q_EMIT timeChanged(d->activePlayer, time);
0210 }
0211 
0212 void Manager::addMoveToHistory(const Move& move) {
0213     Q_D(GameManager);
0214     if ( d->moveHistory.isEmpty() )
0215         Q_EMIT undoPossible(true);
0216     d->moveHistory << move;
0217     if ( !d->moveUndoStack.isEmpty() )
0218         Q_EMIT redoPossible(false);
0219     d->moveUndoStack.clear();
0220     Q_EMIT historyChanged();
0221 }
0222 
0223 Move Manager::nextUndoMove() {
0224     Q_D(GameManager);
0225     Move m = d->moveHistory.pop();
0226     if ( m.pieceData().first == White )
0227         d->whiteTimeControl.currentTime = m.time();
0228     else
0229         d->blackTimeControl.currentTime = m.time();
0230     Q_EMIT timeChanged ( m.pieceData().first, m.time() );
0231     if ( d->moveHistory.isEmpty() )
0232         Q_EMIT undoPossible(false);
0233     if ( d->moveUndoStack.isEmpty() )
0234         Q_EMIT redoPossible(true);
0235     d->moveUndoStack.push( m );
0236 
0237     Q_EMIT historyChanged();
0238 
0239     Move ret = m.reverse();
0240     qCDebug(LOG_KNIGHTS) << m << ret;
0241     ret.setFlag ( Move::Forced, true );
0242     return ret;
0243 }
0244 
0245 Move Manager::nextRedoMove() {
0246     Q_D(GameManager);
0247     Move m = d->moveUndoStack.pop();
0248     if ( d->moveUndoStack.isEmpty() )
0249         Q_EMIT redoPossible(false);
0250     if ( d->moveHistory.isEmpty() )
0251         Q_EMIT undoPossible(true);
0252     d->moveHistory << m;
0253     m.setFlag ( Move::Forced, true );
0254     Q_EMIT historyChanged();
0255 
0256     return m;
0257 }
0258 
0259 
0260 void Manager::setActivePlayer(Color player) {
0261     Q_D(GameManager);
0262     d->activePlayer = player;
0263 }
0264 
0265 void Manager::changeActivePlayer() {
0266     setActivePlayer ( oppositeColor ( activePlayer() ) );
0267     Q_EMIT activePlayerChanged ( activePlayer() );
0268 }
0269 
0270 Color Manager::activePlayer() const {
0271     Q_D(const GameManager);
0272     return d->activePlayer;
0273 }
0274 
0275 void Manager::initialize() {
0276     Q_D(GameManager);
0277     d->gameStarted = false;
0278     d->initComplete = false;
0279     d->running = false;
0280     d->moveHistory.clear();
0281     d->activePlayer = White;
0282     d->whiteTimeControl.currentTime = d->whiteTimeControl.baseTime;
0283     d->blackTimeControl.currentTime = d->blackTimeControl.baseTime;
0284     Protocol::white()->setTimeControl(d->whiteTimeControl);
0285     Protocol::black()->setTimeControl(d->blackTimeControl);
0286     connect ( Protocol::white(), &Protocol::pieceMoved, this, &Manager::moveByProtocol );
0287     connect ( Protocol::white(), &Protocol::initSuccesful, this, &Manager::protocolInitSuccesful, Qt::QueuedConnection );
0288     connect ( Protocol::white(), &Protocol::gameOver, this, &Manager::gameOver );
0289     connect ( Protocol::black(), &Protocol::pieceMoved, this, &Manager::moveByProtocol );
0290     connect ( Protocol::black(), &Protocol::initSuccesful, this, &Manager::protocolInitSuccesful, Qt::QueuedConnection );
0291     connect ( Protocol::black(), &Protocol::gameOver, this, &Manager::gameOver );
0292     Protocol::white()->init();
0293     Protocol::black()->init();
0294     d->extControl = new ExternalControl(this);
0295 }
0296 
0297 void Manager::pause(bool pause) {
0298     Offer o;
0299     o.action = pause ? ActionPause : ActionResume;
0300     o.id = QRandomGenerator::global()->bounded(RAND_MAX);
0301     o.player = NoColor;
0302     sendOffer(o);
0303 }
0304 
0305 /**
0306     * Sets the time control parameters in the same format as XBoard's @c level command works
0307     * @param color specifis to which player this setting will apply. If @p color is NoColor then both player use this setting.
0308     * @param moves the number of moves to be completed before @p baseTime runs out.
0309     * Setting this to 0 causes the timing to be incremental only
0310     * @param baseTime the time in minutes in which the player has to complete @p moves moves, or finish the game if @p moves is zero.
0311     * @param increment the time in seconds that is added to the player's clock for his every move.
0312     */
0313 void Manager::setTimeControl(Color color, const TimeControl& control) {
0314     Q_D(GameManager);
0315     if ( color == White )
0316         d->whiteTimeControl = control;
0317     else if ( color == Black )
0318         d->blackTimeControl = control;
0319     else {
0320         qCDebug(LOG_KNIGHTS) << "Setting time control for both colors";
0321         d->blackTimeControl = control;
0322         d->whiteTimeControl = control;
0323     }
0324 }
0325 
0326 TimeControl Manager::timeControl(Color color) const {
0327     Q_D(const GameManager);
0328     if ( color == White )
0329         return d->whiteTimeControl;
0330     else if ( color == Black )
0331         return d->blackTimeControl;
0332     else {
0333         // FICS protocol needs the time control parameters even before the color is determined
0334         // It only supports equal time for both players, so it doesn't matter which one we return
0335         return d->whiteTimeControl;
0336     }
0337 }
0338 
0339 QTime Manager::timeLimit(Color color) {
0340     return timeControl(color).baseTime;
0341 }
0342 
0343 bool Manager::timeControlEnabled(Color color) const {
0344     TimeControl tc = timeControl(color);
0345 
0346     // For a time to be valid, either the base time or increment must be greater than 0
0347     if ( tc.baseTime.isValid() || tc.increment > 0 )
0348         return true;
0349     return false;
0350 }
0351 
0352 void Manager::undo() {
0353     sendPendingMove();
0354     Q_D(const GameManager);
0355     Offer o;
0356     o.action = ActionUndo;
0357 
0358     // We always undo moves until it's local player's turn again.
0359     if ( Protocol::byColor(d->activePlayer)->isLocal() && !Protocol::byColor(oppositeColor(d->activePlayer))->isLocal() )
0360         o.numberOfMoves = 2;
0361     else
0362         o.numberOfMoves = 1;
0363     o.numberOfMoves = qMin ( o.numberOfMoves, d->moveHistory.size() );
0364     o.player = local()->color();
0365     sendOffer(o);
0366 }
0367 
0368 void Manager::redo() {
0369     sendPendingMove();
0370     Move m = nextRedoMove();
0371     Protocol::white()->move ( m );
0372     Protocol::black()->move ( m );
0373     Q_EMIT pieceMoved ( m );
0374     changeActivePlayer();
0375 }
0376 
0377 void Manager::adjourn() {
0378     sendOffer(ActionAdjourn);
0379 }
0380 
0381 void Manager::abort() {
0382     sendOffer(ActionAbort);
0383 }
0384 
0385 void Manager::offerDraw() {
0386     sendOffer(ActionDraw);
0387 }
0388 
0389 void Manager::resign() {
0390     Q_D(const GameManager);
0391     if (d->activePlayer == Knights::White)
0392         gameOver(Knights::Black);
0393     else
0394         gameOver(Knights::White);
0395 }
0396 
0397 bool Manager::isRunning() {
0398     Q_D(const GameManager);
0399     return d->running;
0400 }
0401 
0402 void Manager::moveByProtocol(const Move& move) {
0403     Q_D(GameManager);
0404     if ( sender() != Protocol::byColor ( d->activePlayer ) || !d->gameStarted ) {
0405         qCDebug(LOG_KNIGHTS) << "Move by the non-active player" << move;
0406         // Ignore duplicates and/or moves by the inactive player
0407         return;
0408     }
0409     processMove(move);
0410 }
0411 
0412 void Manager::protocolInitSuccesful() {
0413     Q_D(GameManager);
0414     if ( Protocol::white() && Protocol::black() ) {
0415         if ( !d->gameStarted && Protocol::white()->isReady() && Protocol::black()->isReady() ) {
0416             if ( Protocol::white()->isLocal() && Protocol::black()->isLocal() ) {
0417                 Protocol::white()->setPlayerName ( i18nc ( "The player of this color", "White" ) );
0418                 Protocol::black()->setPlayerName ( i18nc ( "The player of this color", "Black" ) );
0419             }
0420             if (!d->initComplete) {
0421                 d->initComplete = true;
0422                 Q_EMIT initComplete();
0423             }
0424         }
0425     }
0426 }
0427 
0428 void Manager::startGame() {
0429     Q_D(GameManager);
0430     Q_ASSERT(!d->gameStarted);
0431     setRules(new ChessRules);
0432     if ( Protocol::white()->supportedFeatures() & Protocol::AdjustDifficulty
0433             || Protocol::black()->supportedFeatures() & Protocol::AdjustDifficulty)
0434         levelChanged(KGameDifficulty::global()->currentLevel());
0435 
0436     Protocol::white()->startGame();
0437     Protocol::black()->startGame();
0438     d->gameStarted = true;
0439     Q_EMIT historyChanged();
0440 }
0441 
0442 void Manager::gameOver(Color winner) {
0443     Q_D(GameManager);
0444     if (!d->gameStarted)
0445         return;
0446 
0447     if (!d->gameOverInProcess) {
0448         d->gameOverInProcess = true;
0449         sendPendingMove();
0450     }
0451 
0452     stopTime();
0453     Protocol::white()->setWinner(winner);
0454     Protocol::black()->setWinner(winner);
0455     Q_EMIT winnerNotify(winner);
0456 
0457     reset();
0458     d->gameOverInProcess = false;
0459 }
0460 
0461 void Manager::reset() {
0462     Q_D(GameManager);
0463     if (d->gameStarted) {
0464         if (!d->gameOverInProcess) {
0465             sendPendingMove();
0466             stopTime();
0467         }
0468         Protocol::white()->deleteLater();
0469         Protocol::black()->deleteLater();
0470         if (d->rules) {
0471             delete d->rules;
0472             d->rules = nullptr;
0473         }
0474         d->gameStarted = false;
0475     }
0476 
0477     //don't clear the move history here,
0478     //it should be possible to study the history after the game ended.
0479 
0480     d->moveUndoStack.clear();
0481     Q_EMIT undoPossible( false );
0482     Q_EMIT redoPossible( false );
0483 
0484     d->offers.clear();
0485     d->usedOfferIds.clear();
0486     d->winner = NoColor;
0487 
0488     delete d->extControl;
0489     d->extControl = nullptr;
0490 }
0491 
0492 Rules* Manager::rules() const {
0493     Q_D(const GameManager);
0494     return d->rules;
0495 }
0496 
0497 void Manager::setRules(Rules* rules) {
0498     Q_D(GameManager);
0499     delete d->rules;
0500 
0501     d->rules = rules;
0502 }
0503 
0504 void Manager::sendOffer(GameAction action, Color player, int id) {
0505     Offer o;
0506     o.action = action;
0507     o.player = player;
0508     o.id = id;
0509     sendOffer(o);
0510 }
0511 
0512 void Manager::sendOffer(const Offer& offer) {
0513     Q_D(GameManager);
0514     Offer o = offer;
0515     if ( offer.player == NoColor )
0516         o.player = local()->color();
0517 
0518     if (o.id == 0)
0519         o.id = d->nextOfferId();
0520 
0521     QString name = Protocol::byColor(o.player)->playerName();
0522     if ( o.text.isEmpty() ) {
0523         switch ( offer.action ) {
0524         case ActionDraw:
0525             o.text = i18n("%1 offers you a draw", name);
0526             break;
0527         case ActionUndo:
0528             o.text = i18np("%2 would like to take back a half move", "%2 would like to take back %1 half moves", o.numberOfMoves, name);
0529             break;
0530         case ActionAdjourn:
0531             o.text = i18n("%1 would like to adjourn the game", name);
0532             break;
0533         case ActionAbort:
0534             o.text = i18n("%1 would like to abort the game", name);
0535             break;
0536         default:
0537             break;
0538         }
0539     }
0540     d->offers.insert ( o.id, o );
0541     d->usedOfferIds << o.id;
0542     Protocol* opp = Protocol::byColor( oppositeColor(o.player) );
0543     // Only display a notification if only one player is local.
0544     if ( opp->isLocal() && !Protocol::byColor(o.player)->isLocal() )
0545         Q_EMIT notification(o);
0546     else
0547         opp->makeOffer(o);
0548 }
0549 
0550 void Manager::setOfferResult(int id, OfferAction result) {
0551     Q_D(GameManager);
0552     if ( result == AcceptOffer ) {
0553         Protocol::byColor(d->offers[id].player)->acceptOffer(d->offers[id]);
0554         switch ( d->offers[id].action ) {
0555         case ActionUndo:
0556             for ( int i = 0; i < d->offers[id].numberOfMoves; ++i ) {
0557                 Q_EMIT pieceMoved ( nextUndoMove() );
0558                 changeActivePlayer();
0559             }
0560             break;
0561 
0562         case ActionDraw:
0563             Protocol::white()->setWinner(NoColor);
0564             Protocol::black()->setWinner(NoColor);
0565             break;
0566 
0567         case ActionAbort:
0568             gameOver(NoColor);
0569             break;
0570 
0571         case ActionPause:
0572             stopTime();
0573             break;
0574 
0575         case ActionResume:
0576             startTime();
0577             break;
0578 
0579         default:
0580             break;
0581         }
0582     } else if ( result == DeclineOffer )
0583         Protocol::byColor(d->offers[id].player)->declineOffer(d->offers[id]);
0584     d->offers.remove(id);
0585 }
0586 
0587 Protocol* Manager::local() {
0588     Q_D(const GameManager);
0589 
0590     //Protocol::byColor() can return nulltpr for NoColor,
0591     //add a sanity check here eventhough the case NoColor shouldn't actually happen, but see BUG:336412...
0592     //TODO: debug more later and find out why NoColor can happen here.
0593     if (d->activePlayer == NoColor)
0594         return nullptr;
0595 
0596     if ( Protocol::byColor(d->activePlayer)->isLocal() )
0597         return Protocol::byColor(d->activePlayer);
0598     if ( Protocol::byColor(oppositeColor(d->activePlayer))->isLocal() )
0599         return Protocol::byColor(oppositeColor(d->activePlayer));
0600     qCWarning(LOG_KNIGHTS) << "No local protocols, trying a computer";
0601     if ( Protocol::byColor(d->activePlayer)->isComputer() )
0602         return Protocol::byColor(d->activePlayer);
0603     if ( Protocol::byColor(oppositeColor(d->activePlayer))->isComputer() )
0604         return Protocol::byColor(oppositeColor(d->activePlayer));
0605     qCWarning(LOG_KNIGHTS) << "No local or computer protocols, returning 0";
0606     return nullptr;
0607 }
0608 
0609 bool Manager::canRedo() const {
0610     Q_D(const GameManager);
0611     return !d->moveUndoStack.isEmpty();
0612 }
0613 
0614 void Manager::sendPendingMove() {
0615     if ( pendingMove.isValid() && isGameActive() ) {
0616         Q_D(GameManager);
0617         Protocol::byColor ( oppositeColor ( d->activePlayer ) )->move ( pendingMove );
0618         Q_EMIT pieceMoved ( pendingMove );
0619         rules()->moveMade ( pendingMove );
0620 
0621         if ( Settings::speakOpponentsMoves()
0622                 && !Protocol::byColor(d->activePlayer)->isLocal()
0623                 && Protocol::byColor(oppositeColor(d->activePlayer))->isLocal() ) {
0624             QString toSpeak;
0625             QString name = Protocol::byColor(d->activePlayer)->playerName();
0626             if ( pendingMove.flag(Move::Castle) ) {
0627                 if ( pendingMove.to().first == 3 ) {
0628                     toSpeak = i18nc("string to be spoken when the opponent castles queenside",
0629                                     "%1 castles queenside", name);
0630                 } else {
0631                     toSpeak = i18nc("string to be spoken when the opponent castles queenside",
0632                                     "%1 castles kingside", name);
0633                 }
0634             } else {
0635                 toSpeak = i18nc("string to be spoken when the opponent makes a normal  move",
0636                                 "%1 to %2",
0637                                 pieceTypeName ( pendingMove.pieceData().second ),
0638                                 pendingMove.to().string()
0639                                );
0640             }
0641 #ifdef HAVE_SPEECH
0642             qCDebug(LOG_KNIGHTS) << toSpeak;
0643             if (!d->speech)
0644                 d->speech = new QTextToSpeech(this);
0645             d->speech->say(toSpeak);
0646 
0647             if ( pendingMove.flag(Move::Check) ) {
0648                 if ( d->rules->hasLegalMoves ( oppositeColor( d->activePlayer ) ) )
0649                     d->speech->say ( i18nc( "Your king is under attack", "Check" ) );
0650                 else
0651                     d->speech->say ( i18nc( "Your king is dead", "Checkmate" ) );
0652             }
0653 #endif /* HAVE_SPEECH */
0654         }
0655 
0656         pendingMove = Move();
0657 
0658         Color winner = rules()->winner();
0659         if ( winner != NoColor || !rules()->hasLegalMoves ( oppositeColor( d->activePlayer ) ) ) {
0660             qCDebug(LOG_KNIGHTS) << "Winner: " << winner;
0661             if (!d->gameOverInProcess) {
0662                 d->gameOverInProcess = true;
0663                 gameOver(winner);
0664             }
0665         }
0666 
0667         int moveNumber;
0668         int secondsAdded = 0;
0669         switch ( d->activePlayer ) {
0670         case White:
0671             moveNumber = ( d->moveHistory.size() + 1 ) / 2;
0672             if ( moveNumber > 1 )
0673                 secondsAdded += d->whiteTimeControl.increment;
0674             if ( d->whiteTimeControl.moves > 0 && ( moveNumber % d->whiteTimeControl.moves ) == 0 )
0675                 secondsAdded += QTime().secsTo( d->whiteTimeControl.baseTime );
0676             if ( secondsAdded != 0 )
0677                 setCurrentTime ( White, d->whiteTimeControl.currentTime.addSecs ( secondsAdded ) );
0678             break;
0679 
0680         case Black:
0681             moveNumber = d->moveHistory.size() / 2;
0682             if ( moveNumber > 1 )
0683                 secondsAdded += d->blackTimeControl.increment;
0684             if ( d->blackTimeControl.moves > 0 && ( moveNumber % d->blackTimeControl.moves ) == 0 )
0685                 secondsAdded += QTime().secsTo ( d->blackTimeControl.baseTime );
0686             if ( secondsAdded != 0 )
0687                 setCurrentTime ( Black, d->blackTimeControl.currentTime.addSecs ( secondsAdded ) );
0688             break;
0689 
0690         default:
0691             break;
0692         }
0693 
0694         //don't change the active player if the game is already over
0695         if (d->gameStarted)
0696             changeActivePlayer();
0697     }
0698 }
0699 
0700 void Manager::moveByBoard(const Move& move) {
0701     processMove(move);
0702 }
0703 
0704 void Manager::moveByExternalControl(const Knights::Move& move) {
0705     Q_D(GameManager);
0706     if ( Settings::allowExternalControl() && Protocol::byColor(d->activePlayer)->isLocal() )
0707         processMove(move);
0708 }
0709 
0710 void Manager::processMove(const Move& move) {
0711     Q_D(const GameManager);
0712 
0713     //no need to process the last move coming from the engine if the game is already
0714     //over and we're closing the game
0715     if (d->gameOverInProcess)
0716         return;
0717 
0718     sendPendingMove();
0719     Move m = move;
0720     if ( activePlayer() == White ) {
0721         m.setTime ( d->whiteTimeControl.currentTime );
0722         if (Settings::playSounds() && !d->loading) {
0723             if (!m.flag(Move::Take))
0724                 d->moveWhiteSound->start();
0725             else
0726                 d->captureWhiteSound->start();
0727         }
0728     } else {
0729         m.setTime ( d->blackTimeControl.currentTime );
0730         if (Settings::playSounds() && !d->loading) {
0731             if (!m.flag(Move::Take))
0732                 d->moveBlackSound->start();
0733             else
0734                 d->captureBlackSound->start();
0735         }
0736     }
0737     d->rules->checkSpecialFlags ( &m, d->activePlayer );
0738     if ( m.flag(Move::Illegal) && !m.flag(Move::Forced) )
0739         return;
0740     addMoveToHistory ( m );
0741     if ( d->moveHistory.size() == 2 && timeControlEnabled(d->activePlayer) )
0742         startTime();
0743     pendingMove = m;
0744     if ( Protocol::byColor(d->activePlayer)->isComputer() )
0745         QTimer::singleShot ( Settings::computerDelay(), this, &Manager::sendPendingMove );
0746     else
0747         sendPendingMove();
0748 }
0749 
0750 bool Manager::isGameActive() const {
0751     Q_D(const GameManager);
0752     return d->gameStarted;
0753 }
0754 
0755 bool Manager::canLocalMove() const {
0756     Q_D(const GameManager);
0757     if ( !d->gameStarted )
0758         return false;
0759     if ( d->running || d->moveHistory.size() < 2 || !timeControlEnabled(NoColor) )
0760         return Protocol::byColor(d->activePlayer)->isLocal();
0761     return false;
0762 }
0763 
0764 void Manager::levelChanged ( const KGameDifficultyLevel* level ) {
0765     qCDebug(LOG_KNIGHTS);
0766     int depth = 0;
0767     int size = 32;
0768     switch ( level->standardLevel() ) {
0769     case KGameDifficultyLevel::VeryEasy:
0770         depth = 1;
0771         break;
0772 
0773     case KGameDifficultyLevel::Easy:
0774         depth = 3;
0775         break;
0776 
0777     case KGameDifficultyLevel::Medium:
0778         depth = 8;
0779         break;
0780 
0781     case KGameDifficultyLevel::Hard:
0782         depth = 16;
0783         break;
0784 
0785     case KGameDifficultyLevel::VeryHard:
0786         depth = 32;
0787         break;
0788 
0789     case KGameDifficultyLevel::Custom: {
0790             QPointer<DifficultyDialog> dlg = new DifficultyDialog();
0791             if ( dlg->exec() == QDialog::Accepted ) {
0792                 depth = dlg->searchDepth();
0793                 size = dlg->memorySize();
0794             } else
0795                 return;
0796         }
0797         break;
0798 
0799     default:
0800         break;
0801     }
0802 
0803     Protocol* p = Protocol::white();
0804     if (p && p->supportedFeatures() & Protocol::AdjustDifficulty)
0805         p->setDifficulty(depth, size);
0806 
0807     p = Protocol::black();
0808     if (p && p->supportedFeatures() & Protocol::AdjustDifficulty)
0809         p->setDifficulty(depth, size);
0810 }
0811 
0812 
0813 void Manager::loadGameHistoryFrom(const QString& filename) {
0814     Q_D(GameManager);
0815     qCDebug(LOG_KNIGHTS) << filename;
0816     QFile file(filename);
0817     if ( !file.open(QIODevice::ReadOnly) )
0818         return;
0819 
0820     QRegExp tagPairExp = QRegExp(QLatin1String( "\\[(.*)\\s\\\"(.*)\\\"\\]" ));
0821     while ( file.bytesAvailable() > 0 ) {
0822         QByteArray line = file.readLine();
0823         if ( tagPairExp.indexIn ( QLatin1String(line) ) > -1 ) {
0824             // Parse a tag pair
0825             QString key = tagPairExp.cap(1);
0826             QString value = tagPairExp.cap(2);
0827 
0828             if ( key == QLatin1String("White") )
0829                 Protocol::white()->setPlayerName ( value );
0830             else if ( key == QLatin1String("Black") )
0831                 Protocol::black()->setPlayerName ( value );
0832             else if ( key == QLatin1String("TimeControl") ) {
0833                 // TODO, optional: Parse TimeControl Tag
0834             }
0835         } else {
0836             // Parse a line of moves
0837             d->loading = true;
0838             for ( const QByteArray& str : line.trimmed().split(' ') ) {
0839                 if ( !str.trimmed().isEmpty() && !str.contains('.') && !str.contains("1-0") && !str.contains("0-1") && !str.contains("1/2-1/2") && !str.contains('*') ) {
0840                     // Only move numbers contain dots, not move data itself
0841                     // We also exclude the game termination markers (results)
0842                     qCDebug(LOG_KNIGHTS) << "Read move" << str;
0843                     Move m;
0844                     if (str.contains("O-O-O") || str.contains("o-o-o") || str.contains("0-0-0"))
0845                         m = Move::castling(Move::QueenSide, activePlayer());
0846                     else if (str.contains("O-O") || str.contains("o-o") || str.contains("0-0"))
0847                         m = Move::castling(Move::KingSide, activePlayer());
0848                     else
0849                         m = Move ( QLatin1String(str) );
0850                     m.setFlag ( Move::Forced, true );
0851                     processMove ( m );
0852                 }
0853             }
0854             d->loading = false;
0855         }
0856     }
0857 
0858     Q_EMIT playerNameChanged();
0859 }
0860 
0861 void Manager::saveGameHistoryAs(const QString& filename) {
0862     Q_D(GameManager);
0863 
0864     d->filename = filename;
0865 
0866     QFile file ( d->filename );
0867     file.open(QIODevice::WriteOnly);
0868     QTextStream stream ( &file );
0869 
0870     // Write the player tags first
0871 
0872     // Standard Tag Roster: Event, Site, Date, Round, White, Black, Result
0873 
0874         stream << "[Event \"Casual Game\"]" << "\n";
0875         stream << "[Site \"?\"]" << "\n";
0876         stream << "[Date \"" << QDate::currentDate().toString( QStringLiteral("yyyy.MM.dd") ) << "\"]" << "\n";
0877         stream << "[Round \"-\"]" << "\n";
0878         stream << "[White \"" << Protocol::white()->playerName() << "\"]" << "\n";
0879         stream << "[Black \"" << Protocol::black()->playerName() << "\"]" << "\n";
0880 
0881     QByteArray result;
0882     if ( d->running )
0883         result += '*';
0884     else {
0885         switch ( d->winner ) {
0886         case White:
0887             result = "1-0";
0888             break;
0889         case Black:
0890             result = "0-1";
0891             break;
0892         default:
0893             result = "1/2-1/2";
0894             break;
0895         }
0896     }
0897         stream << "[Result \"" << result << "\"]" << "\n";
0898 
0899     // Supplemental tags, ordered alphabetically.
0900     // Currently, only TimeControl is added
0901 
0902     stream << "[TimeControl \"";
0903     if ( timeControlEnabled ( NoColor ) ) {
0904         // The PGN specification doesn't include a time control combination with both a number of moves
0905         // and an increment per move defined, so we only output one of them
0906         // If the spec will ever be expanded, the two lines should be combined:
0907         // stream << tc.moves << '/' << QTime().secsTo ( tc.baseTime ) << '+' << tc.increment
0908 
0909         TimeControl tc = timeControl ( NoColor );
0910         if ( tc.moves )
0911             stream << tc.moves << '/' << QTime().secsTo ( tc.baseTime );
0912         else
0913             stream << QTime().secsTo ( tc.baseTime ) << '+' << tc.increment;
0914     } else
0915         stream << '-';
0916     stream << "\"]";
0917 
0918     // A single newline separates the tag pairs from the movetext section
0919         stream << "\n";
0920 
0921     qCDebug(LOG_KNIGHTS) << "Starting to write movetext";
0922 
0923     int characters = 0;
0924     int n = d->moveHistory.size();
0925     for (int i = 0; i < n; ++i) {
0926         Move m = d->moveHistory[i];
0927         const QString moveString = m.stringForNotation ( Move::Algebraic );
0928         QString output;
0929 
0930         if ( i % 2 == 0 ) {
0931             // White move
0932             output = QString::number(i/2+1) + QLatin1String(". ") + moveString;
0933         } else {
0934             // Black move
0935             output = moveString;
0936         }
0937 
0938         if ( characters + output.size() > LineLimit ) {
0939                         stream << "\n";
0940             characters = 0;
0941         }
0942 
0943         if ( characters != 0 )
0944             stream << QLatin1Char(' ');
0945 
0946         stream << output;
0947         characters += output.size();
0948     }
0949 
0950     qCDebug(LOG_KNIGHTS);
0951 
0952     stream << ' ' << result;
0953 
0954         stream << "\n";
0955     stream.flush();
0956 
0957     qCDebug(LOG_KNIGHTS) << "Saved";
0958 }
0959 
0960 QStack< Move > Manager::moveHistory() const {
0961     Q_D(const GameManager);
0962     return d->moveHistory;
0963 }
0964 
0965 #include "moc_gamemanager.cpp"