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"