implement logging of arena states
This commit is contained in:
@@ -512,9 +512,13 @@ 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 "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. All buttons and controls in the main window are disabled while an arena is being inspected (REQ-BAL-UI-INSPECT).
|
- REQ-BAL-UI-WINDOW: On startup the tool displays a window containing a "Reload Config" button, a "Start All" button, and a "Log" 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. All buttons and controls in the main window are disabled while an arena is being inspected (REQ-BAL-UI-INSPECT).
|
||||||
- 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-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-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-LOG: The "Log" button is placed in the header to the right of the "Start All" button. It is enabled whenever the main window's controls are enabled — including while simulations are running (it captures a live snapshot) — and, like all main window controls, is disabled only while an arena is being inspected (REQ-BAL-UI-WINDOW). Clicking it writes the current state of every arena to a file named `balancing_log.md` in the tool's current working directory, replacing (overwriting) any previous content of that file. The log captures, for every arena in `balancing.toml` order, exactly the information shown in that arena's widget (REQ-BAL-UI-WIDGET) at the moment the button is clicked. Each arena is written as its own section:
|
||||||
|
- A heading with the arena name followed by the arena's current state — `not started`, `running`, or `ended` (corresponding to the widget border colors of REQ-BAL-UI-WIDGET-BORDER).
|
||||||
|
- A markdown table with one column per team (team 1 left, team 2 right). Each team's column header shows the team name — prefixed with `[WON]` when that team won, matching REQ-BAL-UI-WIDGET — and the team's accumulated threat level (REQ-BAL-UI-WIDGET).
|
||||||
|
- Below the header, each table row holds one of that team's entries, in the same order and text format as the widget (REQ-BAL-UI-WIDGET): the HQ first, then ship types, then defence stations, formatted `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries. When the two teams have different numbers of entries, the shorter column's remaining cells are left blank.
|
||||||
- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Below the threat level, the column shows 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` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries — for example `2/3 Fighter`, `1/1 HQ L1`, or `2/2 Enemy Station L3`. 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, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Below the threat level, the column shows 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` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries — for example `2/3 Fighter`, `1/1 HQ L1`, or `2/2 Enemy Station L3`. 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. 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-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.
|
||||||
|
|||||||
@@ -99,8 +99,23 @@ void ArenaWidget::resetToGrey()
|
|||||||
setStyleSheet("ArenaWidget { border: 2px solid #999999; padding: 8px; }");
|
setStyleSheet("ArenaWidget { border: 2px solid #999999; padding: 8px; }");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArenaWidget::State ArenaWidget::getState() const
|
||||||
|
{
|
||||||
|
if (m_wasFinished)
|
||||||
|
{
|
||||||
|
return State::Ended;
|
||||||
|
}
|
||||||
|
if (m_running)
|
||||||
|
{
|
||||||
|
return State::Running;
|
||||||
|
}
|
||||||
|
return State::NotStarted;
|
||||||
|
}
|
||||||
|
|
||||||
void ArenaWidget::updateStatus(const ArenaStatus& status)
|
void ArenaWidget::updateStatus(const ArenaStatus& status)
|
||||||
{
|
{
|
||||||
|
m_lastStatus = status;
|
||||||
|
|
||||||
for (int ti = 0; ti < 2; ++ti)
|
for (int ti = 0; ti < 2; ++ti)
|
||||||
{
|
{
|
||||||
const ArenaStatus::TeamStatus& team = status.teams[ti];
|
const ArenaStatus::TeamStatus& team = status.teams[ti];
|
||||||
|
|||||||
@@ -14,16 +14,27 @@ class ArenaWidget : public QFrame
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum class State
|
||||||
|
{
|
||||||
|
NotStarted,
|
||||||
|
Running,
|
||||||
|
Ended
|
||||||
|
};
|
||||||
|
|
||||||
ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent = nullptr);
|
ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent = nullptr);
|
||||||
|
|
||||||
void updateStatus(const ArenaStatus& status);
|
void updateStatus(const ArenaStatus& status);
|
||||||
void startSimulation();
|
void startSimulation();
|
||||||
void resetToGrey();
|
void resetToGrey();
|
||||||
|
|
||||||
|
const ArenaStatus& getLastStatus() const { return m_lastStatus; }
|
||||||
|
State getState() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildLayout(const std::string& arenaName);
|
void buildLayout(const std::string& arenaName);
|
||||||
|
|
||||||
int m_arenaIndex;
|
int m_arenaIndex;
|
||||||
|
ArenaStatus m_lastStatus;
|
||||||
QLabel* m_titleLabel;
|
QLabel* m_titleLabel;
|
||||||
QLabel* m_team1Header;
|
QLabel* m_team1Header;
|
||||||
QLabel* m_team2Header;
|
QLabel* m_team2Header;
|
||||||
|
|||||||
@@ -1,13 +1,69 @@
|
|||||||
#include "BalancingWindow.h"
|
#include "BalancingWindow.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QTextStream>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "InspectWindow.h"
|
#include "InspectWindow.h"
|
||||||
#include "VisualsLoader.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));
|
||||||
|
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,
|
BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
||||||
GameConfig gameConfig,
|
GameConfig gameConfig,
|
||||||
const std::string& configDir,
|
const std::string& configDir,
|
||||||
@@ -31,13 +87,16 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|||||||
QHBoxLayout* buttonRow = new QHBoxLayout();
|
QHBoxLayout* buttonRow = new QHBoxLayout();
|
||||||
m_reloadButton = new QPushButton(tr("Reload Config"), this);
|
m_reloadButton = new QPushButton(tr("Reload Config"), this);
|
||||||
m_startAllButton = new QPushButton(tr("Start All"), this);
|
m_startAllButton = new QPushButton(tr("Start All"), this);
|
||||||
|
m_logButton = new QPushButton(tr("Log"), this);
|
||||||
buttonRow->addWidget(m_reloadButton);
|
buttonRow->addWidget(m_reloadButton);
|
||||||
buttonRow->addWidget(m_startAllButton);
|
buttonRow->addWidget(m_startAllButton);
|
||||||
|
buttonRow->addWidget(m_logButton);
|
||||||
buttonRow->addStretch();
|
buttonRow->addStretch();
|
||||||
mainLayout->addLayout(buttonRow);
|
mainLayout->addLayout(buttonRow);
|
||||||
|
|
||||||
connect(m_reloadButton, &QPushButton::clicked, this, &BalancingWindow::reloadConfig);
|
connect(m_reloadButton, &QPushButton::clicked, this, &BalancingWindow::reloadConfig);
|
||||||
connect(m_startAllButton, &QPushButton::clicked, this, &BalancingWindow::startAll);
|
connect(m_startAllButton, &QPushButton::clicked, this, &BalancingWindow::startAll);
|
||||||
|
connect(m_logButton, &QPushButton::clicked, this, &BalancingWindow::writeLog);
|
||||||
|
|
||||||
m_scrollArea = new QScrollArea(this);
|
m_scrollArea = new QScrollArea(this);
|
||||||
m_scrollArea->setWidgetResizable(true);
|
m_scrollArea->setWidgetResizable(true);
|
||||||
@@ -255,6 +314,7 @@ void BalancingWindow::setMainControlsEnabled(bool enabled)
|
|||||||
{
|
{
|
||||||
m_reloadButton->setEnabled(enabled);
|
m_reloadButton->setEnabled(enabled);
|
||||||
m_startAllButton->setEnabled(enabled);
|
m_startAllButton->setEnabled(enabled);
|
||||||
|
m_logButton->setEnabled(enabled);
|
||||||
for (ArenaEntry& entry : m_arenas)
|
for (ArenaEntry& entry : m_arenas)
|
||||||
{
|
{
|
||||||
for (QPushButton* btn : entry.widget->findChildren<QPushButton*>())
|
for (QPushButton* btn : entry.widget->findChildren<QPushButton*>())
|
||||||
@@ -291,4 +351,50 @@ void BalancingWindow::updateButtons()
|
|||||||
|
|
||||||
m_reloadButton->setEnabled(!anyRunning);
|
m_reloadButton->setEnabled(!anyRunning);
|
||||||
m_startAllButton->setEnabled(!allRunning);
|
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();
|
||||||
|
|
||||||
|
out << "\n## Arena: " << QString::fromStdString(entry.config.name)
|
||||||
|
<< " (" << stateText(entry.widget->getState()) << ")\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";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ private slots:
|
|||||||
void pollStatuses();
|
void pollStatuses();
|
||||||
void reloadConfig();
|
void reloadConfig();
|
||||||
void startAll();
|
void startAll();
|
||||||
|
void writeLog();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void startArena(int index);
|
void startArena(int index);
|
||||||
@@ -72,6 +73,7 @@ private:
|
|||||||
unsigned int m_nextSeed;
|
unsigned int m_nextSeed;
|
||||||
QPushButton* m_reloadButton;
|
QPushButton* m_reloadButton;
|
||||||
QPushButton* m_startAllButton;
|
QPushButton* m_startAllButton;
|
||||||
|
QPushButton* m_logButton;
|
||||||
QScrollArea* m_scrollArea;
|
QScrollArea* m_scrollArea;
|
||||||
QTimer* m_pollTimer;
|
QTimer* m_pollTimer;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user