import type { TotalAddressableMarket, TAMSegmentId, MarketShareEntry, Competitor, Era, ObsolescenceState, DeveloperEcosystem, } from '@ai-tycoon/shared'; import { TAM_BASE_SIZES, TAM_GROWTH_PER_TICK, SHARE_TEMPERATURE, SHARE_MIGRATION_SPEED, ATTRACTIVENESS_WEIGHTS, OBSOLESCENCE_PENALTY_WEIGHT, NEW_MODEL_BOOST_VALUE, CONSUMER_TIER_BASE_PERCEIVED_VALUE, PERCEIVED_VALUE_REPUTATION_RANGE, } from '@ai-tycoon/shared'; export interface ParticipantProfile { id: string; qualityScore: number; priceScore: number; reputation: number; ecosystemScore: number; freshness: number; hasFreeTier: boolean; } function computeTamPriceScore(price: number, qualityScore: number, reputation: number): number { if (price <= 0) return 1; const repRange = PERCEIVED_VALUE_REPUTATION_RANGE; const repMult = repRange.min + (Math.max(0, Math.min(100, reputation)) / 100) * (repRange.max - repRange.min); const refPV = CONSUMER_TIER_BASE_PERCEIVED_VALUE.plus * qualityScore * repMult; if (refPV <= 0) return 0; const ratio = price / refPV; return 1 / (1 + ratio * ratio); } export function buildPlayerProfile( qualityScore: number, chatPrice: number, apiOutputPrice: number, reputation: number, ecosystem: DeveloperEcosystem, obsolescence: ObsolescenceState, hasFreeTier: boolean, ): ParticipantProfile { const priceScore = computeTamPriceScore(chatPrice, qualityScore, reputation); let freshness = obsolescence.playerModelFreshness; if (obsolescence.newModelBoostRemaining > 0) { freshness = Math.min(1, freshness + NEW_MODEL_BOOST_VALUE); } return { id: 'player', qualityScore: Math.min(1, qualityScore), priceScore, reputation, ecosystemScore: ecosystem.ecosystemScore, freshness, hasFreeTier, }; } export function buildCompetitorProfile(c: Competitor): ParticipantProfile { const compQuality = Math.min(1, c.estimatedCapability / 100); const priceScore = computeTamPriceScore(c.products.chatPrice, compQuality, c.reputation); return { id: c.id, qualityScore: Math.min(1, c.estimatedCapability / 100), priceScore, reputation: c.reputation, ecosystemScore: c.developerEcosystemScore, freshness: c.modelFreshness, hasFreeTier: c.products.hasFreeTier, }; } function computeAttractiveness( p: ParticipantProfile, segment: TAMSegmentId, qualityBaseline: number, ): number { const w = ATTRACTIVENESS_WEIGHTS[segment]; let score = w.quality * p.qualityScore + w.price * p.priceScore + w.reputation * (p.reputation / 100) + w.ecosystem * (p.ecosystemScore / 100) + w.freshness * p.freshness + w.freeTier * (p.hasFreeTier ? 1 : 0); const qualityGap = qualityBaseline / 100 - p.qualityScore; if (qualityGap > 0) { score -= qualityGap * OBSOLESCENCE_PENALTY_WEIGHT; } return Math.max(0.01, score); } export function computeMarketShares( tam: TotalAddressableMarket, participants: ParticipantProfile[], qualityBaseline: number, ): TotalAddressableMarket { const segments = { ...tam.segments }; const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government']; const n = participants.length; const scores = new Array(n); const targetShares = new Array(n); for (const segId of segmentIds) { const seg = segments[segId]; for (let i = 0; i < n; i++) { scores[i] = computeAttractiveness(participants[i], segId, qualityBaseline); } // Inline softmax let maxScore = scores[0]; for (let i = 1; i < n; i++) { if (scores[i] > maxScore) maxScore = scores[i]; } let sumExp = 0; for (let i = 0; i < n; i++) { targetShares[i] = Math.exp((scores[i] - maxScore) * SHARE_TEMPERATURE); sumExp += targetShares[i]; } for (let i = 0; i < n; i++) { targetShares[i] /= sumExp; } const oldShareMap = new Map(); for (const entry of seg.shares) { oldShareMap.set(entry.playerId, entry); } const newShares: MarketShareEntry[] = new Array(n); let totalShare = 0; for (let i = 0; i < n; i++) { const p = participants[i]; const old = oldShareMap.get(p.id); const oldShare = old?.sharePercent ?? 0; const migratedShare = oldShare + (targetShares[i] - oldShare) * SHARE_MIGRATION_SPEED; totalShare += migratedShare; newShares[i] = { playerId: p.id, sharePercent: migratedShare, customers: 0, attractivenessScore: scores[i], }; } if (totalShare > 0) { for (const entry of newShares) { entry.sharePercent /= totalShare; entry.customers = Math.floor(entry.sharePercent * seg.totalSize); } } segments[segId] = { ...seg, shares: newShares }; } return { segments }; } export function updateTAMGrowth(tam: TotalAddressableMarket, era: Era): TotalAddressableMarket { const baseSizes = TAM_BASE_SIZES[era]; const segments = { ...tam.segments }; const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government']; for (const segId of segmentIds) { const seg = segments[segId]; const base = baseSizes[segId]; const grown = seg.totalSize + seg.totalSize * TAM_GROWTH_PER_TICK; segments[segId] = { ...seg, totalSize: Math.max(base, grown) }; } return { segments }; } export function initializeTAM(era: Era, competitors: Competitor[]): TotalAddressableMarket { const baseSizes = TAM_BASE_SIZES[era]; const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government']; const segments = {} as Record; for (const segId of segmentIds) { const shares: MarketShareEntry[] = [ { playerId: 'player', sharePercent: 0.05, customers: 0, attractivenessScore: 0 }, ...competitors.map(c => ({ playerId: c.id, sharePercent: c.marketShares[segId] ?? 0.1, customers: 0, attractivenessScore: 0, })), ]; const totalShare = shares.reduce((s, e) => s + e.sharePercent, 0); for (const entry of shares) { entry.sharePercent /= totalShare; entry.customers = Math.floor(entry.sharePercent * baseSizes[segId]); } segments[segId] = { totalSize: baseSizes[segId], shares }; } return { segments }; }