sots-engine/tests/game_sim/test_economy.cpp

258 lines
9 KiB
C++

#include "game/sim/economy.h"
#include "game/sim/numeric.h"
#include "check.h"
using namespace sots::sim;
static void test_interest() {
CHECK_EQ(SavingsInterest(1000, true), 10);
CHECK_EQ(SavingsInterest(199, true), 1); // 1.99 truncates
CHECK_EQ(SavingsInterest(1000, false), 0); // no systems, no interest
CHECK_EQ(SavingsInterest(-500, true), 0);
CHECK_EQ(SavingsInterest(0, true), 0);
CHECK_EQ(DebtInterest(-1000), 150);
CHECK_EQ(DebtInterest(-7), 1); // 1.05 truncates
CHECK_EQ(DebtInterest(5), 0);
CHECK_EQ(DebtInterest(0), 0);
CHECK_EQ(MaintenanceCost(100, 2.0), 50);
CHECK_EQ(MaintenanceCost(100, 1.9), 100); // divisor truncates to 1
CHECK_EQ(MaintenanceCost(100, 0.5), 100); // divisor 0 guarded
CHECK_EQ(SaturatingAdd(1900000000, 500000000), 2000000000);
CHECK_EQ(SaturatingAdd(-1900000000, -500000000), -2000000000);
CHECK_EQ(SaturatingAdd(5, -7), -2);
}
static void test_research_points() {
// 50000 / 50 = 1000; x1.15 = 1150; x0.5 = 575; x0.85 = 488.75 -> 488
CHECK_EQ(ResearchPointsFromMoney(50000, 1, 1, 0, 0, 1, 1, 1), 488);
// (ResMod + shrm + TRM) = 2 -> 977.5 -> 977
CHECK_EQ(ResearchPointsFromMoney(50000, 1, 1, 0.5, 0.5, 1, 1, 1), 977);
// difficulty 0.5 -> 244.375 -> 244
CHECK_EQ(ResearchPointsFromMoney(50000, 0.5, 1, 0, 0, 1, 1, 1), 244);
// tech x2, server x0.5, scale x1 -> unchanged 488
CHECK_EQ(ResearchPointsFromMoney(50000, 1, 1, 0, 0, 2, 0.5, 1), 488);
CHECK_EQ(ResearchPointsFromMoney(0, 1, 1, 0, 0, 1, 1, 1), 0);
// 100 money -> 0.9775 -> 0
CHECK_EQ(ResearchPointsFromMoney(100, 1, 1, 0, 0, 1, 1, 1), 0);
}
static void test_expenses() {
std::vector<ExpenseSlider> s = {{100, 300, 250}, {50, 60, 100}};
// minimums 150; extras 150 + 10 = 160; headroom 1000-150 = 850 -> 310
CHECK_EQ(ExpenseTotal(s, 1000), 310);
// headroom 200-150 = 50 -> 200
CHECK_EQ(ExpenseTotal(s, 200), 200);
CHECK_EQ(ExpenseTotal({}, 1000), 0);
}
static BudgetInputs base_inputs() {
BudgetInputs in;
in.savings = 10000;
in.ownsSystems = true;
in.systemIncome = {5000, 3000, -200};
in.tradeIncome = 1000;
in.maintenance = 2000;
in.maintenanceDivisor = 1.0;
in.constructionDemand = 500;
in.researchRate = 0.5;
in.hasResearchTarget = true;
return in;
}
static void test_budget_hand_case() {
Budget b = ComputeBudget(base_inputs(), false);
CHECK_EQ(b.savingsInterest, 100);
CHECK_EQ(b.debtInterest, 0);
CHECK_EQ(b.systemIncomePositive, 8000);
CHECK_EQ(b.systemIncomeNegative, 200);
CHECK_EQ(b.maintenance, 2000);
CHECK_EQ(b.expenses, 0);
// 8000 + 1000 + 100 - 200 - 2000 = 6900
CHECK_EQ(b.available, 6900);
CHECK_EQ(b.construction, 500);
// (6900 - 500) x 0.5 = 3200
CHECK_EQ(b.researchMoney, 3200);
// 3200/50 = 64; x1.15 = 73.6; x0.5 = 36.8; x0.85 = 31.28 -> 31
CHECK_EQ(b.researchPoints, 31);
CHECK_EQ(b.totalResearchPoints, 31);
CHECK(b.hasResearchAllocation);
CHECK_EQ(b.researchMoneyKept, 3200);
CHECK_EQ(b.bonusIncome, 0);
CHECK_EQ(b.savingsGiven, 0);
// 6900 - 500 - 3200
CHECK_EQ(b.net, 3200);
}
static void test_budget_projected() {
Budget b = ComputeBudget(base_inputs(), true);
CHECK_EQ(b.researchMoney, 0);
CHECK_EQ(b.researchPoints, 0);
CHECK_EQ(b.net, 6400);
}
static void test_budget_debt() {
BudgetInputs in = base_inputs();
in.savings = -1000;
Budget b = ComputeBudget(in, false);
CHECK_EQ(b.savingsInterest, 0);
CHECK_EQ(b.debtInterest, 150);
// 8000 + 1000 - 200 - 2000 - 150 = 6650
CHECK_EQ(b.available, 6650);
CHECK_EQ(b.construction, 500);
// (6650 - 500) x 0.5 = 3075
CHECK_EQ(b.researchMoney, 3075);
CHECK_EQ(b.net, 6650 - 500 - 3075);
}
static void test_budget_aid_and_bonus() {
BudgetInputs in = base_inputs();
in.aidResearchPercent = 50;
in.aidSavings = 100;
in.tra = 4;
in.trp = 5;
Budget b = ComputeBudget(in, false);
CHECK_EQ(b.researchMoney, 3200);
CHECK_EQ(b.researchMoneyGiven, 1600);
CHECK_EQ(b.researchMoneyKept, 1600);
// 31 + 4 + 5 = 40; given 20; kept 20
CHECK_EQ(b.researchPointsGiven, 20);
CHECK_EQ(b.totalResearchPoints, 20);
CHECK_EQ(b.savingsGiven, 100);
// 6900 - 500 - 1600 - 1600 - 100
CHECK_EQ(b.net, 3100);
in.aidResearchPercent = 250; // clamps to 100
in.aidSavings = 0;
b = ComputeBudget(in, false);
CHECK_EQ(b.researchMoneyGiven, 3200);
CHECK_EQ(b.totalResearchPoints, 0);
in = base_inputs();
in.techIncomeMult = 1.1;
b = ComputeBudget(in, false);
// remaining before bonus = 3200 -> ftol(0.1 x 3200) = 320
CHECK_EQ(b.bonusIncome, 320);
CHECK_EQ(b.net, 3520);
}
static void test_budget_edges() {
BudgetInputs in = base_inputs();
in.isAI = true;
Budget b = ComputeBudget(in, false);
CHECK_EQ(b.construction, 0); // AI path spends construction elsewhere
CHECK_EQ(b.researchMoney, 3450); // 6900 x 0.5
in = base_inputs();
in.systemIncome = {};
in.tradeIncome = 0;
in.maintenance = 5000;
b = ComputeBudget(in, false);
CHECK_EQ(b.available, 0); // floored at zero
CHECK_EQ(b.construction, 0);
CHECK_EQ(b.researchMoney, 0);
CHECK_EQ(b.net, 100 - 5000); // interest minus maintenance
in = base_inputs();
in.constructionDemand = 100000;
b = ComputeBudget(in, false);
CHECK_EQ(b.construction, 6900); // capped at available
CHECK_EQ(b.researchMoney, 0);
in = base_inputs();
in.expenses = {{1000, 2000, 1500}};
b = ComputeBudget(in, false);
CHECK_EQ(b.expenses, 1500);
CHECK_EQ(b.available, 5400);
}
static void test_trade() {
TuningTable t;
t.TRADE_ROUTE_REQ_CIVPOPULATION = 1e6;
t.TRADE_ROUTE_REQ_IMPPOPULATION = 1e6;
CHECK_EQ(TradeRoutesSupported(2.5e6, 1e6, t), 4); // ceil(2.5) + 1
CHECK_EQ(TradeRoutesSupported(0, 0, t), 1); // minimum one
CHECK_EQ(TradeRoutesSupported(1, 0, t), 1);
TuningTable zero;
CHECK_EQ(TradeRoutesSupported(5e6, 5e6, zero), 1); // zero requirement guarded
t.TRADE_ROUTE_STARTUP_TURNS = 3;
t.TRADE_ROUTE_STARTUP_INCOME = 7;
t.TRADE_ROUTE_MIN_INCOME = 100;
t.TRADE_ROUTE_MAX_FREIGHTERS = 5;
t.TRADE_ROUTE_INCOME_PERFREIGHTER_CRQ = 30;
t.TRADE_ROUTE_INCOME_PERFREIGHTER_CR = 20;
t.TRADE_ROUTE_INCOME_PERFREIGHTER_DE = 10;
t.STATION_BONUS_TRADE_INCOME = 0.1;
t.ADDICTION_TRADE_MOD = 0.5;
t.TRADE_ROUTE_OWNERS_SHARE = 0.6;
TradeRouteState r;
r.ageTurns = 3;
r.freighters[0] = 2; // CRQ: 2 x 30 = 60, cap left 3
r.freighters[1] = 4; // CR: min(4,3) = 3 x 20 = 60, cap left 0
r.freighters[2] = 3; // DE: nothing left
CHECK_EQ(TradeRouteGrossIncome(r, t), 220);
r.tradeStationsAtSystem = 2;
CHECK_EQ(TradeRouteGrossIncome(r, t), 264); // x 1.2
r.partnerAddicted = true;
CHECK_EQ(TradeRouteGrossIncome(r, t), 132); // x 0.5
CHECK_EQ(TradeRouteIncome(r, true, 1.0, t), 79); // 132 x 0.6 = 79.2
CHECK_EQ(TradeRouteIncome(r, false, 1.0, t), 52); // 132 x 0.4 = 52.8
CHECK_EQ(TradeRouteIncome(r, true, 2.0, t), 158); // AI trade multiplier
r.ageTurns = 2;
CHECK_EQ(TradeRouteGrossIncome(r, t), 7); // startup income is flat
TradeRouteState empty;
empty.ageTurns = 10;
CHECK_EQ(TradeRouteGrossIncome(empty, t), 100); // MIN_INCOME with no freighters
t.TRADE_ROUTE_OWNERS_SHARE = 1.5; // clamps to 1
CHECK_EQ(TradeRouteIncome(empty, true, 1.0, t), 100);
CHECK_EQ(TradeRouteIncome(empty, false, 1.0, t), 0);
}
static void test_bankruptcy() {
TuningTable t;
t.BANKRUPTCY_PROTECTION_LIMIT_FACTOR = 3.3;
t.BANKRUPTCY_ELIMINATION_TURNS = 5;
BankruptcyLimits l = ComputeBankruptcyLimits(1000, t);
CHECK_EQ(l.eliminationFloor, -3300);
CHECK_EQ(l.protectionLimit, -1000);
CHECK_EQ(BankruptcyLevel(-3301, l), 2);
CHECK_EQ(BankruptcyLevel(-3300, l), 1);
CHECK_EQ(BankruptcyLevel(-1500, l), 1);
CHECK_EQ(BankruptcyLevel(-1000, l), 0);
CHECK_EQ(BankruptcyLevel(0, l), 0);
BankruptcyLimits none = ComputeBankruptcyLimits(0, t);
CHECK_EQ(none.eliminationFloor, 0);
CHECK_EQ(BankruptcyLevel(-1, none), 2); // no income at all: any debt is terminal
BankruptcyState s;
CHECK(!BankruptcyStep(s, 1, 10, t));
CHECK_EQ(s.warningLevel, 1);
CHECK_EQ(s.startTurn, 10);
CHECK(!BankruptcyStep(s, 2, 12, t)); // level changed: clock restarts at 12
CHECK_EQ(s.startTurn, 12);
CHECK(!BankruptcyStep(s, 2, 16, t)); // 4 turns < 5
CHECK(BankruptcyStep(s, 2, 17, t)); // 5 turns -> eliminated
CHECK(!BankruptcyStep(s, 0, 18, t)); // recovered
CHECK_EQ(s.warningLevel, 0);
}
int main() {
test_interest();
test_research_points();
test_expenses();
test_budget_hand_case();
test_budget_projected();
test_budget_debt();
test_budget_aid_and_bonus();
test_budget_edges();
test_trade();
test_bankruptcy();
return simtest::finish("test_economy");
}