diff --git a/cmake/module_manifest.cmake b/cmake/module_manifest.cmake index dcd8f94d..661d299b 100644 --- a/cmake/module_manifest.cmake +++ b/cmake/module_manifest.cmake @@ -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 diff --git a/examples/economy/economy_demo.nut b/examples/economy/economy_demo.nut new file mode 100644 index 00000000..c247c782 --- /dev/null +++ b/examples/economy/economy_demo.nut @@ -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"); diff --git a/src/engine/common/IEconomy.h b/src/engine/common/IEconomy.h new file mode 100644 index 00000000..1217fa3f --- /dev/null +++ b/src/engine/common/IEconomy.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace eve::economy { + +/** + * @brief 经济账本的跨模块查询/操作接口(提供方:economy 模块)。 + * + * 消费方(AI、UI、生产队列)通过 eve::cap::query() 获取实现, + * 不直接 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 diff --git a/src/modules/economy/Collector.cpp b/src/modules/economy/Collector.cpp new file mode 100644 index 00000000..44fa02fe --- /dev/null +++ b/src/modules/economy/Collector.cpp @@ -0,0 +1,127 @@ +#include "economy/Collector.h" + +#include "common/Assert.h" +#include "economy/EconomySystem.h" +#include "economy/GatherNode.h" + +#include + +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 diff --git a/src/modules/economy/Collector.h b/src/modules/economy/Collector.h new file mode 100644 index 00000000..09123f9f --- /dev/null +++ b/src/modules/economy/Collector.h @@ -0,0 +1,70 @@ +#pragma once + +#include + +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 diff --git a/src/modules/economy/Economy.cpp b/src/modules/economy/Economy.cpp new file mode 100644 index 00000000..645c15f9 --- /dev/null +++ b/src/modules/economy/Economy.cpp @@ -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 + +namespace eve::economy { + +Module_IMPL(Economy, new Economy()); + +Economy::Economy() { eve::cap::provide(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", + std::function( + [](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", + std::function( + [](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 diff --git a/src/modules/economy/Economy.h b/src/modules/economy/Economy.h new file mode 100644 index 00000000..f54db192 --- /dev/null +++ b/src/modules/economy/Economy.h @@ -0,0 +1,56 @@ +#pragma once + +#include "common/IEconomy.h" +#include "common/Module.h" + +#include + +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 diff --git a/src/modules/economy/EconomyLedger.cpp b/src/modules/economy/EconomyLedger.cpp new file mode 100644 index 00000000..e44b731c --- /dev/null +++ b/src/modules/economy/EconomyLedger.cpp @@ -0,0 +1,64 @@ +#include "economy/EconomyLedger.h" + +#include "economy/ResourceType.h" + +#include + +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 diff --git a/src/modules/economy/EconomyLedger.h b/src/modules/economy/EconomyLedger.h new file mode 100644 index 00000000..13161cb0 --- /dev/null +++ b/src/modules/economy/EconomyLedger.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +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 current_; + std::unordered_map wasted_; + std::unordered_map income_; + std::unordered_map expense_; +}; + +} // namespace eve::economy diff --git a/src/modules/economy/EconomySystem.cpp b/src/modules/economy/EconomySystem.cpp new file mode 100644 index 00000000..cc49bc9d --- /dev/null +++ b/src/modules/economy/EconomySystem.cpp @@ -0,0 +1,81 @@ +#include "economy/EconomySystem.h" + +#include "common/Assert.h" + +#include + +namespace eve::economy { + +std::unordered_map& EconomySystem::ledgers() { + static std::unordered_map s_ledgers; + return s_ledgers; +} + +std::vector& EconomySystem::events() { + static std::vector s_events; + return s_events; +} + +std::unordered_map& EconomySystem::hooks() { + static std::unordered_map 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(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(index)]; +} + +void EconomySystem::emit(const EconomyEvent& ev) { + events().push_back(ev); + for (auto& [name, fn] : hooks()) { + (void)name; + fn(ev); + } +} + +} // namespace eve::economy diff --git a/src/modules/economy/EconomySystem.h b/src/modules/economy/EconomySystem.h new file mode 100644 index 00000000..da273342 --- /dev/null +++ b/src/modules/economy/EconomySystem.h @@ -0,0 +1,65 @@ +#pragma once + +#include "economy/EconomyLedger.h" + +#include +#include +#include +#include + +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; + +/** + * @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& ledgers(); + static std::vector& events(); + static std::unordered_map& hooks(); +}; + +} // namespace eve::economy diff --git a/src/modules/economy/GatherNode.cpp b/src/modules/economy/GatherNode.cpp new file mode 100644 index 00000000..78de1a33 --- /dev/null +++ b/src/modules/economy/GatherNode.cpp @@ -0,0 +1,56 @@ +#include "economy/GatherNode.h" + +#include "common/Assert.h" + +#include +#include + +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 diff --git a/src/modules/economy/GatherNode.h b/src/modules/economy/GatherNode.h new file mode 100644 index 00000000..513cd140 --- /dev/null +++ b/src/modules/economy/GatherNode.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +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 diff --git a/src/modules/economy/ResourceType.cpp b/src/modules/economy/ResourceType.cpp new file mode 100644 index 00000000..00a8a10d --- /dev/null +++ b/src/modules/economy/ResourceType.cpp @@ -0,0 +1,26 @@ +#include "economy/ResourceType.h" + +namespace eve::economy { + +std::unordered_map& ResourceTypeRegistry::types() { + static std::unordered_map 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(types().size()); } + +void ResourceTypeRegistry::clear() { types().clear(); } + +} // namespace eve::economy diff --git a/src/modules/economy/ResourceType.h b/src/modules/economy/ResourceType.h new file mode 100644 index 00000000..a6572be9 --- /dev/null +++ b/src/modules/economy/ResourceType.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +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& types(); +}; + +} // namespace eve::economy diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a87e4c3b..4d422992 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -331,6 +331,7 @@ set(EVELIBS EVTensor EVNetwork EVDatabase + EVEconomy EVData EVI18n EVSound diff --git a/test/economy.cpp b/test/economy.cpp new file mode 100644 index 00000000..806c9a4e --- /dev/null +++ b/test/economy.cpp @@ -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 +#include + +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(); + 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"); +}