410 lines
12 KiB
C++
410 lines
12 KiB
C++
#include "BalancingWindow.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <QFile>
|
|
#include <QHBoxLayout>
|
|
#include <QMessageBox>
|
|
#include <QTextStream>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "ConfigLoader.h"
|
|
#include "InspectWindow.h"
|
|
#include "VisualsLoader.h"
|
|
|
|
namespace
|
|
{
|
|
// Escapes characters that would break a markdown table cell.
|
|
QString escapeCell(const QString& text)
|
|
{
|
|
QString escaped = text;
|
|
escaped.replace("|", "\\|");
|
|
return escaped;
|
|
}
|
|
|
|
QString stateText(ArenaWidget::State state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case ArenaWidget::State::NotStarted:
|
|
return QStringLiteral("not started");
|
|
case ArenaWidget::State::Running:
|
|
return QStringLiteral("running");
|
|
case ArenaWidget::State::Ended:
|
|
return QStringLiteral("ended");
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
// Team column header: "[WON] Name — threat N", matching the arena widget.
|
|
QString teamHeaderCell(const ArenaStatus& status, int teamIndex)
|
|
{
|
|
const ArenaStatus::TeamStatus& team = status.teams[teamIndex];
|
|
QString header = QString::fromStdString(team.name);
|
|
if (status.finished && status.winnerTeam == teamIndex)
|
|
{
|
|
header = QStringLiteral("[WON] ") + header;
|
|
}
|
|
header += QStringLiteral(" - threat %1").arg(QString::number(team.threatLevel, 'f', 0));
|
|
header += QStringLiteral(" - EHP %1").arg(QString::fromStdString(team.ehpPercentText()));
|
|
return escapeCell(header);
|
|
}
|
|
|
|
// Single entry line: "surviving/total DisplayName [L<level>]", matching the arena widget.
|
|
QString entryCell(const ArenaStatus::Entry& entry)
|
|
{
|
|
QString cell = QString("%1/%2 %3")
|
|
.arg(entry.surviving)
|
|
.arg(entry.total)
|
|
.arg(QString::fromStdString(entry.displayName));
|
|
if (entry.level.has_value())
|
|
{
|
|
cell += QStringLiteral(" L%1").arg(entry.level.value());
|
|
}
|
|
return escapeCell(cell);
|
|
}
|
|
}
|
|
|
|
BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|
GameConfig gameConfig,
|
|
const std::string& configDir,
|
|
const std::string& balancingConfigPath,
|
|
QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_gameConfig(std::move(gameConfig))
|
|
, m_configDir(configDir)
|
|
, m_balancingConfigPath(balancingConfigPath)
|
|
, m_nextSeed(0)
|
|
, m_inspectWindow(nullptr)
|
|
, m_inspectedArenaIndex(-1)
|
|
{
|
|
m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
|
setWindowTitle(tr("DotaFactory — Balancing Tool"));
|
|
resize(800, 600);
|
|
|
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
QHBoxLayout* buttonRow = new QHBoxLayout();
|
|
m_reloadButton = new QPushButton(tr("Reload Config"), this);
|
|
m_startAllButton = new QPushButton(tr("Start All"), this);
|
|
m_logButton = new QPushButton(tr("Log"), this);
|
|
buttonRow->addWidget(m_reloadButton);
|
|
buttonRow->addWidget(m_startAllButton);
|
|
buttonRow->addWidget(m_logButton);
|
|
buttonRow->addStretch();
|
|
mainLayout->addLayout(buttonRow);
|
|
|
|
connect(m_reloadButton, &QPushButton::clicked, this, &BalancingWindow::reloadConfig);
|
|
connect(m_startAllButton, &QPushButton::clicked, this, &BalancingWindow::startAll);
|
|
connect(m_logButton, &QPushButton::clicked, this, &BalancingWindow::writeLog);
|
|
|
|
m_scrollArea = new QScrollArea(this);
|
|
m_scrollArea->setWidgetResizable(true);
|
|
mainLayout->addWidget(m_scrollArea);
|
|
|
|
populateArenas(balancingConfig);
|
|
|
|
m_pollTimer = new QTimer(this);
|
|
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
|
m_pollTimer->start(100);
|
|
|
|
registerForEvents();
|
|
}
|
|
|
|
BalancingWindow::~BalancingWindow()
|
|
{
|
|
unregisterForEvents();
|
|
|
|
m_pollTimer->stop();
|
|
if (m_inspectWindow)
|
|
{
|
|
delete m_inspectWindow;
|
|
m_inspectWindow = nullptr;
|
|
}
|
|
m_inspectedSim.reset();
|
|
stopAllArenas();
|
|
}
|
|
|
|
void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
|
|
{
|
|
stopAllArenas();
|
|
m_arenas.clear();
|
|
|
|
QWidget* scrollContent = new QWidget(m_scrollArea);
|
|
QVBoxLayout* contentLayout = new QVBoxLayout(scrollContent);
|
|
contentLayout->setSpacing(8);
|
|
contentLayout->setContentsMargins(8, 8, 8, 8);
|
|
|
|
for (const ArenaConfig& arenaConfig : balancingConfig.arenas)
|
|
{
|
|
int index = static_cast<int>(m_arenas.size());
|
|
|
|
ArenaEntry entry;
|
|
entry.config = arenaConfig;
|
|
entry.simulation = std::make_unique<ArenaSimulation>(
|
|
m_gameConfig, arenaConfig, m_nextSeed++);
|
|
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
|
|
contentLayout->addWidget(entry.widget);
|
|
|
|
entry.widget->updateStatus(entry.simulation->status());
|
|
|
|
m_arenas.push_back(std::move(entry));
|
|
}
|
|
|
|
contentLayout->addStretch();
|
|
m_scrollArea->setWidget(scrollContent);
|
|
|
|
updateButtons();
|
|
}
|
|
|
|
void BalancingWindow::stopAllArenas()
|
|
{
|
|
for (ArenaEntry& entry : m_arenas)
|
|
{
|
|
entry.simulation->requestStop();
|
|
}
|
|
for (ArenaEntry& entry : m_arenas)
|
|
{
|
|
if (entry.worker.joinable())
|
|
{
|
|
entry.worker.join();
|
|
}
|
|
}
|
|
}
|
|
|
|
void BalancingWindow::pollStatuses()
|
|
{
|
|
for (ArenaEntry& entry : m_arenas)
|
|
{
|
|
if (entry.worker.joinable())
|
|
{
|
|
const ArenaStatus status = entry.simulation->status();
|
|
entry.widget->updateStatus(status);
|
|
}
|
|
}
|
|
|
|
if (m_inspectedSim && m_inspectedArenaIndex >= 0)
|
|
{
|
|
const ArenaStatus status = m_inspectedSim->status();
|
|
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->updateStatus(status);
|
|
}
|
|
|
|
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, tr("Reload Failed"), QString::fromStdString(e.what()));
|
|
}
|
|
}
|
|
|
|
void BalancingWindow::startAll()
|
|
{
|
|
for (int i = 0; i < static_cast<int>(m_arenas.size()); ++i)
|
|
{
|
|
startArena(i);
|
|
}
|
|
}
|
|
|
|
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event)
|
|
{
|
|
startArena(event->arenaIndex);
|
|
}
|
|
|
|
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event)
|
|
{
|
|
inspectArena(event->arenaIndex);
|
|
}
|
|
|
|
void BalancingWindow::handleEvent(std::shared_ptr<const InspectWindowClosedEvent> /*event*/)
|
|
{
|
|
closeInspectWindow();
|
|
}
|
|
|
|
void BalancingWindow::startArena(int index)
|
|
{
|
|
ArenaEntry& entry = m_arenas[index];
|
|
if (entry.worker.joinable())
|
|
{
|
|
entry.simulation->requestStop();
|
|
entry.worker.join();
|
|
}
|
|
entry.simulation = std::make_unique<ArenaSimulation>(
|
|
m_gameConfig, entry.config, m_nextSeed++);
|
|
entry.widget->startSimulation();
|
|
entry.widget->updateStatus(entry.simulation->status());
|
|
ArenaSimulation* sim = entry.simulation.get();
|
|
entry.worker = std::thread([sim]() { sim->run(); });
|
|
updateButtons();
|
|
}
|
|
|
|
void BalancingWindow::inspectArena(int index)
|
|
{
|
|
if (m_inspectWindow)
|
|
{
|
|
delete m_inspectWindow;
|
|
m_inspectWindow = nullptr;
|
|
|
|
if (m_inspectedSim && m_inspectedArenaIndex >= 0
|
|
&& !m_inspectedSim->isFinished())
|
|
{
|
|
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey();
|
|
}
|
|
m_inspectedSim.reset();
|
|
m_inspectedArenaIndex = -1;
|
|
}
|
|
|
|
ArenaEntry& entry = m_arenas[static_cast<std::size_t>(index)];
|
|
|
|
if (entry.worker.joinable())
|
|
{
|
|
entry.simulation->requestStop();
|
|
entry.worker.join();
|
|
}
|
|
|
|
m_inspectedSim = std::make_unique<ArenaSimulation>(
|
|
m_gameConfig, entry.config, m_nextSeed++);
|
|
m_inspectedArenaIndex = index;
|
|
|
|
entry.widget->resetToGrey();
|
|
entry.widget->startSimulation();
|
|
entry.widget->updateStatus(m_inspectedSim->status());
|
|
|
|
m_inspectWindow = new InspectWindow(
|
|
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
|
|
|
setMainControlsEnabled(false);
|
|
m_inspectWindow->show();
|
|
}
|
|
|
|
void BalancingWindow::closeInspectWindow()
|
|
{
|
|
if (!m_inspectWindow)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_inspectWindow->deleteLater();
|
|
m_inspectWindow = nullptr;
|
|
|
|
if (m_inspectedArenaIndex >= 0 && m_inspectedSim)
|
|
{
|
|
if (!m_inspectedSim->isFinished())
|
|
{
|
|
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey();
|
|
}
|
|
}
|
|
|
|
m_inspectedSim.reset();
|
|
m_inspectedArenaIndex = -1;
|
|
setMainControlsEnabled(true);
|
|
updateButtons();
|
|
}
|
|
|
|
void BalancingWindow::setMainControlsEnabled(bool enabled)
|
|
{
|
|
m_reloadButton->setEnabled(enabled);
|
|
m_startAllButton->setEnabled(enabled);
|
|
m_logButton->setEnabled(enabled);
|
|
for (ArenaEntry& entry : m_arenas)
|
|
{
|
|
for (QPushButton* btn : entry.widget->findChildren<QPushButton*>())
|
|
{
|
|
btn->setEnabled(enabled);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BalancingWindow::updateButtons()
|
|
{
|
|
if (m_inspectWindow)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool anyRunning = false;
|
|
bool allRunning = true;
|
|
for (ArenaEntry& entry : m_arenas)
|
|
{
|
|
if (entry.worker.joinable() && !entry.simulation->status().finished)
|
|
{
|
|
anyRunning = true;
|
|
}
|
|
else
|
|
{
|
|
allRunning = false;
|
|
}
|
|
}
|
|
if (m_arenas.empty())
|
|
{
|
|
allRunning = false;
|
|
}
|
|
|
|
m_reloadButton->setEnabled(!anyRunning);
|
|
m_startAllButton->setEnabled(!allRunning);
|
|
m_logButton->setEnabled(true);
|
|
}
|
|
|
|
void BalancingWindow::writeLog()
|
|
{
|
|
QFile file(QStringLiteral("balancing_log.md"));
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
|
|
{
|
|
QMessageBox::warning(this, tr("Log Failed"),
|
|
tr("Could not open balancing_log.md for writing."));
|
|
return;
|
|
}
|
|
|
|
QTextStream out(&file);
|
|
out.setCodec("UTF-8");
|
|
|
|
out << "# Balancing Log\n";
|
|
|
|
for (const ArenaEntry& entry : m_arenas)
|
|
{
|
|
const ArenaStatus& status = entry.widget->getLastStatus();
|
|
const ArenaWidget::State state = entry.widget->getState();
|
|
|
|
QString stateSuffix = stateText(state);
|
|
if (state == ArenaWidget::State::Ended)
|
|
{
|
|
stateSuffix += QStringLiteral(", %1 s")
|
|
.arg(QString::number(status.durationSeconds, 'f', 1));
|
|
}
|
|
|
|
out << "\n## Arena: " << QString::fromStdString(entry.config.name)
|
|
<< " (" << stateSuffix << ")\n\n";
|
|
|
|
out << "| " << teamHeaderCell(status, 0) << " | "
|
|
<< teamHeaderCell(status, 1) << " |\n";
|
|
out << "|---|---|\n";
|
|
|
|
const std::size_t rowCount = std::max(status.teams[0].entries.size(),
|
|
status.teams[1].entries.size());
|
|
for (std::size_t row = 0; row < rowCount; ++row)
|
|
{
|
|
QString leftCell;
|
|
if (row < status.teams[0].entries.size())
|
|
{
|
|
leftCell = entryCell(status.teams[0].entries[row]);
|
|
}
|
|
QString rightCell;
|
|
if (row < status.teams[1].entries.size())
|
|
{
|
|
rightCell = entryCell(status.teams[1].entries[row]);
|
|
}
|
|
out << "| " << leftCell << " | " << rightCell << " |\n";
|
|
}
|
|
}
|
|
}
|