[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;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.