1 /***************************************************************************
2 * (C) 2008-2010 Michal Rudolf <mrudolf@kdewebdev.org> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 ***************************************************************************/
9
10 #include "settings.h"
11 #include "analysis.h"
12 #include "analysiswidget.h"
13 #include "board.h"
14 #include "databaseinfo.h"
15 #include "enginelist.h"
16 #include "messagedialog.h"
17 #include "move.h"
18 #include "movedata.h"
19 #include "tablebase.h"
20 #include "polyglotdatabase.h"
21
22 #include <QMutexLocker>
23 #include <algorithm>
24
25 using namespace chessx;
26
27 #if defined(_MSC_VER) && defined(_DEBUG)
28 #define DEBUG_NEW new( _NORMAL_BLOCK, __FILE__, __LINE__ )
29 #define new DEBUG_NEW
30 #endif // _MSC_VER
31
AnalysisWidget(QWidget * parent)32 AnalysisWidget::AnalysisWidget(QWidget *parent)
33 : QWidget(parent),
34 m_moveTime(0),
35 m_bUciNewGame(true),
36 m_onHold(false),
37 m_gameMode(false)
38 {
39 ui.setupUi(this);
40 connect(ui.engineList, SIGNAL(activated(int)), SLOT(toggleAnalysis()));
41 connect(ui.bookList, SIGNAL(currentIndexChanged(int)), SLOT(bookActivated(int)));
42 connect(ui.analyzeButton, SIGNAL(clicked(bool)), SLOT(toggleAnalysis()));
43
44 connect(ui.variationText, SIGNAL(anchorClicked(QUrl)),
45 SLOT(slotLinkClicked(QUrl)));
46 connect(ui.vpcount, SIGNAL(valueChanged(int)), SLOT(slotMpvChanged(int)));
47 connect(ui.btPin, SIGNAL(clicked(bool)), SLOT(slotPinChanged(bool)));
48 ui.analyzeButton->setFixedHeight(ui.engineList->sizeHint().height());
49
50 m_tablebase = new OnlineTablebase;
51 connect(m_tablebase, SIGNAL(bestMove(QList<Move>, int)), this, SLOT(showTablebaseMove(QList<Move>, int)));
52 }
53
~AnalysisWidget()54 AnalysisWidget::~AnalysisWidget()
55 {
56 stopEngine();
57 delete m_tablebase;
58 }
59
startEngine()60 void AnalysisWidget::startEngine()
61 {
62 updateBookMoves();
63
64 int index = ui.engineList->currentIndex();
65 stopEngine();
66 m_onHold = false;
67 if(index != -1)
68 {
69 if(parentWidget() && !parentWidget()->isVisible())
70 {
71 parentWidget()->show();
72 }
73 ui.variationText->clear();
74 m_engine = EngineX::newEngine(index);
75 ui.vpcount->setEnabled(m_engine->providesMvp());
76 ui.label->setEnabled(m_engine->providesMvp());
77 if(!m_engine->providesMvp())
78 {
79 ui.vpcount->setValue(1);
80 }
81
82 connect(m_engine, SIGNAL(activated()), SLOT(engineActivated()));
83 connect(m_engine, SIGNAL(error(QProcess::ProcessError)), SLOT(engineError(QProcess::ProcessError)));
84 connect(m_engine, SIGNAL(deactivated()), SLOT(engineDeactivated()));
85 connect(m_engine, SIGNAL(analysisUpdated(const Analysis&)),
86 SLOT(showAnalysis(Analysis)));
87 m_engine->setMoveTime(m_moveTime);
88 m_engine->activate();
89 QString key = QString("/") + objectName() + "/Engine";
90 AppSettings->setValue(key, ui.engineList->itemText(index));
91 }
92 }
93
stopEngine()94 void AnalysisWidget::stopEngine()
95 {
96 engineDeactivated();
97 if(m_engine)
98 {
99 m_engine->deactivate();
100 m_engine->deleteLater();
101 m_engine.clear();
102 }
103 }
104
slotVisibilityChanged(bool visible)105 void AnalysisWidget::slotVisibilityChanged(bool visible)
106 {
107 if(isEngineRunning() && !visible && !parentWidget()->isVisible())
108 {
109 stopEngine();
110 }
111 }
112
isEngineRunning() const113 bool AnalysisWidget::isEngineRunning() const
114 {
115 return m_engine && ui.analyzeButton->isChecked();
116 }
117
isEngineConfigured() const118 bool AnalysisWidget::isEngineConfigured() const
119 {
120 int index = ui.engineList->currentIndex();
121 return (index != -1);
122 }
123
engineActivated()124 void AnalysisWidget::engineActivated()
125 {
126 ui.analyzeButton->setChecked(true);
127 ui.analyzeButton->setText(tr("Stop"));
128 m_analyses.clear();
129 updateBookMoves(); // Delay this to here so that engine process is up
130 if (!sendBookMove())
131 {
132 m_engine->setStartPos(m_startPos);
133 m_engine->startAnalysis(m_board, ui.vpcount->value(), m_moveTime, true, m_line);
134 m_lastEngineStart.start();
135 m_bUciNewGame = false;
136 }
137 }
138
engineError(QProcess::ProcessError e)139 void AnalysisWidget::engineError(QProcess::ProcessError e)
140 {
141 MessageDialog::warning(tr("There was an error (%1) running engine <b>%2</b>.")
142 .arg(e)
143 .arg(ui.engineList->currentText()));
144 stopEngine();
145 }
146
engineDeactivated()147 void AnalysisWidget::engineDeactivated()
148 {
149 ui.analyzeButton->setChecked(false);
150 ui.analyzeButton->setText(tr("Analyze"));
151 }
152
toggleAnalysis()153 void AnalysisWidget::toggleAnalysis()
154 {
155 if(!isAnalysisEnabled())
156 {
157 stopEngine();
158 }
159 else
160 {
161 startEngine();
162 }
163 }
164
bookActivated(int index)165 void AnalysisWidget::bookActivated(int index)
166 {
167 m_pBookDatabase.clear();
168 emit signalSourceChanged(index>=0 ? ui.bookList->itemData(index).toString() : "");
169 updateBookMoves();
170 updateAnalysis();
171 }
172
slotPinChanged(bool pinned)173 void AnalysisWidget::slotPinChanged(bool pinned)
174 {
175 if (!pinned && isAnalysisEnabled())
176 {
177 if (m_board != m_NextBoard)
178 {
179 setPosition(m_NextBoard, m_NextLine);
180 }
181 }
182 else
183 {
184 if(isEngineRunning())
185 {
186 m_engine->setMoveTime(0);
187 }
188 }
189 }
190
slotReconfigure()191 void AnalysisWidget::slotReconfigure()
192 {
193 QString oldEngineName = ui.engineList->currentText();
194 if(oldEngineName.isEmpty())
195 {
196 QString key = QString("/") + objectName() + "/Engine";
197 oldEngineName = AppSettings->getValue(key).toString();
198 }
199
200 EngineList enginesList;
201 enginesList.restore();
202 QStringList names = enginesList.names();
203 ui.engineList->clear();
204 ui.engineList->insertItems(0, names);
205 int index = names.indexOf(oldEngineName);
206 if(index != -1)
207 {
208 ui.engineList->setCurrentIndex(index);
209 }
210 else
211 {
212 ui.engineList->setCurrentIndex(0);
213 stopEngine();
214 }
215
216 int fontSize = AppSettings->getValue("/General/ListFontSize").toInt();
217 fontSize = std::max(fontSize, 8);
218 QFont f = ui.variationText->font();
219 f.setPointSize(fontSize);
220 setFont(f);
221 ui.variationText->setFont(f);
222 }
223
saveConfig()224 void AnalysisWidget::saveConfig()
225 {
226 AppSettings->beginGroup(objectName());
227 AppSettings->setValue("LastBook", ui.bookList->currentText());
228 AppSettings->endGroup();
229 }
230
restoreBook()231 void AnalysisWidget::restoreBook()
232 {
233 AppSettings->beginGroup(objectName());
234 QString lastBook = AppSettings->value("LastBook", "").toString();
235 AppSettings->endGroup();
236 int index = ui.bookList->findText(lastBook);
237 if (index >= 0)
238 {
239 ui.bookList->setCurrentIndex(index);
240 }
241 }
242
slotUpdateBooks(QStringList files)243 void AnalysisWidget::slotUpdateBooks(QStringList files)
244 {
245 QString current = ui.bookList->currentText();
246 ui.bookList->clear();
247 ui.bookList->addItem("-",QVariant(QString()));
248 foreach(QString filename, files)
249 {
250 QFileInfo fi(filename);
251 QString baseName = fi.baseName();
252 if (DatabaseInfo::IsBook(filename))
253 {
254 ui.bookList->addItem(baseName,QVariant(filename));
255 }
256 }
257 int index = ui.bookList->findText(current);
258 if (index < 0) index = 0;
259 ui.bookList->setCurrentIndex(index);
260 }
261
setGameMode(bool gameMode)262 void AnalysisWidget::setGameMode(bool gameMode)
263 {
264 m_gameMode = gameMode;
265 if (!m_gameMode)
266 {
267 updateBookMoves();
268 }
269 }
270
showAnalysis(Analysis analysis)271 void AnalysisWidget::showAnalysis(Analysis analysis)
272 {
273 Move m;
274 if (m_analyses.count() && m_analyses[0].variation().count())
275 {
276 m = m_analyses[0].variation().at(0);
277 }
278 int elapsed = m_lastEngineStart.elapsed();
279 int mpv = analysis.mpv() - 1;
280 bool bestMove = analysis.bestMove();
281 if (bestMove)
282 {
283 if (m_analyses.count() && m_analyses.last().bestMove())
284 {
285 m_analyses.removeLast();
286 }
287 m_analyses.append(analysis);
288 }
289 else if(mpv < 0 || mpv > m_analyses.count() || mpv >= ui.vpcount->value())
290 {
291 return;
292 }
293 else if(mpv == m_analyses.count())
294 {
295 m_analyses.append(analysis);
296 }
297 else
298 {
299 m_analyses[mpv] = analysis;
300 }
301 updateComplexity();
302 updateAnalysis();
303 Analysis c = analysis;
304 if (bestMove && analysis.variation().count())
305 {
306 foreach (Analysis a, m_analyses)
307 {
308 if (a.variation().count())
309 {
310 if (a.variation().at(0)==analysis.variation().at(0))
311 {
312 c = a;
313 c.setBestMove(true);
314 break;
315 }
316 }
317 }
318 }
319
320 c.setTb(m_tb);
321 c.setScoreTb(m_score_tb);
322 if (bestMove)
323 {
324 analysis.setElapsedTimeMS(elapsed);
325 emit receivedBestMove(c);
326 emit currentBestMove(c);
327 }
328 else if (c.getEndOfGame())
329 {
330 emit receivedBestMove(c);
331 }
332 if (c.variation().count() && (m!=c.variation().at(0)))
333 {
334 emit currentBestMove(c);
335 }
336 }
337
setPosition(const BoardX & board,QString line)338 void AnalysisWidget::setPosition(const BoardX& board, QString line)
339 {
340 if (ui.btPin->isChecked())
341 {
342 m_NextBoard = board;
343 m_NextLine = line;
344 return;
345 }
346 if(m_board != board)
347 {
348 m_board = board;
349 m_NextBoard = board;
350 m_NextLine = line;
351 m_line = line;
352 m_analyses.clear();
353 m_tablebase->abortLookup();
354 m_tablebaseEvaluation.clear();
355 m_tablebaseMove.clear();
356 m_tb.setNullMove();
357 m_score_tb = 0;
358
359 updateBookMoves();
360
361 if(AppSettings->getValue("/General/onlineTablebases").toBool())
362 {
363 if (!(m_board.isStalemate() || m_board.isCheckmate() || m_board.chess960()))
364 {
365 if(objectName() == "Analysis")
366 {
367 m_tbBoard = m_board;
368 m_tablebase->getBestMove(m_board.toFen());
369 }
370 }
371 }
372
373 m_lastDepthAdded = 0;
374 updateAnalysis();
375 if (m_engine && m_engine->isActive() && !onHold() && !sendBookMove())
376 {
377 if (m_bUciNewGame)
378 {
379 m_engine->setStartPos(m_startPos);
380 }
381 m_engine->startAnalysis(m_board, ui.vpcount->value(), m_moveTime, m_bUciNewGame, line);
382 m_lastEngineStart.start();
383 m_bUciNewGame = false;
384 }
385 }
386 }
387
sendBookMove()388 bool AnalysisWidget::sendBookMove()
389 {
390 if (moveList.count() && m_moveTime.allowBook)
391 {
392 QTimer::singleShot(500, this, SLOT(sendBookMoveTimeout()));
393 return true;
394 }
395 return false;
396 }
397
sendBookMoveTimeout()398 void AnalysisWidget::sendBookMoveTimeout()
399 {
400 if (moveList.count() && m_moveTime.allowBook)
401 {
402 Analysis analysis;
403 analysis.setElapsedTimeMS(0);
404 Move::List moves;
405 int index = 0;
406 if (m_moveTime.bookMove == 1)
407 {
408 index = rand() % moveList.count();
409 }
410 else if (m_moveTime.bookMove == 2)
411 {
412 index = moveList.count() - 1;
413 int randomPos = rand() % games;
414 for (int i=0; i<moveList.count();++i)
415 {
416 randomPos -= moveList.at(i).results.count();
417 if (randomPos<0)
418 {
419 index = i;
420 break;
421 }
422 }
423 }
424 moves.append(moveList.at(index).move);
425 analysis.setVariation(moves);
426 analysis.setBestMove(true);
427 analysis.setBookMove(true);
428 analysis.setTb(m_tb);
429 analysis.setScoreTb(m_score_tb);
430 emit receivedBestMove(analysis);
431 }
432 }
433
slotLinkClicked(const QUrl & url)434 void AnalysisWidget::slotLinkClicked(const QUrl& url)
435 {
436 if (m_NextBoard != m_board)
437 {
438 return; // Pinned and user moved somewhere else
439 }
440 int mpv = url.toString().toInt() - 1;
441 if(mpv >= 0 && mpv < m_analyses.count())
442 {
443 emit addVariation(m_analyses[mpv], "");
444 }
445 else if(mpv == -1)
446 {
447 emit addVariation(m_tablebaseMove);
448 }
449 else
450 {
451 mpv = (-mpv) - 2;
452 if(mpv < m_analyses.count())
453 {
454 if (!m_analyses[mpv].variation().isEmpty())
455 {
456 emit addVariation(m_analyses[mpv].variation().at(0).toAlgebraic());
457 }
458 }
459 }
460 }
461
setMoveTime(EngineParameter mt)462 void AnalysisWidget::setMoveTime(EngineParameter mt)
463 {
464 m_moveTime = mt;
465 if(isEngineRunning() && !ui.btPin->isChecked())
466 {
467 m_engine->setMoveTime(mt);
468 }
469 }
470
setMoveTime(int n)471 void AnalysisWidget::setMoveTime(int n)
472 {
473 EngineParameter par(n);
474 par.analysisMode = true;
475 setMoveTime(par);
476 }
477
setDepth(int n)478 void AnalysisWidget::setDepth(int n)
479 {
480 m_moveTime.searchDepth = n;
481 m_moveTime.analysisMode = true;
482 setMoveTime(m_moveTime);
483 }
484
slotMpvChanged(int mpv)485 void AnalysisWidget::slotMpvChanged(int mpv)
486 {
487 if(isEngineRunning())
488 {
489 while(m_analyses.count() > mpv)
490 {
491 m_analyses.removeLast();
492 }
493 m_engine->setMpv(mpv);
494 }
495 }
496
isAnalysisEnabled() const497 bool AnalysisWidget::isAnalysisEnabled() const
498 {
499 if(!parentWidget())
500 {
501 return false;
502 }
503 if(!parentWidget()->isVisible() || !ui.analyzeButton->isChecked())
504 {
505 return false;
506 }
507 return true;
508 }
509
showTablebaseMove(QList<Move> bestMoves,int score)510 void AnalysisWidget::showTablebaseMove(QList<Move> bestMoves, int score)
511 {
512 if (m_tbBoard == m_board)
513 {
514 bool first = true;
515 QStringList also;
516 foreach(Move move, bestMoves)
517 {
518 if (first)
519 {
520 first = false;
521 QString result;
522
523 bool dtz = false;
524 if (abs(score) & 0x800)
525 {
526 dtz = true;
527 }
528 m_score_tb = 0;
529 if(score == 0)
530 {
531 result = tr("Draw");
532 }
533 else
534 {
535 if (!dtz)
536 {
537 if((score < 0) == (m_board.toMove() == Black))
538 {
539 result = tr("White wins in %n moves", "", qAbs(score));
540 m_score_tb = 1;
541 }
542 else
543 {
544 result = tr("Black wins in %n moves", "", qAbs(score));
545 m_score_tb = -1;
546 }
547 }
548 else
549 {
550 if((score < 0) == (m_board.toMove() == Black))
551 {
552 result = tr("White wins");
553 m_score_tb = 1;
554 }
555 else
556 {
557 result = tr("Black wins");
558 m_score_tb = -1;
559 }
560 }
561 }
562 Move move1 = m_board.prepareMove(move.from(), move.to());
563 if(move.isPromotion())
564 {
565 move1.setPromoted(pieceType(move.promotedPiece()));
566 }
567 m_tablebaseEvaluation = QString("%1 - %2").arg(m_board.moveToFullSan(move1,true), result);
568 m_tablebaseMove = m_board.moveToFullSan(move1);
569 m_tb = move1;
570 m_lastDepthAdded = 0;
571 }
572 else
573 {
574 Move move1 = m_board.prepareMove(move.from(), move.to());
575 if(move.isPromotion())
576 {
577 move1.setPromoted(pieceType(move.promotedPiece()));
578 }
579 also.append(m_board.moveToFullSan(move1,true));
580 }
581 }
582 if (!also.isEmpty())
583 {
584 m_tablebaseEvaluation.append(QString(" === %1").arg(also.join(" ")));
585 }
586 updateAnalysis();
587 }
588 }
589
updateAnalysis()590 void AnalysisWidget::updateAnalysis()
591 {
592 QString text;
593 if (ui.btPin->isChecked())
594 {
595 unsigned int moveNr = m_board.moveNumber();
596 text = tr("Analysis pinned to move %1").arg(moveNr) + "<br>";
597 }
598 foreach(Analysis a, m_analyses)
599 {
600 QString s = a.toString(m_board);
601 if (!s.isEmpty()) text.append(s + "<br>");
602 }
603 if(!m_tablebaseEvaluation.isEmpty())
604 {
605 text.append(QString("<a href=\"0\" title=\"%1\">[+]</a> <b>%2:</b> ").arg(tr("Click to add move to game"), tr("Tablebase")) + m_tablebaseEvaluation);
606 }
607 if (m_lastDepthAdded == 17)
608 {
609 text.append(QString("<br><b>%1:</b> %2/%3<br>").arg(tr("Complexity")).arg(m_complexity).arg(m_complexity2));
610 }
611 else if (m_lastDepthAdded >= 12)
612 {
613 text.append(tr("<br><b>Complexity:</b> %1<br>").arg(m_complexity));
614 }
615
616 if (moveList.count())
617 {
618 QString bookLine = tr("<i>Book:</i>");
619 foreach (MoveData move, moveList)
620 {
621 bookLine.append(" ");
622 bookLine.append(move.localsan);
623 }
624 text.append(bookLine);
625 }
626 ui.variationText->setText(text);
627 }
628
updateComplexity()629 void AnalysisWidget::updateComplexity()
630 {
631 if (!m_analyses[0].variation().isEmpty())
632 {
633 Move bestMove = m_analyses[0].variation().first();
634 if (m_analyses[0].depth() == 2)
635 {
636 m_lastBestMove = bestMove;
637 m_complexity = 0.0;
638 m_lastDepthAdded = 2;
639 }
640 if ((m_analyses.size() >= 2) && !m_analyses[1].bestMove())
641 {
642 if ((m_lastDepthAdded+1 == m_analyses[0].depth()) && !m_analyses[0].isMate())
643 {
644 if (m_analyses[0].depth() <= 12)
645 {
646 m_lastDepthAdded = m_analyses[0].depth();
647 if (m_lastBestMove != bestMove)
648 {
649 if (abs(m_analyses[0].score()) < 200)
650 {
651 m_lastBestMove = bestMove;
652 m_complexity += fabs(double(m_analyses[0].score()-m_analyses[1].score()))/100.0;
653 }
654 }
655 m_complexity2 = m_complexity;
656 }
657 else if ((m_analyses[0].depth() > 12) && (m_analyses[0].depth() <= 17))
658 {
659 m_lastDepthAdded = m_analyses[0].depth();
660 if (m_lastBestMove != bestMove)
661 {
662 if (abs(m_analyses[0].score()) < 200)
663 {
664 m_lastBestMove = bestMove;
665 m_complexity2 += fabs(double(m_analyses[0].score()-m_analyses[1].score()))/100.0;
666 }
667 }
668 }
669 }
670 }
671 }
672 else
673 {
674 m_lastDepthAdded = 0;
675 }
676 }
677
getMainLine() const678 Analysis AnalysisWidget::getMainLine() const
679 {
680 Analysis a;
681 if(!m_analyses.isEmpty())
682 {
683 a = m_analyses.first();
684 }
685 return a;
686 }
687
hasMainLine() const688 bool AnalysisWidget::hasMainLine() const
689 {
690 return (!m_analyses.isEmpty());
691 }
692
displayName() const693 QString AnalysisWidget::displayName() const
694 {
695 return ui.engineList->currentText();
696 }
697
unPin()698 void AnalysisWidget::unPin()
699 {
700 if (ui.btPin->isChecked())
701 {
702 ui.btPin->setChecked(false);
703 }
704 }
705
slotUciNewGame(const BoardX & b)706 void AnalysisWidget::slotUciNewGame(const BoardX& b)
707 {
708 m_bUciNewGame = true;
709 m_startPos = b;
710 }
711
onHold() const712 bool AnalysisWidget::onHold() const
713 {
714 return m_onHold;
715 }
716
setOnHold(bool onHold)717 void AnalysisWidget::setOnHold(bool onHold)
718 {
719 m_onHold = onHold;
720 if (!onHold && (!m_engine || !m_engine->isActive()))
721 {
722 startEngine();
723 }
724 }
725
engineName() const726 QString AnalysisWidget::engineName() const
727 {
728 return ui.engineList->currentText();
729 }
730
updateBookFile(Database * pgdb)731 void AnalysisWidget::updateBookFile(Database *pgdb)
732 {
733 m_pBookDatabase = pgdb;
734 }
735
updateBookMoves()736 void AnalysisWidget::updateBookMoves()
737 {
738 QMap<Move, MoveData> moves;
739 games = 0;
740
741 if (m_pBookDatabase && !m_gameMode)
742 {
743 games = m_pBookDatabase->getMoveMapForBoard(m_board, moves);
744 }
745
746 moveList.clear();
747 for(QMap<Move, MoveData>::iterator it = moves.begin(); it != moves.end(); ++it)
748 {
749 moveList.append(it.value());
750 }
751
752 std::sort(moveList.begin(), moveList.end());
753 }
754