[games/chessament] src: Add Tiebreaks helper
Manuel Alcaraz Zambrano <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 471b62e6249ca9d71be4b3578c325d96a51fadd7 by Manuel Alcaraz Zambrano.
Committed on 06/08/2026 at 17:55.
Pushed by manuelal into branch 'master'.
Add Tiebreaks helper
M +1 -1 src/standingsmodel.cpp
M +9 -9 src/tiebreakmodel.cpp
M +1 -0 src/tournament/tiebreaks/CMakeLists.txt
M +3 -2 src/tournament/tiebreaks/tiebreak.cpp
A +193 -0 src/tournament/tiebreaks/tiebreaks.cpp [License: GPL(v3.0+)]
A +37 -0 src/tournament/tiebreaks/tiebreaks.h [License: GPL(v3.0+)]
M +10 -99 src/tournament/tournament.cpp
M +3 -9 src/tournament/tournament.h
M +1 -1 src/tournament/trf/writer.cpp
https://invent.kde.org/games/chessament/-/commit/471b62e6249ca9d71be4b3578c325d96a51fadd7
diff --git a/src/standingsmodel.cpp b/src/standingsmodel.cpp
index ce6a81f..2b80829 100644
--- a/src/standingsmodel.cpp
+++ b/src/standingsmodel.cpp
@@ -97,7 +97,7 @@ QVariant StandingsModel::headerData(int section, Qt::Orientation orientation, in
case NameRole:
return i18nc("@title:column Player Name", "Name");
default: {
- const auto tiebreak = m_tournament->tiebreaks().at(section - 4).get();
+ const auto tiebreak = m_tournament->tiebreaks().at(section - 4);
if (!tiebreak->shortName().isNull()) {
return tiebreak->shortName();
}
diff --git a/src/tiebreakmodel.cpp b/src/tiebreakmodel.cpp
index 32971ad..93e31f7 100644
--- a/src/tiebreakmodel.cpp
+++ b/src/tiebreakmodel.cpp
@@ -28,14 +28,14 @@ int TiebreakModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
- return static_cast<int>(m_tournament->tiebreaks().size());
+ return m_tournament->tiebreaks().size();
}
QVariant TiebreakModel::data(const QModelIndex &index, int role) const
{
Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid));
- const auto &tiebreak = m_tournament->tiebreaks()[index.row()];
+ const auto &tiebreak = m_tournament->tiebreaks().at(index.row());
switch (role) {
case Qt::DisplayRole:
@@ -53,9 +53,10 @@ bool TiebreakModel::setData(const QModelIndex &index, const QVariant &value, int
{
Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid));
+ auto tiebreak = m_tournament->tiebreaks().at(index.row());
+
switch (role) {
case TiebreakModel::TiebreakRole::OptionsRole: {
- const auto &tiebreak = m_tournament->tiebreaks()[index.row()];
tiebreak->setOptions(value.value<QList<QVariantMap>>());
break;
}
@@ -63,6 +64,7 @@ bool TiebreakModel::setData(const QModelIndex &index, const QVariant &value, int
return false;
}
+ m_tournament->tiebreaks().setTiebreak(index.row(), std::move(tiebreak));
m_tournament->saveTiebreaks();
Q_EMIT dataChanged(this->index(index.row()), this->index(index.row()));
@@ -84,7 +86,7 @@ void TiebreakModel::addTiebreak(const QString &tiebreak)
beginInsertRows({}, rowCount(), rowCount());
auto &tiebreaks = m_tournament->tiebreaks();
- tiebreaks.push_back(Tournament::tiebreak(tiebreak));
+ tiebreaks.addTiebreak(Tiebreaks::tiebreak(tiebreak));
m_tournament->saveTiebreaks();
endInsertRows();
@@ -95,7 +97,7 @@ void TiebreakModel::remove(int row)
beginRemoveRows({}, row, row);
auto &tiebreaks = m_tournament->tiebreaks();
- tiebreaks.erase(tiebreaks.begin() + row);
+ tiebreaks.removeTiebreak(row);
m_tournament->saveTiebreaks();
endRemoveRows();
@@ -105,8 +107,7 @@ void TiebreakModel::moveUp(int row)
{
Q_ASSERT(row > 0);
- auto &tiebreaks = m_tournament->tiebreaks();
- std::swap(tiebreaks[row], tiebreaks[row - 1]);
+ m_tournament->tiebreaks().swapTiebreaks(row, row - 1);
m_tournament->saveTiebreaks();
Q_EMIT dataChanged(index(row - 1), index(row));
@@ -116,8 +117,7 @@ void TiebreakModel::moveDown(int row)
{
Q_ASSERT(row + 1 < rowCount());
- auto &tiebreaks = m_tournament->tiebreaks();
- std::swap(tiebreaks[row], tiebreaks[row + 1]);
+ m_tournament->tiebreaks().swapTiebreaks(row, row + 1);
m_tournament->saveTiebreaks();
Q_EMIT dataChanged(index(row), index(row + 1));
diff --git a/src/tournament/tiebreaks/CMakeLists.txt b/src/tournament/tiebreaks/CMakeLists.txt
index 745a07e..0202e27 100644
--- a/src/tournament/tiebreaks/CMakeLists.txt
+++ b/src/tournament/tiebreaks/CMakeLists.txt
@@ -3,6 +3,7 @@
target_sources(tournament PRIVATE
tiebreak.cpp
+ tiebreaks.cpp
aob.cpp
buchholz.cpp
diff --git a/src/tournament/tiebreaks/tiebreak.cpp b/src/tournament/tiebreaks/tiebreak.cpp
index 91e4668..b10c043 100644
--- a/src/tournament/tiebreaks/tiebreak.cpp
+++ b/src/tournament/tiebreaks/tiebreak.cpp
@@ -1,10 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 Manuel Alcaraz Zambrano <[email protected]>
-#include <QJsonObject>
-
#include "tiebreak.h"
+#include <QJsonArray>
+#include <QJsonObject>
+
using namespace Qt::Literals::StringLiterals;
QString Tiebreak::shortName()
diff --git a/src/tournament/tiebreaks/tiebreaks.cpp b/src/tournament/tiebreaks/tiebreaks.cpp
new file mode 100644
index 0000000..f783482
--- /dev/null
+++ b/src/tournament/tiebreaks/tiebreaks.cpp
@@ -0,0 +1,193 @@
+// SPDX-FileCopyrightText: 2025-2026 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "tiebreaks.h"
+
+#include <QJsonArray>
+
+#include "tiebreaks/aob.h"
+#include "tiebreaks/buchholz.h"
+#include "tiebreaks/dummy.h"
+#include "tiebreaks/numberwins.h"
+#include "tiebreaks/playedblack.h"
+#include "tiebreaks/points.h"
+#include "tiebreaks/won.h"
+#include "utils.h"
+
+using namespace Qt::Literals::StringLiterals;
+
+int Tiebreaks::size() const
+{
+ const auto value = m_json["tiebreaks"_L1];
+ if (!value.isArray()) {
+ return 0;
+ }
+
+ return static_cast<int>(value.toArray().size());
+}
+
+std::vector<std::unique_ptr<Tiebreak>> Tiebreaks::all() const
+{
+ std::vector<std::unique_ptr<Tiebreak>> result;
+
+ const auto tibreaks = m_json["tiebreaks"_L1].toArray();
+ result.reserve(tibreaks.size());
+
+ for (const auto value : tibreaks) {
+ const auto options = value.toObject();
+ auto tiebreak = Tiebreaks::tiebreak(options);
+
+ result.push_back(std::move(tiebreak));
+ }
+
+ return result;
+}
+
+std::unique_ptr<Tiebreak> Tiebreaks::at(int index) const
+{
+ Q_ASSERT(index >= 0);
+
+ const auto tiebreaks = m_json["tiebreaks"_L1].toArray();
+ Q_ASSERT(index < tiebreaks.size());
+
+ const auto tiebreakJson = tiebreaks.at(index);
+ const auto options = tiebreakJson.toObject();
+ auto tiebreak = Tiebreaks::tiebreak(options);
+
+ return tiebreak;
+}
+
+void Tiebreaks::addTiebreak(std::unique_ptr<Tiebreak> arbiter)
+{
+ auto value = m_json["tiebreaks"_L1];
+ QJsonArray tiebreaks;
+
+ if (value.isArray()) {
+ tiebreaks = value.toArray();
+ }
+
+ tiebreaks << arbiter->toJson();
+ value = tiebreaks;
+}
+
+void Tiebreaks::setTiebreak(int index, std::unique_ptr<Tiebreak> arbiter)
+{
+ Q_ASSERT(index >= 0);
+
+ auto value = m_json["tiebreaks"_L1];
+ Q_ASSERT(value.isArray());
+
+ auto tiebreaks = value.toArray();
+ Q_ASSERT(index < tiebreaks.size());
+
+ auto json = tiebreaks[index].toObject();
+ Utils::updateObject(&json, arbiter->toJson());
+ tiebreaks[index] = json;
+
+ value = tiebreaks;
+}
+
+void Tiebreaks::swapTiebreaks(int a, int b)
+{
+ Q_ASSERT(a >= 0);
+ Q_ASSERT(b >= 0);
+
+ auto value = m_json["tiebreaks"_L1];
+ Q_ASSERT(value.isArray());
+
+ auto tiebreaks = value.toArray();
+ Q_ASSERT(a < tiebreaks.size());
+ Q_ASSERT(b < tiebreaks.size());
+
+ auto tiebreak = tiebreaks.takeAt(a);
+ tiebreaks.insert(b, tiebreak);
+
+ value = tiebreaks;
+}
+
+void Tiebreaks::removeTiebreak(int index)
+{
+ Q_ASSERT(index >= 0);
+
+ auto value = m_json["tiebreaks"_L1];
+ Q_ASSERT(value.isArray());
+
+ auto tiebreaks = value.toArray();
+ Q_ASSERT(index < tiebreaks.size());
+
+ tiebreaks.erase(tiebreaks.begin() + index);
+
+ value = tiebreaks;
+}
+
+QJsonObject Tiebreaks::toJson() const
+{
+ return m_json;
+}
+
+Tiebreaks Tiebreaks::fromJson(const QJsonObject &json)
+{
+ Tiebreaks tiebreaks;
+ tiebreaks.m_json = json;
+ return tiebreaks;
+}
+
+std::unique_ptr<Tiebreak> Tiebreaks::tiebreak(const QString &id)
+{
+ if (id == "pts"_L1) {
+ return std::make_unique<Points>();
+ }
+ if (id == "bh"_L1) {
+ return std::make_unique<Buchholz>();
+ }
+ if (id == "win"_L1) {
+ return std::make_unique<NumberOfWins>();
+ }
+ if (id == "won"_L1) {
+ return std::make_unique<NumberOfGamesWon>();
+ }
+ if (id == "bpg"_L1) {
+ return std::make_unique<NumberOfGamesPlayedWithBlack>();
+ }
+ if (id == "aob"_L1) {
+ return std::make_unique<AverageBuchholzOfOpponents>();
+ }
+ return nullptr;
+}
+
+std::unique_ptr<Tiebreak> Tiebreaks::tiebreak(const QJsonObject &json)
+{
+ const auto id = json["id"_L1].toString();
+
+ auto tiebreak = Tiebreaks::tiebreak(id);
+
+ if (tiebreak == nullptr) {
+ tiebreak = std::make_unique<DummyTiebreak>();
+ }
+
+ tiebreak->setOptions(json.toVariantMap());
+
+ return tiebreak;
+}
+
+std::expected<std::unique_ptr<Tiebreak>, QString> Tiebreaks::tiebreakFromTrf(const QString &code)
+{
+ if (code.startsWith("OTHER_"_L1, Qt::CaseSensitivity::CaseInsensitive)) {
+ qWarning() << "Unsupported tiebreak" << code;
+ return nullptr;
+ }
+
+ const auto options = code.split(u'/', Qt::SkipEmptyParts);
+
+ auto tiebreak = Tiebreaks::tiebreak(options[0].toLower());
+ if (tiebreak == nullptr) {
+ qWarning() << "Unsupported tiebreak" << code;
+ return nullptr;
+ }
+
+ if (const auto ok = tiebreak->setTrfOptions(options.mid(1)); !ok) {
+ return std::unexpected(ok.error());
+ }
+
+ return tiebreak;
+}
diff --git a/src/tournament/tiebreaks/tiebreaks.h b/src/tournament/tiebreaks/tiebreaks.h
new file mode 100644
index 0000000..78cded9
--- /dev/null
+++ b/src/tournament/tiebreaks/tiebreaks.h
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2026 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "tiebreak.h"
+
+#include <QJsonObject>
+
+struct Tiebreaks {
+ [[nodiscard]] int size() const;
+
+ [[nodiscard]] std::vector<std::unique_ptr<Tiebreak>> all() const;
+
+ [[nodiscard]] std::unique_ptr<Tiebreak> at(int index) const;
+
+ void addTiebreak(std::unique_ptr<Tiebreak> arbiter);
+
+ void setTiebreak(int index, std::unique_ptr<Tiebreak> arbiter);
+
+ void swapTiebreaks(int a, int b);
+
+ void removeTiebreak(int index);
+
+ [[nodiscard]] QJsonObject toJson() const;
+
+ static Tiebreaks fromJson(const QJsonObject &json);
+
+ static std::unique_ptr<Tiebreak> tiebreak(const QString &id);
+
+ static std::unique_ptr<Tiebreak> tiebreak(const QJsonObject &json);
+
+ static std::expected<std::unique_ptr<Tiebreak>, QString> tiebreakFromTrf(const QString &code);
+
+private:
+ QJsonObject m_json;
+};
diff --git a/src/tournament/tournament.cpp b/src/tournament/tournament.cpp
index bd06a88..62c7a91 100644
--- a/src/tournament/tournament.cpp
+++ b/src/tournament/tournament.cpp
@@ -14,13 +14,7 @@
#include "event.h"
#include "ratinglists/ratinglist.h"
#include "state.h"
-#include "tiebreaks/aob.h"
-#include "tiebreaks/buchholz.h"
-#include "tiebreaks/dummy.h"
-#include "tiebreaks/numberwins.h"
-#include "tiebreaks/playedblack.h"
#include "tiebreaks/points.h"
-#include "tiebreaks/won.h"
#include "timecontrol.h"
#include "trf/reader.h"
#include "trf/writer.h"
@@ -30,7 +24,7 @@ Tournament::Tournament(Event *event)
: m_event(event)
, m_timeControl({TimeControlPeriod{std::nullopt, 5400, 30}})
{
- m_tiebreaks.push_back(std::make_unique<Points>());
+ m_tiebreaks.addTiebreak(std::make_unique<Points>());
}
QString Tournament::id() const
@@ -120,38 +114,29 @@ void Tournament::saveTimeControl()
setOption(u"time_control"_s, text);
}
-std::vector<std::unique_ptr<Tiebreak>> &Tournament::tiebreaks()
+Tiebreaks &Tournament::tiebreaks()
{
return m_tiebreaks;
}
-void Tournament::setTiebreaks(std::vector<std::unique_ptr<Tiebreak>> tiebreaks)
-{
- if (m_tiebreaks == tiebreaks) {
- return;
- }
- m_tiebreaks = std::move(tiebreaks);
- saveTiebreaks();
-}
-
std::expected<void, QString> Tournament::setTiebreaksFromTrf(const QString &line)
{
- std::vector<std::unique_ptr<Tiebreak>> tiebreaks;
+ auto tiebreaks = Tiebreaks{};
const auto codes = line.split(u',', Qt::SkipEmptyParts);
for (const auto &code : codes) {
- auto tiebreak = tiebreakFromTrf(code);
+ auto tiebreak = Tiebreaks::tiebreakFromTrf(code);
if (!tiebreak) {
return std::unexpected(tiebreak.error());
}
if (tiebreak == nullptr) {
continue;
}
- tiebreaks.push_back(std::move(*tiebreak));
+ tiebreaks.addTiebreak(std::move(*tiebreak));
}
- setTiebreaks(std::move(tiebreaks));
+ m_tiebreaks = tiebreaks;
return {};
}
@@ -483,7 +468,7 @@ QList<Standing> Tournament::standings(const State &state)
// Calculate tiebreaks
QList<Player *> players;
- for (const auto &tiebreak : std::as_const(m_tiebreaks)) {
+ for (const auto &tiebreak : m_tiebreaks.all()) {
uint i = 0;
players.clear();
while (i < m_players.size()) {
@@ -567,51 +552,6 @@ QList<QVariantMap> Tournament::availableTiebreaks()
};
}
-std::unique_ptr<Tiebreak> Tournament::tiebreak(const QString &id)
-{
- if (id == "pts"_L1) {
- return std::make_unique<Points>();
- }
- if (id == "bh"_L1) {
- return std::make_unique<Buchholz>();
- }
- if (id == "win"_L1) {
- return std::make_unique<NumberOfWins>();
- }
- if (id == "won"_L1) {
- return std::make_unique<NumberOfGamesWon>();
- }
- if (id == "bpg"_L1) {
- return std::make_unique<NumberOfGamesPlayedWithBlack>();
- }
- if (id == "aob"_L1) {
- return std::make_unique<AverageBuchholzOfOpponents>();
- }
- return nullptr;
-}
-
-std::expected<std::unique_ptr<Tiebreak>, QString> Tournament::tiebreakFromTrf(const QString &code)
-{
- if (code.startsWith("OTHER_"_L1, Qt::CaseSensitivity::CaseInsensitive)) {
- qWarning() << "Unsupported tiebreak" << code;
- return nullptr;
- }
-
- const auto options = code.split(u'/', Qt::SkipEmptyParts);
-
- auto tiebreak = Tournament::tiebreak(options[0].toLower());
- if (tiebreak == nullptr) {
- qWarning() << "Unsupported tiebreak" << code;
- return nullptr;
- }
-
- if (const auto ok = tiebreak->setTrfOptions(options.mid(1)); !ok) {
- return std::unexpected(ok.error());
- }
-
- return tiebreak;
-}
-
void Tournament::setInitialColor(Tournament::InitialColor color)
{
if (m_initialColor == color) {
@@ -1115,18 +1055,11 @@ State Tournament::state(std::optional<int> maxRound)
void Tournament::saveTiebreaks()
{
- QJsonArray values;
-
- for (const auto &tiebreak : tiebreaks()) {
- values << tiebreak->toJson();
- }
+ const auto text = QJsonDocument{m_tiebreaks.toJson()}.toJson(QJsonDocument::JsonFormat::Compact);
- const auto doc = QJsonDocument{QJsonObject{{"tiebreaks"_L1, values}}};
- const auto text = doc.toJson(QJsonDocument::Compact);
+ qDebug() << m_tiebreaks.toJson();
setOption("tiebreaks"_L1, text);
-
- Q_EMIT tiebreaksChanged();
}
QVariant Tournament::option(const QString &name)
@@ -1456,29 +1389,7 @@ std::expected<void, QString> Tournament::loadPairings()
std::expected<void, QString> Tournament::loadTiebreaks()
{
const auto json = QJsonDocument::fromJson(option("tiebreaks"_L1).toByteArray());
-
- if (const auto tbs = json["tiebreaks"_L1]; tbs.isArray()) {
- m_tiebreaks.clear();
-
- const auto values = tbs.toArray();
- for (const auto value : values) {
- if (!value.isObject()) {
- continue;
- }
-
- const auto options = value.toObject();
- const auto id = options["id"_L1].toString();
-
- auto tiebreak = Tournament::tiebreak(id);
- if (tiebreak == nullptr) {
- tiebreak = std::make_unique<DummyTiebreak>();
- }
-
- tiebreak->setOptions(options.toVariantMap());
-
- m_tiebreaks.push_back(std::move(tiebreak));
- }
- }
+ m_tiebreaks = Tiebreaks::fromJson(json.object());
return {};
}
diff --git a/src/tournament/tournament.h b/src/tournament/tournament.h
index b25bc85..b071f4f 100644
--- a/src/tournament/tournament.h
+++ b/src/tournament/tournament.h
@@ -22,6 +22,7 @@
#include "round.h"
#include "standing.h"
#include "tiebreaks/tiebreak.h"
+#include "tiebreaks/tiebreaks.h"
#include "timecontrol.h"
#include "trf/trf.h"
@@ -98,7 +99,7 @@ public:
*
* This property holds the list of tiebreaks of the tournament.
*/
- [[nodiscard]] std::vector<std::unique_ptr<Tiebreak>> &tiebreaks();
+ [[nodiscard]] Tiebreaks &tiebreaks();
/*!
* \property Tournament::numberOfRounds
@@ -177,12 +178,6 @@ public:
Q_INVOKABLE QList<QVariantMap> availableTiebreaks();
- static std::unique_ptr<Tiebreak> tiebreak(const QString &id);
-
- static std::expected<std::unique_ptr<Tiebreak>, QString> tiebreakFromTrf(const QString &code);
-
- void setTiebreaks(std::vector<std::unique_ptr<Tiebreak>> tiebreaks);
-
std::expected<void, QString> setTiebreaksFromTrf(const QString &line);
[[nodiscard]] Round *round(int number) const;
@@ -393,7 +388,6 @@ Q_SIGNALS:
void nameChanged();
void cityChanged();
void federationChanged();
- void tiebreaksChanged();
void numberOfPlayersChanged();
void numberOfRatedPlayersChanged();
@@ -424,7 +418,7 @@ private:
int m_numberOfRounds = 1;
int m_currentRound = 0;
QVariantMap m_options;
- std::vector<std::unique_ptr<Tiebreak>> m_tiebreaks;
+ Tiebreaks m_tiebreaks;
std::vector<std::unique_ptr<Player>> m_players;
std::vector<std::unique_ptr<Round>> m_rounds;
diff --git a/src/tournament/trf/writer.cpp b/src/tournament/trf/writer.cpp
index 6302c24..8970af5 100644
--- a/src/tournament/trf/writer.cpp
+++ b/src/tournament/trf/writer.cpp
@@ -75,7 +75,7 @@ void TrfWriter::writeArbiters(QTextStream &stream)
void TrfWriter::writeTiebreaks(QTextStream &stream)
{
QStringList codes{};
- for (const auto &tiebreak : m_tournament->tiebreaks()) {
+ for (const auto &tiebreak : m_tournament->tiebreaks().all()) {
const auto code = tiebreak->code();
if (!code.isEmpty()) {
codes << code;