feat(sim): economy rules

This commit is contained in:
Victor Bodinaud committed 2026-10-08 12:30:36 +02:00
1 parent df24cf7516
commit d97d60bf7e
2 files changed
+85

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
import { Catalog, Game, getGame } from './catalog';
import { Condition, ItemRef, itemKey } from './inventory';
import { francsToCents } from './money';
export const WHOLESALE_RATE = 0.65;
export const USED_FACTOR: Record<Exclude<Condition, 'new'>, number> = {
complete: 0.6,
no_manual: 0.5,
loose: 0.4,
};
export const CASH_BUYBACK_RATE = 0.35;
export const TRADE_BUYBACK_RATE = 0.5;
export const MONTHLY_RENT_CENTS = francsToCents(6000);
export const STARTING_CASH_CENTS = francsToCents(30000);
export type BuyBackMode = 'cash' | 'trade';
export function wholesaleCost(game: Game): number {
return Math.round(game.launchPriceCents * WHOLESALE_RATE);
}
export function referencePrice(game: Game, condition: Condition): number {
if (condition === 'new') return game.launchPriceCents;
return Math.round(game.launchPriceCents * USED_FACTOR[condition]);
}
/** 0.5 at the reference price, 1 at half price or less, 0 at 150% or more. */
export function purchaseProbability(priceCents: number, referenceCents: number): number {
const ratio = priceCents / referenceCents;
return Math.min(1, Math.max(0, 1.5 - ratio));
}
export function customerMinimumAsk(game: Game, condition: Condition, mode: BuyBackMode): number {
const rate = mode === 'cash' ? CASH_BUYBACK_RATE : TRADE_BUYBACK_RATE;
return Math.round(referencePrice(game, condition) * rate);
}
export function priceOf(prices: Record<string, number>, catalog: Catalog, item: ItemRef): number {
return prices[itemKey(item)] ?? referencePrice(getGame(catalog, item.gameId), item.condition);
}
+45
View File
@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import {
customerMinimumAsk, MONTHLY_RENT_CENTS, priceOf, purchaseProbability,
referencePrice, wholesaleCost,
} from '../../src/sim/economy';
import { getGame } from '../../src/sim/catalog';
import { testCatalog } from './helpers';
const catalog = testCatalog();
const sml = getGame(catalog, 'sml'); // 3796 cents
describe('economy', () => {
it('buys from the wholesaler at 65% of launch price', () => {
expect(wholesaleCost(sml)).toBe(2467);
});
it('derives used reference prices from the condition', () => {
expect(referencePrice(sml, 'new')).toBe(3796);
expect(referencePrice(sml, 'complete')).toBe(2278);
expect(referencePrice(sml, 'no_manual')).toBe(1898);
expect(referencePrice(sml, 'loose')).toBe(1518);
});
it('maps price ratio to purchase probability', () => {
expect(purchaseProbability(1000, 1000)).toBeCloseTo(0.5);
expect(purchaseProbability(500, 1000)).toBe(1);
expect(purchaseProbability(1500, 1000)).toBe(0);
expect(purchaseProbability(3000, 1000)).toBe(0);
});
it('computes what a customer accepts for a buy-back', () => {
expect(customerMinimumAsk(sml, 'complete', 'cash')).toBe(797);
expect(customerMinimumAsk(sml, 'complete', 'trade')).toBe(1139);
});
it('uses the player price, else the reference price', () => {
const item = { gameId: 'sml', condition: 'new' as const };
expect(priceOf({}, catalog, item)).toBe(3796);
expect(priceOf({ 'sml|new': 3500 }, catalog, item)).toBe(3500);
});
it('charges 6000 F of rent per month', () => {
expect(MONTHLY_RENT_CENTS).toBe(91469);
});
});