331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { processEconomy } from './economySystem';
|
|
import { createTestState, createTestCluster } from '../__test-utils__';
|
|
import type { MarketTickResult } from './marketSystem';
|
|
import type { InfrastructureState } from '@ai-tycoon/shared';
|
|
|
|
function createMarketResult(
|
|
overrides: Partial<MarketTickResult> = {},
|
|
): MarketTickResult {
|
|
return {
|
|
marketState: {} as MarketTickResult['marketState'],
|
|
apiRevenue: 0,
|
|
subscriptionRevenue: 0,
|
|
totalTokenDemand: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createInfraWithCosts(
|
|
energyCosts: number[],
|
|
maintenanceCosts: number[],
|
|
): InfrastructureState {
|
|
const clusters = energyCosts.map((energy, i) => {
|
|
const cluster = createTestCluster();
|
|
const dc = cluster.campuses[0].dataCenters[0];
|
|
dc.energyCostPerTick = energy;
|
|
dc.maintenanceCostPerTick = maintenanceCosts[i];
|
|
return cluster;
|
|
});
|
|
|
|
const state = createTestState();
|
|
return { ...state.infrastructure, clusters };
|
|
}
|
|
|
|
describe('processEconomy', () => {
|
|
it('computes revenue as apiRevenue + subscriptionRevenue', () => {
|
|
const state = createTestState();
|
|
const market = createMarketResult({
|
|
apiRevenue: 200,
|
|
subscriptionRevenue: 300,
|
|
});
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.revenuePerTick).toBe(500);
|
|
});
|
|
|
|
it('includes infrastructure energy and maintenance in expenses', () => {
|
|
const state = createTestState({
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([10, 20], [5, 15]);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
// 10 + 5 + 20 + 15 = 50
|
|
expect(result.expensesPerTick).toBe(50);
|
|
});
|
|
|
|
it('includes talent salary in expenses', () => {
|
|
const state = createTestState({
|
|
talent: { totalSalaryPerTick: 100 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.expensesPerTick).toBe(100);
|
|
});
|
|
|
|
it('includes data partnership costs in expenses', () => {
|
|
const state = createTestState({
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: {
|
|
partnerships: [
|
|
{ costPerTick: 25 },
|
|
{ costPerTick: 75 },
|
|
] as any,
|
|
},
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.expensesPerTick).toBe(100);
|
|
});
|
|
|
|
it('includes compliance cost when bestCapability > 30', () => {
|
|
// compliance = bestCapability * 50 * (1 + eraIdx * 0.5) / 100
|
|
// bestCapability = 60, era = startup (idx 0)
|
|
// 60 * 50 * (1 + 0) / 100 = 30
|
|
const state = createTestState({
|
|
meta: { currentEra: 'startup' },
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 60 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.expensesPerTick).toBe(30);
|
|
});
|
|
|
|
it('scales compliance cost with era index', () => {
|
|
// bestCapability = 60, era = bigtech (idx 2)
|
|
// 60 * 50 * (1 + 2 * 0.5) / 100 = 60
|
|
const state = createTestState({
|
|
meta: { currentEra: 'bigtech' },
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 60 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.expensesPerTick).toBe(60);
|
|
});
|
|
|
|
it('has zero compliance cost when bestCapability <= 30', () => {
|
|
const state = createTestState({
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 30 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.expensesPerTick).toBe(0);
|
|
});
|
|
|
|
it('includes devRel spending in expenses', () => {
|
|
const state = createTestState({
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 42 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.expensesPerTick).toBe(42);
|
|
});
|
|
|
|
it('includes extraCosts in expenses', () => {
|
|
const state = createTestState({
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra, 200);
|
|
|
|
expect(result.expensesPerTick).toBe(200);
|
|
});
|
|
|
|
it('computes money as previousMoney + revenue - expenses', () => {
|
|
const state = createTestState({
|
|
economy: { money: 1000 },
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult({
|
|
apiRevenue: 300,
|
|
subscriptionRevenue: 200,
|
|
});
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.money).toBe(1500);
|
|
});
|
|
|
|
it('floors money at zero', () => {
|
|
const state = createTestState({
|
|
economy: { money: 100 },
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult({ apiRevenue: 0, subscriptionRevenue: 0 });
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra, 500);
|
|
|
|
expect(result.money).toBe(0);
|
|
});
|
|
|
|
it('accumulates totalRevenue and totalExpenses', () => {
|
|
const state = createTestState({
|
|
economy: { money: 10_000, totalRevenue: 5000, totalExpenses: 2000 },
|
|
talent: { totalSalaryPerTick: 50 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult({
|
|
apiRevenue: 100,
|
|
subscriptionRevenue: 200,
|
|
});
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.totalRevenue).toBe(5300);
|
|
expect(result.totalExpenses).toBe(2050);
|
|
});
|
|
|
|
it('adds financial history snapshot when tickCount % 60 === 0', () => {
|
|
const state = createTestState({
|
|
meta: { tickCount: 120 },
|
|
economy: { money: 5000, financialHistory: [] },
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult({
|
|
apiRevenue: 100,
|
|
subscriptionRevenue: 0,
|
|
});
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.financialHistory).toHaveLength(1);
|
|
expect(result.financialHistory[0]).toMatchObject({
|
|
tick: 120,
|
|
revenue: 100,
|
|
expenses: 0,
|
|
});
|
|
});
|
|
|
|
it('does not add financial history snapshot when tickCount % 60 !== 0', () => {
|
|
const state = createTestState({
|
|
meta: { tickCount: 61 },
|
|
economy: { money: 5000, financialHistory: [] },
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
expect(result.financialHistory).toHaveLength(0);
|
|
});
|
|
|
|
it('trims financial history when it exceeds 1000 entries', () => {
|
|
const existingHistory = Array.from({ length: 1000 }, (_, i) => ({
|
|
tick: i * 60,
|
|
money: 1000,
|
|
revenue: 10,
|
|
expenses: 5,
|
|
valuation: 1_000_000,
|
|
}));
|
|
|
|
const state = createTestState({
|
|
meta: { tickCount: 60000 },
|
|
economy: { money: 5000, financialHistory: existingHistory },
|
|
talent: { totalSalaryPerTick: 0 },
|
|
data: { partnerships: [] },
|
|
models: { bestDeployedModelScore: 0 },
|
|
market: { developerEcosystem: { devRelSpending: 0 } },
|
|
});
|
|
const market = createMarketResult();
|
|
const infra = createInfraWithCosts([], []);
|
|
|
|
const result = processEconomy(state, market, infra);
|
|
|
|
// 1000 existing + 1 new = 1001 -> shift -> 1000
|
|
expect(result.financialHistory).toHaveLength(1000);
|
|
// The oldest entry (tick 0) should have been shifted off
|
|
expect(result.financialHistory[0].tick).toBe(60);
|
|
});
|
|
|
|
it('sums all expense categories together', () => {
|
|
// infra: 10+5 = 15, talent: 20, data: 30, compliance (cap=50, era=scaleup idx=1): 50*50*(1+0.5)/100 = 37.5, devRel: 10, extra: 8
|
|
// total = 15 + 20 + 30 + 37.5 + 10 + 8 = 120.5
|
|
const state = createTestState({
|
|
meta: { currentEra: 'scaleup' },
|
|
talent: { totalSalaryPerTick: 20 },
|
|
data: {
|
|
partnerships: [{ costPerTick: 30 }] as any,
|
|
},
|
|
models: { bestDeployedModelScore: 50 },
|
|
market: { developerEcosystem: { devRelSpending: 10 } },
|
|
});
|
|
const market = createMarketResult({
|
|
apiRevenue: 500,
|
|
subscriptionRevenue: 500,
|
|
});
|
|
const infra = createInfraWithCosts([10], [5]);
|
|
|
|
const result = processEconomy(state, market, infra, 8);
|
|
|
|
expect(result.expensesPerTick).toBe(120.5);
|
|
expect(result.revenuePerTick).toBe(1000);
|
|
});
|
|
});
|