sots-engine/tests/game_sim/test_economy.cpp
alex b39bb290e3 L5: the interest literals verified live at a boundary, with a control that fails
`ComputeBudget`'s savings-interest term is now compared against the running game at
a treasury the corpus actually contains. Three runs on VM146 from turn1-state.sav:

  A (widened floats, as shipped)   3,895 calls, 0 diverged, 0 undeclared writes
  B (exact decimals, the control)  2,718 calls, 1,359 diverged

The game fills savingsInterest with 499 at a treasury of 50,000, and with 380 at
38,100 -- the exact decimals pay 500 and 381. Every divergence in B lands on a
treasury that is a multiple of 100 and no other state diverges at all, which is
exactly the arithmetic. G3's rule-23 reading is now measured, not inferred, and the
one-money error is shown to propagate into `available` and `researchMoney` too.

The control also settles why the earlier 4,437-call green run was green: slot 5 IS
diffed and the harness CAN see it, so that run simply presented no boundary state.
Coverage is therefore reported as distinct states, not calls: 5 distinct treasuries,
2 of them on the boundary.

Two further rule-23 constants found in the same routine by an operand-width sweep,
corrected, and honestly marked UNVERIFIED because no reference turn can see them:

  - the research-yield factor is a widened 0.85f while its two neighbours in the
    same product are exact doubles. Boundary: research money a multiple of 40,000;
    the run presented 9 distinct values and none is.
  - the three research modifiers are summed in single precision, not double.
    Boundary: two of the three non-zero; the corpus has shrm = TRM = 0.

Both are pinned by boundary cases in test_economy.cpp that fail with the decimals.

Also verified live, in the same run:
  - T31's difficulty-column recovery. The live ServerPlayer+0xf9 / NPC flags on all
    eight players are exactly what lane PL's save-only inversion claims, including
    the awkward system-owning player that is still ambiguous because it is an NPC.
  - BANKRUPTCY_PROTECTION_LIMIT_FACTOR reads 3.29999995 = (float)3.3. Its file image
    is zero because the loader fills it at run time, so lane PL-3 had to assume the
    value; it is now measured and the assumption was right.

Falsified, and recorded as such: the difficulty-mods record does NOT sit inline at
ServerPlayer+0x36c -- that field is a heap pointer on all eight players. The row IS
reachable from a ServerPlayer (which corrects the hook's standing coverage note),
but the fitted {3.0,1.5}/{1.0,1.0} pair remains unverified. The hook logs the
pointer and does not follow it.

The `verified` column stays 0, deliberately. Every phase this compare touches is
Partial for reasons upstream of it, and promoting one because part of it was checked
is the drift app_test_catalog exists to catch. What moved is models; see
docs/L5-live-verification.md for each one with its coverage.

Gates run separately: clean-room OK, host ctest 54/54, CT111 shim cross-build exit 0.
2026-09-08 17:48:42 -04:00

443 lines
18 KiB
C++

#include "game/sim/economy.h"
#include "game/sim/numeric.h"
#include "check.h"
using namespace sots::sim;
// The two interest rates are WIDENED FLOAT literals in the image, so a round treasury earns
// one less than the exact decimal would give. Every expectation below that moved by exactly 1
// was hand-computed from the exact decimal and is corrected here (G3).
static void test_interest() {
CHECK_EQ(SavingsInterest(1000, true), 9); // 1000 x (double)0.01f = 9.99999977 -> 9
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);
// --- rule 23, and the two boundaries the corpus cannot see (lane L5) ---------------------
// The yield factor is (double)0.85f, not the decimal 0.85. The exact-decimal product is an
// integer exactly when researchMoney is a multiple of 40,000, and the image's slightly
// larger constant lands one ABOVE it there. Every case above is away from that boundary,
// which is why none of them moved when the constant was corrected -- and why the live
// compare stays green on a reference turn.
CHECK_EQ(ResearchPointsFromMoney(40000, 1, 1, 0, 0, 1, 1, 1), 391); // decimal 0.85 -> 390
CHECK_EQ(ResearchPointsFromMoney(80000, 1, 1, 0, 0, 1, 1, 1), 782); // decimal 0.85 -> 781
// 120,000 is a multiple of 40,000 and still agrees: the exact-decimal product rounds UP to
// 1173.0 there rather than landing below it. The boundary is a property of the product, not
// of the multiple, so the period is necessary and not sufficient.
CHECK_EQ(ResearchPointsFromMoney(120000, 1, 1, 0, 0, 1, 1, 1), 1173);
// The three modifiers are summed in SINGLE precision with a float32 store after every add.
// 0.1 + 0.33 + 0.75 is 1.1800000667572021 in float32 and 1.180000014603138 in double, and
// at 4,855 money that is the difference between 56 RP and 55.
CHECK_EQ(ResearchPointsFromMoney(4855, 1, 0.1f, 0.33f, 0.75, 1, 1, 1), 56); // double -> 55
}
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, 99);
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, 6899);
CHECK_EQ(b.construction, 500);
// (6900 - 500) x 0.5 = 3200
CHECK_EQ(b.researchMoney, 3199);
// 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, 3199);
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, 3199);
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, 6399);
}
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, 6399);
}
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, 3199);
CHECK_EQ(b.researchMoneyGiven, 1599);
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, 3199);
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, -11101);
// 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, 3449); // 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, 99 - 5000); // interest minus maintenance
in = base_inputs();
in.constructionDemand = 100000;
b = ComputeBudget(in, false);
CHECK_EQ(b.construction, 6899); // 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, 4899);
}
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;
// LANE PL, rule 23: the protection factor is read `fmul dword ptr`, so it is a FLOAT32
// in the image whatever the data file's decimal is. 3.3f is 3.299999952316284, and the
// conversion TRUNCATES, so every expectation below whose product lands on an exact
// integer under the decimal moves down by one. The two constants disagree at every
// multiple of ten -- exactly 10% of integer max incomes, starting at maxIncome = 10,
// where the decimal gives 33.0 and the image's float gives 32.99999952.
//
// max income 1000: elimination at 1000 / -0.15 = -6666.67 -> -6666; protection -3299
BankruptcyLimits l = ComputeBankruptcyLimits(1000, t);
CHECK_EQ(l.eliminationFloor, -6666);
CHECK_EQ(l.protectionLimit, -3299);
CHECK_EQ(BankruptcyLevel(-6667, l), 2);
CHECK_EQ(BankruptcyLevel(-6666, l), 1);
CHECK_EQ(BankruptcyLevel(-3300, l), 1);
CHECK_EQ(BankruptcyLevel(-3299, l), 0);
CHECK_EQ(BankruptcyLevel(0, l), 0);
l = ComputeBankruptcyLimits(100, t);
CHECK_EQ(l.eliminationFloor, -666);
CHECK_EQ(l.protectionLimit, -329);
// the boundary the corpus never presents (rule 23): a max income NOT a multiple of ten,
// where the two constants agree, next to one that is, where they do not.
l = ComputeBankruptcyLimits(1001, t);
CHECK_EQ(l.protectionLimit, -3303); // 3.3 x 1001 = 3303.3 either way
l = ComputeBankruptcyLimits(10, t);
CHECK_EQ(l.protectionLimit, -32); // decimal would say -33
// 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, -1319999980);
// LANE N: the divisor is the widened float literal, not the decimal -0.15. The two
// disagree for every maxIncome divisible by 3, and maxIncome = 3 is where it first
// bites: 3 / -0.15 is -20 exactly in decimal but -19.99999920... with the image's
// constant, and the conversion TRUNCATES.
CHECK_EQ(ComputeBankruptcyLimits(3, t).eliminationFloor, -19);
CHECK_EQ(ComputeBankruptcyLimits(6, t).eliminationFloor, -39);
CHECK_EQ(ComputeBankruptcyLimits(9, t).eliminationFloor, -59);
// ... and above ~3,000,000 maximum income they differ on essentially every value.
CHECK_EQ(ComputeBankruptcyLimits(238592, t).eliminationFloor, -1590613);
CHECK_EQ(ComputeBankruptcyLimits(3000001, t).eliminationFloor, -20000005);
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");
}