File indexing completed on 2024-12-15 03:48:05

0001 /*
0002     SPDX-FileCopyrightText: 2008 Sascha Peilicke <sasch.pe@gmx.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "gamescene.h"
0008 #include "game/game.h"
0009 #include "preferences.h"
0010 #include "themerenderer.h"
0011 
0012 #include <QPainter>
0013 #include <QGraphicsPixmapItem>
0014 #include <QGraphicsSceneMouseEvent>
0015 #include <QTimer>
0016 
0017 namespace Kigo {
0018 
0019     const int dotPositions9[] = {2,2, 2,6, 6,2, 6,6, 4,4};
0020     const int dotPositions13[] = {3,3, 3,9, 9,3, 9,9, 6,6};
0021     const int dotPositions19[] = {3,3, 3,9, 3,15, 9,3, 9,9, 9,15, 15,3, 15,9, 15,15};
0022 
0023 GameScene::GameScene(Game *game, QObject *parent)
0024     : QGraphicsScene(parent)
0025     , m_game(game)
0026     , m_gamePopup()
0027     , m_showLabels(Preferences::showBoardLabels())
0028     , m_showHint(false)
0029     , m_hintTimer(new QTimer(this))
0030     , m_showMoveNumbers(Preferences::showMoveNumbers())
0031     , m_showPlacementMarker(true)
0032     , m_showTerritory(false)
0033     , m_boardRect()
0034     , m_mouseRect()
0035     , m_gridRect()
0036     , m_cellSize(0)
0037     , m_stonePixmapSize()
0038     , m_placementMarkerPixmapSize()
0039     , m_boardSize(Preferences::boardSize())
0040     , m_placementMarkerItem(nullptr)
0041     , m_stoneItems()
0042     , m_hintItems()
0043     , m_territoryItems()
0044 {
0045     connect(m_game, &Game::boardChanged, this, &GameScene::updateStoneItems);
0046     connect(m_game, &Game::boardSizeChanged, this, &GameScene::changeBoardSize);
0047     connect(m_game, &Game::currentPlayerChanged, this, &GameScene::hideHint);
0048     connect(ThemeRenderer::self(), &ThemeRenderer::themeChanged, this, &GameScene::themeChanged);
0049     connect(m_hintTimer, &QTimer::timeout, this, &GameScene::hideHint);
0050 
0051     m_gamePopup.setMessageTimeout(3000);
0052     m_gamePopup.setHideOnMouseClick(true);
0053     addItem(&m_gamePopup);
0054 }
0055 
0056 void GameScene::resizeScene(int width, int height)
0057 {
0058     setSceneRect(0, 0, width, height);
0059 
0060     int size = qMin(width, height) - 10;    // Add 10 pixel padding around the board
0061     m_boardRect.setRect(width / 2 - size / 2, height / 2 - size / 2, size, size);
0062     m_cellSize = m_boardRect.width() / (m_boardSize + 1);
0063 
0064     size = static_cast<int>(m_cellSize * (m_boardSize - 1));
0065     m_gridRect.setRect(width / 2 - size / 2, height / 2 - size / 2, size, size);
0066     m_mouseRect = m_gridRect.adjusted(-m_cellSize / 2, - m_cellSize / 2, m_cellSize / 2,   m_cellSize / 2);
0067 
0068     m_stonePixmapSize = QSize(static_cast<int>(m_cellSize * 1.4), static_cast<int>(m_cellSize * 1.4));
0069     updateStoneItems();                     // Resize means redraw of board items (stones)
0070     updateHintItems();                      // and move hint items
0071     updateTerritoryItems();
0072 
0073     if (m_placementMarkerItem) {            // Set the mouse/stone postion placementmarker
0074         removeItem(m_placementMarkerItem);
0075     }
0076     m_placementMarkerPixmapSize = QSize(static_cast<int>(m_cellSize / 4), static_cast<int>(m_cellSize / 4));
0077     m_placementMarkerItem = addPixmap(ThemeRenderer::self()->renderElement(ThemeRenderer::Element::PlacementMarker, m_placementMarkerPixmapSize));
0078     m_placementMarkerItem->setVisible(false);
0079     m_placementMarkerItem->setZValue(1);
0080 }
0081 
0082 void GameScene::showLabels(bool show)
0083 {
0084     m_showLabels = show;
0085     invalidate(m_boardRect, QGraphicsScene::BackgroundLayer);
0086 }
0087 
0088 void GameScene::showHint(bool show)
0089 {
0090     m_showHint = show;
0091     updateHintItems();
0092 
0093     if (show == true) {
0094         m_hintTimer->start(static_cast<int>(Preferences::hintVisibleTime() * 1000));
0095     }
0096 }
0097 
0098 void GameScene::showMoveNumbers(bool show)
0099 {
0100     m_showMoveNumbers = show;
0101     updateStoneItems();
0102 }
0103 
0104 void GameScene::showPlacementMarker(bool show)
0105 {
0106     m_showPlacementMarker = show;
0107 }
0108 
0109 void GameScene::showMessage(const QString &message, int msecs)
0110 {
0111     m_gamePopup.setMessageTimeout(msecs);
0112     if (message.isEmpty()) {
0113         m_gamePopup.forceHide();            // Now message hides the last one
0114     } else {
0115         m_gamePopup.showMessage(message, KGamePopupItem::BottomLeft, KGamePopupItem::ReplacePrevious);
0116     }
0117 }
0118 
0119 void GameScene::showTerritory(bool show)
0120 {
0121     m_showTerritory = show;
0122     updateTerritoryItems();
0123 }
0124 
0125 void GameScene::updateStoneItems()
0126 {
0127     const int halfStoneSize = m_stonePixmapSize.width() / 2;
0128 
0129     const Stone lastStone = (m_game->moves().size() > 0) ? m_game->lastMove().stone() : Stone::Invalid;
0130 
0131     for (QGraphicsPixmapItem *stoneItem : std::as_const(m_stoneItems)) {  // Clear all stone items
0132         removeItem(stoneItem);
0133     }
0134     m_stoneItems.clear();
0135 
0136     QGraphicsPixmapItem *stoneItem;
0137     const auto blackStones = m_game->stones(m_game->blackPlayer());
0138     for (const Stone &stone : blackStones) {
0139         ThemeRenderer::Element element = (stone == lastStone) ? ThemeRenderer::Element::BlackStoneLast : ThemeRenderer::Element::BlackStone;
0140         stoneItem = addPixmap(ThemeRenderer::self()->renderElement(element, m_stonePixmapSize));
0141         stoneItem->setZValue(2);
0142         const int xOff = stone.x() >= 'I' ? stone.x() - 'A' - 1 : stone.x() - 'A';
0143         stoneItem->setPos(QPointF(m_gridRect.x() + xOff * m_cellSize - halfStoneSize + 1,
0144                              m_gridRect.y() + (m_boardSize - stone.y()) * m_cellSize - halfStoneSize + 1));
0145         m_stoneItems.append(stoneItem);
0146     }
0147     const auto whiteStones = m_game->stones(m_game->whitePlayer());
0148     for (const Stone &stone : whiteStones) {
0149         ThemeRenderer::Element element = (stone == lastStone) ? ThemeRenderer::Element::WhiteStoneLast : ThemeRenderer::Element::WhiteStone;
0150         stoneItem = addPixmap(ThemeRenderer::self()->renderElement(element, m_stonePixmapSize));
0151         stoneItem->setZValue(2);
0152         const int xOff = stone.x() >= 'I' ? stone.x() - 'A' - 1 : stone.x() - 'A';
0153         stoneItem->setPos(QPointF(m_gridRect.x() + xOff * m_cellSize - halfStoneSize + 1,
0154                              m_gridRect.y() + (m_boardSize - stone.y()) * m_cellSize - halfStoneSize + 1));
0155         m_stoneItems.append(stoneItem);
0156     }
0157 
0158     if (m_showMoveNumbers) {
0159         int i = 0;
0160         const auto moves = m_game->moves();
0161         for (const Move &move : moves) {
0162             int xOff = move.stone().x() >= 'I' ? move.stone().x() - 'A' - 1 : move.stone().x() - 'A';
0163             QPointF pos = QPointF(m_gridRect.x() + xOff * m_cellSize,
0164                                   m_gridRect.y() + (m_boardSize - move.stone().y()) * m_cellSize);
0165 
0166             if (QGraphicsPixmapItem *item = static_cast<QGraphicsPixmapItem *>(itemAt(pos, QTransform()))) {
0167                 // We found an item in the scene that is in our move numbers, so we paint it's move number
0168                 // on top of the item and that's all.
0169                 //TODO: Check for existing move number to do special treatment
0170                 QPixmap pixmap = item->pixmap();
0171                 QPainter painter(&pixmap);
0172                 if (move.player()->isWhite()) {
0173                     painter.setPen(Qt::black);
0174                 } else if (move.player()->isBlack()) {
0175                     painter.setPen(Qt::white);
0176                 }
0177                 QFont f = painter.font();
0178                 f.setPointSizeF(halfStoneSize / 2);
0179                 painter.setFont(f);
0180                 painter.drawText(pixmap.rect(), Qt::AlignCenter, QString::number(i++));
0181                 item->setPixmap(pixmap);
0182             }
0183         }
0184     }
0185 }
0186 
0187 void GameScene::updateHintItems()
0188 {
0189     for (QGraphicsPixmapItem *item : std::as_const(m_hintItems)) {   // Old hint is invalid, remove it first
0190         removeItem(item);
0191     }
0192     m_hintItems.clear();
0193 
0194     if (m_showHint) {
0195         const int halfStoneSize = m_stonePixmapSize.width() / 2;
0196 
0197         const auto moves = m_game->bestMoves(m_game->currentPlayer());
0198         for (const Stone &move : moves) {
0199             QPixmap stonePixmap;
0200             if (m_game->currentPlayer().isWhite()) {
0201                 stonePixmap = ThemeRenderer::self()->renderElement(ThemeRenderer::Element::WhiteStoneTransparent, m_stonePixmapSize);
0202             } else if (m_game->currentPlayer().isBlack()) {
0203                 stonePixmap = ThemeRenderer::self()->renderElement(ThemeRenderer::Element::BlackStoneTransparent, m_stonePixmapSize);
0204             }
0205 
0206             QPainter painter(&stonePixmap);
0207             if (m_game->currentPlayer().isWhite()) {
0208                 painter.setPen(Qt::black);
0209             } else if (m_game->currentPlayer().isBlack()) {
0210                 painter.setPen(Qt::white);
0211             }
0212             QFont f = painter.font();
0213             f.setPointSizeF(m_cellSize / 4);
0214             painter.setFont(f);
0215             painter.drawText(stonePixmap.rect(), Qt::AlignCenter, QString::number(move.value()));
0216             painter.end();
0217 
0218             QGraphicsPixmapItem *item = addPixmap(stonePixmap);
0219             item->setZValue(4);
0220             const int xOff = move.x() >= 'I' ? move.x() - 'A' - 1 : move.x() - 'A';
0221             item->setPos(QPointF(m_gridRect.x() + xOff * m_cellSize - halfStoneSize + 2,
0222                                  m_gridRect.y() + (m_boardSize - move.y()) * m_cellSize - halfStoneSize + 2));
0223             m_hintItems.append(item);
0224         }
0225     }
0226 }
0227 
0228 void GameScene::updateTerritoryItems()
0229 {
0230     for(QGraphicsPixmapItem *item : std::as_const(m_territoryItems)) {  // Old territory is invalid, remove it first
0231         removeItem(item);
0232     }
0233     m_territoryItems.clear();
0234 
0235     if (m_showTerritory) {
0236         QPixmap stonePixmap;
0237         const int halfCellSize = m_cellSize / 2;
0238         //qCDebug(KIGO_LOG) << "Fetching territory from engine ...";
0239 
0240         stonePixmap = ThemeRenderer::self()->renderElement(ThemeRenderer::Element::WhiteTerritory, QSize(m_cellSize, m_cellSize));
0241         const auto whiteStones = m_game->finalStates(Game::FinalState::FinalWhiteTerritory);
0242         for (const Stone &stone : whiteStones) {
0243             QGraphicsPixmapItem *item = addPixmap(stonePixmap);
0244             item->setZValue(8);
0245             const int xOff = stone.x() >= 'I' ? stone.x() - 'A' - 1 : stone.x() - 'A';
0246             item->setPos(QPointF(m_gridRect.x() + xOff * m_cellSize - halfCellSize + 2,
0247                                  m_gridRect.y() + (m_boardSize - stone.y()) * m_cellSize - halfCellSize + 2));
0248             m_territoryItems.append(item);
0249         }
0250 
0251         stonePixmap = ThemeRenderer::self()->renderElement(ThemeRenderer::Element::BlackTerritory, QSize(m_cellSize, m_cellSize));
0252         const auto blackStones = m_game->finalStates(Game::FinalState::FinalBlackTerritory);
0253         for (const Stone &stone : blackStones) {
0254             QGraphicsPixmapItem *item = addPixmap(stonePixmap);
0255             item->setZValue(8);
0256             const int xOff = stone.x() >= 'I' ? stone.x() - 'A' - 1 : stone.x() - 'A';
0257             item->setPos(QPointF(m_gridRect.x() + xOff * m_cellSize - halfCellSize + 2,
0258                                  m_gridRect.y() + (m_boardSize - stone.y()) * m_cellSize - halfCellSize + 2));
0259             m_territoryItems.append(item);
0260         }
0261     }
0262 }
0263 
0264 void GameScene::changeBoardSize(int size)
0265 {
0266     m_boardSize = size;
0267     resizeScene(width(), height());
0268     invalidate(m_boardRect, QGraphicsScene::BackgroundLayer);
0269 }
0270 
0271 void GameScene::themeChanged()
0272 {
0273     invalidate(sceneRect(), QGraphicsScene::AllLayers);
0274 }
0275 
0276 void GameScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
0277 {
0278     QPixmap map;
0279 
0280     if (m_mouseRect.contains(event->scenePos())) {
0281         // Show a placement marker at the nearest board intersection
0282         // as a visual aid for the user.
0283         if (m_showPlacementMarker) {
0284             const int row = static_cast<int>((event->scenePos().x() - m_mouseRect.x()) / m_cellSize);
0285             const int col = static_cast<int>((event->scenePos().y() - m_mouseRect.y()) / m_cellSize);
0286 
0287             const int x = m_mouseRect.x() + row * m_cellSize + m_cellSize/2 - m_placementMarkerPixmapSize.width()/2;
0288             const int y = m_mouseRect.y() + col * m_cellSize + m_cellSize/2 - m_placementMarkerPixmapSize.height()/2;
0289 
0290             m_placementMarkerItem->setVisible(true);
0291             m_placementMarkerItem->setPos(x, y);
0292         } else {
0293             m_placementMarkerItem->setVisible(false);
0294         }
0295 
0296         if (m_game->currentPlayer().isHuman()) {
0297             if (m_game->currentPlayer().isWhite()) {
0298                 map = ThemeRenderer::self()->renderElement(ThemeRenderer::Element::WhiteStoneTransparent, m_stonePixmapSize);
0299             } else if (m_game->currentPlayer().isBlack()) {
0300                 map = ThemeRenderer::self()->renderElement(ThemeRenderer::Element::BlackStoneTransparent, m_stonePixmapSize);
0301             }
0302             Q_EMIT cursorPixmapChanged(map);
0303         }
0304     } else {
0305         m_placementMarkerItem->setVisible(false);
0306         Q_EMIT cursorPixmapChanged(map);
0307     }
0308 }
0309 
0310 void GameScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
0311 {
0312     if (m_mouseRect.contains(event->scenePos())) {
0313         int row = static_cast<int>((event->scenePos().x() - m_mouseRect.x()) / m_cellSize);
0314         const int col = static_cast<int>((event->scenePos().y() - m_mouseRect.y()) / m_cellSize);
0315         if (row < 0 || row >= m_boardSize || col < 0 || col >= m_boardSize) {
0316             return;
0317         }
0318 
0319         // Convert to Go board coordinates and try to play the move. GnuGo coordinates don't use the 'I'
0320         // column, if the row is bigger than 'I', we have to add 1 to jump over that.
0321         if (row >= 8) {
0322             row += 1;
0323         }
0324         m_game->playMove(m_game->currentPlayer(), Stone('A' + row, m_boardSize - col));
0325     }
0326 }
0327 
0328 void GameScene::drawBackground(QPainter *painter, const QRectF &)
0329 {
0330     ThemeRenderer::self()->renderElement(ThemeRenderer::Element::Background, painter, sceneRect());
0331     ThemeRenderer::self()->renderElement(ThemeRenderer::Element::Board, painter, m_boardRect);
0332 
0333     const int width = m_cellSize / 16;
0334     const QColor color = QColor(20, 30, 20);
0335     painter->setPen(QPen(color, width));
0336 
0337     for (int i = 0; i < m_boardSize; i++) {
0338         const qreal offset = i * m_cellSize;
0339         painter->drawLine(QPointF(m_gridRect.left(),  m_gridRect.top() + offset),
0340                           QPointF(m_gridRect.right(), m_gridRect.top() + offset));
0341         painter->drawLine(QPointF(m_gridRect.left() + offset, m_gridRect.top()),
0342                           QPointF(m_gridRect.left() + offset, m_gridRect.bottom()));
0343 
0344         if (m_showLabels) {
0345             QChar c('A' + i);
0346             // GnuGo does not use the 'I' column (for whatever strange reason), we have to skip that too
0347             if (i >= 8) {
0348                 c = QChar('A' + i + 1);
0349             }
0350 
0351             const QString n = QString::number(m_boardSize - i);
0352             QFont f = painter->font();
0353             f.setPointSizeF(m_cellSize / 4);
0354             painter->setFont(f);
0355             const QFontMetrics fm = painter->fontMetrics();
0356 
0357             // Draw vertical numbers for board coordinates
0358             const qreal yVert = m_gridRect.top() + offset + fm.descent();
0359             painter->drawText(QPointF(m_gridRect.left() - m_cellSize + 2, yVert), n);
0360             painter->drawText(QPointF(m_gridRect.right() + m_cellSize - fm.boundingRect(n).width() - 3, yVert), n);
0361 
0362             // Draw horizontal characters for board coordinates
0363             const qreal xHor = m_gridRect.left() + offset - fm.boundingRect(c).width() / 2;
0364             painter->drawText(QPointF(xHor, m_gridRect.top() - m_cellSize + fm.ascent() + 2), QString(c));
0365             painter->drawText(QPointF(xHor, m_gridRect.bottom() + m_cellSize - 3), QString(c));
0366         }
0367     }
0368 
0369     // Draw thicker connections on some defined points.
0370     // This is extremely helpful to orientate oneself especially on the 19x19 board.
0371     const int radius = m_cellSize / 10;
0372     painter->setBrush(color);
0373     painter->setRenderHint(QPainter::Antialiasing);
0374 
0375     // in order to center properly we need to take line width into account
0376     // if the line has an odd width, we shift 1/5 pixel
0377     const qreal centerOffset = (width % 2) ? 0.5 : 0.0;
0378 
0379     // only do this for the common board sizes,
0380     // other sizes are a bit odd anyway
0381     int numDots = 0;
0382     const int *dotPositions;
0383 
0384     if (m_boardSize == 9) {
0385         numDots = 5;
0386         dotPositions = dotPositions9;
0387     } else if (m_boardSize == 13) {
0388         numDots = 5;
0389         dotPositions = dotPositions13;
0390     } else if (m_boardSize == 19) {
0391         numDots = 9;
0392         dotPositions = dotPositions19;
0393     }
0394 
0395     for (int i = 0; i < numDots; ++i) {
0396         painter->drawEllipse(
0397             QPointF(m_gridRect.left() + m_cellSize*dotPositions[i*2] + centerOffset,
0398                     m_gridRect.top() + m_cellSize*dotPositions[i*2+1] + centerOffset),
0399             radius, radius);
0400     }
0401 }
0402 
0403 } // End of namespace Kigo
0404 
0405 #include "moc_gamescene.cpp"