#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 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"); }