201 lines
11 KiB
C++
201 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.
|
|
// `researchMoneyKept` -- the money the turn actually spends on research -- is only charged
|
|
// when the player has a research target; the research money and points are still reported
|
|
// (the UI shows them) but a player with no target keeps the money.
|
|
// 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
|