diff --git a/src/sim/economy.ts b/src/sim/economy.ts new file mode 100644 index 0000000..7090f5d --- /dev/null +++ b/src/sim/economy.ts @@ -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, 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, catalog: Catalog, item: ItemRef): number { + return prices[itemKey(item)] ?? referencePrice(getGame(catalog, item.gameId), item.condition); +} diff --git a/tests/sim/economy.test.ts b/tests/sim/economy.test.ts new file mode 100644 index 0000000..a9e5048 --- /dev/null +++ b/tests/sim/economy.test.ts @@ -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); + }); +});