Skip to content

Commit

Permalink
Re #211 New tool window shows centipawn loss graph
Browse files Browse the repository at this point in the history
There were two gametoolbars. One GameToolBar in
src/gui/gametoolbar.{cpp,h}, and a certain MainWindow::gameToolBar. The
former was a toolbar attached to the notation window showing a material
graph and some clocks shown when "game time" was selected. The latter is
a toolbar at the top of the main window showing "game"-related actions
(such as flipping the board, etc.). The latter was not affected in this
commit.

Limit evaluations to +/- 10.0
  • Loading branch information
limitedAtonement committed Aug 19, 2024
1 parent 1d56792 commit 1ab9ae8
Show file tree
Hide file tree
Showing 14 changed files with 482 additions and 140 deletions.
6 changes: 4 additions & 2 deletions chessx.pro
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ HEADERS += src/database/board.h \
src/database/filteroperator.h \
src/database/filtersearch.h \
src/database/gamecursor.h \
src/database/gameevaluation.h \
src/database/gameid.h \
src/database/gameundocommand.h \
src/database/gamex.h \
Expand Down Expand Up @@ -308,7 +309,7 @@ HEADERS += src/database/board.h \
src/gui/gamelist.h \
src/gui/gamelistsortmodel.h \
src/gui/gamenotationwidget.h \
src/gui/gametoolbar.h \
src/gui/centipawngraph.h \
src/gui/gamewindow.h \
src/gui/helpbrowser.h \
src/gui/helpbrowsershell.h \
Expand Down Expand Up @@ -387,6 +388,7 @@ SOURCES += \
src/database/filtermodel.cpp \
src/database/filtersearch.cpp \
src/database/gamecursor.cpp \
src/database/gameevaluation.cpp \
src/database/gamex.cpp \
src/database/historylist.cpp \
src/database/index.cpp \
Expand Down Expand Up @@ -477,7 +479,7 @@ SOURCES += \
src/gui/gamelist.cpp \
src/gui/gamelistsortmodel.cpp \
src/gui/gamenotationwidget.cpp \
src/gui/gametoolbar.cpp \
src/gui/centipawngraph.cpp \
src/gui/gamewindow.cpp \
src/gui/helpbrowser.cpp \
src/gui/helpbrowsershell.cpp \
Expand Down
6 changes: 4 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ add_library(database-core STATIC
database/gameid.h
database/gamecursor.cpp
database/gamecursor.h
database/gameevaluation.cpp
database/gameevaluation.h
database/gamex.cpp
database/gamex.h
database/index.cpp
Expand Down Expand Up @@ -339,6 +341,8 @@ add_library(gui STATIC
gui/boardviewex.cpp
gui/boardviewex.h
gui/boardviewex.ui
gui/centipawngraph.cpp
gui/centipawngraph.h
gui/chartwidget.cpp
gui/chartwidget.h
gui/chessbrowser.cpp
Expand Down Expand Up @@ -380,8 +384,6 @@ add_library(gui STATIC
gui/gamelistsortmodel.h
gui/gamenotationwidget.cpp
gui/gamenotationwidget.h
gui/gametoolbar.cpp
gui/gametoolbar.h
gui/gamewindow.cpp
gui/gamewindow.h
gui/gamewindow.ui
Expand Down
2 changes: 2 additions & 0 deletions src/database/analysis.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class Analysis
/** Moves to mate. */
/** Convert analysis to formatted text. */
QString toString(const BoardX& board, bool hiddenLine=false) const;
/** If bestMove is true, then only the bestMove, and variation members
* are valid. */
void setBestMove(bool bestMove);
bool bestMove() const;

Expand Down
188 changes: 188 additions & 0 deletions src/database/gameevaluation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#include <QFile>
#include "enginex.h"
#include "gamex.h"
#include "gameevaluation.h"

GameEvaluation::GameEvaluation(int engineIndex, int msPerMove, GameX game) noexcept
: engineIndex{engineIndex}
, msPerMove{msPerMove}
, game{game}
, targetThreadCount{std::max(1, QThread::idealThreadCount())}
, moveNumbers{0}
{
if (targetThreadCount > 4)
// We'll leave one logical core idle if we can afford it.
--targetThreadCount;
connect(&timer, &QTimer::timeout, this, &GameEvaluation::update);
}

void GameEvaluation::start()
{
if (running)
{
throw std::logic_error{"Game evaluation already running"};
}
running = true;
game.moveToStart();
workers.clear();
timer.stop();
timer.start(std::chrono::milliseconds{100});
// We place the first worker for the starting position here.
workers.emplace_back(engineIndex, game.startingBoard(), game.board(), msPerMove, moveNumbers++);
}

void GameEvaluation::update() noexcept
{
std::unordered_map<int, double> evaluations;
for (std::list<GameEvaluationWorker>::iterator i{workers.begin()}; i != workers.end(); ++i)
{
i->update();
evaluations.emplace(i->moveNumber, i->getLastScore());
if (!i->isRunning())
{
// Erase *after* getting the value because erasing will destroy the worker
i = workers.erase(i);
--i;
}
}
emit evaluationChanged(evaluations);
while (static_cast<int>(workers.size()) < targetThreadCount)
{
if (!game.forward())
{
break;
}
try
{
int tempNum {moveNumbers++};
workers.emplace_back(engineIndex, game.startingBoard(), game.board(), msPerMove, tempNum);
}
catch (...)
{
// Destroy any workers that have started.
workers.clear();
break;
}
}
if (!workers.size())
{
timer.stop();
emit evaluationComplete();
running = false;
}
}

void GameEvaluation::stop() noexcept
{
workers.clear();
timer.stop();
emit evaluationComplete();
}

GameEvaluation::~GameEvaluation() noexcept
{
workers.clear();
timer.stop();
}

GameEvaluationWorker::GameEvaluationWorker(int engineIndex, BoardX const & startPosition, BoardX const & currentPosition,
int msPerMove, int moveNumber)
: moveNumber{moveNumber}
, currentPosition{currentPosition}
, msPerMove{msPerMove}
, engine{EngineX::newEngine(engineIndex)}
{
if (!engine)
{
throw std::runtime_error{"Failed to instantiate engine"};
}
if (!engine->m_mapOptionValues.contains("Threads"))
{
throw std::runtime_error{"Could not set engine threads to 1."};
}
engine->m_mapOptionValues["Threads"] = 1;
engine->setStartPos(startPosition);
connect(engine, &EngineX::activated, this, &GameEvaluationWorker::engineActivated);
connect(engine, &EngineX::analysisStarted, this, &GameEvaluationWorker::engineAnalysisStarted);
connect(engine, &EngineX::analysisUpdated, this, &GameEvaluationWorker::engineAnalysisUpdated);
engine->activate();
running = true;
}

GameEvaluationWorker::~GameEvaluationWorker() noexcept
{
if (!engine)
{
return;
}
try
{
engine->deactivate();
delete engine;
}
catch (...)
{
// I don't think we can do anything about this exception.
}
}

void GameEvaluationWorker::engineActivated()
{
EngineParameter parameters {msPerMove};
engine->startAnalysis(currentPosition, 1, parameters, false, "");
}

void GameEvaluationWorker::engineAnalysisStarted()
{
startTimestamp = QDateTime::currentDateTimeUtc();
}

void GameEvaluationWorker::engineAnalysisUpdated(Analysis const & analysis)
{
if (analysis.bestMove())
{
// When the engine reports a best move, no score is reported, so we skip it
return;
}
if (analysis.isMate())
{
lastScore = static_cast<double>(10);
if (std::signbit(analysis.score()))
lastScore *= -1;
// If it's black's turn and black is winning, analysis.score() returns a
// positive number in a "mating" condition. We need a score from white's
// perspective.
bool blacksTurn {static_cast<bool>(analysis.variation().size() % 2)};
if (!blacksTurn)
lastScore *= -1;
}
else
{
lastScore = analysis.fscore();
}
}

double GameEvaluationWorker::getLastScore() const noexcept
{
return lastScore;
}

bool GameEvaluationWorker::isRunning() const noexcept
{
return running;
}

void GameEvaluationWorker::update() noexcept
{
if (!startTimestamp)
{
// Maybe have a timeout and kill the engine if analysis still hasn't started?
return;
}
if (startTimestamp->msecsTo(QDateTime::currentDateTimeUtc()) < msPerMove)
return;
engine->deactivate();
running = false;
// We'll let the destructor delete the engine since this is called during computation when
// processing time is more premium than later.
}
75 changes: 75 additions & 0 deletions src/database/gameevaluation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#ifndef GAME_EVALUATION_H_INCLUDED
#define GAME_EVALUATION_H_INCLUDED
#include <QPromise>
#include "board.h"
#include "gamex.h"
#include "enginex.h"

/** @ingroup Core
The GameEvaluation class represents an algorith for evaluating the centipawn score at
every move in a game.
*/

class GameEvaluationWorker final : public QObject
{
Q_OBJECT
public:
GameEvaluationWorker(int engineIndex, BoardX const & startPosition, BoardX const & currentPosition,
int msPerMove, int moveNumber);
// QObjects can't be copied or moved
GameEvaluationWorker & operator=(GameEvaluationWorker &&) = delete;
GameEvaluationWorker(GameEvaluationWorker&&) = delete;
~GameEvaluationWorker() noexcept;

double getLastScore() const noexcept;
bool isRunning() const noexcept;
void update() noexcept;

int const moveNumber;

private:
BoardX currentPosition;
int msPerMove;
EngineX * engine;
double lastScore{0};
bool running{false};
std::optional<QDateTime> startTimestamp;

private slots:
void engineActivated();
void engineAnalysisStarted();
void engineAnalysisUpdated(Analysis const &);
};

class GameEvaluation final : public QObject
{
Q_OBJECT
public :
GameEvaluation(int engineIndex, int msPerMove, GameX) noexcept;
~GameEvaluation() noexcept;

// This function does not block, but uses a QT Timer to call the
// evaluationChanged signal periodically.
void start();
void stop() noexcept;
signals:
void evaluationChanged(std::unordered_map<int, double> const & blah);
void evaluationComplete();

private:
int const engineIndex;
int const msPerMove;
GameX game;
QTimer timer;
int targetThreadCount;
QString line;
int moveNumbers;

bool running{false};
std::list<GameEvaluationWorker> workers;
void update() noexcept;

};

#endif // GAME_EVALUATION_H_INCLUDED

Loading

0 comments on commit 1ab9ae8

Please sign in to comment.