From 55b42a03d97a4cd4ecdd878c7646471e2e3fa9d8 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Sun, 3 May 2026 20:33:33 +0200 Subject: [PATCH] balancing arenas can be started individually --- docs/requirements.md | 8 ++- .../{ArenaButton.cpp => ArenaWidget.cpp} | 31 +++++++-- .../{ArenaButton.h => ArenaWidget.h} | 11 +++- src/balancing/BalancingWindow.cpp | 65 +++++++++++++++---- src/balancing/BalancingWindow.h | 10 ++- src/balancing/CMakeLists.txt | 4 +- 6 files changed, 100 insertions(+), 29 deletions(-) rename src/balancing/{ArenaButton.cpp => ArenaWidget.cpp} (74%) rename src/balancing/{ArenaButton.h => ArenaWidget.h} (64%) diff --git a/docs/requirements.md b/docs/requirements.md index c1a52df..de37d5e 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -284,6 +284,8 @@ A separate executable target (`balancing`) that links against `lib` but contains ### UI -- REQ-BAL-UI-WINDOW: On startup the tool displays a window containing a dynamically generated vertical list of arena buttons, one per arena defined in `balancing.toml`. -- REQ-BAL-UI-BUTTON: Each arena button displays the arena name and two columns (one per team). Each column shows the team name as a header, followed by a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName Llevel` — for example `2/3 Fighter L5` or `1/1 HQ L1`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`. -- REQ-BAL-UI-BUTTON-BORDER: While an arena's simulation is running, the button border is blue. When the fight ends, the border changes to green. +- REQ-BAL-UI-WINDOW: On startup the tool displays a window containing a "Start All" button at the top, followed by a scrollable vertical list of arena widgets, one per arena defined in `balancing.toml`. Simulations do not start automatically on startup. +- REQ-BAL-UI-START-ALL: The "Start All" button is placed above the scrollable arena list. Clicking it starts the simulation for every arena that has not yet been started. The button is disabled when all arenas are already running or finished. +- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name and two columns (one per team). Each column shows the team name as a header, followed by a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName Llevel` — for example `2/3 Fighter L5` or `1/1 HQ L1`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`. +- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running or after it has finished. +- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended. diff --git a/src/balancing/ArenaButton.cpp b/src/balancing/ArenaWidget.cpp similarity index 74% rename from src/balancing/ArenaButton.cpp rename to src/balancing/ArenaWidget.cpp index aae1b39..f2450a6 100644 --- a/src/balancing/ArenaButton.cpp +++ b/src/balancing/ArenaWidget.cpp @@ -1,30 +1,40 @@ -#include "ArenaButton.h" +#include "ArenaWidget.h" #include #include -ArenaButton::ArenaButton(const std::string& arenaName, QWidget* parent) +ArenaWidget::ArenaWidget(const std::string& arenaName, QWidget* parent) : QFrame(parent) + , m_running(false) , m_wasFinished(false) { buildLayout(arenaName); setFrameStyle(QFrame::Box | QFrame::Plain); setLineWidth(2); - setStyleSheet("ArenaButton { border: 2px solid #3366ff; padding: 8px; }"); + setStyleSheet("ArenaWidget { border: 2px solid #999999; padding: 8px; }"); } -void ArenaButton::buildLayout(const std::string& arenaName) +void ArenaWidget::buildLayout(const std::string& arenaName) { QVBoxLayout* outerLayout = new QVBoxLayout(this); outerLayout->setContentsMargins(8, 8, 8, 8); outerLayout->setSpacing(4); + QHBoxLayout* titleRow = new QHBoxLayout(); m_titleLabel = new QLabel(QString::fromStdString(arenaName), this); QFont titleFont = m_titleLabel->font(); titleFont.setBold(true); titleFont.setPointSize(titleFont.pointSize() + 2); m_titleLabel->setFont(titleFont); - outerLayout->addWidget(m_titleLabel); + titleRow->addWidget(m_titleLabel); + + titleRow->addStretch(); + + m_startButton = new QPushButton("Start", this); + connect(m_startButton, &QPushButton::clicked, this, &ArenaWidget::startRequested); + titleRow->addWidget(m_startButton); + + outerLayout->addLayout(titleRow); QHBoxLayout* teamsLayout = new QHBoxLayout(); teamsLayout->setSpacing(16); @@ -54,7 +64,14 @@ void ArenaButton::buildLayout(const std::string& arenaName) outerLayout->addLayout(teamsLayout); } -void ArenaButton::updateStatus(const ArenaStatus& status) +void ArenaWidget::startSimulation() +{ + m_running = true; + m_startButton->setEnabled(false); + setStyleSheet("ArenaWidget { border: 2px solid #3366ff; padding: 8px; }"); +} + +void ArenaWidget::updateStatus(const ArenaStatus& status) { for (int ti = 0; ti < 2; ++ti) { @@ -90,6 +107,6 @@ void ArenaButton::updateStatus(const ArenaStatus& status) if (status.finished && !m_wasFinished) { m_wasFinished = true; - setStyleSheet("ArenaButton { border: 2px solid #33cc33; padding: 8px; }"); + setStyleSheet("ArenaWidget { border: 2px solid #33cc33; padding: 8px; }"); } } diff --git a/src/balancing/ArenaButton.h b/src/balancing/ArenaWidget.h similarity index 64% rename from src/balancing/ArenaButton.h rename to src/balancing/ArenaWidget.h index c5ff02c..7bc493a 100644 --- a/src/balancing/ArenaButton.h +++ b/src/balancing/ArenaWidget.h @@ -5,17 +5,22 @@ #include #include +#include #include "ArenaSimulation.h" -class ArenaButton : public QFrame +class ArenaWidget : public QFrame { Q_OBJECT public: - explicit ArenaButton(const std::string& arenaName, QWidget* parent = nullptr); + explicit ArenaWidget(const std::string& arenaName, QWidget* parent = nullptr); void updateStatus(const ArenaStatus& status); + void startSimulation(); + +signals: + void startRequested(); private: void buildLayout(const std::string& arenaName); @@ -25,5 +30,7 @@ private: QLabel* m_team2Header; QLabel* m_team1Content; QLabel* m_team2Content; + QPushButton* m_startButton; + bool m_running; bool m_wasFinished; }; diff --git a/src/balancing/BalancingWindow.cpp b/src/balancing/BalancingWindow.cpp index fe70bc3..69cd8b0 100644 --- a/src/balancing/BalancingWindow.cpp +++ b/src/balancing/BalancingWindow.cpp @@ -14,6 +14,10 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig, QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); + m_startAllButton = new QPushButton("Start All", this); + mainLayout->addWidget(m_startAllButton); + connect(m_startAllButton, &QPushButton::clicked, this, &BalancingWindow::startAll); + QScrollArea* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); @@ -25,13 +29,18 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig, unsigned int seed = 0; for (const ArenaConfig& arenaConfig : balancingConfig.arenas) { + int index = static_cast(m_arenas.size()); + ArenaEntry entry; entry.simulation = std::make_unique( gameConfig, arenaConfig, seed++); - entry.button = new ArenaButton(arenaConfig.name, scrollContent); - contentLayout->addWidget(entry.button); + entry.widget = new ArenaWidget(arenaConfig.name, scrollContent); + contentLayout->addWidget(entry.widget); - entry.button->updateStatus(entry.simulation->status()); + entry.widget->updateStatus(entry.simulation->status()); + + connect(entry.widget, &ArenaWidget::startRequested, + this, [this, index]() { startArena(index); }); m_arenas.push_back(std::move(entry)); } @@ -40,14 +49,6 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig, scrollArea->setWidget(scrollContent); mainLayout->addWidget(scrollArea); - // Start worker threads. - for (ArenaEntry& entry : m_arenas) - { - ArenaSimulation* sim = entry.simulation.get(); - entry.worker = std::thread([sim]() { sim->run(); }); - } - - // Poll timer at ~10 Hz to update button statuses. m_pollTimer = new QTimer(this); connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses); m_pollTimer->start(100); @@ -74,7 +75,45 @@ void BalancingWindow::pollStatuses() { for (ArenaEntry& entry : m_arenas) { - const ArenaStatus status = entry.simulation->status(); - entry.button->updateStatus(status); + if (entry.worker.joinable()) + { + const ArenaStatus status = entry.simulation->status(); + entry.widget->updateStatus(status); + } + } + updateStartAllButton(); +} + +void BalancingWindow::startAll() +{ + for (int i = 0; i < static_cast(m_arenas.size()); ++i) + { + startArena(i); } } + +void BalancingWindow::startArena(int index) +{ + ArenaEntry& entry = m_arenas[index]; + if (entry.worker.joinable()) + { + return; + } + entry.widget->startSimulation(); + ArenaSimulation* sim = entry.simulation.get(); + entry.worker = std::thread([sim]() { sim->run(); }); + updateStartAllButton(); +} + +void BalancingWindow::updateStartAllButton() +{ + for (ArenaEntry& entry : m_arenas) + { + if (!entry.worker.joinable()) + { + m_startAllButton->setEnabled(true); + return; + } + } + m_startAllButton->setEnabled(false); +} diff --git a/src/balancing/BalancingWindow.h b/src/balancing/BalancingWindow.h index ab5fbbb..8243792 100644 --- a/src/balancing/BalancingWindow.h +++ b/src/balancing/BalancingWindow.h @@ -4,10 +4,11 @@ #include #include +#include #include #include -#include "ArenaButton.h" +#include "ArenaWidget.h" #include "ArenaSimulation.h" #include "BalancingConfig.h" #include "GameConfig.h" @@ -24,15 +25,20 @@ public: private slots: void pollStatuses(); + void startAll(); + void startArena(int index); private: + void updateStartAllButton(); + struct ArenaEntry { std::unique_ptr simulation; std::thread worker; - ArenaButton* button; + ArenaWidget* widget; }; std::vector m_arenas; + QPushButton* m_startAllButton; QTimer* m_pollTimer; }; diff --git a/src/balancing/CMakeLists.txt b/src/balancing/CMakeLists.txt index 62de17e..d565819 100644 --- a/src/balancing/CMakeLists.txt +++ b/src/balancing/CMakeLists.txt @@ -1,6 +1,6 @@ SET(HDRS ${HDRS} - ${CMAKE_CURRENT_SOURCE_DIR}/ArenaButton.h + ${CMAKE_CURRENT_SOURCE_DIR}/ArenaWidget.h ${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.h ${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h @@ -10,7 +10,7 @@ SET(HDRS SET(SRCS ${SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/ArenaButton.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ArenaWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp