feat(economy): add resource collection MVP module
Data-driven ResourceType registry, per-player EconomyLedger (caps, waste, income/expense), GatherNode (deposits, regen, worker slots) and the WorkerTrip collector state machine (select -> approach -> gather -> carry -> deposit), exposed to Squirrel and reachable cross-module via IEconomy in common. Adds economy unit tests (registry, ledger invariants, worker-trip fixtures, capability provider) plus a printable demo scenario, and wires the module into the module manifest and the test link list.
This commit is contained in:
1 parent
7e1d86a0e1
commit
0057cdfd63
17 files changed
+1350
No files matched your search
@@ -89,6 +89,8 @@ eve_declare_module(NAME database LAYER 0 SCRIPT Database
|
||||
THIRDPARTY poco_data poco)
|
||||
eve_declare_module(NAME rpg LIB EVRPG LAYER 0 SCRIPT RPG)
|
||||
eve_declare_module(NAME inventory LAYER 0 SCRIPT Inventory)
|
||||
eve_declare_module(NAME economy LAYER 0 SCRIPT Economy SLOT economy
|
||||
GROUP minimal 2d 3d web)
|
||||
# PlacementWorld.cpp includes data/JsonDocument.h and Poco JSON (save/load).
|
||||
# THIRDPARTY poco is required so MSVC compiles those TUs with
|
||||
# POCO_NO_AUTOMATIC_LIBS; otherwise the obj records a link of
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// EVEngine economy 模块 MVP 演示(eve run -r <本文件>)
|
||||
//
|
||||
// 演示内容:
|
||||
// 1) SC2 式矿物采集:一个双槽矿点 + 两个采集者,各自完成
|
||||
// "接近 -> 采集 -> 满载 -> 返回 -> 卸货入账" 的 WorkerTrip 循环;
|
||||
// 2) 满仓浪费:玩家持有上限 10,节点储量 25,超限部分记 waste 事件。
|
||||
//
|
||||
// 运行方式(无需窗口/Vulkan):
|
||||
// build/win32-debug/src/engine/eve.exe run -r examples/economy/economy_demo.nut
|
||||
|
||||
local eco = eve.Economy();
|
||||
|
||||
function pad(s, w) {
|
||||
while (s.len() < w) s += " ";
|
||||
return s;
|
||||
}
|
||||
|
||||
function dump_events(from) {
|
||||
while (eco.getEventCount() > from) {
|
||||
local i = from;
|
||||
local action = pad(eco.getEventAction(i), 6);
|
||||
print(format(" [event] %s %-10s x%d (player %d)\n",
|
||||
action, eco.getEventType(i), eco.getEventAmount(i),
|
||||
eco.getEventPlayer(i)));
|
||||
from++;
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
print("====================================================================\n");
|
||||
print(" EVEngine economy MVP demo\n");
|
||||
print("====================================================================\n");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 场景 1:双工人矿物采集(SC2 式 WorkerTrip)
|
||||
// ---------------------------------------------------------------------------
|
||||
print("\n[1] 矿物采集:矿点 30,双槽位,工人 A(载荷5, 速率1, 单程2t) + 工人 B(载荷3, 速率2, 单程3t)\n");
|
||||
|
||||
eco.registerResourceType("minerals", "stock", 100, "finite");
|
||||
eco.clearEvents();
|
||||
|
||||
local mine = eve.GatherNode("minerals", 30, 2, 0);
|
||||
local workerA = eve.Collector(5, 1, 2);
|
||||
local workerB = eve.Collector(3, 2, 3);
|
||||
|
||||
print(" 槽位: " + mine.freeSlots() + " 空闲\n");
|
||||
workerA.assign(mine);
|
||||
print(" A 进入矿点, 槽位: " + mine.freeSlots() + " 空闲\n");
|
||||
workerB.assign(mine);
|
||||
print(" B 进入矿点, 槽位: " + mine.freeSlots() + " 空闲\n");
|
||||
|
||||
local events = 0;
|
||||
local doneTick = -1;
|
||||
for (local t = 0; t < 300; ++t) {
|
||||
workerA.tick(1);
|
||||
workerB.tick(1);
|
||||
events = dump_events(events);
|
||||
if (workerA.isIdle() && workerB.isIdle()) {
|
||||
doneTick = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
print("\n 矿点采空于 t=" + doneTick + "\n");
|
||||
print(" A: 往返 " + workerA.trips() + " 次, 累计入账 " + workerA.totalGathered() + "\n");
|
||||
print(" B: 往返 " + workerB.trips() + " 次, 累计入账 " + workerB.totalGathered() + "\n");
|
||||
print(" 玩家1 矿物余额: " + eco.get(1, "minerals") + " / 上限 " + eco.getCap(1, "minerals") +
|
||||
", 浪费 " + eco.getWasted(1, "minerals") + "\n");
|
||||
print(" 矿点剩余: " + mine.amount() + "\n");
|
||||
|
||||
workerA.destroy();
|
||||
workerB.destroy();
|
||||
mine.destroy();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 场景 2:持有上限导致的浪费
|
||||
// ---------------------------------------------------------------------------
|
||||
print("\n[2] 满仓浪费:玩家持有上限 10,节点 25,载荷 5(第 3 次起全部浪费)\n");
|
||||
|
||||
eco.registerResourceType("gold", "stock", 10, "finite");
|
||||
eco.clearEvents();
|
||||
|
||||
local goldNode = eve.GatherNode("gold", 25, 1, 0);
|
||||
local miner = eve.Collector(5, 1, 1);
|
||||
miner.assign(goldNode);
|
||||
|
||||
events = 0;
|
||||
doneTick = -1;
|
||||
for (local t = 0; t < 300; ++t) {
|
||||
miner.tick(2);
|
||||
events = dump_events(events);
|
||||
if (miner.isIdle()) {
|
||||
doneTick = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
print("\n 采空于 t=" + doneTick + "\n");
|
||||
print(" 玩家2 黄金余额: " + eco.get(2, "gold") + " / 上限 " + eco.getCap(2, "gold") +
|
||||
", 浪费 " + eco.getWasted(2, "gold") + "\n");
|
||||
print(" 矿工往返 " + miner.trips() + " 次, 入账 " + miner.totalGathered() + "\n");
|
||||
|
||||
miner.destroy();
|
||||
goldNode.destroy();
|
||||
|
||||
print("\n====================================================================\n");
|
||||
print(" 演示结束\n");
|
||||
print("====================================================================\n");
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
/**
|
||||
* @brief 经济账本的跨模块查询/操作接口(提供方:economy 模块)。
|
||||
*
|
||||
* 消费方(AI、UI、生产队列)通过 eve::cap::query<IEconomy>() 获取实现,
|
||||
* 不直接 include economy 模块;无提供方时返回 nullptr,调用方自行兜底。
|
||||
*/
|
||||
class IEconomy {
|
||||
public:
|
||||
static constexpr const char* capabilityName = "IEconomy";
|
||||
virtual ~IEconomy() = default;
|
||||
|
||||
/**
|
||||
* @brief 为玩家入账 amount,受资源类型上限约束。
|
||||
* @return 实际入账量(超出上限的部分记为浪费)。
|
||||
*/
|
||||
virtual int credit(int player, const std::string& type, int amount) = 0;
|
||||
/** @brief 为玩家出账 amount;余额不足返回 false 且不扣款。 */
|
||||
virtual bool debit(int player, const std::string& type, int amount) = 0;
|
||||
/** @brief 玩家当前持有量。 */
|
||||
virtual int get(int player, const std::string& type) const = 0;
|
||||
/** @brief 玩家该类型的持有上限(0=不限)。 */
|
||||
virtual int getCap(int player, const std::string& type) const = 0;
|
||||
/** @brief 因满仓浪费的累计量。 */
|
||||
virtual int getWasted(int player, const std::string& type) const = 0;
|
||||
/** @brief 累计入账量。 */
|
||||
virtual int getIncome(int player, const std::string& type) const = 0;
|
||||
/** @brief 累计出账量。 */
|
||||
virtual int getExpense(int player, const std::string& type) const = 0;
|
||||
};
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "economy/Collector.h"
|
||||
|
||||
#include "common/Assert.h"
|
||||
#include "economy/EconomySystem.h"
|
||||
#include "economy/GatherNode.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
Collector::Collector(int carryCapacity, int gatherRatePerTick, int travelTicks)
|
||||
: carryCapacity_(carryCapacity), gatherRatePerTick_(gatherRatePerTick),
|
||||
travelTicks_(travelTicks) {
|
||||
EV_PARAM_CHECK(carryCapacity > 0, "carry capacity must be > 0");
|
||||
EV_PARAM_CHECK(gatherRatePerTick > 0, "gather rate must be > 0");
|
||||
EV_PARAM_CHECK(travelTicks >= 0, "travel ticks must be >= 0");
|
||||
}
|
||||
|
||||
bool Collector::assign(GatherNode* node) {
|
||||
if (node == node_) return true;
|
||||
if (node_) {
|
||||
node_->releaseSlot();
|
||||
node_ = nullptr;
|
||||
}
|
||||
if (node == nullptr || !node->tryOccupySlot()) {
|
||||
state_ = State::Idle;
|
||||
return false;
|
||||
}
|
||||
node_ = node;
|
||||
type_ = node->resourceType();
|
||||
cargo_ = 0;
|
||||
startMoveToNode();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Collector::clearAssignment() {
|
||||
if (node_) node_->releaseSlot();
|
||||
node_ = nullptr;
|
||||
state_ = State::Idle;
|
||||
}
|
||||
|
||||
void Collector::tick(int player) {
|
||||
if (state_ == State::Idle) return;
|
||||
if (node_ == nullptr) {
|
||||
state_ = State::Idle;
|
||||
return;
|
||||
}
|
||||
switch (state_) {
|
||||
case State::MovingToNode:
|
||||
if (--travelRemaining_ <= 0) state_ = State::Gathering;
|
||||
break;
|
||||
case State::Gathering: {
|
||||
const int freeSpace = carryCapacity_ - cargo_;
|
||||
const int take = std::min({freeSpace, node_->amount(), gatherRatePerTick_});
|
||||
if (take > 0) cargo_ += node_->extract(take);
|
||||
if (cargo_ >= carryCapacity_ || node_->depleted()) startMoveToDrop();
|
||||
break;
|
||||
}
|
||||
case State::MovingToDrop:
|
||||
if (--travelRemaining_ <= 0) state_ = State::Depositing;
|
||||
break;
|
||||
case State::Depositing:
|
||||
deposit(player);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool Collector::isIdle() const { return state_ == State::Idle; }
|
||||
|
||||
Collector::State Collector::state() const { return state_; }
|
||||
|
||||
std::string Collector::stateName() const {
|
||||
switch (state_) {
|
||||
case State::Idle:
|
||||
return "idle";
|
||||
case State::MovingToNode:
|
||||
return "moving_to_node";
|
||||
case State::Gathering:
|
||||
return "gathering";
|
||||
case State::MovingToDrop:
|
||||
return "moving_to_drop";
|
||||
case State::Depositing:
|
||||
return "depositing";
|
||||
}
|
||||
return "idle";
|
||||
}
|
||||
|
||||
int Collector::cargo() const { return cargo_; }
|
||||
|
||||
int Collector::carryCapacity() const { return carryCapacity_; }
|
||||
|
||||
std::string Collector::resourceType() const { return type_; }
|
||||
|
||||
int Collector::totalGathered() const { return totalGathered_; }
|
||||
|
||||
int Collector::trips() const { return trips_; }
|
||||
|
||||
void Collector::startMoveToNode() {
|
||||
state_ = State::MovingToNode;
|
||||
travelRemaining_ = travelTicks_;
|
||||
}
|
||||
|
||||
void Collector::startMoveToDrop() {
|
||||
state_ = State::MovingToDrop;
|
||||
travelRemaining_ = travelTicks_;
|
||||
}
|
||||
|
||||
void Collector::deposit(int player) {
|
||||
if (cargo_ > 0) {
|
||||
totalGathered_ += EconomySystem::credit(player, type_, cargo_);
|
||||
++trips_;
|
||||
cargo_ = 0;
|
||||
}
|
||||
if (node_ && !node_->depleted()) {
|
||||
startMoveToNode();
|
||||
} else {
|
||||
if (node_) {
|
||||
node_->releaseSlot();
|
||||
node_ = nullptr;
|
||||
}
|
||||
state_ = State::Idle;
|
||||
}
|
||||
}
|
||||
|
||||
void Collector::destroy() { delete this; }
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
class GatherNode;
|
||||
|
||||
/**
|
||||
* @brief 采集者:WorkerTrip 基线的状态机
|
||||
* (选目标→接近→采集→载荷判定→运输→卸货入账)。
|
||||
*/
|
||||
class Collector {
|
||||
public:
|
||||
/** @brief 采集状态机的相位。 */
|
||||
enum class State { Idle, MovingToNode, Gathering, MovingToDrop, Depositing };
|
||||
|
||||
/**
|
||||
* @param carryCapacity 单次最大携带量。
|
||||
* @param gatherRatePerTick 每 tick 入舱量。
|
||||
* @param travelTicks 单程移动耗时(去程/回程共用)。
|
||||
*/
|
||||
Collector(int carryCapacity, int gatherRatePerTick, int travelTicks);
|
||||
|
||||
/** @brief 指派目标节点并占用其采集槽位;无空位时保持待命并返回 false。 */
|
||||
bool assign(GatherNode* node);
|
||||
/** @brief 取消指派并释放槽位。 */
|
||||
void clearAssignment();
|
||||
|
||||
/** @brief 推进一个 tick;卸货时入账到指定玩家的账本。 */
|
||||
void tick(int player);
|
||||
|
||||
/** @brief 是否处于待命状态。 */
|
||||
bool isIdle() const;
|
||||
/** @brief 当前相位。 */
|
||||
State state() const;
|
||||
/** @brief 当前相位的字符串名(脚本友好)。 */
|
||||
std::string stateName() const;
|
||||
/** @brief 当前携带量。 */
|
||||
int cargo() const;
|
||||
/** @brief 单次最大携带量。 */
|
||||
int carryCapacity() const;
|
||||
/** @brief 正在采集的资源类型 id。 */
|
||||
std::string resourceType() const;
|
||||
/** @brief 累计入账量(不含浪费)。 */
|
||||
int totalGathered() const;
|
||||
/** @brief 完成的往返次数。 */
|
||||
int trips() const;
|
||||
|
||||
/** @brief 脚本生命周期入口。 */
|
||||
void destroy();
|
||||
|
||||
private:
|
||||
void startMoveToNode();
|
||||
void startMoveToDrop();
|
||||
void deposit(int player);
|
||||
|
||||
GatherNode* node_ = nullptr;
|
||||
State state_ = State::Idle;
|
||||
int carryCapacity_ = 0;
|
||||
int gatherRatePerTick_ = 0;
|
||||
int travelTicks_ = 0;
|
||||
int travelRemaining_ = 0;
|
||||
int cargo_ = 0;
|
||||
std::string type_;
|
||||
int totalGathered_ = 0;
|
||||
int trips_ = 0;
|
||||
};
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "economy/Economy.h"
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "economy/Collector.h"
|
||||
#include "economy/EconomySystem.h"
|
||||
#include "economy/GatherNode.h"
|
||||
#include "economy/ResourceType.h"
|
||||
|
||||
#include <simplesquirrel/simplesquirrel.hpp>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
Module_IMPL(Economy, new Economy());
|
||||
|
||||
Economy::Economy() { eve::cap::provide<eve::economy::IEconomy>(this); }
|
||||
|
||||
int Economy::credit(int player, const std::string& type, int amount) {
|
||||
return EconomySystem::credit(player, type, amount);
|
||||
}
|
||||
|
||||
bool Economy::debit(int player, const std::string& type, int amount) {
|
||||
return EconomySystem::debit(player, type, amount);
|
||||
}
|
||||
|
||||
int Economy::get(int player, const std::string& type) const { return EconomySystem::get(player, type); }
|
||||
|
||||
int Economy::getCap(int player, const std::string& type) const {
|
||||
return EconomySystem::getCap(player, type);
|
||||
}
|
||||
|
||||
int Economy::getWasted(int player, const std::string& type) const {
|
||||
return EconomySystem::getWasted(player, type);
|
||||
}
|
||||
|
||||
int Economy::getIncome(int player, const std::string& type) const {
|
||||
return EconomySystem::getIncome(player, type);
|
||||
}
|
||||
|
||||
int Economy::getExpense(int player, const std::string& type) const {
|
||||
return EconomySystem::getExpense(player, type);
|
||||
}
|
||||
|
||||
bool Economy::registerResourceType(const std::string& id, const std::string& category, int stockMax,
|
||||
const std::string& depletion) {
|
||||
ResourceTypeDef def;
|
||||
def.id = id;
|
||||
def.category = category;
|
||||
def.stockMax = stockMax;
|
||||
def.displayName = id;
|
||||
if (depletion == "renewable")
|
||||
def.depletion = DepletionModel::Renewable;
|
||||
else if (depletion == "infinite")
|
||||
def.depletion = DepletionModel::Infinite;
|
||||
else if (depletion == "growing")
|
||||
def.depletion = DepletionModel::Growing;
|
||||
else
|
||||
def.depletion = DepletionModel::Finite;
|
||||
return ResourceTypeRegistry::registerType(def);
|
||||
}
|
||||
|
||||
void Economy::clearTypes() { ResourceTypeRegistry::clear(); }
|
||||
|
||||
int Economy::typeCount() { return ResourceTypeRegistry::count(); }
|
||||
|
||||
bool Economy::hasType(const std::string& id) { return ResourceTypeRegistry::find(id) != nullptr; }
|
||||
|
||||
int Economy::getStockMax(const std::string& id) {
|
||||
const auto* def = ResourceTypeRegistry::find(id);
|
||||
return def ? def->stockMax : 0;
|
||||
}
|
||||
|
||||
void Economy::clearEvents() { EconomySystem::clearEvents(); }
|
||||
|
||||
int Economy::eventCount() { return EconomySystem::eventCount(); }
|
||||
|
||||
std::string Economy::eventAction(int index) {
|
||||
if (index < 0 || index >= EconomySystem::eventCount()) return {};
|
||||
return EconomySystem::eventAt(index).action;
|
||||
}
|
||||
|
||||
int Economy::eventPlayer(int index) {
|
||||
if (index < 0 || index >= EconomySystem::eventCount()) return 0;
|
||||
return EconomySystem::eventAt(index).player;
|
||||
}
|
||||
|
||||
std::string Economy::eventType(int index) {
|
||||
if (index < 0 || index >= EconomySystem::eventCount()) return {};
|
||||
return EconomySystem::eventAt(index).type;
|
||||
}
|
||||
|
||||
int Economy::eventAmount(int index) {
|
||||
if (index < 0 || index >= EconomySystem::eventCount()) return 0;
|
||||
return EconomySystem::eventAt(index).amount;
|
||||
}
|
||||
|
||||
void Economy::expose(ssq::Table& table) {
|
||||
auto cls = table.addClass(name, Economy::create, false);
|
||||
expose(cls);
|
||||
|
||||
auto node = table.addClass<GatherNode>(
|
||||
"GatherNode",
|
||||
std::function<GatherNode*(std::string, int, int, int)>(
|
||||
[](std::string type, int capacity, int slots, int regen) {
|
||||
return new GatherNode(std::move(type), capacity, slots, regen);
|
||||
}),
|
||||
true);
|
||||
node.addFunc("destroy", &GatherNode::destroy);
|
||||
node.addFunc("resourceType", &GatherNode::resourceType);
|
||||
node.addFunc("amount", &GatherNode::amount);
|
||||
node.addFunc("capacity", &GatherNode::capacity);
|
||||
node.addFunc("depleted", &GatherNode::depleted);
|
||||
node.addFunc("freeSlots", &GatherNode::freeSlots);
|
||||
node.addFunc("regenTick", &GatherNode::regenTick);
|
||||
|
||||
auto collector = table.addClass<Collector>(
|
||||
"Collector",
|
||||
std::function<Collector*(int, int, int)>(
|
||||
[](int capacity, int rate, int travel) { return new Collector(capacity, rate, travel); }),
|
||||
true);
|
||||
collector.addFunc("destroy", &Collector::destroy);
|
||||
collector.addFunc("assign", &Collector::assign);
|
||||
collector.addFunc("clearAssignment", &Collector::clearAssignment);
|
||||
collector.addFunc("tick", &Collector::tick);
|
||||
collector.addFunc("isIdle", &Collector::isIdle);
|
||||
collector.addFunc("stateName", &Collector::stateName);
|
||||
collector.addFunc("cargo", &Collector::cargo);
|
||||
collector.addFunc("carryCapacity", &Collector::carryCapacity);
|
||||
collector.addFunc("resourceType", &Collector::resourceType);
|
||||
collector.addFunc("totalGathered", &Collector::totalGathered);
|
||||
collector.addFunc("trips", &Collector::trips);
|
||||
}
|
||||
|
||||
void Economy::expose(ssq::Class& cls) {
|
||||
cls.addFunc("getName", &Economy::getName);
|
||||
cls.addFunc("registerResourceType",
|
||||
[](Economy*, std::string id, std::string category, int stockMax,
|
||||
std::string depletion) {
|
||||
return Economy::registerResourceType(id, category, stockMax, depletion);
|
||||
});
|
||||
cls.addFunc("clearTypes", [](Economy*) { Economy::clearTypes(); });
|
||||
cls.addFunc("typeCount", [](Economy*) { return Economy::typeCount(); });
|
||||
cls.addFunc("hasType", [](Economy*, std::string id) { return Economy::hasType(id); });
|
||||
cls.addFunc("getStockMax", [](Economy*, std::string id) { return Economy::getStockMax(id); });
|
||||
cls.addFunc("credit", &Economy::credit);
|
||||
cls.addFunc("debit", &Economy::debit);
|
||||
cls.addFunc("get", &Economy::get);
|
||||
cls.addFunc("getCap", &Economy::getCap);
|
||||
cls.addFunc("getWasted", &Economy::getWasted);
|
||||
cls.addFunc("getIncome", &Economy::getIncome);
|
||||
cls.addFunc("getExpense", &Economy::getExpense);
|
||||
cls.addFunc("clearEvents", [](Economy*) { Economy::clearEvents(); });
|
||||
cls.addFunc("getEventCount", [](Economy*) { return Economy::eventCount(); });
|
||||
cls.addFunc("getEventAction", [](Economy*, int index) { return Economy::eventAction(index); });
|
||||
cls.addFunc("getEventPlayer", [](Economy*, int index) { return Economy::eventPlayer(index); });
|
||||
cls.addFunc("getEventType", [](Economy*, int index) { return Economy::eventType(index); });
|
||||
cls.addFunc("getEventAmount", [](Economy*, int index) { return Economy::eventAmount(index); });
|
||||
}
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/IEconomy.h"
|
||||
#include "common/Module.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
/**
|
||||
* @brief 经济模块(eve.Economy):资源类型注册 + 玩家账本 + 事件 + 采集流水线。
|
||||
*/
|
||||
class Economy : public Module, public IEconomy {
|
||||
public:
|
||||
Module_REG(Economy);
|
||||
Economy();
|
||||
~Economy() override = default;
|
||||
|
||||
// ---- IEconomy ----
|
||||
int credit(int player, const std::string& type, int amount) override;
|
||||
bool debit(int player, const std::string& type, int amount) override;
|
||||
int get(int player, const std::string& type) const override;
|
||||
int getCap(int player, const std::string& type) const override;
|
||||
int getWasted(int player, const std::string& type) const override;
|
||||
int getIncome(int player, const std::string& type) const override;
|
||||
int getExpense(int player, const std::string& type) const override;
|
||||
|
||||
// ---- 注册表门面 ----
|
||||
/** @brief 注册资源类型;depletion 取值 finite/renewable/infinite/growing。 */
|
||||
static bool registerResourceType(const std::string& id, const std::string& category,
|
||||
int stockMax, const std::string& depletion);
|
||||
/** @brief 清空资源类型(测试用)。 */
|
||||
static void clearTypes();
|
||||
/** @brief 已注册类型数量。 */
|
||||
static int typeCount();
|
||||
/** @brief 类型是否已注册。 */
|
||||
static bool hasType(const std::string& id);
|
||||
/** @brief 类型持有上限(未注册返回 0)。 */
|
||||
static int getStockMax(const std::string& id);
|
||||
|
||||
// ---- 事件 ----
|
||||
/** @brief 清空事件队列。 */
|
||||
static void clearEvents();
|
||||
/** @brief 事件队列长度。 */
|
||||
static int eventCount();
|
||||
/** @brief 第 index 个事件的动作(credit/debit/waste;越界返回空串)。 */
|
||||
static std::string eventAction(int index);
|
||||
/** @brief 第 index 个事件的玩家(越界返回 0)。 */
|
||||
static int eventPlayer(int index);
|
||||
/** @brief 第 index 个事件的资源类型(越界返回空串)。 */
|
||||
static std::string eventType(int index);
|
||||
/** @brief 第 index 个事件的数值(越界返回 0)。 */
|
||||
static int eventAmount(int index);
|
||||
};
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "economy/EconomyLedger.h"
|
||||
|
||||
#include "economy/ResourceType.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
int EconomyLedger::credit(const std::string& type, int amount) {
|
||||
if (amount <= 0) return 0;
|
||||
const auto* def = ResourceTypeRegistry::find(type);
|
||||
const int cap = def ? def->stockMax : 0;
|
||||
|
||||
int& current = current_[type];
|
||||
int accepted = amount;
|
||||
if (cap > 0 && current + amount > cap) accepted = std::max(0, cap - current);
|
||||
current += accepted;
|
||||
income_[type] += accepted;
|
||||
if (accepted < amount) wasted_[type] += amount - accepted;
|
||||
return accepted;
|
||||
}
|
||||
|
||||
bool EconomyLedger::debit(const std::string& type, int amount) {
|
||||
if (amount <= 0) return true;
|
||||
auto it = current_.find(type);
|
||||
const int current = it == current_.end() ? 0 : it->second;
|
||||
if (current < amount) return false;
|
||||
it->second = current - amount;
|
||||
expense_[type] += amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EconomyLedger::canAfford(const std::string& type, int amount) const {
|
||||
if (amount <= 0) return true;
|
||||
auto it = current_.find(type);
|
||||
return it != current_.end() && it->second >= amount;
|
||||
}
|
||||
|
||||
int EconomyLedger::get(const std::string& type) const {
|
||||
auto it = current_.find(type);
|
||||
return it == current_.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
int EconomyLedger::getCap(const std::string& type) const {
|
||||
const auto* def = ResourceTypeRegistry::find(type);
|
||||
return def ? def->stockMax : 0;
|
||||
}
|
||||
|
||||
int EconomyLedger::getWasted(const std::string& type) const {
|
||||
auto it = wasted_.find(type);
|
||||
return it == wasted_.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
int EconomyLedger::getIncome(const std::string& type) const {
|
||||
auto it = income_.find(type);
|
||||
return it == income_.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
int EconomyLedger::getExpense(const std::string& type) const {
|
||||
auto it = expense_.find(type);
|
||||
return it == expense_.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
/**
|
||||
* @brief 单个玩家的资源账本:当前量、上限、收支与浪费统计。
|
||||
*/
|
||||
class EconomyLedger {
|
||||
public:
|
||||
/**
|
||||
* @brief 入账 amount。
|
||||
* @return 实际入账量(受类型上限约束,超限部分记为浪费)。
|
||||
*/
|
||||
int credit(const std::string& type, int amount);
|
||||
/** @brief 出账 amount;余额不足时返回 false 且不扣款。 */
|
||||
bool debit(const std::string& type, int amount);
|
||||
/** @brief 是否可负担 amount。 */
|
||||
bool canAfford(const std::string& type, int amount) const;
|
||||
/** @brief 当前持有量。 */
|
||||
int get(const std::string& type) const;
|
||||
/** @brief 类型上限(来自注册表;未注册返回 0=不限)。 */
|
||||
int getCap(const std::string& type) const;
|
||||
/** @brief 因满仓而浪费的累计量。 */
|
||||
int getWasted(const std::string& type) const;
|
||||
/** @brief 累计入账。 */
|
||||
int getIncome(const std::string& type) const;
|
||||
/** @brief 累计出账。 */
|
||||
int getExpense(const std::string& type) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, int> current_;
|
||||
std::unordered_map<std::string, int> wasted_;
|
||||
std::unordered_map<std::string, int> income_;
|
||||
std::unordered_map<std::string, int> expense_;
|
||||
};
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "economy/EconomySystem.h"
|
||||
|
||||
#include "common/Assert.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
std::unordered_map<int, EconomyLedger>& EconomySystem::ledgers() {
|
||||
static std::unordered_map<int, EconomyLedger> s_ledgers;
|
||||
return s_ledgers;
|
||||
}
|
||||
|
||||
std::vector<EconomyEvent>& EconomySystem::events() {
|
||||
static std::vector<EconomyEvent> s_events;
|
||||
return s_events;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, EconomyHook>& EconomySystem::hooks() {
|
||||
static std::unordered_map<std::string, EconomyHook> s_hooks;
|
||||
return s_hooks;
|
||||
}
|
||||
|
||||
EconomyLedger& EconomySystem::ledger(int player) { return ledgers()[player]; }
|
||||
|
||||
void EconomySystem::clear() {
|
||||
ledgers().clear();
|
||||
events().clear();
|
||||
hooks().clear();
|
||||
}
|
||||
|
||||
int EconomySystem::credit(int player, const std::string& type, int amount) {
|
||||
const int accepted = ledger(player).credit(type, amount);
|
||||
emit(EconomyEvent{player, type, "credit", accepted});
|
||||
const int wasted = amount - accepted;
|
||||
if (wasted > 0) emit(EconomyEvent{player, type, "waste", wasted});
|
||||
return accepted;
|
||||
}
|
||||
|
||||
bool EconomySystem::debit(int player, const std::string& type, int amount) {
|
||||
if (!ledger(player).debit(type, amount)) return false;
|
||||
emit(EconomyEvent{player, type, "debit", amount});
|
||||
return true;
|
||||
}
|
||||
|
||||
int EconomySystem::get(int player, const std::string& type) { return ledger(player).get(type); }
|
||||
|
||||
int EconomySystem::getCap(int player, const std::string& type) { return ledger(player).getCap(type); }
|
||||
|
||||
int EconomySystem::getWasted(int player, const std::string& type) { return ledger(player).getWasted(type); }
|
||||
|
||||
int EconomySystem::getIncome(int player, const std::string& type) { return ledger(player).getIncome(type); }
|
||||
|
||||
int EconomySystem::getExpense(int player, const std::string& type) { return ledger(player).getExpense(type); }
|
||||
|
||||
void EconomySystem::registerHook(const std::string& name, EconomyHook fn) {
|
||||
EV_PARAM_CHECK(!name.empty(), "hook name must not be empty");
|
||||
hooks()[name] = std::move(fn);
|
||||
}
|
||||
|
||||
void EconomySystem::unregisterHook(const std::string& name) { hooks().erase(name); }
|
||||
|
||||
void EconomySystem::clearEvents() { events().clear(); }
|
||||
|
||||
int EconomySystem::eventCount() { return static_cast<int>(events().size()); }
|
||||
|
||||
const EconomyEvent& EconomySystem::eventAt(int index) {
|
||||
const bool valid = index >= 0 && size_t(index) < events().size();
|
||||
EV_PARAM_CHECK(valid, "event index out of range");
|
||||
return events()[static_cast<size_t>(index)];
|
||||
}
|
||||
|
||||
void EconomySystem::emit(const EconomyEvent& ev) {
|
||||
events().push_back(ev);
|
||||
for (auto& [name, fn] : hooks()) {
|
||||
(void)name;
|
||||
fn(ev);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "economy/EconomyLedger.h"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
/** @brief 一次经济事件(credit / debit / waste)。 */
|
||||
struct EconomyEvent {
|
||||
int player = 0;
|
||||
std::string type;
|
||||
std::string action; // "credit" | "debit" | "waste"
|
||||
int amount = 0;
|
||||
};
|
||||
|
||||
using EconomyHook = std::function<void(const EconomyEvent&)>;
|
||||
|
||||
/**
|
||||
* @brief 经济系统门面:按玩家持有账本、转发事件、注册钩子。
|
||||
*/
|
||||
class EconomySystem {
|
||||
public:
|
||||
/** @brief 重置所有玩家账本、事件与钩子(测试用)。 */
|
||||
static void clear();
|
||||
|
||||
/** @brief 入账并广播事件。@return 实际入账量。 */
|
||||
static int credit(int player, const std::string& type, int amount);
|
||||
/** @brief 出账并广播事件;余额不足返回 false。 */
|
||||
static bool debit(int player, const std::string& type, int amount);
|
||||
|
||||
/** @brief 玩家当前持有量。 */
|
||||
static int get(int player, const std::string& type);
|
||||
/** @brief 玩家该类型持有上限(0=不限)。 */
|
||||
static int getCap(int player, const std::string& type);
|
||||
/** @brief 因满仓浪费的累计量。 */
|
||||
static int getWasted(int player, const std::string& type);
|
||||
/** @brief 累计入账量。 */
|
||||
static int getIncome(int player, const std::string& type);
|
||||
/** @brief 累计出账量。 */
|
||||
static int getExpense(int player, const std::string& type);
|
||||
|
||||
/** @brief 注册事件钩子(同名替换)。 */
|
||||
static void registerHook(const std::string& name, EconomyHook fn);
|
||||
/** @brief 注销事件钩子。 */
|
||||
static void unregisterHook(const std::string& name);
|
||||
|
||||
/** @brief 事件队列(按发生顺序)。 */
|
||||
static void clearEvents();
|
||||
static int eventCount();
|
||||
static const EconomyEvent& eventAt(int index);
|
||||
|
||||
private:
|
||||
static EconomyLedger& ledger(int player);
|
||||
static void emit(const EconomyEvent& ev);
|
||||
|
||||
static std::unordered_map<int, EconomyLedger>& ledgers();
|
||||
static std::vector<EconomyEvent>& events();
|
||||
static std::unordered_map<std::string, EconomyHook>& hooks();
|
||||
};
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "economy/GatherNode.h"
|
||||
|
||||
#include "common/Assert.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
GatherNode::GatherNode(std::string type, int capacity, int workerSlots, int regenPerTick)
|
||||
: type_(std::move(type)), capacity_(capacity), amount_(capacity), regenPerTick_(regenPerTick),
|
||||
workerSlots_(workerSlots) {
|
||||
EV_PARAM_CHECK(capacity >= 0, "node capacity must be >= 0");
|
||||
EV_PARAM_CHECK(workerSlots > 0, "node workerSlots must be > 0");
|
||||
EV_PARAM_CHECK(regenPerTick >= 0, "node regenPerTick must be >= 0");
|
||||
if (capacity < 0) capacity_ = amount_ = 0;
|
||||
if (workerSlots <= 0) workerSlots_ = 1;
|
||||
if (regenPerTick < 0) regenPerTick_ = 0;
|
||||
}
|
||||
|
||||
std::string GatherNode::resourceType() const { return type_; }
|
||||
|
||||
int GatherNode::amount() const { return amount_; }
|
||||
|
||||
int GatherNode::capacity() const { return capacity_; }
|
||||
|
||||
bool GatherNode::depleted() const { return amount_ <= 0; }
|
||||
|
||||
int GatherNode::extract(int amount) {
|
||||
if (amount <= 0 || amount_ <= 0) return 0;
|
||||
const int take = std::min(amount, amount_);
|
||||
amount_ -= take;
|
||||
return take;
|
||||
}
|
||||
|
||||
bool GatherNode::tryOccupySlot() {
|
||||
if (occupiedSlots_ >= workerSlots_) return false;
|
||||
++occupiedSlots_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GatherNode::releaseSlot() {
|
||||
EV_ASSERT(occupiedSlots_ > 0, "releaseSlot without an occupied slot");
|
||||
if (occupiedSlots_ > 0) --occupiedSlots_;
|
||||
}
|
||||
|
||||
int GatherNode::freeSlots() const { return workerSlots_ - occupiedSlots_; }
|
||||
|
||||
void GatherNode::regenTick() {
|
||||
if (regenPerTick_ > 0 && amount_ < capacity_)
|
||||
amount_ = std::min(capacity_, amount_ + regenPerTick_);
|
||||
}
|
||||
|
||||
void GatherNode::destroy() { delete this; }
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
/**
|
||||
* @brief 地图上的资源节点:储量、再生、采集槽位。
|
||||
*/
|
||||
class GatherNode {
|
||||
public:
|
||||
/**
|
||||
* @param type 资源类型 id(须已在注册表中)。
|
||||
* @param capacity 总储量。
|
||||
* @param workerSlots 同时采集槽位上限(>0)。
|
||||
* @param regenPerTick 每 tick 再生量(0=不再生)。
|
||||
*/
|
||||
GatherNode(std::string type, int capacity, int workerSlots = 1, int regenPerTick = 0);
|
||||
|
||||
/** @brief 资源类型 id。 */
|
||||
std::string resourceType() const;
|
||||
/** @brief 当前储量。 */
|
||||
int amount() const;
|
||||
/** @brief 总储量。 */
|
||||
int capacity() const;
|
||||
/** @brief 是否已采空。 */
|
||||
bool depleted() const;
|
||||
|
||||
/** @brief 尝试取出 amount;返回实际取出量(不超过当前储量)。 */
|
||||
int extract(int amount);
|
||||
/** @brief 占用一个采集槽位;无空位返回 false。 */
|
||||
bool tryOccupySlot();
|
||||
/** @brief 释放一个采集槽位。 */
|
||||
void releaseSlot();
|
||||
/** @brief 剩余空槽数。 */
|
||||
int freeSlots() const;
|
||||
|
||||
/** @brief 每 tick 再生(Growing/Renewable 模型)。 */
|
||||
void regenTick();
|
||||
|
||||
/** @brief 脚本生命周期入口。 */
|
||||
void destroy();
|
||||
|
||||
private:
|
||||
std::string type_;
|
||||
int capacity_ = 0;
|
||||
int amount_ = 0;
|
||||
int regenPerTick_ = 0;
|
||||
int workerSlots_ = 1;
|
||||
int occupiedSlots_ = 0;
|
||||
};
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "economy/ResourceType.h"
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
std::unordered_map<std::string, ResourceTypeDef>& ResourceTypeRegistry::types() {
|
||||
static std::unordered_map<std::string, ResourceTypeDef> s_types;
|
||||
return s_types;
|
||||
}
|
||||
|
||||
bool ResourceTypeRegistry::registerType(const ResourceTypeDef& def) {
|
||||
if (def.id.empty()) return false;
|
||||
types()[def.id] = def;
|
||||
return true;
|
||||
}
|
||||
|
||||
const ResourceTypeDef* ResourceTypeRegistry::find(const std::string& id) {
|
||||
auto& all = types();
|
||||
auto it = all.find(id);
|
||||
return it == all.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
int ResourceTypeRegistry::count() { return static_cast<int>(types().size()); }
|
||||
|
||||
void ResourceTypeRegistry::clear() { types().clear(); }
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace eve::economy {
|
||||
|
||||
/** @brief 资源节点的供给模型。 */
|
||||
enum class DepletionModel {
|
||||
Finite, // 采完即枯竭(SC 矿簇)
|
||||
Renewable, // 可再生(AoE 农田复种)
|
||||
Infinite, // 无限供给
|
||||
Growing // 随时间增长(C&C 泰伯利亚)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 资源类型的静态定义(数据驱动配置的最小单元)。
|
||||
*/
|
||||
struct ResourceTypeDef {
|
||||
std::string id; // 唯一标识("minerals" / "food" ...)
|
||||
std::string category = "stock"; // 存量型 / 速率型 / 特殊(人口)
|
||||
int stockMax = 0; // 玩家持有上限;0 表示不限
|
||||
DepletionModel depletion = DepletionModel::Finite; // 供给模型
|
||||
std::string displayName; // UI 显示名(可空)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 资源类型注册表:注册 / 查询 / 清理。
|
||||
*/
|
||||
class ResourceTypeRegistry {
|
||||
public:
|
||||
/** @brief 注册或替换一个资源类型定义。@return false 当 id 为空。 */
|
||||
static bool registerType(const ResourceTypeDef& def);
|
||||
/** @brief 按 id 查询;未注册时返回 nullptr。 */
|
||||
static const ResourceTypeDef* find(const std::string& id);
|
||||
/** @brief 已注册类型数量。 */
|
||||
static int count();
|
||||
/** @brief 清空注册表(测试用)。 */
|
||||
static void clear();
|
||||
|
||||
private:
|
||||
static std::unordered_map<std::string, ResourceTypeDef>& types();
|
||||
};
|
||||
|
||||
} // namespace eve::economy
|
||||
@@ -331,6 +331,7 @@ set(EVELIBS
|
||||
EVTensor
|
||||
EVNetwork
|
||||
EVDatabase
|
||||
EVEconomy
|
||||
EVData
|
||||
EVI18n
|
||||
EVSound
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
#include "zeroerr/assert.h"
|
||||
#include "zeroerr/unittest.h"
|
||||
|
||||
#include "common/Capability.h"
|
||||
#include "common/IEconomy.h"
|
||||
#include "economy/Collector.h"
|
||||
#include "economy/Economy.h"
|
||||
#include "economy/EconomySystem.h"
|
||||
#include "economy/GatherNode.h"
|
||||
#include "economy/ResourceType.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
using namespace eve::economy;
|
||||
|
||||
namespace {
|
||||
|
||||
ResourceTypeDef makeType(const std::string& id, int stockMax,
|
||||
const std::string& category = "stock") {
|
||||
ResourceTypeDef def;
|
||||
def.id = id;
|
||||
def.category = category;
|
||||
def.stockMax = stockMax;
|
||||
def.depletion = DepletionModel::Finite;
|
||||
def.displayName = id;
|
||||
return def;
|
||||
}
|
||||
|
||||
/** 每个用例从干净状态开始(进程内单测)。 */
|
||||
struct Reset {
|
||||
Reset() {
|
||||
ResourceTypeRegistry::clear();
|
||||
EconomySystem::clear();
|
||||
}
|
||||
~Reset() {
|
||||
ResourceTypeRegistry::clear();
|
||||
EconomySystem::clear();
|
||||
}
|
||||
};
|
||||
|
||||
/** 推进 worker 直到待命;返回消耗的 tick 数(超时返回 maxTicks)。 */
|
||||
int runUntilIdle(Collector& worker, int player, int maxTicks = 1000) {
|
||||
int ticks = 0;
|
||||
while (!worker.isIdle() && ticks < maxTicks) {
|
||||
worker.tick(player);
|
||||
++ticks;
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("economy.registry.registerFindClear") {
|
||||
Reset reset;
|
||||
CHECK_EQ(ResourceTypeRegistry::count(), 0);
|
||||
CHECK(ResourceTypeRegistry::find("minerals") == nullptr);
|
||||
|
||||
CHECK(ResourceTypeRegistry::registerType(makeType("minerals", 200)));
|
||||
CHECK_EQ(ResourceTypeRegistry::count(), 1);
|
||||
REQUIRE(ResourceTypeRegistry::find("minerals") != nullptr);
|
||||
CHECK_EQ(ResourceTypeRegistry::find("minerals")->stockMax, 200);
|
||||
CHECK_EQ(ResourceTypeRegistry::find("minerals")->category, "stock");
|
||||
|
||||
// 重复注册替换旧定义
|
||||
CHECK(ResourceTypeRegistry::registerType(makeType("minerals", 500)));
|
||||
CHECK_EQ(ResourceTypeRegistry::count(), 1);
|
||||
CHECK_EQ(ResourceTypeRegistry::find("minerals")->stockMax, 500);
|
||||
|
||||
ResourceTypeRegistry::clear();
|
||||
CHECK_EQ(ResourceTypeRegistry::count(), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.registry.rejectsEmptyId") {
|
||||
Reset reset;
|
||||
ResourceTypeDef empty;
|
||||
CHECK(!ResourceTypeRegistry::registerType(empty));
|
||||
CHECK_EQ(ResourceTypeRegistry::count(), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.ledger.creditRespectsCapAndTracksWaste") {
|
||||
Reset reset;
|
||||
ResourceTypeRegistry::registerType(makeType("minerals", 10));
|
||||
|
||||
EconomyLedger ledger;
|
||||
int accepted = ledger.credit("minerals", 6);
|
||||
CHECK_EQ(accepted, 6);
|
||||
CHECK_EQ(ledger.get("minerals"), 6);
|
||||
CHECK_EQ(ledger.getWasted("minerals"), 0);
|
||||
|
||||
accepted = ledger.credit("minerals", 6);
|
||||
CHECK_EQ(accepted, 4); // 只收 4,超限 2 记为浪费
|
||||
CHECK_EQ(ledger.get("minerals"), 10);
|
||||
CHECK_EQ(ledger.getWasted("minerals"), 2);
|
||||
|
||||
// 未注册类型视为无上限
|
||||
CHECK_EQ(ledger.credit("unregistered", 5), 5);
|
||||
CHECK_EQ(ledger.get("unregistered"), 5);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.ledger.debitNeverGoesNegative") {
|
||||
Reset reset;
|
||||
ResourceTypeRegistry::registerType(makeType("gold", 100));
|
||||
|
||||
EconomyLedger ledger;
|
||||
ledger.credit("gold", 5);
|
||||
|
||||
CHECK(ledger.canAfford("gold", 5));
|
||||
CHECK(!ledger.canAfford("gold", 6));
|
||||
CHECK(ledger.debit("gold", 3));
|
||||
CHECK_EQ(ledger.get("gold"), 2);
|
||||
CHECK(!ledger.debit("gold", 3));
|
||||
CHECK_EQ(ledger.get("gold"), 2); // 失败不扣款
|
||||
CHECK_EQ(ledger.getIncome("gold"), 5);
|
||||
CHECK_EQ(ledger.getExpense("gold"), 3);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.system.emitsEventsAndHooks") {
|
||||
Reset reset;
|
||||
ResourceTypeRegistry::registerType(makeType("wood", 20));
|
||||
EconomySystem::clearEvents();
|
||||
|
||||
int hookCalls = 0;
|
||||
EconomySystem::registerHook("test", [&](const EconomyEvent& ev) {
|
||||
++hookCalls;
|
||||
if (ev.action == "credit") CHECK_EQ(ev.amount, 5);
|
||||
});
|
||||
|
||||
EconomySystem::credit(1, "wood", 5);
|
||||
CHECK_EQ(EconomySystem::get(1, "wood"), 5);
|
||||
CHECK_EQ(EconomySystem::eventCount(), 1);
|
||||
CHECK(EconomySystem::eventAt(0).action == "credit");
|
||||
CHECK_EQ(EconomySystem::eventAt(0).player, 1);
|
||||
CHECK(EconomySystem::eventAt(0).type == "wood");
|
||||
CHECK_EQ(EconomySystem::eventAt(0).amount, 5);
|
||||
CHECK_EQ(hookCalls, 1);
|
||||
|
||||
CHECK(EconomySystem::debit(1, "wood", 2));
|
||||
CHECK_EQ(EconomySystem::eventCount(), 2);
|
||||
CHECK(EconomySystem::eventAt(1).action == "debit");
|
||||
|
||||
// 当前 3,上限 20 → 收 17,浪费 2
|
||||
EconomySystem::credit(1, "wood", 19);
|
||||
CHECK_EQ(EconomySystem::getWasted(1, "wood"), 2);
|
||||
CHECK_EQ(EconomySystem::eventCount(), 4);
|
||||
CHECK(EconomySystem::eventAt(2).action == "credit");
|
||||
CHECK(EconomySystem::eventAt(3).action == "waste");
|
||||
CHECK_EQ(EconomySystem::eventAt(3).amount, 2);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.workerTrip.gathersUntilNodeDepleted") {
|
||||
Reset reset;
|
||||
ResourceTypeRegistry::registerType(makeType("minerals", 1000));
|
||||
|
||||
GatherNode node("minerals", 30, 2);
|
||||
Collector worker(5, 1, 2);
|
||||
const int player = 1;
|
||||
|
||||
CHECK(worker.assign(&node));
|
||||
const int ticks = runUntilIdle(worker, player);
|
||||
CHECK(ticks < 1000);
|
||||
CHECK(worker.isIdle());
|
||||
CHECK(node.depleted());
|
||||
CHECK_EQ(worker.cargo(), 0);
|
||||
CHECK_EQ(worker.totalGathered(), 30);
|
||||
CHECK_EQ(EconomySystem::get(player, "minerals"), 30);
|
||||
CHECK_EQ(EconomySystem::getWasted(player, "minerals"), 0);
|
||||
|
||||
// 6 次往返,每次 5 单位
|
||||
int deposits = 0;
|
||||
for (int i = 0; i < EconomySystem::eventCount(); ++i) {
|
||||
if (EconomySystem::eventAt(i).action == "credit") ++deposits;
|
||||
}
|
||||
CHECK_EQ(deposits, 6);
|
||||
CHECK_EQ(worker.trips(), 6);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.workerTrip.wastesWhenCapReached") {
|
||||
Reset reset;
|
||||
ResourceTypeRegistry::registerType(makeType("minerals", 10));
|
||||
|
||||
GatherNode node("minerals", 100, 2);
|
||||
Collector worker(5, 1, 1);
|
||||
const int player = 2;
|
||||
|
||||
CHECK(worker.assign(&node));
|
||||
const int ticks = runUntilIdle(worker, player);
|
||||
CHECK(ticks < 1000);
|
||||
CHECK_EQ(EconomySystem::get(player, "minerals"), 10);
|
||||
CHECK_EQ(EconomySystem::getWasted(player, "minerals"), 90);
|
||||
CHECK_EQ(worker.totalGathered(), 10);
|
||||
CHECK_EQ(worker.trips(), 20);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.workerTrip.depositsPartialLoadWhenNodeDepletes") {
|
||||
Reset reset;
|
||||
ResourceTypeRegistry::registerType(makeType("food", 100));
|
||||
|
||||
GatherNode node("food", 3, 2);
|
||||
Collector worker(5, 1, 2);
|
||||
const int player = 3;
|
||||
|
||||
CHECK(worker.assign(&node));
|
||||
const int ticks = runUntilIdle(worker, player);
|
||||
CHECK(ticks < 1000);
|
||||
CHECK_EQ(EconomySystem::get(player, "food"), 3);
|
||||
CHECK_EQ(worker.trips(), 1);
|
||||
CHECK_EQ(worker.totalGathered(), 3);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.workerTrip.saturationSlotsLimitWorkers") {
|
||||
Reset reset;
|
||||
ResourceTypeRegistry::registerType(makeType("minerals", 1000));
|
||||
|
||||
GatherNode node("minerals", 30, 1); // 单槽位
|
||||
Collector a(5, 1, 1);
|
||||
Collector b(5, 1, 1);
|
||||
const int player = 4;
|
||||
|
||||
CHECK(a.assign(&node));
|
||||
CHECK_EQ(node.freeSlots(), 0);
|
||||
CHECK(!b.assign(&node)); // 槽位已满,保持待命
|
||||
CHECK(b.isIdle());
|
||||
|
||||
a.clearAssignment();
|
||||
CHECK_EQ(node.freeSlots(), 1);
|
||||
CHECK(b.assign(&node));
|
||||
CHECK_EQ(node.freeSlots(), 0);
|
||||
|
||||
const int ticks = runUntilIdle(b, player);
|
||||
CHECK(ticks < 1000);
|
||||
CHECK(node.depleted());
|
||||
CHECK_EQ(EconomySystem::get(player, "minerals"), 30);
|
||||
CHECK_EQ(node.freeSlots(), 1);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.capability.moduleProvidesIEconomy") {
|
||||
Reset reset;
|
||||
auto* eco = Economy::create();
|
||||
REQUIRE(eco != nullptr);
|
||||
|
||||
auto* q = eve::cap::query<eve::economy::IEconomy>();
|
||||
REQUIRE(q != nullptr);
|
||||
|
||||
Economy::registerResourceType("gold", "stock", 50, "finite");
|
||||
CHECK_EQ(q->credit(3, "gold", 60), 50);
|
||||
CHECK_EQ(q->get(3, "gold"), 50);
|
||||
CHECK_EQ(q->getWasted(3, "gold"), 10);
|
||||
CHECK(q->debit(3, "gold", 20));
|
||||
CHECK_EQ(q->get(3, "gold"), 30);
|
||||
CHECK_EQ(q->getCap(3, "gold"), 50);
|
||||
CHECK_EQ(q->getIncome(3, "gold"), 50);
|
||||
CHECK_EQ(q->getExpense(3, "gold"), 20);
|
||||
}
|
||||
|
||||
TEST_CASE("economy.demoScenario") {
|
||||
Reset reset;
|
||||
|
||||
printf("====================================================================\n");
|
||||
printf(" EVEngine economy MVP demo\n");
|
||||
printf("====================================================================\n");
|
||||
|
||||
// ---- 场景 1:双工人矿物采集(SC2 式 WorkerTrip) ----
|
||||
ResourceTypeRegistry::registerType(makeType("minerals", 100));
|
||||
EconomySystem::clearEvents();
|
||||
|
||||
GatherNode mine("minerals", 30, 2);
|
||||
Collector workerA(5, 1, 2);
|
||||
Collector workerB(3, 2, 3);
|
||||
const int player = 1;
|
||||
|
||||
printf("\n[1] 矿物采集: 矿点 30, 双槽位, A(载荷5/速率1/单程2t) + B(载荷3/速率2/单程3t)\n");
|
||||
printf(" 矿点槽位: %d 空闲\n", mine.freeSlots());
|
||||
CHECK(workerA.assign(&mine));
|
||||
printf(" A 进入矿点 -> 槽位: %d 空闲\n", mine.freeSlots());
|
||||
CHECK(workerB.assign(&mine));
|
||||
printf(" B 进入矿点 -> 槽位: %d 空闲\n", mine.freeSlots());
|
||||
printf("\n 采集循环:\n");
|
||||
|
||||
int lastEvents = 0;
|
||||
int doneTick = -1;
|
||||
for (int t = 0; t < 300; ++t) {
|
||||
workerA.tick(player);
|
||||
workerB.tick(player);
|
||||
if (t % 12 == 0) {
|
||||
printf(" t=%3d A[%-13s cargo=%d] B[%-13s cargo=%d] node=%2d balance=%d\n", t,
|
||||
workerA.stateName().c_str(), workerA.cargo(), workerB.stateName().c_str(),
|
||||
workerB.cargo(), mine.amount(), EconomySystem::get(player, "minerals"));
|
||||
}
|
||||
while (EconomySystem::eventCount() > lastEvents) {
|
||||
const auto& ev = EconomySystem::eventAt(lastEvents);
|
||||
printf(" -> %s %s x%d (balance=%d)\n", ev.action.c_str(), ev.type.c_str(),
|
||||
ev.amount, EconomySystem::get(player, "minerals"));
|
||||
++lastEvents;
|
||||
}
|
||||
if (workerA.isIdle() && workerB.isIdle()) {
|
||||
doneTick = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n 矿点采空于 t=%d\n", doneTick);
|
||||
printf(" A: 往返 %d 次, 累计入账 %d\n", workerA.trips(), workerA.totalGathered());
|
||||
printf(" B: 往返 %d 次, 累计入账 %d\n", workerB.trips(), workerB.totalGathered());
|
||||
printf(" 玩家%d %s 余额 %d / 上限 %d, 浪费 %d\n", player, "minerals",
|
||||
EconomySystem::get(player, "minerals"), EconomySystem::getCap(player, "minerals"),
|
||||
EconomySystem::getWasted(player, "minerals"));
|
||||
printf(" 矿点剩余 %d\n", mine.amount());
|
||||
|
||||
CHECK(doneTick >= 0);
|
||||
CHECK(mine.depleted());
|
||||
CHECK_EQ(EconomySystem::get(player, "minerals"), 30);
|
||||
CHECK_EQ(EconomySystem::getWasted(player, "minerals"), 0);
|
||||
CHECK_EQ(mine.freeSlots(), 2);
|
||||
CHECK_EQ(workerA.totalGathered() + workerB.totalGathered(), 30);
|
||||
|
||||
// ---- 场景 2:持有上限导致的浪费 ----
|
||||
ResourceTypeRegistry::registerType(makeType("gold", 10));
|
||||
EconomySystem::clearEvents();
|
||||
|
||||
GatherNode goldNode("gold", 25, 1);
|
||||
Collector miner(5, 1, 1);
|
||||
const int player2 = 2;
|
||||
CHECK(miner.assign(&goldNode));
|
||||
|
||||
printf("\n[2] 满仓浪费: 玩家上限 10, 节点 25, 载荷 5 (第 3 次起全部浪费)\n");
|
||||
printf(" 采集循环:\n");
|
||||
|
||||
lastEvents = 0;
|
||||
doneTick = -1;
|
||||
for (int t = 0; t < 300; ++t) {
|
||||
miner.tick(player2);
|
||||
while (EconomySystem::eventCount() > lastEvents) {
|
||||
const auto& ev = EconomySystem::eventAt(lastEvents);
|
||||
printf(" -> %s %s x%d (balance=%d)\n", ev.action.c_str(), ev.type.c_str(),
|
||||
ev.amount, EconomySystem::get(player2, "gold"));
|
||||
++lastEvents;
|
||||
}
|
||||
if (miner.isIdle()) {
|
||||
doneTick = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n 采空于 t=%d\n", doneTick);
|
||||
printf(" 玩家%d 黄金余额 %d / 上限 %d, 浪费 %d\n", player2,
|
||||
EconomySystem::get(player2, "gold"), EconomySystem::getCap(player2, "gold"),
|
||||
EconomySystem::getWasted(player2, "gold"));
|
||||
printf(" 矿工往返 %d 次, 入账 %d\n", miner.trips(), miner.totalGathered());
|
||||
|
||||
CHECK(doneTick >= 0);
|
||||
CHECK_EQ(EconomySystem::get(player2, "gold"), 10);
|
||||
CHECK_EQ(EconomySystem::getWasted(player2, "gold"), 15);
|
||||
CHECK_EQ(miner.trips(), 5);
|
||||
CHECK_EQ(miner.totalGathered(), 10);
|
||||
|
||||
printf("\n====================================================================\n");
|
||||
printf(" 演示结束\n");
|
||||
printf("====================================================================\n");
|
||||
}
|
||||
Reference in new issue
Block a user