sots-engine/src/game/sim/economy.h

198 lines
11 KiB
C++

// Economy: per-player income roll-up, research points, trade income, bankruptcy.
//
// Pure functions over plain input structs. Money is int (the treasury is a 32-bit
// integer clamped to +/-2e9); rates and multipliers are double.
#pragma once
#include <cstdint>
#include <vector>
#include "game/sim/tuning.h"
namespace sots::sim {
// ---------------------------------------------------------------------------------------
// Budget
// ---------------------------------------------------------------------------------------
// One per-category expense slider (the player's expense entries {xmin, xmax, xper}).
// `fraction` is the share of the pre-expense available income the slider asks for; the
// request is honoured between min and max, and the total of the above-minimum parts is
// capped by what is left after every minimum is paid. A maximum of 0 means unlimited.
struct ExpenseSlider {
int minimum = 0;
int maximum = 0; // 0 = no upper bound
float fraction = 0.f; // xper: stored as a single-precision value by the game
};
// Everything the budget roll-up reads from the player and the server. Names follow the
// save-tag names where one exists (Sav, Maint, ResRate, ResMod, ResScl, TRM/TRA/TRP,
// shrm) so a save dump maps onto this struct directly.
struct BudgetInputs {
int savings = 0; // Sav
bool ownsSystems = false; // savings interest only accrues to landed players
std::vector<int> systemIncome; // money output of every owned, non-abandoned system
int tradeIncome = 0; // sum of the player's trade-route incomes
int secondaryManagerIncome = 0; // income reported by a second server manager
int shipCarriedPopIncome = 0; // income from population carried in slaver/colony hulls
int maintenance = 0; // Maint (raw fleet upkeep before difficulty)
double maintenanceDivisor = 1.0; // difficulty table: upkeep is divided by ftol(this)
double researchDifficultyMult = 1.0; // difficulty table: research-point multiplier
std::vector<ExpenseSlider> expenses;
bool isAI = false; // AI players do not take the human construction path
int constructionDemand = 0; // what the build queues would consume this turn
double researchRate = 0.0; // ResRate: share of available money to research (0..1)
double resMod = 1.0; // ResMod
double shrm = 0.0; // shrm (shared research modifier)
double trm = 0.0; // TRM (timed research multiplier bonuses)
double techResearchMult = 1.0; // research multiplier set by tech effects
double serverResMod = 1.0; // game-option research modifier
double resScl = 1.0; // ResScl
int tra = 0; // TRA: per-turn research-point contribution
int trp = 0; // TRP: per-turn research-point contribution
int aidResearchPercent = 0; // sum of active research-aid entries (clamped 0..100)
int aidSavings = 0; // sum of active savings-aid entries
double techIncomeMult = 1.0; // income multiplier set by tech effects (1 = none)
bool hasResearchTarget = false; // ResT set
};
struct Budget {
// income side
int systemIncomePositive = 0; // sum of positive system money outputs
int tradeIncome = 0;
int shipCarriedPopIncome = 0;
int secondaryManagerIncome = 0;
int savingsInterest = 0; // 1 % of a non-negative treasury
int bonusIncome = 0; // tech income multiplier applied to the running net
// expense side
int systemIncomeNegative = 0; // sum of |negative| system money outputs
int maintenance = 0;
int researchMoneyKept = 0; // research money minus the part given as aid
int debtInterest = 0; // 15 % of a negative treasury
int construction = 0;
int expenses = 0;
int researchMoneyGiven = 0;
int savingsGiven = 0;
// derived
int available = 0; // money left for construction/research after fixed costs
int researchMoney = 0; // money routed to research before aid
int researchPoints = 0; // RP from the research money alone
int researchPointsGiven = 0;
int totalResearchPoints = 0; // RP allocated to the current research target
bool hasResearchAllocation = false;
int net = 0; // change in savings this turn
};
// Savings interest: 1 % of a non-negative treasury, only for players who own systems.
// CONFIDENCE: high.
int SavingsInterest(int savings, bool ownsSystems);
// Debt interest: 15 % of the magnitude of a negative treasury. CONFIDENCE: high.
int DebtInterest(int savings);
// Fleet upkeep after the difficulty divisor. CONFIDENCE: high.
int MaintenanceCost(int maintenance, double difficultyDivisor);
// Total of the expense sliders. Per entry, with `availPre` = the non-negative net before
// expenses:
// minC = max(min, 0); maxC = clamp(max, 0, 2e9), 0 meaning 2e9; room = maxC - minC
// request = ftol(fraction x float(availPre)) - minC; take = min(max(request, 0), room)
// total = sum(minC) + min(max(sum(take), 0), availPre - sum(minC)).
// CONFIDENCE: high -- the request term is a fraction of the pre-expense available
// income, multiplied in single precision, minus the mandatory minimum.
int ExpenseTotal(const std::vector<ExpenseSlider>& sliders, int availableBeforeExpenses);
// Research points bought with `researchMoney`:
// RP = ftol( difficulty x (money/50 x 1.15 x 0.5 x 0.85) x (ResMod + shrm + TRM)
// x techMult x serverResMod x ResScl )
// i.e. about 0.009775 RP per unit of money before multipliers. CONFIDENCE: high.
int ResearchPointsFromMoney(int researchMoney, double difficultyMult, double resMod,
double shrm, double trm, double techMult, double serverResMod,
double resScl);
// The full per-turn budget. The order of evaluation matters because later slots read
// the running totals: interest -> system income -> trade/other income -> maintenance ->
// expenses -> available -> construction -> research money/points -> aid -> bonus ->
// savings aid -> net.
// The tech income bonus reads the full net (every income line including interest and
// trade, minus maintenance, research money, construction, expenses and research aid)
// and is only granted when that net is positive: bonus = max(0, ftol((mult - 1) x net)).
// Savings aid is capped by the projected treasury after this turn, not by the turn net:
// given = min(max(Sav + net, 0), max(aid, 0)), evaluated with the bonus already added.
// CONFIDENCE: high (line items, signs, and both running-total readers).
Budget ComputeBudget(const BudgetInputs& in, bool projected);
// ---------------------------------------------------------------------------------------
// Trade
// ---------------------------------------------------------------------------------------
// Routes a system can host: ceil(civilians / REQ_CIV) + ceil(imperials / REQ_IMP), at
// least 1. A zero requirement contributes nothing. CONFIDENCE: high.
int TradeRoutesSupported(double civilianPop, double imperialPop, const TuningTable& t);
enum class FreighterClass : int { Cruiser = 0 /*CRQ*/, CruiserRefit = 1 /*CR*/, Destroyer = 2 /*DE*/ };
struct TradeRouteState {
int ageTurns = 0; // turns since the route was established
int freighters[3] = {0, 0, 0}; // by FreighterClass index
int tradeStationsAtSystem = 0;
bool partnerAddicted = false;
};
// Gross income of one route before the owner/partner split:
// young route (age < STARTUP_TURNS): STARTUP_INCOME flat
// else MIN_INCOME + sum over classes in order CRQ, CR, DE of
// min(n_class, capLeft) x PERFREIGHTER[class], capLeft starting at MAX_FREIGHTERS,
// then x (1 + STATION_BONUS_TRADE_INCOME x stations), x ADDICTION_TRADE_MOD if addicted.
// CONFIDENCE: high on the freighter sum; medium on where the multipliers truncate.
int TradeRouteGrossIncome(const TradeRouteState& route, const TuningTable& t);
// The share one side of the route receives: owner gets OWNERS_SHARE (clamped 0..1), the
// partner the rest; AI players additionally scale by their difficulty trade multiplier.
// CONFIDENCE: high.
int TradeRouteIncome(const TradeRouteState& route, bool asOwner, double difficultyTradeMult,
const TuningTable& t);
// ---------------------------------------------------------------------------------------
// Bankruptcy
// ---------------------------------------------------------------------------------------
struct BankruptcyLimits {
int eliminationFloor = 0; // BnkEl: below this the player is on the elimination clock
int protectionLimit = 0; // BnkPr: below this cost-cutting starts
};
// Limits from the sum of every owned system's maximum money output:
// eliminationFloor = max(ftol(maxIncome / -0.15), -2e9)
// (the debt at which 15 %/turn interest eats the whole maximum income)
// protectionLimit = max(-ftol(BANKRUPTCY_PROTECTION_LIMIT_FACTOR x maxIncome), eliminationFloor)
// The limits a turn's check uses are the ones computed at the end of the previous turn
// (and on load); the caller keeps them on the player.
// CONFIDENCE: high -- the factor is on the protection limit, the elimination limit is
// the interest break-even, both read with their constants.
BankruptcyLimits ComputeBankruptcyLimits(int maxIncome, const TuningTable& t);
// 2 = elimination pending, 1 = protection (cost cutting), 0 = solvent. CONFIDENCE: high.
int BankruptcyLevel(int savings, const BankruptcyLimits& limits);
struct BankruptcyState {
int warningLevel = 0; // BnkWrn
int startTurn = -1; // BnkTrn: turn the current level began; -1 when solvent
};
struct BankruptcyDecision {
bool costCutting = false; // run the cost-cutting pass this turn
bool eliminate = false; // the player is eliminated this turn
};
// Per-turn bankruptcy bookkeeping. The stored state is updated first -- any change of
// level (0<->1, 1<->2 alike) restamps the start turn with the current turn, level 0
// stamps -1 -- and the decisions are then taken on the *previous* state:
// costCutting = level != 0 && old.level != 0
// eliminate = old.level == 2 && currentTurn - old.startTurn >= BANKRUPTCY_ELIMINATION_TURNS
// so both actions begin the turn after the level was reached.
// CONFIDENCE: high -- stamp-on-transition and act-on-old-state read from the code.
BankruptcyDecision BankruptcyStep(BankruptcyState& state, int level, int currentTurn,
const TuningTable& t);
} // namespace sots::sim