balancing arenas can be started individually
This commit is contained in:
@@ -284,6 +284,8 @@ A separate executable target (`balancing`) that links against `lib` but contains
|
|||||||
|
|
||||||
### UI
|
### 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-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-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-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-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-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.
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
#include "ArenaButton.h"
|
#include "ArenaWidget.h"
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
ArenaButton::ArenaButton(const std::string& arenaName, QWidget* parent)
|
ArenaWidget::ArenaWidget(const std::string& arenaName, QWidget* parent)
|
||||||
: QFrame(parent)
|
: QFrame(parent)
|
||||||
|
, m_running(false)
|
||||||
, m_wasFinished(false)
|
, m_wasFinished(false)
|
||||||
{
|
{
|
||||||
buildLayout(arenaName);
|
buildLayout(arenaName);
|
||||||
setFrameStyle(QFrame::Box | QFrame::Plain);
|
setFrameStyle(QFrame::Box | QFrame::Plain);
|
||||||
setLineWidth(2);
|
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);
|
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
||||||
outerLayout->setContentsMargins(8, 8, 8, 8);
|
outerLayout->setContentsMargins(8, 8, 8, 8);
|
||||||
outerLayout->setSpacing(4);
|
outerLayout->setSpacing(4);
|
||||||
|
|
||||||
|
QHBoxLayout* titleRow = new QHBoxLayout();
|
||||||
m_titleLabel = new QLabel(QString::fromStdString(arenaName), this);
|
m_titleLabel = new QLabel(QString::fromStdString(arenaName), this);
|
||||||
QFont titleFont = m_titleLabel->font();
|
QFont titleFont = m_titleLabel->font();
|
||||||
titleFont.setBold(true);
|
titleFont.setBold(true);
|
||||||
titleFont.setPointSize(titleFont.pointSize() + 2);
|
titleFont.setPointSize(titleFont.pointSize() + 2);
|
||||||
m_titleLabel->setFont(titleFont);
|
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();
|
QHBoxLayout* teamsLayout = new QHBoxLayout();
|
||||||
teamsLayout->setSpacing(16);
|
teamsLayout->setSpacing(16);
|
||||||
@@ -54,7 +64,14 @@ void ArenaButton::buildLayout(const std::string& arenaName)
|
|||||||
outerLayout->addLayout(teamsLayout);
|
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)
|
for (int ti = 0; ti < 2; ++ti)
|
||||||
{
|
{
|
||||||
@@ -90,6 +107,6 @@ void ArenaButton::updateStatus(const ArenaStatus& status)
|
|||||||
if (status.finished && !m_wasFinished)
|
if (status.finished && !m_wasFinished)
|
||||||
{
|
{
|
||||||
m_wasFinished = true;
|
m_wasFinished = true;
|
||||||
setStyleSheet("ArenaButton { border: 2px solid #33cc33; padding: 8px; }");
|
setStyleSheet("ArenaWidget { border: 2px solid #33cc33; padding: 8px; }");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,17 +5,22 @@
|
|||||||
|
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
#include "ArenaSimulation.h"
|
#include "ArenaSimulation.h"
|
||||||
|
|
||||||
class ArenaButton : public QFrame
|
class ArenaWidget : public QFrame
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
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 updateStatus(const ArenaStatus& status);
|
||||||
|
void startSimulation();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void startRequested();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildLayout(const std::string& arenaName);
|
void buildLayout(const std::string& arenaName);
|
||||||
@@ -25,5 +30,7 @@ private:
|
|||||||
QLabel* m_team2Header;
|
QLabel* m_team2Header;
|
||||||
QLabel* m_team1Content;
|
QLabel* m_team1Content;
|
||||||
QLabel* m_team2Content;
|
QLabel* m_team2Content;
|
||||||
|
QPushButton* m_startButton;
|
||||||
|
bool m_running;
|
||||||
bool m_wasFinished;
|
bool m_wasFinished;
|
||||||
};
|
};
|
||||||
@@ -14,6 +14,10 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|||||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
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);
|
QScrollArea* scrollArea = new QScrollArea(this);
|
||||||
scrollArea->setWidgetResizable(true);
|
scrollArea->setWidgetResizable(true);
|
||||||
|
|
||||||
@@ -25,13 +29,18 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|||||||
unsigned int seed = 0;
|
unsigned int seed = 0;
|
||||||
for (const ArenaConfig& arenaConfig : balancingConfig.arenas)
|
for (const ArenaConfig& arenaConfig : balancingConfig.arenas)
|
||||||
{
|
{
|
||||||
|
int index = static_cast<int>(m_arenas.size());
|
||||||
|
|
||||||
ArenaEntry entry;
|
ArenaEntry entry;
|
||||||
entry.simulation = std::make_unique<ArenaSimulation>(
|
entry.simulation = std::make_unique<ArenaSimulation>(
|
||||||
gameConfig, arenaConfig, seed++);
|
gameConfig, arenaConfig, seed++);
|
||||||
entry.button = new ArenaButton(arenaConfig.name, scrollContent);
|
entry.widget = new ArenaWidget(arenaConfig.name, scrollContent);
|
||||||
contentLayout->addWidget(entry.button);
|
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));
|
m_arenas.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
@@ -40,14 +49,6 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|||||||
scrollArea->setWidget(scrollContent);
|
scrollArea->setWidget(scrollContent);
|
||||||
mainLayout->addWidget(scrollArea);
|
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);
|
m_pollTimer = new QTimer(this);
|
||||||
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
||||||
m_pollTimer->start(100);
|
m_pollTimer->start(100);
|
||||||
@@ -74,7 +75,45 @@ void BalancingWindow::pollStatuses()
|
|||||||
{
|
{
|
||||||
for (ArenaEntry& entry : m_arenas)
|
for (ArenaEntry& entry : m_arenas)
|
||||||
{
|
{
|
||||||
const ArenaStatus status = entry.simulation->status();
|
if (entry.worker.joinable())
|
||||||
entry.button->updateStatus(status);
|
{
|
||||||
|
const ArenaStatus status = entry.simulation->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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPushButton>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "ArenaButton.h"
|
#include "ArenaWidget.h"
|
||||||
#include "ArenaSimulation.h"
|
#include "ArenaSimulation.h"
|
||||||
#include "BalancingConfig.h"
|
#include "BalancingConfig.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
@@ -24,15 +25,20 @@ public:
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void pollStatuses();
|
void pollStatuses();
|
||||||
|
void startAll();
|
||||||
|
void startArena(int index);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void updateStartAllButton();
|
||||||
|
|
||||||
struct ArenaEntry
|
struct ArenaEntry
|
||||||
{
|
{
|
||||||
std::unique_ptr<ArenaSimulation> simulation;
|
std::unique_ptr<ArenaSimulation> simulation;
|
||||||
std::thread worker;
|
std::thread worker;
|
||||||
ArenaButton* button;
|
ArenaWidget* widget;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<ArenaEntry> m_arenas;
|
std::vector<ArenaEntry> m_arenas;
|
||||||
|
QPushButton* m_startAllButton;
|
||||||
QTimer* m_pollTimer;
|
QTimer* m_pollTimer;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
SET(HDRS
|
SET(HDRS
|
||||||
${HDRS}
|
${HDRS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaButton.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaWidget.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
||||||
@@ -10,7 +10,7 @@ SET(HDRS
|
|||||||
SET(SRCS
|
SET(SRCS
|
||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
|
${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}/ArenaSimulation.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
||||||
|
|||||||
Reference in New Issue
Block a user