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