balancing arenas can be started individually

This commit is contained in:
2026-05-03 20:33:33 +02:00
parent 426870158c
commit 55b42a03d9
6 changed files with 100 additions and 29 deletions

View File

@@ -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.

View File

@@ -1,30 +1,40 @@
#include "ArenaButton.h"
#include "ArenaWidget.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
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; }");
}
}

View File

@@ -5,17 +5,22 @@
#include <QFrame>
#include <QLabel>
#include <QPushButton>
#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;
};

View File

@@ -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<int>(m_arenas.size());
ArenaEntry entry;
entry.simulation = std::make_unique<ArenaSimulation>(
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);
@@ -73,8 +74,46 @@ BalancingWindow::~BalancingWindow()
void BalancingWindow::pollStatuses()
{
for (ArenaEntry& entry : m_arenas)
{
if (entry.worker.joinable())
{
const ArenaStatus status = entry.simulation->status();
entry.button->updateStatus(status);
entry.widget->updateStatus(status);
}
}
updateStartAllButton();
}
void BalancingWindow::startAll()
{
for (int i = 0; i < static_cast<int>(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);
}

View File

@@ -4,10 +4,11 @@
#include <thread>
#include <vector>
#include <QPushButton>
#include <QTimer>
#include <QWidget>
#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<ArenaSimulation> simulation;
std::thread worker;
ArenaButton* button;
ArenaWidget* widget;
};
std::vector<ArenaEntry> m_arenas;
QPushButton* m_startAllButton;
QTimer* m_pollTimer;
};

View File

@@ -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