diff --git a/src/game/game.cpp b/src/game/game.cpp index f11667c..37c62eb 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -1,788 +1,788 @@ /* Copyright 2008 Sascha Peilicke 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 . */ #include "game.h" #include "score.h" #include #include #include #include namespace Kigo { class UndoCommand : public QUndoCommand { public: enum class MoveType { Stone, Passed, Resigned }; UndoCommand(Player *player, MoveType moveType, const QString &undoStr) : QUndoCommand(undoStr), m_player(player), m_moveType(moveType) {} Player *player () const { return m_player; } MoveType moveType () const { return m_moveType; } private: Player *m_player; MoveType m_moveType; }; Game::Game(QObject *parent) : QObject(parent) , m_currentMove(0), m_lastUndoIndex(0), m_currentPlayer(&m_blackPlayer) , m_blackPlayer(Player::Color::Black), m_whitePlayer(Player::Color::White) , m_komi(4.5), m_boardSize(19), m_fixedHandicap(5), m_consecutivePassMoveNumber(0) , m_gameFinished(false) { connect(&m_process, &QProcess::readyRead, this, &Game::readyRead); connect(&m_undoStack, &QUndoStack::canRedoChanged, this, &Game::canRedoChanged); connect(&m_undoStack, &QUndoStack::canUndoChanged, this, &Game::canUndoChanged); connect(&m_undoStack, &QUndoStack::indexChanged, this, &Game::undoIndexChanged); } Game::~Game() { stop(); } bool Game::start(const QString &command) { stop(); // Close old session if there's one - m_process.start(command.toLatin1()); // Start new process with provided command + m_process.start(command); // Start new process with provided command if (!m_process.waitForStarted()) { // Blocking wait for process start - m_response = "Unable to execute command: " + command; + m_response = QStringLiteral("Unable to execute command: ") + command; //qDebug() << m_response; return false; } m_engineCommand = command; ////qDebug() << "Game" << command << "started..."; // Test if we started a GTP-compatible Go game m_process.write("name\n"); m_process.waitForReadyRead(); const QString response = m_process.readAllStandardOutput(); if (response.isEmpty() || !response.startsWith(QLatin1String("="))) { m_response = QLatin1String("Game did not respond to GTP command \"name\""); //qDebug() << m_response; stop(); return false; } else { m_engineName = m_response; } //qDebug() << "Game is a GTP-compatible Go game"; m_process.write("version\n"); if (waitResponse()) { m_engineVersion = m_response; } return true; } void Game::stop() { if (m_process.isOpen()) { m_process.write("quit\n"); m_process.close(); } } bool Game::init() { if (!isRunning()) { return false; } //qDebug() << "Init game!"; m_process.write("clear_board\n"); if (waitResponse()) { // The board is wiped empty, start again with black player setCurrentPlayer(m_blackPlayer); m_fixedHandicap = 0; m_consecutivePassMoveNumber = 0; m_currentMove = 0; m_gameFinished = false; m_movesList.clear(); m_undoStack.clear(); emit boardChanged(); return true; } else { return false; } } bool Game::init(const QString &fileName, int moveNumber) { Q_ASSERT(moveNumber >= 0); if (!isRunning() || fileName.isEmpty() || !QFile::exists(fileName)) { return false; } m_process.write("loadsgf " + fileName.toLatin1() + ' ' + QByteArray::number(moveNumber) + '\n'); if (waitResponse()) { if (m_response.startsWith(QLatin1String("white"))) { // Check which player is current setCurrentPlayer(m_whitePlayer); } else { setCurrentPlayer(m_blackPlayer); } m_process.write("query_boardsize\n"); // Query board size from game if (waitResponse()) { m_boardSize = m_response.toInt(); } m_process.write("get_komi\n"); // Query komi from game and store it if (waitResponse()) { m_komi = m_response.toFloat(); } m_process.write("get_handicap\n"); // Query fixed handicap and store it if (waitResponse()) { m_fixedHandicap = m_response.toInt(); } //qDebug() << "Loaded komi is" << m_komi << "and handicap is" << m_fixedHandicap; m_consecutivePassMoveNumber = 0; m_currentMove = moveNumber; m_gameFinished = false; m_movesList.clear(); m_undoStack.clear(); emit boardSizeChanged(m_boardSize); emit boardChanged(); // All done, tell the world! return true; } else { return false; } } bool Game::save(const QString &fileName) { if (!isRunning() || fileName.isEmpty()) { return false; } m_process.write("printsgf " + fileName.toLatin1() + '\n'); return waitResponse(); } bool Game::setBoardSize(int size) { Q_ASSERT(size >= 1 && size <= 19); if (!isRunning()) { return false; } m_process.write("boardsize " + QByteArray::number(size) + '\n'); if (waitResponse()) { // Changing size wipes the board, start again with black player. setCurrentPlayer(m_blackPlayer); m_boardSize = size; m_fixedHandicap = 0; m_consecutivePassMoveNumber = 0; m_currentMove = 0; m_movesList.clear(); m_undoStack.clear(); emit boardSizeChanged(size); emit boardChanged(); return true; } else { return false; } } bool Game::setKomi(float komi) { Q_ASSERT(komi >= 0); if (!isRunning()) { return false; } m_process.write("komi " + QByteArray::number(komi) + '\n'); if (waitResponse()) { m_komi = komi; return true; } else { return false; } } bool Game::setFixedHandicap(int handicap) { Q_ASSERT(handicap >= 2 && handicap <= 9); if (!isRunning()) { return false; } if (handicap <= fixedHandicapUpperBound()) { m_process.write("fixed_handicap " + QByteArray::number(handicap) + '\n'); if (waitResponse()) { // Black starts with setting his (fixed) handicap as it's first turn // which means, white is next. setCurrentPlayer(m_whitePlayer); m_fixedHandicap = handicap; emit boardChanged(); return true; } else { return false; } } else { //qWarning() << "Handicap" << handicap << " not set, it is too high!"; return false; } } int Game::fixedHandicapUpperBound() { switch (m_boardSize) { // Handcrafted values reflect what GnuGo accepts case 7: case 8: case 10: case 12: case 14: case 16: case 18: return 4; case 9: case 11: case 13: case 15: case 17: case 19: return 9; default: return 0; } } bool Game::playMove(const Move &move, bool undoable) { return playMove(*move.player(), move.stone(), undoable); } bool Game::playMove(const Player &player, const Stone &stone, bool undoable) { if (!isRunning()) { return false; } const Player *tmp = &player; if (!tmp->isValid()) { //qDebug() << "Invalid player argument, using current player!"; tmp = m_currentPlayer; } QByteArray msg("play "); // The command to be sent if (tmp->isWhite()) { msg.append("white "); } else { msg.append("black "); } if (stone.isValid()) { msg.append(stone.toLatin1() + '\n'); } else { msg.append("pass\n"); } m_process.write(msg); // Send command to backend if (waitResponse()) { if (stone.isValid()) { // Normal move handling m_movesList.append(Move(tmp, stone)); m_consecutivePassMoveNumber = 0; } else { // And pass move handling m_movesList.append(Move(tmp, Stone::Pass)); emit passMovePlayed(*m_currentPlayer); if (m_consecutivePassMoveNumber > 0) { emit consecutivePassMovesPlayed(m_consecutivePassMoveNumber); } m_consecutivePassMoveNumber++; } m_currentMove++; if (undoable) { // Do undo stuff if desired Player *playerTemp; UndoCommand::MoveType moveType; QString undoStr; if (tmp->isWhite()) { playerTemp = &m_whitePlayer; if (stone.isValid()) { moveType = UndoCommand::MoveType::Stone; undoStr = i18nc("%1 stone coordinate", "White %1", stone.toString()); } else { moveType = UndoCommand::MoveType::Passed; undoStr = i18n("White passed"); } } else { playerTemp = &m_blackPlayer; if (stone.isValid()) { moveType = UndoCommand::MoveType::Stone; undoStr = i18nc("%1 stone coordinate", "Black %1", stone.toString()); } else { moveType = UndoCommand::MoveType::Passed; undoStr = i18n("Black passed"); } } //qDebug() << "Push new undo command" << undoStr; m_undoStack.push(new UndoCommand(playerTemp, moveType, undoStr)); } if (tmp->isWhite()) { // Determine the next current player setCurrentPlayer(m_blackPlayer); } else { setCurrentPlayer(m_whitePlayer); } emit boardChanged(); return true; } else { return false; } } bool Game::generateMove(const Player &player, bool undoable) { if (!isRunning()) { return false; } const Player *tmp = &player; if (!tmp->isValid()) { //qDebug() << "Invalid player argument, using current player!"; tmp = m_currentPlayer; } if (tmp->isWhite()) { m_process.write("level " + QByteArray::number(m_whitePlayer.strength()) + '\n'); waitResponse(); // Setting level is not mission-critical, no error checking m_process.write("genmove white\n"); } else { m_process.write("level " + QByteArray::number(m_blackPlayer.strength()) + '\n'); waitResponse(); // Setting level is not mission-critical, no error checking m_process.write("genmove black\n"); } if (waitResponse(true)) { bool boardChange = false; Player *playerTemp; UndoCommand::MoveType moveType; QString undoStr; if (tmp->isWhite()) { playerTemp = &m_whitePlayer; } else { playerTemp = &m_blackPlayer; } if (m_response == QLatin1String("PASS")) { m_currentMove++; emit passMovePlayed(*m_currentPlayer); if (m_consecutivePassMoveNumber > 0) { emit consecutivePassMovesPlayed(m_consecutivePassMoveNumber); m_gameFinished = true; } m_consecutivePassMoveNumber++; moveType = UndoCommand::MoveType::Passed; if (tmp->isWhite()) { undoStr = i18n("White passed"); } else { undoStr = i18n("Black passed"); } } else if (m_response == QLatin1String("resign")) { emit resigned(*m_currentPlayer); m_gameFinished = true; moveType = UndoCommand::MoveType::Resigned; if (tmp->isWhite()) { undoStr = i18n("White resigned"); } else { undoStr = i18n("Black resigned"); } } else { m_currentMove++; m_movesList.append(Move(tmp, Stone(m_response))); m_consecutivePassMoveNumber = 0; moveType = UndoCommand::MoveType::Stone; if (tmp->isWhite()) { undoStr = i18nc("%1 response from Go engine", "White %1", m_response); } else { undoStr = i18nc("%1 response from Go engine", "Black %1", m_response); } boardChange = true; } if (undoable) { //qDebug() << "Push new undo command" << undoStr; m_undoStack.push(new UndoCommand(playerTemp, moveType, undoStr)); } if (tmp->isWhite()) { setCurrentPlayer(m_blackPlayer); } else { setCurrentPlayer(m_whitePlayer); } if (boardChange) { emit boardChanged(); } return true; } else { return false; } } bool Game::undoMove() { if (!isRunning()) { return false; } m_process.write("undo\n"); if (waitResponse()) { Move lastMove = m_movesList.takeLast(); m_currentMove--; if (lastMove.player()->isComputer()) { // Do one more undo m_process.write("undo\n"); if (waitResponse()) { lastMove = m_movesList.takeLast(); m_currentMove--; } m_undoStack.undo(); } if (lastMove.player()->isWhite()) { setCurrentPlayer(m_whitePlayer); } else { setCurrentPlayer(m_blackPlayer); } if (m_consecutivePassMoveNumber > 0) { m_consecutivePassMoveNumber--; } //TODO: What happens with pass moves deeper in the history? m_undoStack.undo(); emit boardChanged(); return true; } else { return false; } } bool Game::redoMove() { if (!isRunning()) { return false; } const UndoCommand *undoCmd = static_cast(m_undoStack.command(m_undoStack.index())); const Player *player = undoCmd->player(); if (undoCmd->moveType() == UndoCommand::MoveType::Passed) { //qDebug() << "Redo a pass move for" << player << undoCmd->text(); playMove(*player, Stone(), false); // E.g. pass move } else if (undoCmd->moveType() == UndoCommand::MoveType::Resigned) { // Note: Although it is possible to undo after a resign and redo it, // it is a bit questionable whether this makes sense logically. //qDebug() << "Redo a resign for" << player << undoCmd->text(); emit resigned(*player); m_gameFinished = true; //emit resigned(*m_currentPlayer); } else { //qDebug() << "Redo a normal move for" << player << undoCmd->text(); playMove(*player, Stone(undoCmd->text()), false); } m_undoStack.redo(); return false; } Move Game::lastMove() const { Q_ASSERT(!m_movesList.isEmpty()); return m_movesList.last(); } int Game::moveCount() { if (!isRunning()) { return 0; } m_process.write("move_history\n"); // Query fixed handicap and store it if (waitResponse()) { - return m_response.count('\n') + 1; + return m_response.count(QLatin1Char('\n')) + 1; } return 0; } QList Game::stones(const Player &player) { QList list; if (!isRunning()) { return list; } // Invalid player means all stones if (!player.isWhite()) { m_process.write("list_stones black\n"); if (waitResponse() && !m_response.isEmpty()) { - foreach (const QString &pos, m_response.split(' ')) { + foreach (const QString &pos, m_response.split(QLatin1Char(' '))) { list.append(Stone(pos)); } } } if (!player.isBlack()) { m_process.write("list_stones white\n"); if (waitResponse() && !m_response.isEmpty()) { - foreach (const QString &pos, m_response.split(' ')) { + foreach (const QString &pos, m_response.split(QLatin1Char(' '))) { list.append(Stone(pos)); } } } return list; } QList Game::moves(const Player &player) { QList list; if (!isRunning()) { return list; } if (!player.isValid()) { list = m_movesList; } else { foreach (const Move &move, m_movesList) { if (move.player()->color() == player.color()) { list.append(move); } } } return list; } QList Game::liberties(const Stone &stone) { QList list; if (!isRunning() || !stone.isValid()) { return list; } m_process.write("findlib " + stone.toLatin1() + '\n'); if (waitResponse() && !m_response.isEmpty()) { - foreach (const QString &entry, m_response.split(' ')) { + foreach (const QString &entry, m_response.split(QLatin1Char(' '))) { list.append(Stone(entry)); } } return list; } QList Game::bestMoves(const Player &player) { QList list; if (!isRunning() || !player.isValid()) { return list; } if (player.isWhite()) { m_process.write("top_moves_white\n"); } else { m_process.write("top_moves_black\n"); } if (waitResponse(true) && !m_response.isEmpty()) { - const QStringList parts = m_response.split(' '); + const QStringList parts = m_response.split(QLatin1Char(' ')); if (parts.size() % 2 == 0) { for (int i = 0; i < parts.size(); i += 2) { list.append(Stone(parts[i], QString(parts[i + 1]).toFloat())); } } } return list; } QList Game::legalMoves(const Player &player) { QList list; if (!isRunning() || !player.isValid()) { return list; } if (player.isWhite()) { m_process.write("all_legal white\n"); } else { m_process.write("all_legal black\n"); } if (waitResponse() && !m_response.isEmpty()) { - foreach (const QString &entry, m_response.split(' ')) { + foreach (const QString &entry, m_response.split(QLatin1Char(' '))) { list.append(Stone(entry)); } } return list; } int Game::captures(const Player &player) { if (!isRunning() || !player.isValid()) { return 0; } if (player.isWhite()) { m_process.write("captures white\n"); } else { m_process.write("captures black\n"); } return waitResponse() ? m_response.toInt() : 0; } Game::FinalState Game::finalState(const Stone &stone) { if (!isRunning() || !stone.isValid()) { return FinalState::FinalStateInvalid; } m_process.write("final_status " + stone.toLatin1() + '\n'); if (waitResponse()) { if (m_response == QLatin1String("alive")) { return FinalState::FinalAlive; } else if (m_response == QLatin1String("dead")) { return FinalState::FinalDead; } else if (m_response == QLatin1String("seki")) { return FinalState::FinalSeki; } else if (m_response == QLatin1String("white_territory")) { return FinalState::FinalWhiteTerritory; } else if (m_response == QLatin1String("blacK_territory")) { return FinalState::FinalBlackTerritory; } else if (m_response == QLatin1String("dame")) { return FinalState::FinalDame; } else { return FinalState::FinalStateInvalid; } } else { return FinalState::FinalStateInvalid; } } QList Game::finalStates(FinalState state) { QList list; if (!isRunning() || state == FinalState::FinalStateInvalid) { return list; } QByteArray msg("final_status_list "); switch (state) { case FinalState::FinalAlive: msg.append("alive"); break; case FinalState::FinalDead: msg.append("dead"); break; case FinalState::FinalSeki: msg.append("seki"); break; case FinalState::FinalWhiteTerritory: msg.append("white_territory"); break; case FinalState::FinalBlackTerritory: msg.append("black_territory"); break; case FinalState::FinalDame: msg.append("dame"); break; case FinalState::FinalStateInvalid: /* Will never happen */ break; } msg.append('\n'); m_process.write(msg); if (waitResponse() && !m_response.isEmpty()) { - foreach (const QString &entry, m_response.split(' ')) { + foreach (const QString &entry, m_response.split(QLatin1Char(' '))) { list.append(Stone(entry)); } } return list; } Score Game::finalScore() { if (!isRunning()) { return Score(); } m_process.write("final_score\n"); return waitResponse() ? Score(m_response) : Score(); } Score Game::estimatedScore() { if (!isRunning()) { return Score(); } m_process.write("estimate_score\n"); return waitResponse() ? Score(m_response) : Score(); } bool Game::waitResponse(bool nonBlocking) { if (m_process.state() != QProcess::Running) { // No GTP connection means no computing fun! switch (m_process.error()) { case QProcess::FailedToStart: m_response = QLatin1String("No Go game is running!"); break; case QProcess::Crashed: m_response = QLatin1String("The Go game crashed!"); break; case QProcess::Timedout: m_response = QLatin1String("The Go game timed out!"); break; case QProcess::WriteError: m_response = m_process.readAllStandardError(); break; case QProcess::ReadError: m_response = m_process.readAllStandardError(); break; case QProcess::UnknownError: m_response = QLatin1String("Unknown error!"); break; } qWarning() << "Command failed:" << m_response; return false; } if (nonBlocking) { emit waiting(true); } // Wait for finished command execution. We have to do this untill '\n\n' (or '\r\n\r\n' or // '\r\r' in case of MS Windows and Mac, respectively) arrives in our input buffer to show // that the Go game is done processing our request. The 'nonBlocking' parameter decides // whether we block and wait (suitable for short commands) or if we continue processing // events in between to stop the UI from blocking (suitable for longer commands). // The latter may introduce flickering. m_response.clear(); do { if (nonBlocking) { while (m_waitAndProcessEvents) { // The flag is modified by another slot qApp->processEvents(); // called when QProcess signals readyRead() } m_waitAndProcessEvents = true; } else { m_process.waitForReadyRead(); // Blocking wait } m_response += m_process.readAllStandardOutput(); } while (!m_response.endsWith(QLatin1String("\r\r")) && !m_response.endsWith(QLatin1String("\n\n")) && !m_response.endsWith(QLatin1String("\r\n\r\n"))); if (nonBlocking) { emit waiting(false); } if (m_response.size() < 1) { return false; } QChar tmp = m_response[0]; // First message character indicates success or error m_response.remove(0, 2); // Remove the first two chars (e.g. "? " or "= ") m_response = m_response.trimmed(); // Remove further whitespaces, newlines, ... - return tmp != '?'; // '?' Means the game didn't understand the query + return tmp != QLatin1Char('?'); // '?' Means the game didn't understand the query } void Game::gameSetup() { emit boardInitialized(); } void Game::readyRead() { m_waitAndProcessEvents = false; } void Game::undoIndexChanged(int index) { m_lastUndoIndex = index; } void Game::setCurrentPlayer(Player &player) { m_currentPlayer = &player; emit currentPlayerChanged(*m_currentPlayer); } } // End of namespace Kigo diff --git a/src/game/score.cpp b/src/game/score.cpp index 1753bfb..bacb58a 100644 --- a/src/game/score.cpp +++ b/src/game/score.cpp @@ -1,72 +1,72 @@ /* Copyright 2008 Sascha Peilicke 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 . */ #include "score.h" namespace Kigo { Score::Score(const QString &score) - : m_color('?'), m_score(0), m_lowerBound(0), m_upperBound(0) + : m_color(QLatin1Char('?')), m_score(0), m_lowerBound(0), m_upperBound(0) { if (score.size() >= 2) { - if (score[0] == 'W') { - m_color = 'W'; - } else if (score[0] == 'B') { - m_color = 'B'; + if (score[0] == QLatin1Char('W')) { + m_color = QLatin1Char('W'); + } else if (score[0] == QLatin1Char('B')) { + m_color = QLatin1Char('B'); } - int i = score.indexOf(' '); + int i = score.indexOf(QLatin1Char(' ')); m_score = score.midRef(2, i - 1).toFloat(); - QString upperBound = score.section(' ', 3, 3); + QString upperBound = score.section(QLatin1Char(' '), 3, 3); upperBound.chop(1); m_upperBound = upperBound.toFloat(); - QString lowerBound = score.section(' ', 5, 5); + QString lowerBound = score.section(QLatin1Char(' '), 5, 5); lowerBound.chop(1); m_lowerBound = lowerBound.toFloat(); } } Score::Score(const Score &other) : QObject(), m_color(other.m_color), m_score(other.m_score) , m_lowerBound(other.m_lowerBound), m_upperBound(other.m_upperBound) { } Score &Score::operator=(const Score &other) { m_color = other.m_color; m_score = other.m_score; m_lowerBound = other.m_lowerBound; m_upperBound = other.m_upperBound; return *this; } bool Score::isValid() const { - return m_score >= 0 && (m_color == 'W' || m_color == 'B'); + return m_score >= 0 && (m_color == QLatin1Char('W') || m_color == QLatin1Char('B')); } QString Score::toString() const { QString ret(m_color + '+'); ret += QString::number(m_score) + " (" + QString::number(m_lowerBound) + " - " + QString::number(m_upperBound) + ')'; return ret; } } // End of namespace Kigo diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index a0315bd..0e7b06d 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1,501 +1,501 @@ /* Copyright 2008 Sascha Peilicke 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 . */ #include "mainwindow.h" #include "game/game.h" #include "game/score.h" #include "gui/config/generalconfig.h" #include "gui/graphicsview/gamescene.h" #include "gui/graphicsview/gameview.h" #include "gui/graphicsview/themerenderer.h" #include "gui/widgets/errorwidget.h" #include "gui/widgets/gamewidget.h" #include "gui/widgets/setupwidget.h" #include "preferences.h" #include #include #include #include #include #include #include #include #include #define USE_UNSTABLE_LIBKDEGAMESPRIVATE_API #include #include #include #include #include namespace Kigo { MainWindow::MainWindow(const QString &fileName, QWidget *parent) : KXmlGuiWindow(parent), m_game(new Game(this)) , m_gameScene(new GameScene(m_game)), m_gameView(new GameView(m_gameScene)) { setCentralWidget(m_gameView); setupActions(); setupDockWindows(); setupGUI(QSize(800, 700), KXmlGuiWindow::ToolBar | KXmlGuiWindow::Keys | KXmlGuiWindow::Save | KXmlGuiWindow::Create); connect(m_game, &Game::waiting, this, &MainWindow::showBusy); connect(m_game, &Game::consecutivePassMovesPlayed, this, &MainWindow::showFinishGameAction); connect(m_game, &Game::resigned, this, &MainWindow::finishGame); connect(m_game, &Game::passMovePlayed, this, &MainWindow::passMovePlayed); if (isBackendWorking()) { if (!loadGame(fileName)) { newGame(); } } else { backendError(); } } void MainWindow::newGame() { m_game->start(Preferences::engineCommand()); m_gameScene->showTerritory(false); m_gameView->setInteractive(false); m_newGameAction->setEnabled(true); m_loadGameAction->setEnabled(true); m_saveAction->setEnabled(false); m_startGameAction->setEnabled(true); m_finishGameAction->setEnabled(false); m_undoMoveAction->setEnabled(false); m_redoMoveAction->setEnabled(false); m_passMoveAction->setEnabled(false); m_hintAction->setEnabled(false); disconnect(m_game, &Game::canRedoChanged, m_redoMoveAction, &QAction::setEnabled); disconnect(m_game, &Game::canUndoChanged, m_undoMoveAction, &QAction::setEnabled); disconnect(m_game, &Game::currentPlayerChanged, this, &MainWindow::playerChanged); m_gameDock->setVisible(false); m_gameDock->toggleViewAction()->setEnabled(false); m_movesDock->setVisible(false); m_movesDock->toggleViewAction()->setEnabled(false); m_setupDock->setVisible(true); m_errorDock->setVisible(false); m_setupWidget->newGame(); QTimer::singleShot(0, this, [this] { m_gameScene->showMessage(i18n("Set up a new game...")); } ); } void MainWindow::loadGame() { const QString folderName = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("games"), QStandardPaths::LocateDirectory); const QString fileName = QFileDialog::getOpenFileName(this, QString(), folderName, i18n("Kigo Game Files (*.sgf)")); if (!fileName.isEmpty()) { loadGame(fileName); } } bool MainWindow::loadGame(const QString &fileName) { if (!fileName.isEmpty() && QFile(fileName).exists()) { m_game->start(Preferences::engineCommand()); m_gameScene->showTerritory(false); m_gameView->setInteractive(false); m_newGameAction->setEnabled(true); m_loadGameAction->setEnabled(true); m_saveAction->setEnabled(false); m_startGameAction->setEnabled(true); m_finishGameAction->setEnabled(false); m_undoMoveAction->setEnabled(false); m_redoMoveAction->setEnabled(false); m_passMoveAction->setEnabled(false); m_hintAction->setEnabled(false); disconnect(m_game, &Game::canRedoChanged, m_redoMoveAction, &QAction::setEnabled); disconnect(m_game, &Game::canUndoChanged, m_undoMoveAction, &QAction::setEnabled); disconnect(m_game, &Game::currentPlayerChanged, this, &MainWindow::playerChanged); m_gameDock->setVisible(false); m_gameDock->toggleViewAction()->setEnabled(false); m_movesDock->setVisible(false); m_movesDock->toggleViewAction()->setEnabled(false); m_setupDock->setVisible(true); m_errorDock->setVisible(false); m_setupWidget->loadedGame(fileName); m_gameScene->showMessage(i18n("Set up a loaded game...")); return true; } else { m_gameScene->showMessage(i18n("Unable to load game...")); //Note: New game implied here return false; } } void MainWindow::getMoreGames() { KNS3::DownloadDialog dialog(QStringLiteral("kigo-games.knsrc"), this); dialog.exec(); /*KNS3::Entry::List entries = dialog.changedEntries(); if (entries.size() > 0) { // do something with the modified entries here if you want // such as rescaning your data folder or whatnot }*/ } void MainWindow::backendError() { m_gameView->setInteractive(false); m_newGameAction->setEnabled(false); m_loadGameAction->setEnabled(false); m_saveAction->setEnabled(false); m_startGameAction->setEnabled(false); m_finishGameAction->setEnabled(false); m_undoMoveAction->setEnabled(false); m_redoMoveAction->setEnabled(false); m_passMoveAction->setEnabled(false); m_hintAction->setEnabled(false); disconnect(m_game, &Game::canRedoChanged, m_redoMoveAction, &QAction::setEnabled); disconnect(m_game, &Game::canUndoChanged, m_undoMoveAction, &QAction::setEnabled); disconnect(m_game, &Game::currentPlayerChanged, this, &MainWindow::playerChanged); m_gameDock->setVisible(false); m_gameDock->toggleViewAction()->setEnabled(false); m_movesDock->setVisible(false); m_movesDock->toggleViewAction()->setEnabled(false); m_setupDock->setVisible(false); m_errorDock->setVisible(true); } void MainWindow::saveGame() { const QString fileName = QFileDialog::getSaveFileName(this, QString(), QStandardPaths::writableLocation(QStandardPaths::HomeLocation), i18n("Kigo Game Files (*.sgf)")); if (!fileName.isEmpty()) { if (m_game->save(fileName)) { m_gameScene->showMessage(i18n("Game saved...")); } else { m_gameScene->showMessage(i18n("Unable to save game.")); } } } void MainWindow::startGame() { m_saveAction->setEnabled(true); m_startGameAction->setEnabled(false); m_finishGameAction->setEnabled(false); m_setupWidget->commit(); //Decide on players how to display the UI if (m_game->whitePlayer().isHuman() || m_game->blackPlayer().isHuman()) { connect(m_game, &Game::canRedoChanged, m_redoMoveAction, &QAction::setEnabled); connect(m_game, &Game::canUndoChanged, m_undoMoveAction, &QAction::setEnabled); m_passMoveAction->setEnabled(true); m_hintAction->setEnabled(true); m_gameView->setInteractive(true); m_undoView->setEnabled(true); m_gameScene->showPlacementMarker(true); } else { m_passMoveAction->setEnabled(false); m_hintAction->setEnabled(false); m_gameView->setInteractive(false); m_undoView->setEnabled(false); m_gameScene->showPlacementMarker(false); } connect(m_game, &Game::currentPlayerChanged, this, &MainWindow::playerChanged); // Trigger the slot once to make a move if the starting player // (black) is a computer player. playerChanged(); m_setupDock->setVisible(false); m_errorDock->setVisible(false); m_gameDock->setVisible(true); m_gameDock->toggleViewAction()->setEnabled(true); m_movesDock->setVisible(true); m_movesDock->toggleViewAction()->setEnabled(true); m_gameScene->showMessage(i18n("Game started...")); //TODO: Fix undo view clicking somehow m_undoView->setEnabled(false); } void MainWindow::finishGame() { m_gameView->setInteractive(false); m_gameScene->showHint(false); m_gameScene->showTerritory(true); m_undoMoveAction->setEnabled(false); m_redoMoveAction->setEnabled(false); m_passMoveAction->setEnabled(false); m_hintAction->setEnabled(false); m_startGameAction->setEnabled(false); m_finishGameAction->setEnabled(false); m_undoView->setEnabled(false); //qDebug() << "Fetching final score from engine ..."; Score score = m_game->estimatedScore(); QString name; - if (score.color() == 'W') { + if (score.color() == QLatin1Char('W')) { name = m_game->whitePlayer().name() + " (White)"; } else { name = m_game->blackPlayer().name() + " (Black)"; } // Show a score message that stays visible until the next // popup message arrives if (score.upperBound() == score.lowerBound()) { m_gameScene->showMessage(i18n("%1 won with a score of %2.", name, score.score()), 0); } else { m_gameScene->showMessage(i18n("%1 won with a score of %2 (bounds: %3 and %4).", name, score.score(), score.upperBound(), score.lowerBound()), 0); } } void MainWindow::undo() { if (m_game->undoMove()) { m_gameScene->showMessage(i18n("Undone move")); m_gameScene->showHint(false); } } void MainWindow::redo() { if (m_game->redoMove()) { m_gameScene->showMessage(i18n("Redone move")); m_gameScene->showHint(false); } } void MainWindow::pass() { if (m_game->playMove(m_game->currentPlayer())) { // E.g. Pass move m_gameScene->showHint(false); } } void MainWindow::hint() { m_gameScene->showHint(true); } void MainWindow::showPreferences() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } KConfigDialog *dialog = new KConfigDialog(this, QStringLiteral("settings"), Preferences::self()); dialog->addPage(new GeneralConfig(), i18n("General"), QStringLiteral("preferences-other")); dialog->addPage(new KGameThemeSelector(dialog, Preferences::self(), KGameThemeSelector::NewStuffDisableDownload), i18n("Themes"), QStringLiteral("games-config-theme")); if (QPushButton *restore = dialog->button(QDialogButtonBox::RestoreDefaults)) { restore->hide(); } connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::applyPreferences); dialog->show(); } void MainWindow::applyPreferences() { //qDebug() << "Update settings based on changed configuration..."; m_gameScene->showLabels(Preferences::showBoardLabels()); ThemeRenderer::self()->loadTheme(Preferences::theme()); if (!isBackendWorking()) { backendError(); } else if (m_game->engineCommand() != Preferences::engineCommand()) { // Restart the Go game if the game command was changed by the user. m_gameScene->showMessage(i18n("Backend was changed, restart necessary...")); newGame(); } } void MainWindow::showBusy(bool busy) { //Decide on players how to display the UI if (m_game->whitePlayer().isHuman() || m_game->blackPlayer().isHuman()) { if (busy) { m_undoMoveAction->setDisabled(true); m_redoMoveAction->setDisabled(true); } else { // Only re-enable undo/redo if it actually makes sense if (m_game->canUndo()) { m_undoMoveAction->setDisabled(false); } if (m_game->canRedo()) { m_redoMoveAction->setDisabled(false); } } m_passMoveAction->setDisabled(busy); m_hintAction->setDisabled(busy); m_undoView->setEnabled(false); } m_gameView->setInteractive(!busy); } void MainWindow::showFinishGameAction() { m_finishGameAction->setEnabled(true); } void MainWindow::playerChanged() { if (!m_game->currentPlayer().isHuman() && !m_game->isFinished()) { QTimer::singleShot(200, this, &MainWindow::generateMove); } } void MainWindow::generateMove() { m_game->generateMove(m_game->currentPlayer()); } void MainWindow::passMovePlayed(const Player &player) { if (player.isComputer()) { if (player.isWhite()) { m_gameScene->showMessage(i18n("White passed")); } else { m_gameScene->showMessage(i18n("Black passed")); } } } void MainWindow::setupActions() { // Game menu m_newGameAction = KStandardGameAction::gameNew(this, SLOT(newGame()), actionCollection()); m_loadGameAction = KStandardGameAction::load(this, SLOT(loadGame()), actionCollection()); m_getMoreGamesAction = new QAction(QIcon::fromTheme( QStringLiteral( "get-hot-new-stuff") ), i18nc("@action", "Get More Games..." ), this); actionCollection()->setDefaultShortcut(m_getMoreGamesAction, Qt::CTRL + Qt::Key_G); m_getMoreGamesAction->setToolTip(i18nc("@action", "Get More Games...")); connect(m_getMoreGamesAction, &QAction::triggered, this, &MainWindow::getMoreGames); actionCollection()->addAction( QStringLiteral( "get_more_games" ), m_getMoreGamesAction); m_saveAction = KStandardGameAction::save(this, SLOT(saveGame()), actionCollection()); KStandardGameAction::quit(this, SLOT(close()), actionCollection()); m_startGameAction = new QAction(QIcon::fromTheme( QStringLiteral( "media-playback-start") ), i18nc("@action", "Start Game" ), this); actionCollection()->setDefaultShortcut(m_startGameAction, Qt::Key_S); m_startGameAction->setToolTip(i18nc("@action", "Start Game")); connect(m_startGameAction, &QAction::triggered, this, &MainWindow::startGame); actionCollection()->addAction( QStringLiteral( "game_start" ), m_startGameAction); m_finishGameAction = new QAction(QIcon::fromTheme( QStringLiteral( "media-playback-stop") ), i18nc("@action", "Finish Game" ), this); actionCollection()->setDefaultShortcut(m_finishGameAction,Qt::Key_F); m_finishGameAction->setToolTip(i18nc("@action", "Finish Game")); connect(m_finishGameAction, &QAction::triggered, this, &MainWindow::finishGame); actionCollection()->addAction( QStringLiteral( "game_finish" ), m_finishGameAction); // Move menu m_undoMoveAction = KStandardGameAction::undo(this, SLOT(undo()), actionCollection()); m_redoMoveAction = KStandardGameAction::redo(this, SLOT(redo()), actionCollection()); m_passMoveAction = KStandardGameAction::endTurn(this, SLOT(pass()), actionCollection()); m_passMoveAction->setText(i18nc("@action:inmenu Move", "Pass Move")); actionCollection()->setDefaultShortcut(m_passMoveAction,Qt::Key_P); m_hintAction = KStandardGameAction::hint(this, SLOT(hint()), actionCollection()); // View menu m_moveNumbersAction = new KToggleAction(QIcon::fromTheme( QStringLiteral( "lastmoves") ), i18nc("@action:inmenu View", "Show Move &Numbers" ), this); actionCollection()->setDefaultShortcut(m_moveNumbersAction, Qt::Key_N); m_moveNumbersAction->setChecked(Preferences::showMoveNumbers()); connect(m_moveNumbersAction, &KToggleAction::toggled, m_gameScene, &GameScene::showMoveNumbers); actionCollection()->addAction( QStringLiteral( "move_numbers" ), m_moveNumbersAction); // Settings menu KStandardAction::preferences(this, SLOT(showPreferences()), actionCollection()); } void MainWindow::setupDockWindows() { // Setup dock m_setupDock = new QDockWidget(i18nc("@title:window", "Game Setup"), this); m_setupDock->setObjectName( QStringLiteral("setupDock" )); m_setupDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); m_setupWidget = new SetupWidget(m_game, this); m_setupDock->setWidget(m_setupWidget); connect(m_setupWidget, &SetupWidget::startClicked, this, &MainWindow::startGame); addDockWidget(Qt::RightDockWidgetArea, m_setupDock); // Game dock m_gameDock = new QDockWidget(i18nc("@title:window", "Information"), this); m_gameDock->setObjectName( QStringLiteral("gameDock" )); m_gameDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); auto gameWidget = new GameWidget(m_game, this); connect(gameWidget, &GameWidget::finishClicked, this, &MainWindow::finishGame); m_gameDock->setWidget(gameWidget); m_gameDock->toggleViewAction()->setText(i18nc("@title:window", "Information")); actionCollection()->setDefaultShortcut(m_gameDock->toggleViewAction(), Qt::Key_I); actionCollection()->addAction( QStringLiteral( "show_game_panel" ), m_gameDock->toggleViewAction()); addDockWidget(Qt::RightDockWidgetArea, m_gameDock); // Move history dock m_movesDock = new QDockWidget(i18nc("@title:window", "Moves"), this); m_movesDock->setObjectName( QStringLiteral("movesDock" )); m_undoView = new QUndoView(m_game->undoStack()); m_undoView->setEmptyLabel(i18n("No move")); m_undoView->setAlternatingRowColors(true); m_undoView->setFocusPolicy(Qt::NoFocus); m_undoView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_undoView->setSelectionMode(QAbstractItemView::NoSelection); m_movesDock->setWidget(m_undoView); m_movesDock->toggleViewAction()->setText(i18nc("@title:window", "Moves")); actionCollection()->setDefaultShortcut(m_movesDock->toggleViewAction(), Qt::Key_M); actionCollection()->addAction( QStringLiteral( "show_moves_panel" ), m_movesDock->toggleViewAction()); addDockWidget(Qt::RightDockWidgetArea, m_movesDock); m_errorDock = new QDockWidget(i18nc("@title:window", "Error"), this); m_errorDock->setObjectName( QStringLiteral("errorDock" )); m_errorDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); auto errorWidget = new ErrorWidget(this); connect(errorWidget, &ErrorWidget::configureClicked, this, &MainWindow::showPreferences); m_errorDock->setWidget(errorWidget); addDockWidget(Qt::BottomDockWidgetArea, m_errorDock); } bool MainWindow::isBackendWorking() { Game game; return game.start(Preferences::engineCommand()); } } // End of namespace Kigo