add balancing tool target

This commit is contained in:
2026-05-03 11:17:54 +02:00
parent 5153129909
commit a4427f7f67
12 changed files with 1164 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
#include "ArenaButton.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
ArenaButton::ArenaButton(const std::string& arenaName, QWidget* parent)
: QFrame(parent)
, m_wasFinished(false)
{
buildLayout(arenaName);
setFrameStyle(QFrame::Box | QFrame::Plain);
setLineWidth(2);
setStyleSheet("ArenaButton { border: 2px solid #3366ff; padding: 8px; }");
}
void ArenaButton::buildLayout(const std::string& arenaName)
{
QVBoxLayout* outerLayout = new QVBoxLayout(this);
outerLayout->setContentsMargins(8, 8, 8, 8);
outerLayout->setSpacing(4);
m_titleLabel = new QLabel(QString::fromStdString(arenaName), this);
QFont titleFont = m_titleLabel->font();
titleFont.setBold(true);
titleFont.setPointSize(titleFont.pointSize() + 2);
m_titleLabel->setFont(titleFont);
outerLayout->addWidget(m_titleLabel);
QHBoxLayout* teamsLayout = new QHBoxLayout();
teamsLayout->setSpacing(16);
// Team 1 column.
QVBoxLayout* team1Layout = new QVBoxLayout();
m_team1Header = new QLabel(this);
QFont headerFont = m_team1Header->font();
headerFont.setBold(true);
m_team1Header->setFont(headerFont);
team1Layout->addWidget(m_team1Header);
m_team1Content = new QLabel(this);
team1Layout->addWidget(m_team1Content);
team1Layout->addStretch();
teamsLayout->addLayout(team1Layout);
// Team 2 column.
QVBoxLayout* team2Layout = new QVBoxLayout();
m_team2Header = new QLabel(this);
m_team2Header->setFont(headerFont);
team2Layout->addWidget(m_team2Header);
m_team2Content = new QLabel(this);
team2Layout->addWidget(m_team2Content);
team2Layout->addStretch();
teamsLayout->addLayout(team2Layout);
outerLayout->addLayout(teamsLayout);
}
void ArenaButton::updateStatus(const ArenaStatus& status)
{
for (int ti = 0; ti < 2; ++ti)
{
const ArenaStatus::TeamStatus& team = status.teams[ti];
QLabel* header = (ti == 0) ? m_team1Header : m_team2Header;
QLabel* content = (ti == 0) ? m_team1Content : m_team2Content;
header->setText(QString::fromStdString(team.name));
QString lines;
for (const ArenaStatus::Entry& entry : team.entries)
{
if (!lines.isEmpty())
{
lines += "\n";
}
lines += QString("%1/%2 %3 L%4")
.arg(entry.surviving)
.arg(entry.total)
.arg(QString::fromStdString(entry.displayName))
.arg(entry.level);
}
content->setText(lines);
}
if (status.finished && !m_wasFinished)
{
m_wasFinished = true;
setStyleSheet("ArenaButton { border: 2px solid #33cc33; padding: 8px; }");
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <string>
#include <vector>
#include <QFrame>
#include <QLabel>
#include "ArenaSimulation.h"
class ArenaButton : public QFrame
{
Q_OBJECT
public:
explicit ArenaButton(const std::string& arenaName, QWidget* parent = nullptr);
void updateStatus(const ArenaStatus& status);
private:
void buildLayout(const std::string& arenaName);
QLabel* m_titleLabel;
QLabel* m_team1Header;
QLabel* m_team2Header;
QLabel* m_team1Content;
QLabel* m_team2Content;
bool m_wasFinished;
};

View File

@@ -0,0 +1,439 @@
#include "ArenaSimulation.h"
#include <algorithm>
#include <cassert>
#include <QVector2D>
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "CombatSystem.h"
#include "ScrapSystem.h"
#include "Ship.h"
#include "ShipSystem.h"
#include "ShipsConfig.h"
#include "StationsConfig.h"
#include "SurfaceMask.h"
ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
ArenaConfig arenaConfig,
unsigned int seed)
: m_gameConfig(gameConfig)
, m_arenaConfig(std::move(arenaConfig))
, m_rng(seed)
, m_currentTick(0)
, m_nextId(1)
, m_beltSystem(1.0)
, m_team1HqId(kInvalidEntityId)
, m_team2HqId(kInvalidEntityId)
, m_finished(false)
, m_stopRequested(false)
{
m_buildingSystem = std::make_unique<BuildingSystem>(
m_gameConfig,
m_beltSystem,
[this]() { return allocateId(); },
[](int) {},
[](const std::string&, QVector2D) {},
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(
m_gameConfig, [this]() { return allocateId(); });
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
placeStructures();
spawnShips();
m_shipSystem->triggerRallyDeparture();
updateStatus();
}
ArenaSimulation::~ArenaSimulation() = default;
EntityId ArenaSimulation::allocateId()
{
return m_nextId++;
}
void ArenaSimulation::placeStructures()
{
const int totalWidth = m_arenaConfig.playerBufferWidth
+ m_arenaConfig.contestZoneWidth
+ m_arenaConfig.enemyBufferWidth;
const int midY = m_arenaConfig.heightTiles / 2;
// Team 1 HQ at left edge, placed as Hq (enemy ships target Hq).
{
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::East);
const int anchorX = 0;
const int anchorY = midY - hqParsed.footprint.height() / 2;
const float hp = static_cast<float>(
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
m_team1HqId = m_buildingSystem->placeImmediate(
BuildingType::Hq,
m_gameConfig.stations.hq.surfaceMask,
QPoint(anchorX, anchorY),
Rotation::East, hp, hp);
}
// Team 2 HQ at right edge, placed as EnemyDefenceStation (player ships target these).
// No weapon — it's just a destructible target.
{
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::West);
const int anchorX = totalWidth - hqParsed.footprint.width();
const int anchorY = midY - hqParsed.footprint.height() / 2;
const float hp = static_cast<float>(
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
m_team2HqId = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_gameConfig.stations.hq.surfaceMask,
QPoint(anchorX, anchorY),
Rotation::West, hp, hp);
}
// Team 1 defence stations (PlayerDefenceStation — targeted by team 2).
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
{
float hp = 0.0f;
StationWeapon weapon;
weapon.cooldownTicks = 0.0f;
const double lv = static_cast<double>(entry.level);
if (entry.stationType == "player_station")
{
hp = static_cast<float>(
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
}
else
{
hp = static_cast<float>(
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const EntityId stationId = m_buildingSystem->placeImmediate(
BuildingType::PlayerDefenceStation,
m_gameConfig.stations.playerStation.surfaceMask,
entry.position,
Rotation::East, hp, hp);
m_buildingSystem->initStationWeapon(stationId, weapon);
}
// Team 2 defence stations (EnemyDefenceStation — targeted by team 1).
for (const ArenaStationEntry& entry : m_arenaConfig.teams[1].stations)
{
float hp = 0.0f;
StationWeapon weapon;
weapon.cooldownTicks = 0.0f;
const double lv = static_cast<double>(entry.level);
if (entry.stationType == "player_station")
{
hp = static_cast<float>(
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
}
else
{
hp = static_cast<float>(
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const EntityId stationId = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_gameConfig.stations.enemyStation.surfaceMask,
entry.position,
Rotation::East, hp, hp);
m_buildingSystem->initStationWeapon(stationId, weapon);
}
}
void ArenaSimulation::spawnShips()
{
const int contestStart = m_arenaConfig.playerBufferWidth;
const int team2Start = contestStart + m_arenaConfig.contestZoneWidth;
const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth;
std::uniform_real_distribution<float> yDist(0.0f,
static_cast<float>(m_arenaConfig.heightTiles));
// Team 1: isEnemy=false, spawn in player buffer zone.
{
std::uniform_real_distribution<float> xDist(0.0f,
static_cast<float>(m_arenaConfig.playerBufferWidth));
for (const ArenaShipEntry& entry : m_arenaConfig.teams[0].ships)
{
for (int i = 0; i < entry.count; ++i)
{
const QVector2D pos(xDist(m_rng), yDist(m_rng));
m_shipSystem->spawn(entry.schematicId, entry.level, pos, false);
}
}
}
// Team 2: isEnemy=true, spawn in enemy buffer zone.
{
std::uniform_real_distribution<float> xDist(
static_cast<float>(team2Start),
static_cast<float>(totalWidth));
for (const ArenaShipEntry& entry : m_arenaConfig.teams[1].ships)
{
for (int i = 0; i < entry.count; ++i)
{
const QVector2D pos(xDist(m_rng), yDist(m_rng));
m_shipSystem->spawn(entry.schematicId, entry.level, pos, true);
}
}
}
}
void ArenaSimulation::run()
{
while (!m_finished && !m_stopRequested.load(std::memory_order_relaxed))
{
tick();
}
{
std::lock_guard<std::mutex> lock(m_statusMutex);
m_status.finished = true;
}
}
void ArenaSimulation::requestStop()
{
m_stopRequested.store(true, std::memory_order_relaxed);
}
ArenaStatus ArenaSimulation::status() const
{
std::lock_guard<std::mutex> lock(m_statusMutex);
return m_status;
}
void ArenaSimulation::tick()
{
// Ship behavior systems (tick step 7).
m_shipSystem->clearMovementIntents();
m_shipSystem->tickHomeReturn();
m_shipSystem->tickThreatResponse(*m_buildingSystem);
m_shipSystem->tickRepairBehavior(*m_buildingSystem);
m_shipSystem->tickScrapCollector(*m_scrapSystem, *m_buildingSystem);
// Combat resolution (tick step 8).
std::vector<FireEvent> fireEvents;
m_combatSystem->tick(m_currentTick, *m_shipSystem, *m_buildingSystem, fireEvents);
m_combatSystem->applyPendingDamage(m_currentTick, *m_shipSystem, *m_buildingSystem);
// Deaths (tick step 9, simplified).
tickDeaths();
// Movement (tick step 10).
m_shipSystem->tickMovement();
// Scrap despawn (tick step 11).
m_scrapSystem->tickDespawn(m_currentTick);
++m_currentTick;
if (m_currentTick % 30 == 0)
{
updateStatus();
}
}
void ArenaSimulation::tickDeaths()
{
// Dead ships.
std::vector<EntityId> deadShipIds;
m_shipSystem->forEach([&deadShipIds](Ship& s)
{
if (s.hp <= 0.0f)
{
deadShipIds.push_back(s.id);
}
});
for (EntityId deadId : deadShipIds)
{
const Ship* s = m_shipSystem->findShip(deadId);
if (!s)
{
continue;
}
for (const ShipDef& def : m_gameConfig.ships.ships)
{
if (def.id == s->schematicId && def.loot.scrapDrop > 0)
{
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_gameConfig.world.scrapDespawnSeconds);
m_scrapSystem->spawn(s->position, def.loot.scrapDrop, despawnAt);
break;
}
}
m_shipSystem->despawn(deadId);
}
// Dead buildings (HQ and defence stations).
std::vector<EntityId> deadBuildingIds;
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.hp <= 0.0f
&& (b.type == BuildingType::Hq
|| b.type == BuildingType::PlayerDefenceStation
|| b.type == BuildingType::EnemyDefenceStation))
{
deadBuildingIds.push_back(b.id);
}
}
for (EntityId deadId : deadBuildingIds)
{
m_buildingSystem->removeBuilding(deadId);
}
// Check end conditions.
const bool team1HqGone =
(m_buildingSystem->findBuilding(m_team1HqId) == nullptr);
const bool team2HqGone =
(m_buildingSystem->findBuilding(m_team2HqId) == nullptr);
if (team1HqGone || team2HqGone)
{
m_finished = true;
updateStatus();
return;
}
// Check if all ships of one team are destroyed.
bool team1HasShips = false;
bool team2HasShips = false;
m_shipSystem->forEach([&team1HasShips, &team2HasShips](Ship& s)
{
if (s.isEnemy)
{
team2HasShips = true;
}
else
{
team1HasShips = true;
}
});
if (!team1HasShips || !team2HasShips)
{
m_finished = true;
updateStatus();
}
}
void ArenaSimulation::updateStatus()
{
ArenaStatus newStatus;
newStatus.finished = m_finished;
for (int ti = 0; ti < 2; ++ti)
{
ArenaStatus::TeamStatus& teamStatus = newStatus.teams[ti];
teamStatus.name = m_arenaConfig.teams[ti].name;
// HQ entry (always first).
{
ArenaStatus::Entry hqEntry;
hqEntry.displayName = "HQ";
hqEntry.level = 1;
hqEntry.total = 1;
const EntityId hqId = (ti == 0) ? m_team1HqId : m_team2HqId;
hqEntry.surviving = (m_buildingSystem->findBuilding(hqId) != nullptr) ? 1 : 0;
teamStatus.entries.push_back(hqEntry);
}
// Ship entries.
for (const ArenaShipEntry& shipEntry : m_arenaConfig.teams[ti].ships)
{
ArenaStatus::Entry entry;
entry.displayName = shipEntry.schematicId;
entry.level = shipEntry.level;
entry.total = shipEntry.count;
int surviving = 0;
const bool isEnemyTeam = (ti == 1);
m_shipSystem->forEach(
[&surviving, &shipEntry, isEnemyTeam](Ship& s)
{
if (s.isEnemy == isEnemyTeam
&& s.schematicId == shipEntry.schematicId
&& s.level == shipEntry.level
&& s.hp > 0.0f)
{
++surviving;
}
});
entry.surviving = surviving;
teamStatus.entries.push_back(entry);
}
// Station entries.
for (std::size_t si = 0; si < m_arenaConfig.teams[ti].stations.size(); ++si)
{
const ArenaStationEntry& stationEntry = m_arenaConfig.teams[ti].stations[si];
ArenaStatus::Entry entry;
entry.displayName = "Station";
entry.level = stationEntry.level;
entry.total = 1;
// Count surviving stations of this team at this position.
const BuildingType expectedType = (ti == 0)
? BuildingType::PlayerDefenceStation
: BuildingType::EnemyDefenceStation;
int surviving = 0;
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.type == expectedType && b.anchor == stationEntry.position)
{
surviving = 1;
break;
}
}
entry.surviving = surviving;
teamStatus.entries.push_back(entry);
}
}
std::lock_guard<std::mutex> lock(m_statusMutex);
m_status = newStatus;
}

View File

@@ -0,0 +1,84 @@
#pragma once
#include <atomic>
#include <memory>
#include <mutex>
#include <random>
#include <string>
#include <vector>
#include "BalancingConfig.h"
#include "BeltSystem.h"
#include "EntityId.h"
#include "FireEvent.h"
#include "GameConfig.h"
#include "Tick.h"
class BuildingSystem;
class CombatSystem;
class ShipSystem;
class ScrapSystem;
struct ArenaStatus
{
struct Entry
{
std::string displayName;
int level;
int total;
int surviving;
};
struct TeamStatus
{
std::string name;
std::vector<Entry> entries; // HQ first, then ships, then stations
};
TeamStatus teams[2];
bool finished = false;
};
class ArenaSimulation
{
public:
ArenaSimulation(const GameConfig& gameConfig,
ArenaConfig arenaConfig,
unsigned int seed = 0);
~ArenaSimulation();
void run();
void requestStop();
ArenaStatus status() const;
private:
EntityId allocateId();
void placeStructures();
void spawnShips();
void tick();
void tickDeaths();
void updateStatus();
const GameConfig& m_gameConfig;
ArenaConfig m_arenaConfig;
std::mt19937 m_rng;
Tick m_currentTick;
EntityId m_nextId;
BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<CombatSystem> m_combatSystem;
std::unique_ptr<ScrapSystem> m_scrapSystem;
EntityId m_team1HqId;
EntityId m_team2HqId;
bool m_finished;
std::atomic<bool> m_stopRequested;
mutable std::mutex m_statusMutex;
ArenaStatus m_status;
};

View File

@@ -0,0 +1,166 @@
#include "BalancingConfig.h"
#include <stdexcept>
#include <string>
#include "toml.hpp"
namespace
{
std::runtime_error makeError(const std::string& path, const std::string& why)
{
return std::runtime_error("balancing.toml: '" + path + "' " + why);
}
template <typename NodeView>
int64_t requireInt(NodeView node, const std::string& path)
{
const std::optional<int64_t> value = node.template value<int64_t>();
if (!value)
{
throw makeError(path, "missing or not an integer");
}
return *value;
}
template <typename NodeView>
std::string requireString(NodeView node, const std::string& path)
{
const std::optional<std::string> value = node.template value<std::string>();
if (!value)
{
throw makeError(path, "missing or not a string");
}
return *value;
}
} // namespace
BalancingConfig loadBalancingConfig(const std::string& path)
{
toml::table root;
try
{
root = toml::parse_file(path);
}
catch (const toml::parse_error& e)
{
throw std::runtime_error(std::string("balancing.toml: parse error: ") + e.what());
}
const toml::array* arenaArray = root["arena"].as_array();
if (!arenaArray)
{
throw makeError("arena", "missing or not an array of tables");
}
BalancingConfig config;
for (std::size_t ai = 0; ai < arenaArray->size(); ++ai)
{
const toml::table* arenaTbl = (*arenaArray)[ai].as_table();
if (!arenaTbl)
{
throw makeError("arena[" + std::to_string(ai) + "]", "not a table");
}
const std::string prefix = "arena[" + std::to_string(ai) + "]";
ArenaConfig arena;
arena.name = requireString((*arenaTbl)["name"], prefix + ".name");
arena.heightTiles = static_cast<int>(
requireInt((*arenaTbl)["height_tiles"], prefix + ".height_tiles"));
arena.playerBufferWidth = static_cast<int>(
requireInt((*arenaTbl)["player_buffer_width"], prefix + ".player_buffer_width"));
arena.contestZoneWidth = static_cast<int>(
requireInt((*arenaTbl)["contest_zone_width"], prefix + ".contest_zone_width"));
arena.enemyBufferWidth = static_cast<int>(
requireInt((*arenaTbl)["enemy_buffer_width"], prefix + ".enemy_buffer_width"));
const toml::array* teamArray = (*arenaTbl)["team"].as_array();
if (!teamArray || teamArray->size() != 2)
{
throw makeError(prefix + ".team", "must contain exactly 2 teams");
}
for (int ti = 0; ti < 2; ++ti)
{
const toml::table* teamTbl = (*teamArray)[static_cast<std::size_t>(ti)].as_table();
if (!teamTbl)
{
throw makeError(prefix + ".team[" + std::to_string(ti) + "]", "not a table");
}
const std::string tPrefix = prefix + ".team[" + std::to_string(ti) + "]";
ArenaTeamConfig& team = arena.teams[ti];
team.name = requireString((*teamTbl)["name"], tPrefix + ".name");
const toml::array* shipArray = (*teamTbl)["ship"].as_array();
if (shipArray)
{
for (std::size_t si = 0; si < shipArray->size(); ++si)
{
const toml::table* shipTbl = (*shipArray)[si].as_table();
if (!shipTbl)
{
throw makeError(tPrefix + ".ship[" + std::to_string(si) + "]",
"not a table");
}
const std::string sPrefix =
tPrefix + ".ship[" + std::to_string(si) + "]";
ArenaShipEntry entry;
entry.schematicId = requireString((*shipTbl)["schematic"],
sPrefix + ".schematic");
entry.level = static_cast<int>(
requireInt((*shipTbl)["level"], sPrefix + ".level"));
entry.count = static_cast<int>(
requireInt((*shipTbl)["count"], sPrefix + ".count"));
team.ships.push_back(entry);
}
}
const toml::array* stationArray = (*teamTbl)["station"].as_array();
if (stationArray)
{
for (std::size_t si = 0; si < stationArray->size(); ++si)
{
const toml::table* stationTbl = (*stationArray)[si].as_table();
if (!stationTbl)
{
throw makeError(tPrefix + ".station[" + std::to_string(si) + "]",
"not a table");
}
const std::string sPrefix =
tPrefix + ".station[" + std::to_string(si) + "]";
ArenaStationEntry entry;
entry.stationType = requireString((*stationTbl)["type"],
sPrefix + ".type");
entry.level = static_cast<int>(
requireInt((*stationTbl)["level"], sPrefix + ".level"));
entry.position = QPoint(
static_cast<int>(requireInt((*stationTbl)["x"], sPrefix + ".x")),
static_cast<int>(requireInt((*stationTbl)["y"], sPrefix + ".y")));
if (entry.stationType != "player_station"
&& entry.stationType != "enemy_station")
{
throw makeError(sPrefix + ".type",
"must be 'player_station' or 'enemy_station'");
}
team.stations.push_back(entry);
}
}
}
config.arenas.push_back(arena);
}
return config;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <string>
#include <vector>
#include <QPoint>
struct ArenaStationEntry
{
std::string stationType; // "player_station" or "enemy_station"
int level;
QPoint position;
};
struct ArenaShipEntry
{
std::string schematicId;
int level;
int count;
};
struct ArenaTeamConfig
{
std::string name;
std::vector<ArenaShipEntry> ships;
std::vector<ArenaStationEntry> stations;
};
struct ArenaConfig
{
std::string name;
int heightTiles;
int playerBufferWidth;
int contestZoneWidth;
int enemyBufferWidth;
ArenaTeamConfig teams[2];
};
struct BalancingConfig
{
std::vector<ArenaConfig> arenas;
};
BalancingConfig loadBalancingConfig(const std::string& path);

View File

@@ -0,0 +1,80 @@
#include "BalancingWindow.h"
#include <QScrollArea>
#include <QVBoxLayout>
BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
const GameConfig& gameConfig,
QWidget* parent)
: QWidget(parent)
{
setWindowTitle("DotaFactory — Balancing Tool");
resize(800, 600);
QVBoxLayout* mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
QScrollArea* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
QWidget* scrollContent = new QWidget(scrollArea);
QVBoxLayout* contentLayout = new QVBoxLayout(scrollContent);
contentLayout->setSpacing(8);
contentLayout->setContentsMargins(8, 8, 8, 8);
unsigned int seed = 0;
for (const ArenaConfig& arenaConfig : balancingConfig.arenas)
{
ArenaEntry entry;
entry.simulation = std::make_unique<ArenaSimulation>(
gameConfig, arenaConfig, seed++);
entry.button = new ArenaButton(arenaConfig.name, scrollContent);
contentLayout->addWidget(entry.button);
entry.button->updateStatus(entry.simulation->status());
m_arenas.push_back(std::move(entry));
}
contentLayout->addStretch();
scrollArea->setWidget(scrollContent);
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);
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
m_pollTimer->start(100);
}
BalancingWindow::~BalancingWindow()
{
m_pollTimer->stop();
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)
{
const ArenaStatus status = entry.simulation->status();
entry.button->updateStatus(status);
}
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <memory>
#include <thread>
#include <vector>
#include <QTimer>
#include <QWidget>
#include "ArenaButton.h"
#include "ArenaSimulation.h"
#include "BalancingConfig.h"
#include "GameConfig.h"
class BalancingWindow : public QWidget
{
Q_OBJECT
public:
BalancingWindow(const BalancingConfig& balancingConfig,
const GameConfig& gameConfig,
QWidget* parent = nullptr);
~BalancingWindow() override;
private slots:
void pollStatuses();
private:
struct ArenaEntry
{
std::unique_ptr<ArenaSimulation> simulation;
std::thread worker;
ArenaButton* button;
};
std::vector<ArenaEntry> m_arenas;
QTimer* m_pollTimer;
};

View File

@@ -0,0 +1,18 @@
SET(HDRS
${HDRS}
${CMAKE_CURRENT_SOURCE_DIR}/ArenaButton.h
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.h
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
PARENT_SCOPE
)
SET(SRCS
${SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ArenaButton.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ArenaSimulation.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BalancingConfig.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
PARENT_SCOPE
)

34
src/balancing/main.cpp Normal file
View File

@@ -0,0 +1,34 @@
#include <memory>
#include <QApplication>
#include "BalancingConfig.h"
#include "BalancingWindow.h"
#include "ConfigLoader.h"
#include "ConsoleLogger.h"
#include "logging.h"
#include "LogManager.h"
int main(int argc, char* argv[])
{
LogManager::getInstance()->addLogger(std::make_shared<ConsoleLogger>());
LogManager::getInstance()->setLoggingEnabled(true);
LOG_INFO("Balancing tool starting");
QApplication::setApplicationName("DotaFactory Balancing");
if (QSysInfo::windowsVersion() != QSysInfo::WV_None)
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
}
QApplication application(argc, argv);
GameConfig gameConfig = ConfigLoader::loadFromDirectory(CONFIG_DIR);
BalancingConfig balancingConfig = loadBalancingConfig(BALANCING_CONFIG);
BalancingWindow window(balancingConfig, gameConfig);
window.show();
return application.exec();
}