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"