397 lines
15 KiB
C++
397 lines
15 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, 0.5f}, {50, 60, 0.1f}};
|
|
// s1: request ftol(0.5 x 1000) - 100 = 400, room 200 -> 200
|
|
// s2: request 100 - 50 = 50, room 10 -> 10; minimums 150 + takes 210 = 360
|
|
CHECK_EQ(ExpenseTotal(s, 1000), 360);
|
|
// avail 200: s1 request 0, s2 request -30 -> 0; only the minimums
|
|
CHECK_EQ(ExpenseTotal(s, 200), 150);
|
|
// avail below the minimums: total is capped at avail
|
|
CHECK_EQ(ExpenseTotal(s, 100), 100);
|
|
CHECK_EQ(ExpenseTotal({}, 1000), 0);
|
|
|
|
// max 0 = unlimited
|
|
CHECK_EQ(ExpenseTotal({{0, 0, 0.25f}}, 1000), 250);
|
|
// a negative minimum counts as 0
|
|
CHECK_EQ(ExpenseTotal({{-50, 100, 0.1f}}, 1000), 100);
|
|
// min above max: the entry contributes its minimum, its (negative) take is absorbed
|
|
CHECK_EQ(ExpenseTotal({{100, 50, 1.0f}}, 1000), 100);
|
|
// takes are capped by what is left after the minimums
|
|
CHECK_EQ(ExpenseTotal({{100, 0, 1.0f}, {200, 0, 1.0f}}, 1000), 1000);
|
|
// the product is taken in single precision: 0.7f x 1000 = 699.99998 -> 699
|
|
CHECK_EQ(ExpenseTotal({{0, 0, 0.7f}}, 1000), 699);
|
|
// ... and the income itself is rounded to a float first
|
|
CHECK_EQ(ExpenseTotal({{0, 0, 1.0f}}, 16777217), 16777216);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// A player with no research target still reports its research money and points -- the UI
|
|
// shows them -- but never spends the money: the "kept" line and the research allocation are
|
|
// written in the same branch. (Verified against the live game, docs/B1.md.)
|
|
static void test_budget_without_research_target() {
|
|
BudgetInputs in = base_inputs();
|
|
in.hasResearchTarget = false;
|
|
Budget b = ComputeBudget(in, false);
|
|
CHECK_EQ(b.researchMoney, 3200);
|
|
CHECK_EQ(b.researchPoints, 31);
|
|
CHECK_EQ(b.totalResearchPoints, 31);
|
|
CHECK(!b.hasResearchAllocation);
|
|
CHECK_EQ(b.researchMoneyKept, 0);
|
|
// The research money stays in the treasury: 6900 - 500 construction.
|
|
CHECK_EQ(b.net, 6400);
|
|
}
|
|
|
|
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);
|
|
// full net before the bonus = 3200 -> ftol(0.1 x 3200) = 320
|
|
CHECK_EQ(b.bonusIncome, 320);
|
|
CHECK_EQ(b.net, 3520);
|
|
|
|
// the bonus reads the net *after* research aid was deducted
|
|
in.aidResearchPercent = 50;
|
|
b = ComputeBudget(in, false);
|
|
CHECK_EQ(b.bonusIncome, 320); // 1600 kept + 1600 given: net unchanged
|
|
in.aidResearchPercent = 0;
|
|
|
|
// a multiplier below 1 never takes money away
|
|
in.techIncomeMult = 0.5;
|
|
b = ComputeBudget(in, false);
|
|
CHECK_EQ(b.bonusIncome, 0);
|
|
CHECK_EQ(b.net, 3200);
|
|
|
|
// no bonus on a negative net
|
|
in.techIncomeMult = 1.1;
|
|
in.maintenance = 20000;
|
|
b = ComputeBudget(in, false);
|
|
CHECK_EQ(b.available, 0);
|
|
CHECK_EQ(b.bonusIncome, 0);
|
|
CHECK_EQ(b.net, -11100);
|
|
|
|
// savings aid is capped by the projected treasury, not by the turn net
|
|
in = base_inputs();
|
|
in.savings = -5000; // debt interest 750
|
|
in.aidSavings = 100;
|
|
b = ComputeBudget(in, false);
|
|
// available 8000+1000-200-2000-750 = 6050; construction 500; research 2775; net 2775
|
|
CHECK_EQ(b.researchMoney, 2775);
|
|
CHECK_EQ(b.savingsGiven, 0); // -5000 + 2775 < 0: nothing to give
|
|
CHECK_EQ(b.net, 2775);
|
|
|
|
in.savings = -2000; // debt interest 300
|
|
in.aidSavings = 5000;
|
|
b = ComputeBudget(in, false);
|
|
// available 6500; construction 500; research 3000; net 3000; projected 1000
|
|
CHECK_EQ(b.savingsGiven, 1000);
|
|
CHECK_EQ(b.net, 2000);
|
|
|
|
in.savings = 10000;
|
|
in.techIncomeMult = 1.1; // bonus 320 counts toward the projection
|
|
in.aidSavings = 20000;
|
|
b = ComputeBudget(in, false);
|
|
CHECK_EQ(b.bonusIncome, 320);
|
|
CHECK_EQ(b.savingsGiven, 13520); // 10000 + 3200 + 320: the whole projected treasury
|
|
CHECK_EQ(b.net, -10000); // ... so the treasury ends the turn at 0
|
|
|
|
in.aidSavings = -5; // negative aid gives nothing
|
|
b = ComputeBudget(in, false);
|
|
CHECK_EQ(b.savingsGiven, 0);
|
|
}
|
|
|
|
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, 0.5f}};
|
|
b = ComputeBudget(in, false);
|
|
// pre-expense avail 6900: request 3450 - 1000 = 2450, room 1000 -> 2000 total
|
|
CHECK_EQ(b.expenses, 2000);
|
|
CHECK_EQ(b.available, 4900);
|
|
}
|
|
|
|
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;
|
|
// max income 1000: elimination at 1000 / -0.15 = -6666.67 -> -6666; protection -3300
|
|
BankruptcyLimits l = ComputeBankruptcyLimits(1000, t);
|
|
CHECK_EQ(l.eliminationFloor, -6666);
|
|
CHECK_EQ(l.protectionLimit, -3300);
|
|
CHECK_EQ(BankruptcyLevel(-6667, l), 2);
|
|
CHECK_EQ(BankruptcyLevel(-6666, l), 1);
|
|
CHECK_EQ(BankruptcyLevel(-3301, l), 1);
|
|
CHECK_EQ(BankruptcyLevel(-3300, l), 0);
|
|
CHECK_EQ(BankruptcyLevel(0, l), 0);
|
|
|
|
l = ComputeBankruptcyLimits(100, t);
|
|
CHECK_EQ(l.eliminationFloor, -666);
|
|
CHECK_EQ(l.protectionLimit, -330);
|
|
|
|
// a factor beyond the interest break-even is pinned to the elimination floor
|
|
TuningTable big = t;
|
|
big.BANKRUPTCY_PROTECTION_LIMIT_FACTOR = 10.0;
|
|
l = ComputeBankruptcyLimits(1000, big);
|
|
CHECK_EQ(l.eliminationFloor, -6666);
|
|
CHECK_EQ(l.protectionLimit, -6666);
|
|
|
|
// huge income: the floor saturates at the treasury limit
|
|
l = ComputeBankruptcyLimits(400000000, t);
|
|
CHECK_EQ(l.eliminationFloor, -2000000000);
|
|
CHECK_EQ(l.protectionLimit, -1320000000);
|
|
|
|
BankruptcyLimits none = ComputeBankruptcyLimits(0, t);
|
|
CHECK_EQ(none.eliminationFloor, 0);
|
|
CHECK_EQ(none.protectionLimit, 0);
|
|
CHECK_EQ(BankruptcyLevel(-1, none), 2); // no income at all: any debt is terminal
|
|
|
|
// Stamping happens on the transition; actions run on the previous state.
|
|
BankruptcyState s;
|
|
CHECK_EQ(s.startTurn, -1);
|
|
BankruptcyDecision d = BankruptcyStep(s, 1, 10, t);
|
|
CHECK(!d.costCutting); // first turn at level 1: not yet
|
|
CHECK(!d.eliminate);
|
|
CHECK_EQ(s.warningLevel, 1);
|
|
CHECK_EQ(s.startTurn, 10);
|
|
d = BankruptcyStep(s, 1, 11, t);
|
|
CHECK(d.costCutting); // second turn: cost cutting
|
|
CHECK_EQ(s.startTurn, 10); // no restamp while the level holds
|
|
d = BankruptcyStep(s, 2, 12, t); // 1 -> 2 restamps the clock at 12
|
|
CHECK(d.costCutting);
|
|
CHECK(!d.eliminate);
|
|
CHECK_EQ(s.startTurn, 12);
|
|
d = BankruptcyStep(s, 2, 16, t);
|
|
CHECK(!d.eliminate); // 4 turns < 5
|
|
d = BankruptcyStep(s, 2, 17, t);
|
|
CHECK(d.eliminate); // 5 turns -> eliminated
|
|
d = BankruptcyStep(s, 0, 18, t); // recovering on the turn after the clock
|
|
CHECK(!d.costCutting);
|
|
CHECK(d.eliminate); // ... still acts on the old level-2 state
|
|
CHECK_EQ(s.warningLevel, 0);
|
|
CHECK_EQ(s.startTurn, -1);
|
|
|
|
// 2 -> 1 -> 2 restarts the clock each time
|
|
BankruptcyState r;
|
|
BankruptcyStep(r, 2, 20, t);
|
|
CHECK_EQ(r.startTurn, 20);
|
|
d = BankruptcyStep(r, 1, 22, t);
|
|
CHECK(d.costCutting);
|
|
CHECK_EQ(r.startTurn, 22);
|
|
d = BankruptcyStep(r, 2, 25, t);
|
|
CHECK(!d.eliminate); // old level was 1
|
|
CHECK_EQ(r.startTurn, 25);
|
|
d = BankruptcyStep(r, 2, 29, t);
|
|
CHECK(!d.eliminate);
|
|
d = BankruptcyStep(r, 2, 30, t);
|
|
CHECK(d.eliminate);
|
|
|
|
// even with a zero-turn limit, elimination happens the turn after level 2 is reached
|
|
TuningTable instant = t;
|
|
instant.BANKRUPTCY_ELIMINATION_TURNS = 0;
|
|
BankruptcyState q;
|
|
d = BankruptcyStep(q, 2, 5, instant);
|
|
CHECK(!d.eliminate);
|
|
d = BankruptcyStep(q, 2, 6, instant);
|
|
CHECK(d.eliminate);
|
|
}
|
|
|
|
int main() {
|
|
test_interest();
|
|
test_research_points();
|
|
test_expenses();
|
|
test_budget_hand_case();
|
|
test_budget_without_research_target();
|
|
test_budget_projected();
|
|
test_budget_debt();
|
|
test_budget_aid_and_bonus();
|
|
test_budget_edges();
|
|
test_trade();
|
|
test_bankruptcy();
|
|
return simtest::finish("test_economy");
|
|
}
|