Compare commits

...

2 Commits

Author SHA1 Message Date
afd8cd28fa allow to re-load the config for the balancing app via button 2026-05-03 21:00:36 +02:00
6405ad6b3f allow to re-start arenas 2026-05-03 20:48:13 +02:00
5 changed files with 109 additions and 31 deletions

View File

@@ -258,7 +258,7 @@ A separate executable target (`balancing`) that links against `lib` but contains
### Config ### Config
- REQ-BAL-CONFIG: The balancing tool reads arena definitions from a `balancing.toml` file. The file is read once at startup. If parsing fails or required fields are missing, the tool aborts with a clear error message. - REQ-BAL-CONFIG: The balancing tool reads arena definitions from a `balancing.toml` file. The file is read at startup and again each time the player triggers a config reload (REQ-BAL-UI-RELOAD). If parsing fails or required fields are missing at startup, the tool aborts with a clear error message. If parsing fails during a reload, a modal error dialog is shown describing the failure and the current arena list is left unchanged.
- REQ-BAL-CONFIG-GAME: Ship stats are read from `ships.toml` and defence station stats are read from `stations.toml`, using the same config loading as the main game. Formula evaluation uses the levels specified in the arena config. - REQ-BAL-CONFIG-GAME: Ship stats are read from `ships.toml` and defence station stats are read from `stations.toml`, using the same config loading as the main game. Formula evaluation uses the levels specified in the arena config.
### Arena Definition ### Arena Definition
@@ -284,8 +284,9 @@ 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 "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-WINDOW: On startup the tool displays a window containing a "Reload Config" button and a "Start All" button at the top (in that order, left to right), 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-RELOAD: The "Reload Config" button reloads all config files from disk (`balancing.toml`, `ships.toml`, `stations.toml`), stops any running simulations, and replaces the arena widget list with freshly created widgets from the reloaded config. The button is disabled while any arena simulation is currently running.
- REQ-BAL-UI-START-ALL: The "Start All" button is placed above the scrollable arena list, to the right of the "Reload Config" button. Clicking it starts (or restarts) the simulation for every arena that is not currently running. The button is disabled when all arenas are currently running.
- 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: 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-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. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
- 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. - 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

@@ -67,6 +67,7 @@ void ArenaWidget::buildLayout(const std::string& arenaName)
void ArenaWidget::startSimulation() void ArenaWidget::startSimulation()
{ {
m_running = true; m_running = true;
m_wasFinished = false;
m_startButton->setEnabled(false); m_startButton->setEnabled(false);
setStyleSheet("ArenaWidget { border: 2px solid #3366ff; padding: 8px; }"); setStyleSheet("ArenaWidget { border: 2px solid #3366ff; padding: 8px; }");
} }
@@ -107,6 +108,8 @@ void ArenaWidget::updateStatus(const ArenaStatus& status)
if (status.finished && !m_wasFinished) if (status.finished && !m_wasFinished)
{ {
m_wasFinished = true; m_wasFinished = true;
m_running = false;
m_startButton->setEnabled(true);
setStyleSheet("ArenaWidget { border: 2px solid #33cc33; padding: 8px; }"); setStyleSheet("ArenaWidget { border: 2px solid #33cc33; padding: 8px; }");
} }
} }

View File

@@ -1,12 +1,21 @@
#include "BalancingWindow.h" #include "BalancingWindow.h"
#include <QScrollArea> #include <QHBoxLayout>
#include <QMessageBox>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "ConfigLoader.h"
BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig, BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
const GameConfig& gameConfig, GameConfig gameConfig,
const std::string& configDir,
const std::string& balancingConfigPath,
QWidget* parent) QWidget* parent)
: QWidget(parent) : QWidget(parent)
, m_gameConfig(std::move(gameConfig))
, m_configDir(configDir)
, m_balancingConfigPath(balancingConfigPath)
, m_nextSeed(0)
{ {
setWindowTitle("DotaFactory — Balancing Tool"); setWindowTitle("DotaFactory — Balancing Tool");
resize(800, 600); resize(800, 600);
@@ -14,26 +23,52 @@ 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);
QHBoxLayout* buttonRow = new QHBoxLayout();
m_reloadButton = new QPushButton("Reload Config", this);
m_startAllButton = new QPushButton("Start All", this); m_startAllButton = new QPushButton("Start All", this);
mainLayout->addWidget(m_startAllButton); buttonRow->addWidget(m_reloadButton);
buttonRow->addWidget(m_startAllButton);
buttonRow->addStretch();
mainLayout->addLayout(buttonRow);
connect(m_reloadButton, &QPushButton::clicked, this, &BalancingWindow::reloadConfig);
connect(m_startAllButton, &QPushButton::clicked, this, &BalancingWindow::startAll); connect(m_startAllButton, &QPushButton::clicked, this, &BalancingWindow::startAll);
QScrollArea* scrollArea = new QScrollArea(this); m_scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true); m_scrollArea->setWidgetResizable(true);
mainLayout->addWidget(m_scrollArea);
QWidget* scrollContent = new QWidget(scrollArea); populateArenas(balancingConfig);
m_pollTimer = new QTimer(this);
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
m_pollTimer->start(100);
}
BalancingWindow::~BalancingWindow()
{
m_pollTimer->stop();
stopAllArenas();
}
void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
{
stopAllArenas();
m_arenas.clear();
QWidget* scrollContent = new QWidget(m_scrollArea);
QVBoxLayout* contentLayout = new QVBoxLayout(scrollContent); QVBoxLayout* contentLayout = new QVBoxLayout(scrollContent);
contentLayout->setSpacing(8); contentLayout->setSpacing(8);
contentLayout->setContentsMargins(8, 8, 8, 8); contentLayout->setContentsMargins(8, 8, 8, 8);
unsigned int seed = 0;
for (const ArenaConfig& arenaConfig : balancingConfig.arenas) for (const ArenaConfig& arenaConfig : balancingConfig.arenas)
{ {
int index = static_cast<int>(m_arenas.size()); int index = static_cast<int>(m_arenas.size());
ArenaEntry entry; ArenaEntry entry;
entry.config = arenaConfig;
entry.simulation = std::make_unique<ArenaSimulation>( entry.simulation = std::make_unique<ArenaSimulation>(
gameConfig, arenaConfig, seed++); m_gameConfig, arenaConfig, m_nextSeed++);
entry.widget = new ArenaWidget(arenaConfig.name, scrollContent); entry.widget = new ArenaWidget(arenaConfig.name, scrollContent);
contentLayout->addWidget(entry.widget); contentLayout->addWidget(entry.widget);
@@ -46,18 +81,13 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
} }
contentLayout->addStretch(); contentLayout->addStretch();
scrollArea->setWidget(scrollContent); m_scrollArea->setWidget(scrollContent);
mainLayout->addWidget(scrollArea);
m_pollTimer = new QTimer(this); updateButtons();
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
m_pollTimer->start(100);
} }
BalancingWindow::~BalancingWindow() void BalancingWindow::stopAllArenas()
{ {
m_pollTimer->stop();
for (ArenaEntry& entry : m_arenas) for (ArenaEntry& entry : m_arenas)
{ {
entry.simulation->requestStop(); entry.simulation->requestStop();
@@ -81,7 +111,22 @@ void BalancingWindow::pollStatuses()
entry.widget->updateStatus(status); entry.widget->updateStatus(status);
} }
} }
updateStartAllButton(); updateButtons();
}
void BalancingWindow::reloadConfig()
{
try
{
GameConfig newGameConfig = ConfigLoader::loadFromDirectory(m_configDir);
BalancingConfig newBalancingConfig = loadBalancingConfig(m_balancingConfigPath);
m_gameConfig = std::move(newGameConfig);
populateArenas(newBalancingConfig);
}
catch (const std::exception& e)
{
QMessageBox::critical(this, "Reload Failed", QString::fromStdString(e.what()));
}
} }
void BalancingWindow::startAll() void BalancingWindow::startAll()
@@ -97,23 +142,38 @@ void BalancingWindow::startArena(int index)
ArenaEntry& entry = m_arenas[index]; ArenaEntry& entry = m_arenas[index];
if (entry.worker.joinable()) if (entry.worker.joinable())
{ {
return; entry.simulation->requestStop();
entry.worker.join();
} }
entry.simulation = std::make_unique<ArenaSimulation>(
m_gameConfig, entry.config, m_nextSeed++);
entry.widget->startSimulation(); entry.widget->startSimulation();
entry.widget->updateStatus(entry.simulation->status());
ArenaSimulation* sim = entry.simulation.get(); ArenaSimulation* sim = entry.simulation.get();
entry.worker = std::thread([sim]() { sim->run(); }); entry.worker = std::thread([sim]() { sim->run(); });
updateStartAllButton(); updateButtons();
} }
void BalancingWindow::updateStartAllButton() void BalancingWindow::updateButtons()
{ {
bool anyRunning = false;
bool allRunning = true;
for (ArenaEntry& entry : m_arenas) for (ArenaEntry& entry : m_arenas)
{ {
if (!entry.worker.joinable()) if (entry.worker.joinable() && !entry.simulation->status().finished)
{ {
m_startAllButton->setEnabled(true); anyRunning = true;
return; }
else
{
allRunning = false;
} }
} }
m_startAllButton->setEnabled(false); if (m_arenas.empty())
{
allRunning = false;
}
m_reloadButton->setEnabled(!anyRunning);
m_startAllButton->setEnabled(!allRunning);
} }

View File

@@ -1,10 +1,12 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <string>
#include <thread> #include <thread>
#include <vector> #include <vector>
#include <QPushButton> #include <QPushButton>
#include <QScrollArea>
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
@@ -19,26 +21,38 @@ class BalancingWindow : public QWidget
public: public:
BalancingWindow(const BalancingConfig& balancingConfig, BalancingWindow(const BalancingConfig& balancingConfig,
const GameConfig& gameConfig, GameConfig gameConfig,
const std::string& configDir,
const std::string& balancingConfigPath,
QWidget* parent = nullptr); QWidget* parent = nullptr);
~BalancingWindow() override; ~BalancingWindow() override;
private slots: private slots:
void pollStatuses(); void pollStatuses();
void reloadConfig();
void startAll(); void startAll();
void startArena(int index); void startArena(int index);
private: private:
void updateStartAllButton(); void populateArenas(const BalancingConfig& balancingConfig);
void stopAllArenas();
void updateButtons();
struct ArenaEntry struct ArenaEntry
{ {
ArenaConfig config;
std::unique_ptr<ArenaSimulation> simulation; std::unique_ptr<ArenaSimulation> simulation;
std::thread worker; std::thread worker;
ArenaWidget* widget; ArenaWidget* widget;
}; };
std::vector<ArenaEntry> m_arenas; std::vector<ArenaEntry> m_arenas;
GameConfig m_gameConfig;
std::string m_configDir;
std::string m_balancingConfigPath;
unsigned int m_nextSeed;
QPushButton* m_reloadButton;
QPushButton* m_startAllButton; QPushButton* m_startAllButton;
QScrollArea* m_scrollArea;
QTimer* m_pollTimer; QTimer* m_pollTimer;
}; };

View File

@@ -27,7 +27,7 @@ int main(int argc, char* argv[])
GameConfig gameConfig = ConfigLoader::loadFromDirectory(CONFIG_DIR); GameConfig gameConfig = ConfigLoader::loadFromDirectory(CONFIG_DIR);
BalancingConfig balancingConfig = loadBalancingConfig(BALANCING_CONFIG); BalancingConfig balancingConfig = loadBalancingConfig(BALANCING_CONFIG);
BalancingWindow window(balancingConfig, gameConfig); BalancingWindow window(balancingConfig, std::move(gameConfig), CONFIG_DIR, BALANCING_CONFIG);
window.show(); window.show();
return application.exec(); return application.exec();