Files
dota_factory/src/balancing/BalancingWindow.cpp

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.getEhpPercentText()));
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->getStatus());
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->getStatus();
entry.widget->updateStatus(status);
}
}
if (m_inspectedSim && m_inspectedArenaIndex >= 0)
{
const ArenaStatus status = m_inspectedSim->getStatus();
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->getStatus());
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->getStatus());
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->getStatus().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";
}
}
}